Làm thế nào tôi có thể tránh các lớp người chơi khổng lồ?


46

Hầu như luôn có một lớp người chơi trong một trò chơi. Người chơi thường có thể làm rất nhiều thứ trong trò chơi, điều đó có nghĩa là đối với tôi, lớp này kết thúc rất lớn với hàng tấn biến để hỗ trợ từng phần chức năng mà người chơi có thể làm. Mỗi phần khá nhỏ nhưng kết hợp với tôi, hàng ngàn dòng mã và việc tìm ra thứ bạn cần và đáng sợ để thay đổi là điều khó khăn. Với một cái gì đó về cơ bản là một điều khiển chung cho toàn bộ trò chơi, làm thế nào để bạn tránh được vấn đề này?


26
Nhiều tệp hoặc một tệp, mã phải đi đâu đó. Trò chơi rất phức tạp. Để tìm thấy những gì bạn cần, viết tên phương pháp tốt và nhận xét mô tả. Đừng sợ thực hiện các thay đổi - chỉ cần thử nghiệm. Và sao lưu công việc của bạn :)
Chris McFarland

7
Tôi nhận được nó phải đi đâu đó nhưng thiết kế mã có vấn đề về tính linh hoạt và bảo trì. Có một lớp hoặc một nhóm mã có hàng ngàn dòng cũng không tấn công tôi.
dùng441521

17
@ChrisMcFarland không đề xuất sao lưu, đề xuất mã phiên bản XD.
Nhà phát triển Game 22/2/2017

1
@ChrisMcFarland Tôi đồng ý với GameDeveloper. Việc kiểm soát phiên bản như Git, svn, TFS, ... giúp việc phát triển trở nên dễ dàng hơn rất nhiều do có thể hoàn tác các thay đổi lớn dễ dàng hơn nhiều và có thể dễ dàng phục hồi từ những thứ như vô tình xóa dự án, lỗi phần cứng hoặc hỏng tệp.
Nzall

3
@TylerH: Tôi rất không đồng ý. Các bản sao lưu không cho phép hợp nhất nhiều thay đổi khám phá lại với nhau, cũng như không liên kết bất kỳ nơi nào gần nhiều siêu dữ liệu hữu ích với các bộ thay đổi, cũng không cho phép các luồng công việc đa nhà phát triển lành mạnh. Bạn có thể sử dụng kiểm soát phiên bản như một hệ thống sao lưu theo thời gian rất mạnh mẽ, nhưng điều đó thiếu rất nhiều tiềm năng của các công cụ đó.
Phoshi

Câu trả lời:


67

Bạn thường sử dụng một hệ thống thành phần thực thể (Một hệ thống thành phần thực thể là một kiến ​​trúc dựa trên thành phần). Điều này cũng làm cho việc tạo các thực thể khác dễ dàng hơn và cũng có thể làm cho kẻ thù / NPC có các thành phần giống như người chơi.

Cách tiếp cận này đi theo hướng ngược lại chính xác như một cách tiếp cận hướng đối tượng. Tất cả mọi thứ trong trò chơi là một thực thể. Các thực thể chỉ là một trường hợp mà không có bất kỳ cơ chế trò chơi được xây dựng trong nó. Nó có một danh sách các thành phần và cách để thao tác chúng.

Ví dụ: trình phát có thành phần vị trí, thành phần hoạt hình và thành phần đầu vào và khi người dùng nhấn phím cách, bạn muốn trình phát nhảy.

Bạn có thể đạt được điều này bằng cách cung cấp cho thực thể người chơi một thành phần nhảy, khi được gọi sẽ làm cho thành phần hoạt hình thay đổi thành hoạt hình nhảy và bạn làm cho người chơi có vận tốc y dương trong thành phần vị trí. Trong thành phần đầu vào, bạn lắng nghe phím cách và bạn gọi thành phần nhảy. (Đây chỉ là một ví dụ, bạn nên có một thành phần điều khiển để di chuyển).

Điều này giúp chia mã thành các mô-đun nhỏ hơn, có thể tái sử dụng và có thể dẫn đến một dự án có tổ chức hơn.


Bình luận không dành cho thảo luận mở rộng; cuộc trò chuyện này đã được chuyển sang trò chuyện .
MichaelHouse

