Cultorios MVC приклад


Всім привіт. При створенні гри Cultorios на Unity ми мали досвід створення проектів середнього розміру ( Anoxemia, Skyship Aurora ) і завжди стикалися з тим, що Unity пропонує писати ігри за допомогою архітектури, що не масштабується.

Стиль за яким купа скриптів Behaviour лежить на об'єктах підходить для маленького проекту, проте вже у середньому виникають наступні проблеми:

- Орієнтація в коді ( знайти щось важко )
- Послідовність виконання( для скриптів її можна налаштувати, але якщо треба виконати Start, Awake в винятковому порядку виникає проблема )
- Зміна керування ( при розробці часто доводиться змінювати керування і якщо ви змішуєте його з логікою, то зміни можуть створити нові баги )
- Втрата посилань ( якщо Behaviour посилається на якісь префаби, то при зміні імен змінної виникають втрати посилань, які важко відстежити в формі null ref exception на пізніших етапах )
- Реюзабельність ( змішаний код важче реюзать в інших проектах )
- Відсутність архітектури

В такому випадку вихід простйй: використовувати MVC:

View  - в нас вже є - це сам GameObject з спрайтами або текстурами, анімацієй, й всім що відповідає за зовнішній вигляд.

Model - будемо реалізовувати в формі власних класів не успадкованих від MonoBehavior, проте маючих схожу архітектуру - зі своїм Awake, Start та Update. При цьому Model замінить нам Behaviour на об'єкті

Controller - реалізуємо в формі Model, котра абстрагує керування від платформи до реальних дій й будемо направляти вже абстраговане керування в моделі. Це надасть нам можливість в грі змінити керування на будь яке  - тач чи Wii контролер не чіпаючи логіку.

Де це все робити ? В якості точки входу нам все же знадобиться один Monobehavior. Нехай він лежить на Root й має в собі моделі ( в тому ж і числі  контролер ). Маючи Root в кожній сцені забезпечмо єдину точку входу.

Розберемо мінімальний приклад. є персонаж й при натисканні вліво-вправо він рухається відповідно:

1. Як рекомендує Unity:
ви робити один скрипт ( наприклад Hero, який успадковується від Monobehaviour ) в котрому оброблюєте натискання клавіш й рухаєте персонажа вліво-вправо, вішаєте скрипт на героя. Це все.

public class HeroNoMVC : MonoBehaviour {

void Update ()
{

  float move = Input.GetAxis("Horizontal");
  transform.position += new Vector3( move * Time.deltaTime, 0f , 0f );
 }
}


2. Як робимо ми:

Клас Root буде вхідною точкою для сцени. В ньому будуть знаходитись всі моделі. Контролер буде представлений в формі моделі. Логіка героя  в моделі Hero, яка пов'язана з Gameobject зі сцени. Обмінюватися інформацією моделі будуть через події, які розсилає всім моделям Root.



- Створимо скрипт - базова модель. Це дуже простий клас, який реалізує найпопулярніші методи MonoBehaviour, проте не успадковується від нього, що забезпечує незалежність логіки від рушія, чи то платформи, відсутність необхідності класти скрипт на об'єкт.

public class ModelBase {

public virtual void Start(){

}

public virtual void Awake (){

}

public virtual void Update(){

}
}

- Створимо дочірній клас ModelGameObject, який розширює функціонал ModelBase, дозволяючи зв'язати модель з GameObject ( чи то Model з View ). У ньому потрібно реалізувати пошук об'єкта в сцені за ім'ям ( ми використовували власний метод пошуку який знаходить сховані GameObject, проте для спрощення прикладу візьмемо дефолтну Unity функцію ):

using UnityEngine;
using System.Collections;

public class ModelGameObject : ModelBase{

public GameObject gameObject;
public string name;

public ModelGameObject(){
name = "GameObject";
}

public ModelGameObject( string goName ){
name = goName;
}

public override void Awake () {
base.Awake();
gameObject = GameObject.Find( name );
}

#region props
public Transform transform
{
get
{
return gameObject.transform;
}
}

public Vector3 position
{
get
{
return transform.position;
}
set
{
transform.position = value;
}
}
#endregion
}

