вторник, 31 августа 2010 г.

Реализация шаблона MVC на примере игры типа Packman – Часть 2

В первой статье мы рассмотрели понятие паттернов проектирования и определились с заданием, которое нужно будет реализовать. В этой статье я приведу более подробное описание шаблона MVC и уже начну программирование базовых объектов игры типа Packman.

Шаблон Model-View-Controller

Шаблон Model-View-Controller (в переводе на русский Модель – Представление– Контроллер) представляет собой способ написания программы, при котором вся программа делится на отдельные независимые элементы, которые работают вне зависимости от состояния соседнего элемента, но работают совместно. Как это может быть, спросите Вы, ведь невозможно написать программу, чтобы события не влияли одно на другое, ведь в программе все взаимосвязано, и если нужно что-то изменить, то необходимо переписывать огромное количество программного кода. Именно чтобы этого не делать и была придумана структура паттерн MVC.

Если обратиться к Википедии то мы увидим следующее описание паттерна.Шаблон MVC позволяет разделить данные, представление и обработку действий пользователя на три отдельных компонента:

· Модель (Model). Модель предоставляет данные (обычно для View), а также реагирует на запросы (обычно от контроллера), изменяя своё состояние.

· Представление (View). Отвечает за отображение информации (пользовательский интерфейс).

· Поведение (Controller). Интерпретирует данные, введённые пользователем, и информирует модель и представление о необходимости соответствующей реакции.

Важно отметить, что как представление, так и поведение зависят от модели. Однако модель не зависит ни от представления, ни от поведения. Это одно из ключевых достоинств подобного разделения. Оно позволяет строить модель независимо от визуального представления, а также создавать несколько различных представлений для одной модели. Стандартная схема архитектуры «Модель-Вид-Контроллер» изображена на следующем рисунке: (схема заимствована из книги «Ajax in action» издательского дома «Вильямс»)

clip_image002

С теорией разобрались. Теперь давайте начнём уже проектирование и разработку нашей системы.

Проектирование и программирование моделей игры

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

Начнём с описания моделей приложения. У меня в приложении есть несколько сущностей, таких как яблоко, танк, стена и сам пакмэн. Напишем для начала базовый класс для всех этих объектов приложения.

В конструкторе базового класса объектов определяется позиция объекта на карте рандомно, либо передавая координаты параметрами. Далее в классе я определяю два события PositionChanged и ReplaceNeeded, которые сообщают представлению об изменении позиции объекта и о необходимости перемещения объекта в другую позицию экрана. Также в классе реализован обработчик события пересечения объектов и соответствующие необходимые функции, которые определяют, пересекаются ли объекты и вызывает функцию ухода от препятствия.

using System;

using System.Collections.Generic;

using System.Text;

using System.Drawing;

using System.ComponentModel;

using PackMan.MVC;

namespace PackMan

{

/// <summary>

/// Класс статического объекта

/// </summary>

public class MapObject

{

/// <summary>

/// Координаты объекта

/// </summary>

protected Point position;

/// <summary>

/// Свойство координат

/// </summary>

public Point Position

{

get { return position; }

set

{

position = value;

OnPositionChanged();

}

}

/// <summary>

/// Ширина объекта

/// </summary>

private const int width = 25;

/// <summary>

/// Длина объекта

/// </summary>

private const int height = 25;

/// <summary>

/// Свойство длины объекта

/// </summary>

public int Width

{

get { return width; }

}

/// <summary>

/// Свойство ширины объекта

/// </summary>

public int Height

{

get { return height; }

}

/// <summary>

/// Размеры поля

/// </summary>

private Point mapSize;

/// <summary>

/// Свойство размеров поля

/// </summary>

public Point MapSize

{

get { return mapSize; }

set { mapSize = value; }

}

/// <summary>

/// Конструктор

/// </summary>

public MapObject()

{

this.position = new Point(GameForm.rand.Next(0, GameForm.MAXX - Width - 2), GameForm.rand.Next(0, GameForm.MAXY - Height - 2));

}

/// <summary>

/// Конструктор

/// </summary>

/// <param name="position">Координаты</param>

public MapObject(Point position)

{

this.position = position;

}

/// <summary>

/// Событие перемещения

/// </summary>

public event EventHandler ReplaceNeeded;

/// <summary>

/// Обработчик события перемещения

/// </summary>

public void OnReplaceNeeded()

{

if (ReplaceNeeded != null)

ReplaceNeeded(this, new EventArgs());

}

/// <summary>

/// Проверка лежит ли точка в области объекта this

/// </summary>

/// <param name="p">координаты объекта</param>

/// <returns>true-пересекаются

/// false-не пересекаются</returns>

protected bool CheckCrossing(Point p)

{

if (this.Position.X + this.Width >= p.X && this.Position.X <= p.X)

{

if (this.Position.Y + this.Height >= p.Y && this.Position.Y <= p.Y)

{

return true;

}

}

return false;

}

/// <summary>

/// Проверка на пересечение двух объектов

/// </summary>

/// <param name="rect"></param>

/// <returns></returns>

public bool CollidesWith(Rectangle rect)

{

if (CheckCrossing(new Point(rect.Left, rect.Top)) ||

CheckCrossing(new Point(rect.Right, rect.Top)) ||

CheckCrossing(new Point(rect.Right, rect.Bottom)) ||

CheckCrossing(new Point(rect.Left, rect.Bottom)))

return true;

return false;

}

/// <summary>

/// Обработчик события пересечения объектов

/// </summary>

/// <param name="sender"></param>

/// <param name="e"></param>

public virtual void OnCheckPosition(object sender, EventArgs e)

{

PositionChangedEventArgs positionArgs = e as PositionChangedEventArgs;

if (positionArgs == null)

return;

if (CollidesWith(positionArgs.NewRectangle))

{

((DynamicMapObject)sender).Deviate();

}

}

/// <summary>

/// Событие сменились координаты

/// </summary>

public event EventHandler PositionChanged;

/// <summary>

/// Обработчик события сменились координаты

/// </summary>

/// <param name="position"></param>

protected virtual void OnPositionChanged()

{

if (PositionChanged != null)

PositionChanged(this, new EventArgs());

}

}

}