8
Mặc dù tôi hiểu những bình luận chuyển động cần được di chuyển, nhưng đừng di chuyển những bình luận thách thức tính chính xác của câu trả lời. Điều đó là hiển nhiên, không?
bug-a-lot

20

Trò chơi không phải là duy nhất trong điều này; các lớp thần là một mô hình chống ở khắp mọi nơi.

Một giải pháp phổ biến là phá vỡ lớp lớn trong một cây các lớp nhỏ hơn. Nếu người chơi có hàng tồn kho, đừng biến phần quản lý kho class Player. Thay vào đó, tạo một class Inventory. Đây là một thành viên cho class Player, nhưng trong nội bộ class Inventorycó thể bọc rất nhiều mã.

Một ví dụ khác: một nhân vật người chơi có thể có quan hệ với NPC, vì vậy bạn có thể có một class Relationtham chiếu cả Playerđối tượng và NPCđối tượng, nhưng không thuộc về cả hai.


Vâng, tôi chỉ tìm kiếm ý tưởng về cách làm điều này. Suy nghĩ là gì bởi vì có rất nhiều chức năng nhỏ nên trong khi mã hóa, điều đó không tự nhiên, đối với tôi, để phá vỡ những phần chức năng nhỏ đó. Tuy nhiên, rõ ràng là tất cả những phần chức năng nhỏ đó bắt đầu làm cho lớp người chơi trở nên khổng lồ.
dùng441521

1
Mọi người thường nói một cái gì đó là một lớp thần hoặc đối tượng thần, khi nó chứa và quản lý mọi lớp / đối tượng khác trong trò chơi.
Bálint

11

1) Trình phát: Kiến trúc dựa trên thành phần máy-nhà nước.

Các thành phần thông thường cho Trình phát: HealthSystem, MovementSystem, InventorySystem, ActionSystem. Đó là tất cả các lớp như thế class HealthSystem.

Tôi không khuyên bạn nên sử dụng Update()ở đó (Không có nghĩa gì trong các trường hợp thông thường phải cập nhật trong hệ thống y tế trừ khi bạn cần nó cho một số hành động ở mọi khung hình, những trường hợp này hiếm khi xảy ra. Một trường hợp bạn cũng có thể nghĩ đến - người chơi bị đầu độc và bạn cần anh ta thỉnh thoảng để mất sức khỏe - ở đây tôi khuyên bạn nên sử dụng coroutines. Một người khác liên tục tái tạo sức khỏe hoặc sức mạnh đang chạy, bạn chỉ cần lấy sức khỏe hoặc sức mạnh hiện tại và gọi coroutine để làm đầy đến mức đó khi thời gian đến. anh ta bị hư hoặc anh ta bắt đầu chạy lại và cứ thế. OK đó là một chút không chính đáng nhưng tôi hy vọng nó hữu ích) .

Các tiểu bang: LootState, RunState, WalkState, AttackState, IDLEState.

Mọi nhà nước đều thừa hưởng từ interface IState. IStatetrong trường hợp của chúng tôi có 4 phương thức chỉ là một ví dụ.Loot() Run() Walk() Attack()

Ngoài ra, chúng tôi có class InputControllernơi chúng tôi kiểm tra mọi Đầu vào của người dùng.

Bây giờ đến ví dụ thực tế: trong InputControllerchúng tôi kiểm tra xem người chơi có nhấn bất kỳ nút nào không WASD or arrowsvà sau đó nếu anh ta cũng nhấn Shift. Nếu anh ta chỉ nhấn WASDthì chúng ta gọi _currentPlayerState.Walk();khi điều này xảy ra và chúng ta phải currentPlayerStatebằng nhau WalkStatethì WalkState.Walk() chúng ta có tất cả các thành phần cần thiết cho trạng thái này - trong trường hợp này MovementSystem, vì vậy chúng ta làm cho người chơi di chuyển public void Walk() { _playerMovementSystem.Walk(); }- bạn thấy chúng ta có gì ở đây? Chúng tôi có lớp hành vi thứ hai và điều đó rất tốt cho việc duy trì và gỡ lỗi mã.

