Програмистите за първи път обикновено започват да учат занаята с класическата Hello World
програма. Оттам нататък непременно ще последват все по-големи и по-големи задачи. Всяко ново предизвикателство носи у дома важен урок:
Колкото по-голям е проектът, толкова по-големи са спагетите.
Скоро е лесно да се види, че в големи или малки екипи човек не може безразсъдно да прави каквото си иска. Кодът трябва да се поддържа и може да продължи дълго време. Компаниите, в които сте работили, не могат просто да търсят данните ви за контакт и да ви питат всеки път, когато искат да поправят или подобрят кодовата база (а вие не искате и те).
Това е причината модели на софтуерно проектиране съществуват; те налагат прости правила, които да диктуват цялостната структура на софтуерен проект. Те помагат на един или повече програмисти да отделят основни части от голям проект и да ги организират по стандартизиран начин, като елиминират объркването, когато се срещне някаква непозната част от кодовата база.
Тези правила, когато се спазват от всички, позволяват по-добре да се поддържа и навигира наследения код и да се добавя по-бързо нов код. По-малко време се отделя за планиране на методологията на разработване. Тъй като проблемите не идват в един вкус, няма модел на сребърен куршум. Човек трябва внимателно да обмисли силните и слабите страни на всеки модел и да намери най-подходящото за предизвикателството.
В този урок ще свържа моя опит с популярното Unity платформа за разработка на игри и моделът на модел-изглед-контролер (MVC) за разработване на игри. През седемте си години на разработка, след като се борих с моя справедлив дял от спагети за разработчици на игри, постигнах страхотна структура на кода и скорост на разработка, използвайки този модел на проектиране.
Ще започна, като обясня малко от основната архитектура на Unity, модела на Entity-Component. След това ще продължа, за да обясня как MVC се вписва отгоре му, и ще използвам малък фалшив проект като пример.
python масив от екземпляри на клас
В литературата за софтуера ще открием голям брой дизайнерски модели. Въпреки че имат набор от правила, разработчиците обикновено правят малко огъване на правила, за да адаптират по-добре модела към специфичния си проблем.
Тази „свобода на програмиране“ е доказателство, че все още не сме намерили нито един окончателен метод за проектиране на софтуер. По този начин тази статия не е предназначена да бъде най-доброто решение на проблема ви, а по-скоро да покаже предимствата и възможностите на два добре познати модела: Entity-Component и Model-View-Controller.
Entity-Component (EC) е модел на проектиране, при който първо дефинираме йерархията на елементите, съставляващи приложението (Entities), а по-късно дефинираме характеристиките и данните, които всеки ще съдържа (Components). По-точно „програмист“, Обект може да бъде обект с масив от 0 или повече Компоненти. Нека изобразим Обект като този:
some-entity [component0, component1, ...]
Ето един прост пример за EC дърво.
- app [Application] - game [Game] - player [KeyboardInput, Renderer] - enemies - spider [SpiderAI, Renderer] - ogre [OgreAI, Renderer] - ui [UI] - hud [HUD, MouseInput, Renderer] - pause-menu [PauseMenu, MouseInput, Renderer] - victory-modal [VictoryModal, MouseInput, Renderer] - defeat-modal [DefeatModal, MouseInput, Renderer]
EC е добър модел за облекчаване на проблемите на множественото наследяване, където сложна структура на класа може да доведе до проблеми като диамантен проблем където клас D, наследяващ два класа, B и C, с един и същ основен клас A, може да въведе конфликти, защото как B и C модифицират характеристиките на A по различен начин.
Този тип проблеми могат да бъдат често срещани при разработването на игри, където наследяването често се използва широко.
Чрез разбиване на функциите и обработващите данни на по-малки Компоненти, те могат да бъдат прикачени и използвани повторно в различни Обекти, без да се разчита на множество наследства (което между другото дори не е функция на C # или Javascript, основните езици, използвани от Unity ).
Като едно ниво над OOP, EC помага за дефрагментиране и по-добра организация на вашата архитектура на кода. В големите проекти обаче ние все още сме „твърде свободни“ и можем да се озовем в „океан на характеристиките“, трудно да намерим правилните обекти и компоненти или да разберем как трябва да си взаимодействат. Има безкрайни начини за сглобяване на обекти и компоненти за дадена задача.
Един от начините да се избегне бъркотията е да се наложат някои допълнителни насоки върху Entity-Component. Например един начин, по който обичам да мисля за софтуера, е да го разделя на три различни категории:
За щастие вече имаме модел, който се държи точно по този начин.
The Модел модел на изглед-контролер (MVC) разделя софтуера на три основни компонента: модели (Data CRUD), изгледи (интерфейс / откриване) и контролери (решение / действие). MVC е достатъчно гъвкав, за да бъде приложен дори върху ECS или OOP.
Разработването на игри и потребителски интерфейс има обичайния работен процес на изчакване за въвеждане от потребителя или друго задействащо условие, изпращане на известие за тези събития някъде подходящо, вземане на решение какво да се направи в отговор и актуализиране на данните съответно. Тези действия ясно показват съвместимостта на тези приложения с MVC.
Тази методология въвежда още един абстракционен слой, който ще помогне при софтуерното планиране, а също така ще позволи на новите програмисти да се ориентират дори в по-голяма кодова база. Чрез разделяне на процеса на мислене на данни, интерфейс и решения, разработчиците могат да намалят броя на изходните файлове, които трябва да бъдат търсени, за да добавят или поправят функционалност.
Нека първо разгледаме отблизо какво ни дава Unity отпред.
Unity е базирана на ЕО платформа за развитие, където всички субекти са екземпляри на GameObject
и функциите, които ги правят „видими“, „подвижни“, „взаимодействащи“ и т.н., се предоставят от класове, разширяващи се Component
Редакторът на Unity Йерархичен панел и Инспекторски панел предоставят мощен начин за сглобяване на вашето приложение, прикачване на компоненти, конфигуриране на първоначалното им състояние и стартиране на играта ви с много по-малко изходен код, отколкото обикновено.
Йерархичен панел с четири GameObjects вдясно
Инспекторски панел с компоненти на GameObject
И все пак, както вече обсъждахме, можем да ударим проблема „твърде много функции“ и да се озовем в гигантска йерархия, с функции, разпръснати навсякъде, което прави живота на разработчика много по-труден.
Мислейки по MVC начин, вместо това можем да започнем с разделяне на нещата според тяхната функция, като структурираме нашето приложение като примера по-долу:
Сега бих искал да представя две малки модификации на общия модел на MVC, които помагат да се адаптира към уникалните ситуации, които съм срещал при изграждането на проекти на Unity с MVC:
GetComponent( ... )
- Адът на загубените препратки ще настъпи, ако Unity се срине или някоя грешка накара всички изтеглени препратки да изчезнат. - Това налага наличието на единичен коренен обект, чрез който всички екземпляри в Приложение може да бъде достигнато и възстановено.Rotator
Компонент, който само върти нещата с дадена ъглова скорост и не уведомява, съхранява или решава нищо.За да помогна за облекчаване на тези два проблема, измислих модифициран модел, който наричам AMVCC , или Application-Model-View-Controller-Component.
Тези две модификации задоволиха нуждите ми за всички проекти, в които съм ги използвал.
Като прост пример, нека разгледаме малка игра, наречена 10 отскока , където ще използвам основните елементи на модела AMVCC.
Настройката на играта е проста: A Ball
с SphereCollider
и a Rigidbody
(което ще започне да пада след „Игра“), a Cube
като основа и 5 скрипта за съставяне на AMVCC.
Преди да създавам скриптове, обикновено започвам от йерархията и създавам контур на моя клас и активи. Винаги следвайки този нов AMVCC стил.
Както виждаме, view
GameObject съдържа всички визуални елементи, както и такива с други View
скриптове. model
и controller
GameObjects, за малки проекти, обикновено съдържат само съответните им скриптове. За по-големи проекти те ще съдържат GameObjects с по-специфични скриптове.
Когато някой, който навигира във вашия проект, иска достъп:
application > model > ...
application > controller > ...
application > view > ...
Ако всички екипи спазват тези прости правила, наследствените проекти не трябва да се превръщат в проблем.
Имайте предвид, че няма Component
контейнер, защото, както вече обсъждахме, те са по-гъвкави и могат да бъдат прикрепени към различни елементи в свободното време на разработчика.
Забележка: Скриптовете, показани по-долу, са абстрактни версии на реални реализации. Подробно изпълнение няма да е от полза за читателя много. Ако обаче искате да проучите повече, ето връзката към моята лична MVC рамка за Unity, Unity MVC. Ще намерите основни класове, които прилагат AMVCC структурна рамка, необходима за повечето приложения.
компилиране на C++ код
Нека да разгледаме структурата на скриптовете за 10 отскока .
Преди да започнем, за тези, които не са запознати с работния процес на Unity, нека изясним накратко как скриптовете и GameObjects работят заедно. В Unity „Компонентите“, в смисъла Entity-Component, са представени от MonoBehaviour
клас. За да съществува такъв по време на изпълнение, разработчикът трябва или да плъзне и пусне своя изходен файл в GameObject (който е „Entity“ на модела Entity-Component) или да използва командата AddComponent()
След това скриптът ще бъде създаден и готов за използване по време на изпълнение.
За начало дефинираме клас Application („A“ в AMVCC), който ще бъде основният клас, съдържащ препратки към всички екземпляри на игровите елементи. Също така ще създадем помощен основен клас, наречен Element
, който ни дава достъп до екземпляра на Приложението и екземплярите на MVC на децата му.
Имайки това предвид, нека дефинираме Application
клас („A“ в AMVCC), който ще има уникален екземпляр. В него три променливи, model
, view
и controller
, ще ни дадат точки за достъп за всички екземпляри на MVC по време на изпълнение. Тези променливи трябва да бъдат MonoBehaviour
s с public
препратки към желаните скриптове.
След това ще създадем и помощен основен клас, наречен Element
, който ни дава достъп до екземпляра на Приложението. Този достъп ще позволи на всеки MVC клас да достигне всеки друг.
Имайте предвид, че и двата класа се разширяват MonoBehaviour
. Те са „Компоненти“, които ще бъдат прикачени към GameObject „Entities“.
// BounceApplication.cs // Base class for all elements in this application. public class BounceElement : MonoBehaviour { // Gives access to the application and all instances. public BounceApplication app { get { return GameObject.FindObjectOfType(); }} } // 10 Bounces Entry Point. public class BounceApplication : MonoBehaviour { // Reference to the root instances of the MVC. public BounceModel model; public BounceView view; public BounceController controller; // Init things here void Start() { } }
От BounceElement
можем да създадем основните класове на MVC. BounceModel
, BounceView
И BounceController
скриптовете обикновено действат като контейнери за по-специализирани екземпляри, но тъй като това е прост пример, само View ще има вложена структура. Моделът и контролерът могат да бъдат направени в един скрипт за всеки:
// BounceModel.cs // Contains all data related to the app. public class BounceModel : BounceElement { // Data public int bounces; public int winCondition; }
// BounceView .cs // Contains all views related to the app. public class BounceView : BounceElement { // Reference to the ball public BallView ball; }
// BallView.cs // Describes the Ball view and its features. public class BallView : BounceElement { // Only this is necessary. Physics is doing the rest of work. // Callback called upon collision. void OnCollisionEnter() { app.controller.OnBallGroundHit(); } }
// BounceController.cs // Controls the app workflow. public class BounceController : BounceElement { // Handles the ball hit event public void OnBallGroundHit() { app.model.bounces++; Debug.Log(“Bounce ”+app.model.bounce); if(app.model.bounces >= app.model.winCondition) { app.view.ball.enabled = false; app.view.ball.GetComponent().isKinematic=true; // stops the ball OnGameComplete(); } } // Handles the win condition public void OnGameComplete() { Debug.Log(“Victory!!”); } }
С всички създадени скриптове можем да продължим с прикачването и конфигурирането им.
Йерархичното оформление трябва да бъде такова:
- application [BounceApplication] - model [BounceModel] - controller [BounceController] - view [BounceView] - ... - ball [BallView] - ...
Използване на BounceModel
като пример можем да видим как изглежда в редактора на Unity:
BounceModel
с bounces
и winCondition
полета.
След като всички скриптове са настроени и играта работи, трябва да получим този изход в Конзолен панел .
Както е показано в горния пример, когато топката се удари в земята, нейният изглед изпълнява app.controller.OnBallGroundHit()
което е метод. По никакъв начин не е „погрешно“ да се прави това за всички известия в приложението. Според моя опит обаче постигнах по-добри резултати, използвайки проста система за уведомяване, внедрена в класа на приложението AMVCC.
За да приложим това, нека актуализираме оформлението на BounceApplication
да бъде:
// BounceApplication.cs class BounceApplication { // Iterates all Controllers and delegates the notification data // This method can easily be found because every class is “BounceElement” and has an “app” // instance. public void Notify(string p_event_path, Object p_target, params object[] p_data) { BounceController[] controller_list = GetAllControllers(); foreach(BounceController c in controller_list) { c.OnNotification(p_event_path,p_target,p_data); } } // Fetches all scene Controllers. public BounceController[] GetAllControllers() { /* ... */ } }
След това се нуждаем от нов скрипт, където всички разработчици ще добавят имената на събитието за уведомяване, които могат да бъдат изпратени по време на изпълнението.
// BounceNotifications.cs // This class will give static access to the events strings. class BounceNotification { static public string BallHitGround = “ball.hit.ground”; static public string GameComplete = “game.complete”; /* ... */ static public string GameStart = “game.start”; static public string SceneLoad = “scene.load”; /* ... */ }
Лесно е да се види, че по този начин четливостта на кода се подобрява, тъй като разработчиците не трябва да търсят в целия изходен код за controller.OnSomethingComplexName
методи, за да се разбере какъв вид действия могат да се случат по време на изпълнение. Чрез проверка само на един файл е възможно да се разбере цялостното поведение на приложението.
Сега трябва само да адаптираме BallView
и BounceController
да се справят с тази нова система.
// BallView.cs // Describes the Ball view and its features. public class BallView : BounceElement { // Only this is necessary. Physics is doing the rest of work. // Callback called upon collision. void OnCollisionEnter() { app.Notify(BounceNotification.BallHitGround,this); } }
// BounceController.cs // Controls the app workflow. public class BounceController : BounceElement { // Handles the ball hit event public void OnNotification(string p_event_path,Object p_target,params object[] p_data) { switch(p_event_path) { case BounceNotification.BallHitGround: app.model.bounces++; Debug.Log(“Bounce ”+app.model.bounce); if(app.model.bounces >= app.model.winCondition) { app.view.ball.enabled = false; app.view.ball.GetComponent().isKinematic=true; // stops the ball // Notify itself and other controllers possibly interested in the event app.Notify(BounceNotification.GameComplete,this); } break; case BounceNotification.GameComplete: Debug.Log(“Victory!!”); break; } } }
По-големите проекти ще имат много известия. Така че, за да се избегне получаване на голяма структура на случай на превключване, препоръчително е да създадете различни контролери и да ги накарате да обработват различни обхвати за уведомяване.
Този пример показа прост случай на използване на модела AMVCC. Коригирането на начина ви на мислене по отношение на трите елемента на MVC и научаването да визуализирате обектите като подредена йерархия са уменията, които трябва да бъдат усъвършенствани.
При по-големи проекти разработчиците ще се сблъскат с по-сложни сценарии и съмнения дали нещо трябва да бъде View или Controller, или дали даден клас трябва да бъде по-щателно разделен в по-малки.
Никъде няма „Универсално ръководство за MVC сортиране“. Но има някои прости правила, които обикновено спазвам, за да ми помогнат да определя дали да определя нещо като Модел, Изглед или Контролер, както и кога да разделя даден клас на по-малки парчета.
Обикновено това се случва органично, докато мисля за софтуерната архитектура или по време на скриптове.
Модели
health
или пистолет ammo
.Изгледи
player.Run()
може да използва вътрешно model.speed
за да прояви способностите на играча.PlayerView
не трябва да прилагат откриване на вход или да променят състоянието на играта.Контролери
В този случай няма много стъпки, които следвам. Обикновено възприемам, че някой клас трябва да бъде разделен, когато променливите започват да показват твърде много „префикси“ или започват да се появяват твърде много варианти на един и същ елемент (като Player
класове в MMO или Gun
типове в FPS).
Например единичен Model
съдържащи данните на Player ще имат много playerDataA, playerDataB,...
или a Controller
обработка на известия на Player ще има OnPlayerDidA,OnPlayerDidB,...
. Искаме да намалим размера на скрипта и да се отървем от player
и OnPlayer
представки.
Позволете ми да демонстрирам с помощта на Model
клас, защото е по-лесно да се разбере само с данни.
По време на програмирането обикновено започвам с един Model
клас, съдържащ всички данни за играта.
// Model.cs class Model { public float playerHealth; public int playerLives; public GameObject playerGunPrefabA; public int playerGunAmmoA; public GameObject playerGunPrefabB; public int playerGunAmmoB; // Ops Gun[C D E ...] will appear... /* ... */ public float gameSpeed; public int gameLevel; }
Лесно е да се види, че колкото по-сложна е играта, толкова по-многобройни ще бъдат променливите. С достатъчно сложност бихме могли да получим гигантски клас, съдържащ model.playerABCDFoo
променливи. Влагащите елементи ще опростят завършването на кода и също така ще дадат възможност за превключване между варианти на данни.
плюсове и минуси на корпорациите
// Model.cs class Model { public PlayerModel player; // Container of the Player data. public GameModel game; // Container of the Game data. }
// GameModel.cs class GameModel { public float speed; // Game running speed (influencing the difficulty) public int level; // Current game level/stage loaded }
// PlayerModel.cs class PlayerModel { public float health; // Player health from 0.0 to 1.0. public int lives; // Player “retry” count after he dies. public GunModel[] guns; // Now a Player can have an array of guns to switch ingame. }
// GunModel.cs class GunModel { public GunType type; // Enumeration of Gun types. public GameObject prefab; // Template of the 3D Asset of the weapon. public int ammo; // Current number of bullets public int clips; // Number of reloads possible }
С тази конфигурация на класове разработчиците могат да се ориентират интуитивно в изходния код една по една концепция. Нека приемем игра за стрелба от първо лице, където оръжията и техните конфигурации могат да станат наистина многобройни. Фактът, че GunModel
се съдържа в клас позволява създаването на списък от Prefabs
(предварително конфигурирани GameObjects за бързо дублиране и повторно използване в играта) за всяка категория и съхранявани за по-късна употреба.
За разлика от това, ако информацията за пистолета е била съхранявана заедно в единичната GunModel
клас, в променливи като gun0Ammo
, gun1Ammo
, gun0Clips
и т.н., след това потребителят, когато е изправен пред необходимостта от съхраняване Gun
данни, ще трябва да съхранява цялата Model
включително нежеланото Player
данни. В този случай би било очевидно, че нов GunModel
клас би било по-добре.
Подобряване на йерархията на класовете.
Както при всичко, има две страни на медала. Понякога човек може ненужно да прекалява и да увеличи сложността на кода. Само опитът може да усъвършенства уменията ви достатъчно, за да намерите най-доброто MVC сортиране за вашия проект.
Отключена е нова способност за разработчици на специални възможности: Unity игри с MVC модел. TweetИма много софтуерни модели там. В този пост се опитах да покажа този, който ми помогна най-много в минали проекти. Разработчици винаги трябва да поглъща нови знания, но винаги също ги поставя под съмнение. Надявам се, че този урок ще ви помогне да научите нещо ново и в същото време ще ви послужи като стъпка, докато развивате свой собствен стил.
Също така наистина ви насърчавам да изследвате други модели и да намерите този, който ви подхожда най-добре. Една добра отправна точка е тази статия в Уикипедия , с отличния си списък с модели и техните характеристики.
Ако харесвате модела AMVCC и искате да го тествате, не забравяйте да изпробвате библиотеката ми, Единство MVC , който съдържа всички основни класове, необходими за стартиране на AMVCC приложение.