Як я можу уникнути гігантських класів гравців?


46

У грі майже завжди є клас гравців. Гравець, як правило, може зробити багато в грі, що означає, що для мене цей клас величезний з великою кількістю змінних для підтримки кожного функціоналу, який гравець може зробити. Кожен фрагмент досить невеликий сам по собі, але в поєднанні я закінчую тисячами рядків коду, і стає болю знайти те, що вам потрібно, і страшно вносити зміни. Що ви, в основному, є загальним контролем для всієї гри, як ви уникаєте цього питання?


26
Кілька файлів або один файл, код повинен кудись піти. Ігри складні. Щоб знайти те, що вам потрібно, напишіть хороші назви методів та описові коментарі. Не бійтеся вносити зміни - просто тестуйте. І створіть резервну копію вашої роботи :)
Кріс Макфарланд

7
Я розумію, він повинен кудись піти, але дизайн коду має значення в гнучкості та технічному обслуговуванні. Маючи клас чи групу коду у тисячах рядків, це також не вражає мене.
user441521

17
@ChrisMcFarland не пропонують створювати резервні копії, пропонують код версії XD.
GameDeveloper

1
@ChrisMcFarland Я згоден з GameDeveloper. Такий контроль над версіями, як Git, svn, TFS, ... робить процес набагато простішим завдяки тому, що ви можете скасувати великі зміни набагато легше та мати можливість легко відновитись із таких речей, як випадкове видалення проекту, збій обладнання або пошкодження файлу.
Nzall

3
@TylerH: Я абсолютно не згоден. Резервні копії не дозволяють об'єднати багато дослідницьких змін разом, а також не прив'язують десь поруч стільки ж корисних метаданих до наборів змін, ні вони не дозволяють нормальним робочим процесам для багатьох розробників. Ви можете використовувати управління версіями, як дуже потужна система резервного копіювання, але це не вистачає великого потенціалу цих інструментів.
Phoshi

Відповіді:


67

Зазвичай ви використовуєте компонентну систему компонентів (Система компонентів сутності - це архітектура, заснована на компонентах). Це також полегшує створення інших об'єктів, а також може змусити ворогів / NPC мати ті ж компоненти, що й плеєр.

Цей підхід іде в прямо протилежному напрямку як об'єктно-орієнтований підхід. Все в грі - це сутність. Сутність - це лише випадок без вбудованої в неї ігрової механіки. У ньому є список компонентів та спосіб маніпулювання ними.

Наприклад, у гравця є компонент позиції, компонент анімації та компонент введення, і коли користувач натискає простір, ви хочете, щоб гравець стрибав.

Ви можете досягти цього, додавши сутністю гравця стрибковий компонент, який при виклику змінює компонент аніотіоми на анімацію стрибків, і ви змушуєте гравця мати позитивну швидкість у позиційній складовій. У вхідному компоненті ви слухаєте клавішу пробілу і викликаєте компонент стрибка. (Це лише приклад, у вас повинен бути компонент контролера для руху).

Це допомагає розбивати код на більш дрібні модулі для багаторазового використання і може призвести до більш організованого проекту.


Коментарі не для розширеного обговорення; ця розмова переміщена до чату .
MichaelHouse

8
Хоча я розумію зворушливі коментарі, які потрібно перемістити, не рухайте тих, хто заперечує точність відповіді. Це повинно бути очевидно, ні?
баг-лот

20

Ігри в цьому не унікальні; бого-класи скрізь є анти-зразком.

Поширене рішення - розбити великий клас на дереві менших класів. Якщо у гравця є інвентар, не робіть частину управління запасами class Player. Замість цього створіть class Inventory. Це один член для class Player, але внутрішньо class Inventoryможе містити багато коду.

Інший приклад: персонаж гравця може мати стосунки з NPC, тому ви можете мати class Relationпосилання як на Playerоб'єкт, так і на NPCоб'єкт, але не належить ні до одного.


Так, я просто шукав ідеї, як це зробити. Який спосіб мислення полягав у тому, що є багато функціональних фрагментів, тому хоч кодування - це не природно, як мені здається, розбивати ці маленькі фрагменти функціоналу. Однак стає очевидним, що всі ці невеличкі функціональні можливості починають робити клас гравців величезним.
user441521