Bây giờ đến trường hợp thứ hai: nếu chúng ta có WASD+ Shiftnhấn thì sao? Nhưng trạng thái trước đây của chúng tôi là WalkState. Trong trường hợp Run()này sẽ được gọi trong InputController(không trộn cái này lên, Run()được gọi vì chúng tôi có WASD+ Shiftđăng ký InputControllerkhông phải vì WalkState). Khi chúng tôi gọi _currentPlayerState.Run();trong WalkState- chúng ta biết rằng chúng ta phải chuyển đổi _currentPlayerStateđể RunStatevà chúng tôi làm như vậy trong Run()các WalkStatevà gọi nó một lần nữa trong phương pháp này nhưng bây giờ với một trạng thái khác nhau bởi vì chúng tôi không muốn hành động mất khung này. Và bây giờ tất nhiên chúng tôi gọi _playerMovementSystem.Run();.

Nhưng điều gì sẽ xảy ra LootStatekhi người chơi không thể đi bộ hoặc chạy cho đến khi anh ta nhả nút? Vâng, trong trường hợp này khi chúng tôi bắt đầu cướp bóc, ví dụ khi nhấn nút, Echúng tôi gọi _currentPlayerState.Loot();chúng tôi chuyển sang LootStatevà bây giờ gọi nó được gọi từ đó. Ở đó chúng tôi ví dụ gọi phương thức collsion để lấy nếu có thứ gì đó để loot trong phạm vi. Và chúng tôi gọi coroutine là nơi chúng tôi có một hình ảnh động hoặc nơi chúng tôi bắt đầu nó và cũng kiểm tra xem người chơi có còn giữ nút không, nếu không phá vỡ coroutine, nếu có, chúng tôi sẽ cho anh ta loot vào cuối coroutine. Nhưng nếu người chơi nhấn WASDthì sao? - _currentPlayerState.Walk();được gọi, nhưng đây là điều hay về máy trạng thái, trongLootState.Walk()chúng tôi có một phương thức trống không có gì hoặc như tôi sẽ làm như một tính năng - người chơi nói: "Này anh bạn, tôi chưa bị cướp bóc điều này, bạn có thể đợi không?". Khi anh ta kết thúc cướp bóc, chúng tôi đổi thành IDLEState.

Ngoài ra, bạn có thể thực hiện một tập lệnh khác được gọi là class BaseState : IStatecó tất cả các hành vi phương thức mặc định này được triển khai, nhưng có chúng virtualđể bạn có thể thực hiện overridechúng trong class LootState : BaseStateloại lớp.


Hệ thống dựa trên thành phần là tuyệt vời, điều duy nhất làm phiền tôi về nó là Instances, nhiều trong số chúng. Và nó cần thêm bộ nhớ và làm việc cho người thu gom rác. Ví dụ, nếu bạn có 1000 trường hợp của kẻ thù. Tất cả đều có 4 thành phần. 4000 đối tượng thay vì 1000. Mb không phải là vấn đề lớn (tôi chưa chạy thử nghiệm hiệu năng) nếu chúng tôi xem xét tất cả các thành phần mà trò chơi thống nhất có.


2) Kiến trúc dựa trên kế thừa. Mặc dù bạn sẽ nhận thấy rằng chúng ta không thể loại bỏ hoàn toàn các thành phần - thực sự không thể nếu chúng ta muốn có mã sạch và hoạt động. Ngoài ra, nếu chúng tôi muốn sử dụng Mẫu thiết kế được khuyến nghị sử dụng trong các trường hợp thích hợp (đừng quá lạm dụng chúng, nó được gọi là quá mức).

Hãy tưởng tượng chúng ta có một lớp Người chơi có tất cả các thuộc tính cần thiết để thoát ra trong một trò chơi. Nó có sức khỏe, mana hoặc năng lượng, có thể di chuyển, chạy và sử dụng các khả năng, có một kho đồ, có thể chế tạo vật phẩm, cướp đồ, thậm chí có thể xây dựng một số chướng ngại vật hoặc tháp pháo.

Trước hết tôi sẽ nói rằng Inventory, Crafting, Movement, Building nên dựa trên thành phần vì người chơi không có trách nhiệm phải có các phương thức như AddItemToInventoryArray()- mặc dù người chơi có thể có một phương thức như thế PutItemToInventory()sẽ gọi phương thức được mô tả trước đó (2 lớp - chúng ta có thể thêm một số điều kiện tùy thuộc vào các lớp khác nhau).

