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
. IState
trong 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 InputController
nơ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 InputController
chú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 arrows
và sau đó nếu anh ta cũng nhấn Shift
. Nếu anh ta chỉ nhấn WASD
thì chúng ta gọi _currentPlayerState.Walk();
khi điều này xảy ra và chúng ta phải currentPlayerState
bằng nhau WalkState
thì 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
+ Shift
nhấ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ý InputController
khô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
để RunState
và chúng tôi làm như vậy trong Run()
các WalkState
và 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 LootState
khi 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, E
chúng tôi gọi _currentPlayerState.Loot();
chúng tôi chuyển sang LootState
và 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 WASD
thì 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 : IState
có 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 override
chúng trong class LootState : BaseState
loạ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 Building
sẽ 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
. IDamagable
Tấ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.