1) Програвач: архітектура, заснована на стані і машині.
Звичайні компоненти для програвача: HealthSystem, MovementSystem, InventorySystem, ActionSystem. Це всі класи на кшталт class HealthSystem
.
Я не рекомендую користуватися Update()
там (Немає сенсу в звичайних випадках оновлюватись в системі охорони здоров’я, якщо вам це не потрібно для деяких дій там кожен кадр, такі випадки трапляються рідко. Один випадок, який ви також можете подумати - гравець отруїться, і вам він потрібен час від часу втрачати здоров'я - тут я пропоную скористатися процедурами. Інший постійно відновлює здоров’я або працює сила, ви просто приймаєте поточне здоров'я або потужність і зателефонуєте до програми, щоб заповнити цей рівень, коли прийде час. він був пошкоджений або він знову почав працювати і так далі. Добре, це було трохи офтопіком, але я сподіваюся, що це було корисно) .
Штати: LootState, RunState, WalkState, AttackState, IDLEState.
Кожна держава успадковує від interface IState
. IState
У нашому випадку є 4 методи лише для прикладу.Loot() Run() Walk() Attack()
Крім того, у нас є місце, class InputController
де ми перевіряємо кожен вхід користувача.
Тепер до реального прикладу: InputController
ми перевіряємо, чи гравець натискає якийсь із, WASD or arrows
а потім, чи він також натискає Shift
. Якщо він натиснув лише WASD
тоді, ми зателефонуємо, _currentPlayerState.Walk();
коли це станеться, і ми повинні currentPlayerState
бути рівними WalkState
тоді, коли у WalkState.Walk()
нас є всі компоненти, необхідні для цього стану - в цьому випадку MovementSystem
, тому ми змушуємо гравця рухатися public void Walk() { _playerMovementSystem.Walk(); }
- ви бачите, що ми маємо тут? У нас є другий рівень поведінки, що дуже добре для підтримки коду та налагодження.
Тепер до другого випадку: що робити, якщо ми WASD
+ Shift
натиснули? Але наш попередній стан був WalkState
. У цьому випадку Run()
буде введено дзвінок InputController
(не змішуйте це, Run()
називається тому, що у нас є WASD
+ Shift
реєстрація InputController
не через WalkState
). Коли ми викликаємо _currentPlayerState.Run();
в WalkState
- ми знаємо , що ми повинні перейти _currentPlayerState
до RunState
і ми робимо це Run()
з WalkState
і викликати його знову в цьому методі , але тепер з іншою державою , тому що ми не хочемо втратити дію цього кадру. І тепер, звичайно, телефонуємо _playerMovementSystem.Run();
.
Але для чого, LootState
коли гравець не може ходити чи бігати, поки він не відпустить кнопку? Добре в цьому випадку, коли ми почали грабувати, наприклад, коли E
натискали кнопку, ми викликаємо, _currentPlayerState.Loot();
ми переходимо до цього, LootState
а тепер називаємо його викликом звідти. Там ми, наприклад, зателефонуємо методом змови, щоб отримати, якщо є щось, щоб грабувати в діапазоні. І ми називаємо corout, де у нас є анімація, або де ми її запускаємо, а також перевіряємо, чи гравець все ще тримає кнопку, якщо не порушується програма, якщо так, ми даємо йому цикл у кінці програми. Але що робити, якщо гравець натискає WASD
? - _currentPlayerState.Walk();
закликається, але ось ця чудова річ про стан-машина, вLootState.Walk()
у нас є порожній метод, який нічого не робить або як я би робив як особливість - гравці кажуть: "Ей, чоловіче, я цього ще не розграбував, ти можеш почекати?". Коли він закінчить грабувати, ми переходимо до IDLEState
.
Крім того, ви можете зробити інший скрипт, який називається class BaseState : IState
, у якому реалізовані всі ці методи за замовчуванням, але вони мають їх, virtual
щоб ви могли override
їх використовувати у class LootState : BaseState
класах типу.
Система, що базується на компонентах, чудова, єдине, що мене турбує, - це екземпляри, багато з яких. А це потребує більше пам’яті та роботи для збору сміття. Наприклад, якщо у вас 1000 екземплярів противника. Усі вони мають 4 компоненти. 4000 об'єктів замість 1000. Мб - це не так вже й багато (я не виконував тести на працездатність), якщо врахувати всі компоненти, які є в ігровому об’єкті єдності.
2) Архітектура, заснована на спадщині. Хоча ви помітите, що ми не можемо повністю позбутися компонентів - це насправді неможливо, якщо ми хочемо мати чистий і робочий код. Крім того, якщо ми хочемо використовувати шаблони дизайну, які настійно рекомендується використовувати у належних випадках (не занадто їх занадто зловживайте, це називається надмірною генерацією).
Уявіть, у нас є клас Player, який має всі властивості, які йому потрібні для гри. Він має здоров'я, ману чи енергію, може рухатись, бігати та використовувати здібності, має інвентар, вміє обробляти предмети, грабувати предмети, навіть може будувати деякі барикади чи башточки.
Перш за все, я хочу сказати, що інвентаризація, крафтинг, рух, будівництво повинні складатись на основі компонентів, оскільки це не відповідальність гравця за наявність таких методів, AddItemToInventoryArray()
- хоча гравець може мати такий метод, PutItemToInventory()
який називатиме попередній описаний метод (2 шари - ми можемо додайте деякі умови залежно від різних шарів).
Ще один приклад із будівництвом. Гравець може викликати щось на кшталт OpenBuildingWindow()
, але Building
піклується про все інше, і коли користувач вирішить побудувати якусь конкретну будівлю, він передає гравцеві всю необхідну інформацію, Build(BuildingInfo someBuildingInfo)
і гравець починає будувати її з усіма необхідними анімаціями.
Принципи SOLID - OOP. S - одна відповідальність: те, що ми бачили в попередніх прикладах. Так добре, але де спадщина?
Тут: чи має здоров’я та інші характеристики гравця керувати іншим суб'єктом господарювання? Я думаю, що не. Не може бути гравця без здоров'я, якщо він є, ми просто не успадковуємо. Наприклад, у нас є IDamagable
, LivingEntity
, IGameActor
, GameActor
. IDamagable
Звичайно, є 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.
}
Тому тут я фактично не міг розділити компоненти від успадкування, але ми можемо їх змішувати, як бачите. Ми також можемо зробити кілька базових класів для Building Building, наприклад, якщо у нас є різні типи, і ми не хочемо писати більше коду, ніж потрібно. Дійсно, ми також можемо мати різні типи будівель, і насправді немає хорошого способу зробити це на основі компонентів!
OrganicBuilding : Building
, TechBuilding : Building
. Вам не потрібно створювати 2 компоненти та писати там код двічі для загальних операцій або властивостей будівлі. А потім додайте їх по-різному, ви можете використовувати силу успадкування, а пізніше поліморфізм та інкапсуляцію.
Я б запропонував використовувати щось середнє. І не зловживати компонентами.
Я настійно рекомендую прочитати цю книгу про шаблони ігрового програмування - це безкоштовно в WEB.