Добавляем MVP в игры на Unity3D

в 7:38, , рубрики: C#, game development, unity3d, unity3d; game development; mvp; c#, метки:

image Всем доброго времени суток. В данной статье хотел бы рассказать о том как можно применить шаблон MVP в процессе разработки игр на платформе Unity3D. Использование этого шаблона может способствовать упорядочению кода и улучшению структуры проекта. Стоит сразу отметить, что в статье не дается детального описания самого шаблона, а предполагается наличие у читателя базовых знаний о нем.

Как мы все с Вами знаем, MVP — это шаблон призванный отделить презентационную логику от логики приложения. В случае Unity3D, представлением может быть GameObject с набором прикрепленных к нему компонентов, необходимых для реализации презентационной логики ( в том числе и компонент самой презентационной логики — MonoBehaviour имплементирующий соответствующий интерфейс представления (View)).

В роли презентера (Presenter) может выступать любой тип .NET, реализующий логику определённой части приложения и взаимодействующий с остальными его частями, такими как модели, сервисы и т.д.

Переходим к практике

Итак, предположим перед, нами стоит задача создать очень простую игру в которой игроку представлен не самый широкий арсенал возможных действий: все что он может делать — кликать на изображении цветного прямоугольника. В ответ на действия игрока прямоугольник может прыгать, переворачиваться и изменять свой цвет на произвольный, в зависимости от логики игры. Это очень простой пример, но его вполне достаточно для того чтобы понять каким образом можно применять шаблон MVP при разработке игр на Unity3D.

Давайте взглянем на возможный вариант организации проекта приведенный на следующем рисунке:

image

Обратите внимание на скрипт FunnyRectView в списке компонентов объекта FunnyRect. Этот скрипт — реализация презентационной функциональности. Тип FunnyRectView имплементирует интерфейс IFunnyRectView, а так же, этот интерфейс активно используется презентером для взаимодействия с представлением.

Ниже приведен код FunnyRectView.

public class FunnyRectView : MonoBehaviour, IFunnyRectView 
{     
	private readonly IFunnyRectPresenter _presenter;
	
	public FunnyRectView()
	{
		_presenter = new FunnyRectPresenter(this);
	}
	
	public void Awake()
	{
		_presenter.Initialize();
	}
}

Связка представления с презентером происходит в конструкторе. Презентер, в качестве параметра конструктора принимает ссылку типа IFunnyRectView. Этот интерфейс и служит тем мостом который соединяет представление с презентером. Он позволяет вызывать методы определенные в FunnyRectView, подписываться и реагировать на его события. Представление так же имеет возможность взаимодействовать с презентером через сохраненную в _presenter ссылку.

Давайте добавим необходимую для нормального функционирования логику в FunnyRectView.

public class FunnyRectView : MonoBehaviour, IFunnyRectView
{
	private readonly IFunnyRectPresenter _presenter;
	private bool _isInputEnabled;
	private Animator _animator;
	private const string JumpAnimationTriggerName = "JumpTrigger";
	private const string RotateAnimationTriggerName = "RotateTrigger";
	public FunnyRectView()
	{
		_presenter = new FunnyRectPresenter(this);
	}
	public void Awake()
	{
		_animator = gameObject.GetComponent<Animator>();
		_presenter.Initialize();
	}
	public void OnDestroy()
	{
		_presenter.Uninitialize();
	}
	public event EventHandler RotationEnd;
	public event EventHandler JumpEnd;
	public event EventHandler Clicked;
	public void OnMouseDown()
	{
		if (!_isInputEnabled) return;
		Clicked(this, EventArgs.Empty);
	}
	public void NotifyRotationEnded()     
	{
		RotationEnd(this, EventArgs.Empty);
	}
	public void NotifyJumpEnded()     
	{
		JumpEnd(this, EventArgs.Empty);
	}
	public void DisableInput()     
	{
		_isInputEnabled = false;
	}
	public void EnableInput()     
	{
		_isInputEnabled = true;
	}
	public void Rotate()     
	{
		_animator.SetTrigger(RotateAnimationTriggerName);
	}
	public void Jump()
	{
		_animator.SetTrigger(JumpAnimationTriggerName);     
	}
	public void ChangeColor(Color color)     
	{         
		var spriteRenderer = gameObject.GetComponent<SpriteRenderer>();
		spriteRenderer.color = color;
		_isInputEnabled = true;
	}
}