Клас реалізує проперті за якими можна отримати доступ до GameObject та Position

- Нарешті робимо клас героя, в якому реалізуємо логіку переміщення пізніше:

using UnityEngine;
using System.Collections;

public class Hero : ModelGameObject {

public Hero(string goName) : base ( goName ){

}
}

З героєм ми закінчили. Давайте зробимо Controller. Він також буде успадковуватися, але не від ModelGameObject, а від ModelBase бо він є чимось абстрактним і не зв'язаним з GameObject

public class Controller : ModelBase {

public override void Update (){
base.Update ();

float h = Input.GetAxis("Horizontal");
if ( Mathf.Abs( h ) != 0f )
{
//Broadcast event
}
}
}

Ми поки що не розіслали event, зробимо механізм для цього пізніше в Root та моделі.

Наш найпопулярніший клас в сцені - Root. Він повинен створювати моделі, перерасприділяти Monobehavior та наші подіі до кастомних моделей..

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class Root : MonoBehaviour {

private List<ModelBase> mModels;

void Awake(){

mModels = new List<ModelBase>();

mModels.Add( new Hero("Hero") );
mModels.Add( new Controller() );

foreach( ModelBase mb in mModels )
{
mb.Awake();
}

}

void Start () {

foreach( ModelBase mb in mModels )
{
mb.Start();
}
}
void Update () {

foreach( ModelBase mb in mModels )
{
mb.Update();
}
}
}

Ми ще не зробили механізм переадресаціі подій, бо тут якщо ви знайомі з подіями та делегатами (events ) в C# краще робити через подій, ми знову ж таки для спрощення зробимо методами.

добавимо метод до класу Root, який дозволить це зробити.

public void ForwardEvent( EEvents ev, object data ){

foreach( ModelBase mb in mModels )
{
mb.ForwardEvent( ev, data );
}
}, 

тоді до класу ModelBase добавимо ресівер цих подій

public virtual void ForwardEvent ( EEvents ev, object data ){

}

ну і оскільки ми передаємо і оброблюємо enum ( для пришвидшення оскільки Input дуже часто приходить до моделей ), то додаймо enum який опише всі подіі, які ми передаємо

public enum EEvents { Move };

залишилося лише обробити цю подію в Hero:

public override void ForwardEvent (EEvents ev, object data){

base.ForwardEvent (ev, data);

if ( ev == EEvents.Move )
{
float hor = (float) data;
position += new Vector3( hor, 0f, 0f );
}

}

Це все. Залишаеться створити GameObject з ім'ям Root в сцені. Кинути на нього скрипт Root. Та створити GameObject з ім'ям Hero, який буде переміщуватися в сцені.

Отже, що ми маємо:

Плюси:

- Незалежний Controller, який можна міняти згідно з платформою, чи то якщо гра підтримує кілько типів контролю ( Джойстик, миша чи клавіатуру ). А якщо гра на тач, то віртуальний стік, або мобільний контролер. При цьому логіка залишається незмінною
- Єдина точка входу, що дозволяє керувати послідовністю виконання коду, та його впорядковувати
- Система, що дозволяє редагувати View незалежно від Model, не втрачаючи посилання. Але при цьому звісно краще все інстанціювати з Resources
- Незалежність логіки від рушія ( при переносі коду наприклад на CryEngine чи ОSG, змінюєте лише базові класи )

Мінуси:

- Громіздкість. Так все це більше ніж один скрипт, але врешті базові класи написані лише раз, а використовуються багато разів
- Швидкість втрачається на обробці подій,
кастуванні типів та на успадкуванні, але порівняно з графічним навантаженням CPU в Unity ( drawcall, тіні, фізики ) є незначним


Комментариев нет:

Отправить комментарий