Một ví dụ khác với việc xây dựng. Người chơi có thể gọi một cái gì đó giống như OpenBuildingWindow(), nhưng Buildingsẽ lo tất cả phần còn lại, và khi người dùng quyết định xây dựng một tòa nhà cụ thể, anh ta chuyển tất cả thông tin cần thiết cho người chơi Build(BuildingInfo someBuildingInfo)và người chơi bắt đầu xây dựng nó với tất cả hình ảnh động cần thiết.

RẮN - Nguyên tắc OOP. S - trách nhiệm duy nhất: đó là những gì chúng ta đã thấy trong các ví dụ trước. Vâng, nhưng quyền thừa kế ở đâu?

Ở đây: sức khỏe và các đặc điểm khác của người chơi nên được xử lý bởi một thực thể khác? Tôi nghĩ là không. Không thể có một cầu thủ không có sức khỏe, nếu có một người, chúng ta không được thừa hưởng. Ví dụ, chúng tôi có IDamagable, LivingEntity, IGameActor, GameActor. IDamagableTất nhiên là có TakeDamage().

class LivinEntity : IDamagable {

   private float _health; // For fields that are the same between Instances I would use Flyweight Pattern.

   public void TakeDamage() {
       ....
   }
}

class GameActor : LivingEntity, IGameActor {
    // Here goes state machine and other attached components needed.
}

class Player : GameActor {
   // Inventory, Building, Crafting.... components.
}

Vì vậy, ở đây tôi không thể thực sự phân chia các thành phần từ thừa kế, nhưng chúng ta có thể trộn chúng như bạn thấy. Chúng tôi cũng có thể tạo một số lớp cơ sở cho hệ thống Xây dựng chẳng hạn nếu chúng tôi có các loại khác nhau và chúng tôi không muốn viết thêm bất kỳ mã nào cần thiết. Thật vậy, chúng ta cũng có thể có các loại tòa nhà khác nhau và thực sự không có cách nào tốt để thực hiện thành phần này!

OrganicBuilding : Building, TechBuilding : Building. Bạn không cần tạo 2 thành phần và viết mã ở đó hai lần cho các hoạt động chung hoặc thuộc tính của tòa nhà. Và sau đó thêm chúng một cách khác nhau, bạn có thể sử dụng sức mạnh của sự kế thừa và sau này là đa hình và bao bọc.


Tôi sẽ đề nghị sử dụng một cái gì đó ở giữa. Và không lạm dụng các thành phần.


Tôi đặc biệt khuyên bạn nên đọc cuốn sách này về Mô hình lập trình trò chơi - nó miễn phí trên WEB.


Tôi sẽ đào vào tối nay nhưng FYI tôi không sử dụng sự thống nhất vì vậy tôi sẽ phải điều chỉnh một số thứ vẫn ổn.
dùng441521

Oh, sry tôi nghĩ ở đây là một thẻ Unity, xấu của tôi. Điều duy nhất là MonoBehavior - nó chỉ là một lớp cơ sở cho mọi Trường hợp trong bối cảnh trong trình soạn thảo Unity. Đối với Vật lý.OverlapSphere () - đó là một phương pháp tạo ra máy va chạm hình cầu trong khung và kiểm tra những gì nó chạm vào. Các coroutines giống như Cập nhật giả, các cuộc gọi của họ có thể được giảm xuống số lượng nhỏ hơn fps trên PC của người chơi - tốt cho hiệu suất. Start () - chỉ là một phương thức được gọi một lần khi Instance được tạo. Mọi thứ khác nên áp dụng ở mọi nơi khác. Phần tiếp theo tôi sẽ không sử dụng bất cứ thứ gì với Unity. Sry. Hy vọng điều này làm rõ một cái gì đó.
Mặt trăng thí sinh _Max_

Tôi đã sử dụng Unity trước đây để tôi hiểu ý tưởng. Tôi cũng đang sử dụng Lua có coroutines nên mọi thứ sẽ dịch khá tốt.
dùng441521