В методе Awake, получаем ссылку на компонент аниматора. Он необходим для воспроизведения анимации прыжка и переворота прямоугольника. Событие Clicked необходимо для уведомления презентера о том что игрок кликнул на прямоугольнике. Это происходит в OnMousedDown (на прямоугольнике присутствует коллайдер). Методы NotifyRotationEnded и NotifyJumpEnded вызываются аниматором, в тот момент когда заканчивается анимация прыжка или переворота и генерируют события JumpEnd и RotationEnd соответственно. Все остальные методы вызываются из презентера.

public class FunnyRectPresenter : IFunnyRectPresenter
{
	private readonly IFunnyRectView _view;
	private const int FunnyRectRotateActionCode = 0;
	private const int FunnyRectJumpActionCode = 1;
	private const int FunnyRectChangeColorActionCode = 2;
	public FunnyRectPresenter(IFunnyRectView view)
	{         
		_view = view;
	}
	private void OnRectClicked(object sender, EventArgs e)
	{
		_view.DisableInput();
		var action = GenerateRandomAction();
		switch (action)
		{
			case FunnyRectRotateActionCode:
				_view.Rotate();
				break;
			case FunnyRectJumpActionCode:
				_view.Jump();
				break;
			case FunnyRectChangeColorActionCode:
				var color = GenerateRandomColor();
				_view.ChangeColor(color);
				break;
		}
	}
	private void OnRotationEnd(object sender, EventArgs e)
	{
		_view.EnableInput();
	}
	private void OnJumpEnd(object sender, EventArgs e)
	{
		_view.EnableInput();
	}
	private Color GenerateRandomColor()
	{
		var random = new Random();
		var c = Color.white;
		c.r = (float)random.NextDouble();
		c.g = (float)random.NextDouble();
		c.b = (float)random.NextDouble();
		return c;
	}
	private int GenerateRandomAction()
	{
		var random = new Random();
		return random.Next(FunnyRectRotateActionCode, FunnyRectChangeColorActionCode+1);
	}
	public void Initialize()
	{
		_view.Clicked += OnRectClicked;
		_view.RotationEnd += OnRotationEnd;
		_view.JumpEnd += OnJumpEnd;
		_view.EnableInput();
	}
	public void Uninitialize()
	{
		_view.Clicked -= OnRectClicked;
		_view.RotationEnd -= OnRotationEnd;
		_view.JumpEnd -= OnJumpEnd;
	}
}

Думаю, что этот код достаточно прост и понятен. В Initialize происходит подписка на события генерируемые в представлении, в Uninitialize отписка от них. В конструкторе сохраняем ссылку на представление. Метод OnRectClicked вызывается каждый раз когда игрок кликает по прямоугольнику. В нем происходит генерация случайного действия и вызывается соответствующий ему метод представления. Осталось взглянуть на код IFunnyRectView и IFunnyRectPresenter.

IFunnyRectView:

public interface IFunnyRectView 
{     
	event EventHandler RotationEnd;
	event EventHandler JumpEnd;
	event EventHandler Clicked;
	
	void EnableInput();
	void DisableInput();
	void Rotate();
	void Jump();
	void ChangeColor(Color color);
}

IFunnyRectPresenter:

public interface IFunnyRectPresenter
{
	void Initialize();
	void Uninitialize();
} 

Применение MVP дало нам возможность в последующем изменять презентационную логику по нашему усмотрению не оказывая влияния на логику приложения. Так же необходимо заметить, что исходя из концепций MVP, необходимо избегать прямого вызова методов и установки свойств презентера из представления. Вместо этого его нужно уведомлять о наступлении тех или иных событий.

Полный исходный код с коментариями можно взять на github.com/rumyancevpavel/FunnyRect.

Автор: rumyancevpavel

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js