В следующем классе я опишу поведение уже не статического объекта а движущегося, назову класс . DynamicMapObject, который отнаследую от только что написанного класса MapObject. У класса DynamicMapObject уже есть спецальные переменные, отвечающие за перемещение объекта, такие как dx, dy, delta, и за его направление (directionNow). Также в классе описаны основные методы движения, поворота и ухода от препятствий и самый главный метод который запускает движение объекта это метод Run, который каждые проверяет позицию объекта на столкновение с другими объектами и перемещает его далее каждые 50 милисекунд (Thread.Sleep(50)). Посмотрим как всё это выглядит.

using System;

using System.Collections.Generic;

using System.Text;

using MVC;

using PackMan.MVC;

using System.Drawing;

using System.ComponentModel;

using System.Windows.Forms;

using PackMan.Properties;

using System.Threading;

namespace PackMan

{

public class DynamicMapObject : MapObject

{

int dy;

int dx;

bool flag = false;

protected int delta;

private int directionNow;

public int DirectionNow

{

get { return directionNow; }

}

public DynamicMapObject() : base()

{

}

public DynamicMapObject(Point position) : base (position)

{

delta = 1;

dy = 0;

dx = delta;

Turn();

}

virtual public void Move()

{

if (position.X + dx >= 0 && position.X + this.Width + dx < MapSize.X)

position.X += dx;

else

Deviate();

if (position.Y + dy >= 0 && position.Y + this.Height + dy < MapSize.Y)

position.Y += dy;

else

{

Deviate();

}

flag = true;

OnPositionChanged();

Thread.Sleep(50);

}

public void Stop()

{

dx = 0;

dy = 0;

}

public void Deviate()

{

if (flag == true)

{

dx = -dx;

dy = -dy;

switch (directionNow)

{

case (int)Direction.Left:

{

directionNow = (int)Direction.Right;

break;

}

case (int)Direction.Right:

{

directionNow = (int)Direction.Left;

break;

}

case (int)Direction.Up:

{

directionNow = (int)Direction.Down;

break;

}

case (int)Direction.Down:

{

directionNow = (int)Direction.Up;

break;

}

}

flag = false;

}

}

public void Turn()

{

IdentifyDirection(GameForm.rand.Next(0, 4));

}

public void IdentifyDirection(int direction)

{

switch (direction)

{

case (int)Direction.Down:

{

dy = delta;

dx = 0;

directionNow = (int)Direction.Down;

break;

}

case (int)Direction.Left:

{

dy = 0;

dx = -delta;

directionNow = (int)Direction.Left;

break;

}

case (int)Direction.Right:

{

dy = 0;

dx = delta;

directionNow = (int)Direction.Right;

break;

}

case (int)Direction.Up:

{

dy = -delta;

dx = 0;

directionNow = (int)Direction.Up;

break;

}

default:

break;

}

}

public virtual void Run()

{

while (true)

{

if (GameForm.rand.Next(0, 50) == 1)

Turn();

OnCheck();

Move();

}

}

public event EventHandler CheckPosition;

protected virtual void OnCheck()

{

if (CheckPosition != null)

CheckPosition(this, new PositionChangedEventArgs( new Rectangle(this.position.X + dx, this.position.Y + dy, this.Width, this.Height)));

}

public override void OnCheckPosition(object sender, EventArgs e)

{

PositionChangedEventArgs positionArgs = e as PositionChangedEventArgs;

if (positionArgs == null)

return;

if (CollidesWith(positionArgs.NewRectangle))

{

if (sender is Tank)

{

this.Deviate();

((DynamicMapObject) sender).Deviate();

}

if (sender is PackMan)

{

(sender as PackMan).Die();

}

}

}

}

}