1
Зазвичай люди кажуть, що щось є класом богів або об'єктом бога, коли воно містить і керує всіма іншими класами / об’єктами в грі.
Bálint

11

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.


Я пізнаю пізніше сьогодні вночі, але FYI я не використовую єдність, тому мені доведеться коригувати деякі, що добре.
user441521

О, Шрі, я подумав, що це тег Unity, мій поганий. Єдине, що MonoBehavior - це просто базовий клас для кожної інстанції на сцені в редакторі Unity. Що стосується Physics.OverlapSphere () - це метод, який створює кульовий колайдер під час кадру і перевіряє, до чого він торкається. Програми, як підроблені оновлення, їх дзвінки можуть бути зменшені до меншої кількості, ніж кадрів в секунду на ПК гравців - це добре для продуктивності. Start () - просто метод, який викликається один раз, коли створено Instance. Все інше має застосовуватися скрізь. Наступна частина я нічого не буду використовувати з Unity. Шрі. Сподіваюся, це щось прояснило.
відвертий Місяць _Max_

Раніше я використовував Unity, тому я розумію ідею. Я також використовую Lua, який має супровідні програми, тому все повинно бути досить добре.
user441521

Ця відповідь здається занадто характерною для Unity, враховуючи відсутність тегу Unity. Якщо ви зробили це більш загальним і зробили матеріал єдності більш прикладом, це буде набагато кращою відповіддю.
Фарап

@CandidMoon Так, це краще.
Фарап

4

У цій проблемі немає жодної срібної кулі, але існують різні підходи, майже всі вони обертаються навколо принципу "розділення проблем". Інші відповіді вже обговорювали популярний підхід, що базується на компонентах, але є й інші підходи, які можна використовувати замість або разом із рішенням на основі компонентів. Я буду обговорювати підхід суб'єкта-контролера, оскільки це одне з моїх бажаних рішень цієї проблеми.

По-перше, сама ідея Playerкласу вводить в оману в першу чергу. Багато людей схильні думати про персонажа гравця, персонажів npc та монстрів / ворогів як про різні класи, коли насправді всі вони мають дуже багато спільного: всі вони намальовані на екрані, вони всі рухаються, вони можуть всі мають запаси тощо.

Такий спосіб мислення призводить до підходу, коли до персонажів гравців, персонажів, які не належать гравцям, та монстрів / ворогів, всі трактуються як " Entitys", а не по-різному. Природно, що вони повинні поводитись інакше - персонажем гравця потрібно керувати за допомогою введення, і npcs потребує ai.

Рішенням цього є створення Controllerкласів, які використовуються для управління Entitys. Таким чином, вся важка логіка закінчується в контролері, і всі дані та спільність зберігаються в об'єкті.

Крім того, підкласифікація Controllerв InputControllerі AIControllerдозволяє гравцеві ефективно контролювати будь-яку Entityкімнату. Цей підхід також допомагає мультиплеєру, маючи RemoteControllerабо NetworkControllerклас, який працює за допомогою команд з мережевого потоку.

Це може призвести до того, що багато логіки вбираються в одну, Controllerякщо ви не будете обережні. Спосіб уникнути цього - це наявність Controllers, які складаються з інших Controllers, або створення Controllerфункціональних можливостей залежать від різних властивостей Controller. Наприклад, до AIControllerнього DecisionTreeдодаються додатки, і вони PlayerCharacterControllerмогли б складатися з різних інших Controllers, таких як a MovementController, a JumpController(містить державну машину зі станами OnGround, Ascending and спадаючи), an InventoryUIController. Додатковою перевагою цього є те, що нові Controllers можна додавати в міру додавання нових функцій - якщо гра починається без інвентарної системи, а додається одна, контролер для неї можна буде застосувати пізніше.


Мені подобається ідея цього, але, схоже, він переніс увесь код до класу контролера, залишивши мене з тим же питанням.
user441521

@ user441521 Щойно зрозумів, що є додатковий абзац, який я збирався додати, але втратив його, коли мій браузер вийшов з ладу. Додам зараз. В основному, у вас можуть бути різні контролери, можна складати їх у сукупні контролери, тому кожен контролер обробляє різні речі. наприклад, AggregateController.Controllers = {JumpController (keybinds), MoveController (keybinds), InventoryUIController (keybinds, uisystem)}
Pharap
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.