Câu trả lời này có vẻ hơi quá cụ thể của Unity khi xem xét việc thiếu thẻ Unity. Nếu bạn làm cho nó chung chung hơn và làm cho công cụ thống nhất trở thành một ví dụ, đây sẽ là một câu trả lời tốt hơn nhiều.
Pharap

@CandidMoon Vâng, điều đó tốt hơn.
Pharap

4

Không có viên đạn bạc nào cho vấn đề này, nhưng có nhiều cách tiếp cận khác nhau, hầu hết tất cả đều xoay quanh nguyên tắc 'tách mối quan tâm'. Các câu trả lời khác đã thảo luận về cách tiếp cận dựa trên thành phần phổ biến, nhưng có những cách tiếp cận khác có thể được sử dụng thay vì hoặc cùng với giải pháp dựa trên thành phần. Tôi sẽ thảo luận về cách tiếp cận bộ điều khiển thực thể vì đây là một trong những giải pháp ưa thích của tôi cho vấn đề này.

Thứ nhất, ngay từ đầu, ý tưởng về một Playerlớp học đã gây hiểu nhầm. Nhiều người có xu hướng nghĩ rằng một nhân vật người chơi, nhân vật npc và quái vật / kẻ thù là những lớp khác nhau, trong khi thực tế tất cả những người đó có khá nhiều điểm chung: tất cả đều được vẽ trên màn hình, tất cả họ đều di chuyển xung quanh, họ có thể tất cả đều có hàng tồn kho, vv

Cách suy nghĩ này dẫn đến một cách tiếp cận trong đó các nhân vật người chơi, nhân vật không phải người chơi và quái vật / kẻ thù đều được coi là ' Entitys' thay vì bị đối xử khác nhau. Mặc dù vậy, tự nhiên, họ phải cư xử khác đi - nhân vật người chơi phải được điều khiển thông qua đầu vào và npc cần ai.

Giải pháp cho vấn đề này là có Controllercác lớp được sử dụng để điều khiển Entitys. Bằng cách này, tất cả logic nặng kết thúc trong bộ điều khiển và tất cả dữ liệu và tính phổ biến được lưu trữ trong thực thể.

Ngoài ra, bằng cách phân lớp Controllervào InputControllerAIController, nó cho phép người chơi kiểm soát hiệu quả bất kỳ thứ gì Entitytrong phòng. Cách tiếp cận này cũng giúp nhiều người chơi bằng cách có một RemoteControllerhoặc NetworkControllerlớp hoạt động thông qua các lệnh từ luồng mạng.

Điều này có thể dẫn đến rất nhiều logic bị buộc thành một Controllernếu bạn không cẩn thận. Cách để tránh điều đó là có Controllercác s bao gồm các Controllers khác hoặc làm cho Controllerchức năng phụ thuộc vào các thuộc tính khác nhau của Controller. Ví dụ, cái AIControllerDecisionTreegắn liền với nó và PlayerCharacterControllercó thể bao gồm nhiều loại khác Controllernhư a MovementController, a JumpController(chứa một máy trạng thái với các trạng thái OnGround, Tăng dần và Giảm dần), một InventoryUIController. Một lợi ích bổ sung của điều này là Controllercó thể thêm các tính năng mới khi các tính năng mới được thêm vào - nếu một trò chơi bắt đầu mà không có hệ thống kiểm kê và được thêm vào, một bộ điều khiển cho nó có thể được xử lý sau.


Tôi thích ý tưởng này nhưng dường như nó đã chuyển tất cả mã sang lớp trình điều khiển để lại cho tôi cùng một vấn đề.
dùng441521

@ user441521 Chỉ cần nhận ra có thêm một đoạn tôi sẽ thêm nhưng tôi đã mất nó khi trình duyệt của tôi bị sập. Tôi sẽ thêm nó ngay bây giờ. Về cơ bản, bạn có thể có các bộ điều khiển khác nhau có thể kết hợp chúng thành các bộ điều khiển tổng hợp để mỗi bộ điều khiển xử lý những thứ khác nhau. ví dụ: AggregateControll.Controllers = {JumpContoder (keybinds), MoveControll (keybinds), InventoryUIControll (keybinds, uisystem)}
Pharap 22/2/2017
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.