А теперь уже запрограммируем самого пакмена, который является двигающимся объектом, следовательно, будет отнаследован от класса DynamicMapObject. Класс включает в себя количество жизней пакмена, обработчик события смены координат и метод Die(), который вызывается при столкновении пакмена с танком. В данной реализации, для того чтобы играть вечно (и потому что мне очень жалко пакмена), при столкновении с танком наш главный герой просто перемещается в другую точку карты, а не умирает как многие бы могли подумать.

namespace PackMan

{

/// <summary>

/// Класс пакмена

/// </summary>

public class PackMan:DynamicMapObject

{

/// <summary>

/// Количество жизней

/// </summary>

private int life = 3;

/// <summary>

/// Метод запуска

/// </summary>

public override void Run()

{

while (true)

{

OnCheck();

Move();

}

}

/// <summary>

/// Конструктор

/// </summary>

public PackMan() : base()

{

delta = 2;

}

/// <summary>

/// Конструктор

/// </summary>

/// <param name="position">Координаты</param>

public PackMan(Point position) : base (position)

{

delta = 2;

}

/// <summary>

/// Смерть Пакмена.

/// </summary>

public void Die()

{

// viewGame.Model.Dispose();

OnReplaceNeeded();

}

/// <summary>

/// Обработчик события смена координат

/// </summary>

/// <param name="sender"></param>

/// <param name="e"></param>

public override void OnCheckPosition(object sender, EventArgs e)

{

PositionChangedEventArgs positionArgs = e as PositionChangedEventArgs;

if (positionArgs == null)

return;

if (CollidesWith(positionArgs.NewRectangle))

{

if (sender is Tank)

Die();

}

}

}

}

Модели танка и стены ничем не превосходят базовый класс, описывающий динамический и статический объект соответственно. Поэтому это классы просто отнаследованные от DynamicMapObject и от MapObject.

/// <summary>

/// Класс танка

/// </summary>

public class Tank:DynamicMapObject

{

#region Конструкторы

/// <summary>

/// Конструктор

/// </summary>

public Tank()

{

}

/// <summary>

/// Конструктор

/// </summary>

/// <param name="position">Координаты стены</param>

public Tank(Point position) : base(position)

{

}

#endregion

}

/// <summary>

/// Класс стены

/// </summary>

public class Wall:MapObject

{

#region Конструкторы

/// <summary>

/// Конструктор

/// </summary>

/// <param name="position">Координаты стены</param>

public Wall(Point position) : base(position)

{

}

/// <summary>

/// Конструктор

/// </summary>

public Wall()

{

}

#endregion

}

}

А вот класс яблока немного отличается по функциональности от своего базового, поэтому в нём переопределён обработчик события проверки координат. Если яблоко пересекается с пакмэном то пакмену зачисляется 10 очков на счёт, а яблоко перемещается в другое место.

/// <summary>

/// Класс яблока

/// </summary>

public class Apple:MapObject

{

/// <summary>

/// Конструктор

/// </summary>

/// <param name="position">Координаты</param>

public Apple(Point position) : base(position)

{

}

/// <summary>

/// Конструктор

/// </summary>

public Apple()

{

}

/// <summary>

/// Перемещает яблоко при съедании

/// </summary>

private void Replace()

{

OnReplaceNeeded();

}

/// <summary>

/// Обработчик события проверки координат

/// </summary>

/// <param name="sender">Объект,сгенеривщий событие</param>

/// <param name="e">Аргументы события</param>

public override void OnCheckPosition(object sender, EventArgs e)

{

PositionChangedEventArgs positionArgs = e as PositionChangedEventArgs;

if (positionArgs == null)

return;

if (CollidesWith(positionArgs.NewRectangle))

{

if (sender is PackMan)

{

Replace();

Game.Score += 10;

}

}

}

}

В следующей статье я запрограммирую представления описанных сегодня сущностей.

1 комментарий:

  1. ну код то можно было и отформатировать и подсветить

    ОтветитьУдалить