Показаны сообщения с ярлыком Packman. Показать все сообщения
Показаны сообщения с ярлыком Packman. Показать все сообщения

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

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

Эта статья, заключающая и самая интересная, в ней мы опишем контроллер пакмэна для управления игрой и методы запускающие все наши описанные выше модели сущностей и их представления.

Хотя термин контроллер подразумевает интерфейс, который управляет приложением, в шаблоне MVC, контроллер не содержит элементов пользовательского интерфейса. Контроллер определяет, как представления отвечают на пользовательский ввод.

Хотя у каждого элемента в триаде MVC есть свои обязанности, они не функционируют в изоляции друг от друга. Фактически, чтобы быть шаблоном MVC, каждый элемент должен общаться с одним или более элементами.

Опишем контроллер управляющий объектами нашей игры.

Namespace PackMan

{

/// <summary>

/// Контроллер пакмена

/// </summary>

class PackManController

{

/// <summary>

/// Обработчик нажатия клавиши

/// </summary>

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

/// <param name=”e”>Клавиша,которая нажата</param>

public void OnKeyPress(object sender, KeyEventArgs e)

{

if (sender is PackMan)

{

switch (e.KeyData)

{

case Keys.Up:

{

((PackMan)sender).IdentifyDirection((int)Direction.Up);

break;

}

case Keys.Down:

{

((PackMan)sender).IdentifyDirection((int)Direction.Down);

break;

}

case Keys.Left:

{

((PackMan)sender).IdentifyDirection((int)Direction.Left);

break;

}

case Keys.Right:

{

((PackMan)sender).IdentifyDirection((int)Direction.Right);

break;

}

default:

break;

}

}

}

}

Последнее что осталось реализовать, это метод, создающий модель и представление нашей игры и методы обработки кнопок формы.

public partial class GameForm : Form

{

public static Random rand = new Random();

private readonly ViewGame viewGame;

/// <summary>

/// Количество танков

/// </summary>

public static int countTank = 10;

/// <summary>

/// Количество стен

/// </summary>

public static int countWall = 0;

/// <summary>

/// Количество яблок

/// </summary>

public static int countApples = 4;

/// <summary>

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

/// </summary>

public static int MAXX;

/// <summary>

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

/// </summary>

public static int MAXY;

/// <summary>

/// Массив расстановки стен

/// </summary>

public static string[] map = {

"********************",

"*..................*",

"*..................*",

"*...*************..*",

"*.........*........*",

"*.........*........*",

"*...*..*.....*..*..*",

"*...*..*.....*..*..*",

"*...*..*******..*..*",

"*...*...........*..*",

"*...*...........*..*",

"*...*.....*.....*..*",

"*...*.....*.....*..*",

"*...****..*..****..*",

"*......*..*........*",

"*......*..*........*",

"*.........*..*.....*",

"*.........*..*.....*",

"*......*..*..*.....*",

"********************"

};

public GameForm()

{

InitializeComponent();

MAXX = p_Map.Width;

MAXY = p_Map.Height;

//Расстановка стен по массиву map

for (int i = 0; i < map.Length; i++)

for (int j = 0; j < map[0].Length; j++)

countWall += map[i][j] == '*' ? 1 : 0;

p_Map.Controls.Add(l_points);

Game game = new Game();

viewGame = new ViewGame(p_Map,l_points);

viewGame.SubscribeKeyPress();

viewGame.Model = game;

viewGame.Model.Start();

}

private void button1_Click(object sender, EventArgs e)

{

viewGame.Model.Dispose();

}

private void GameForm_KeyDown(object sender, KeyEventArgs e)

{

viewGame.OnKeyPress(e.KeyCode);

}

private void GameForm_FormClosing(object sender, FormClosingEventArgs e)

{

viewGame.Model.Dispose();

}

}

Теперь мы можем посмотреть, что у нас получилось и насладиться нашей игрой.

clip_image002

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

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

Модель в шаблоне Model-View-Controller

Начнём с программирования модели игры. Модель является «сутью» нашей игры и отвечает за непосредственные алгоритмы, расчёты и тому подобное внутреннее устройство игры. Обычно под Моделью понимается, часть содержащая в себе функциональную логику приложения, иными словами то, что мы обычно называем «Business Layer», «Бизнес-слой» или «Слой бизнес логики». Как именно организован этот слой, по большому счету не важно, однако есть ряд ключевых моментов, на которых мы остановимся позднее. Основная цель паттерна - сделать так, чтобы Модель была полностью независима от остальных частей и практически ничего не знала об их существовании, что позволило бы менять и Контроллер и Представление модели, не трогая саму Модель и даже позволить функционирование нескольких экземпляров Представлений и Контроллеров с одной Моделью одновременно. Вследствие чего, Модель ни при каких условиях не может содержать ссылок на объекты Представления или Контроллера.

Обычно различают несколько типов паттернов в зависимости от роли модели.

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

Active Model (активная модель) - Модель имеет возможность оповестить Представление о том, что в ней произошли некие изменения, и Представление может эти изменения отобразить. Как правило, механизм оповещения реализуется на основе паттерна Observer (обозреватель), Модель просто бросает сообщение, а Представления, которые заинтересованы в оповещении, подписываются на эти сообщения, что позволяет сохранить независимость Модели, как от Контроллера так и от Представления, не нарушая тем самым основного свойства паттерна. Классической реализацией паттерна MVC принято считать версию именно с активной Моделью.

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

Мы будем программировать модель нашей игры как активную модель. Для начала создадим массивы объектов игры, таких как яблоки, танки, стены и объект пакмена. Все классы этих сущностей описаны в статьях ранне. В техническом задании было сказано, что каждый объект должен существовать в своём потоке. Поэтому далее мы создаём для каждого движущегося объекта свой поток. Затем наконец-то пишем самый главный метод Start(). Также нам необходимо написать метод проверки на пересечение объектов, и обработчик события перемещение объекта в заданные координаты, соответственно нам нужно подписать каждый элемент с каждым на пересечение и написать. Размещение стен происходит в методе PlaceWalls() в соответствии с картой, заданной массивом GameForm.map. А теперь посмотрим на сам класс.

namespace PackMan

{

/// <summary>

/// Класс игры.

/// </summary>

class Game

{

#region Объекты елементов и их свойства

/// <summary>

/// Список танков

/// </summary>

List<Tank> arrTank = new List<Tank>();

/// <summary>

/// Список стен

/// </summary>

List<Wall> arrWall = new List<Wall>();

/// <summary>

/// Список яблок

/// </summary>

List<Apple> arrApple = new List<Apple>();

/// <summary>

/// Пакмен.

/// </summary>

PackMan packMan;

/// <summary>

/// Свойство списка яблок,доступное только для чтения

/// </summary>

public List<Apple> ArrApple

{

get { return arrApple; }

}

/// <summary>

/// Свойство списка стен,доступное только для чтения

/// </summary>

public List<Wall> ArrWall

{

get { return arrWall; }

}

/// <summary>

/// Свойство пакмена,доступное только для чтения

/// </summary>

public PackMan PackMan

{

get { return packMan; }

}

/// <summary>

/// Свойство списка танков,доступное только для чтения

/// </summary>

public List<Tank> ArrTank

{

get { return arrTank; }

}

#endregion

#region Вспомогательные поля класса

/// <summary>

/// Список потоков,в которых запускаются объекты

/// </summary>

List<Thread> arrThread = new List<Thread>();

/// <summary>

/// Поток в котором запускается пакмен.

/// </summary>

private Thread packManThread;

/// <summary>

/// Очки.

/// </summary>

private static int score = 0;

/// <summary>

/// Свойство очков.

/// </summary>

public static int Score

{

get { return Game.score; }

set { Game.score = value; }

}

#endregion

/// <summary>

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

/// </summary>

public Game()

{

//Создание стен

for (int i = 0; i < GameForm.countWall; i++)

{

Rectangle rect = new Rectangle();

arrWall.Add(new Wall(rect.Location));

}

PlaceWalls();

//Создание танков

for (int i = 0; i < GameForm.countTank; i++)

{

Rectangle rect = new Rectangle();

do

{

rect = new Rectangle(GameForm.rand.Next(0, GameForm.MAXX), GameForm.rand.Next(0, GameForm.MAXY), (new Tank()).Width, (new Tank()).Height);

} while (Collides(rect));

arrTank.Add(new Tank(rect.Location));

Thread thread = new Thread(new ThreadStart(arrTank[i].Run));

arrThread.Add(thread);

}

//Создание яблок

for (int i = 0; i < GameForm.countApples; i++)

{

Rectangle rect = new Rectangle();

do

{

rect = new Rectangle(GameForm.rand.Next(0, GameForm.MAXX), GameForm.rand.Next(0, GameForm.MAXY), (new Apple()).Width, (new Apple()).Height);

} while (Collides(rect));

arrApple.Add(new Apple(rect.Location));

}

Rectangle rect2 = new Rectangle();

do

{

rect2 = new Rectangle(GameForm.rand.Next(0, GameForm.MAXX), GameForm.rand.Next(0, GameForm.MAXY), (new PackMan()).Width, (new PackMan()).Height);

} while (Collides(rect2));

//Создание пакмена.

packMan = new PackMan(rect2.Location);

packManThread = new Thread(new ThreadStart(PackMan.Run));

SubscribePos();

}

/// <summary>

/// Начало игры

/// </summary>

public void Start()

{

foreach (Thread thread in arrThread)

{

thread.Start();

}

packManThread.Start();

}

/// <summary>

/// Размещение стен в соответствии с картой,заданной массивом GameForm.map

/// </summary>

private void PlaceWalls()

{

int cur = 0;

for (int i = 0; i < GameForm.map.Length; i++)

for (int j = 0; j < GameForm.map[0].Length; j++)

if (GameForm.map[i][j] == '*')

{

arrWall[cur].Position = new Point(i * arrWall[cur].Width, j * arrWall[cur].Height);

cur++;

}

}

/// <summary>

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

/// </summary>

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

/// <returns></returns>

private bool Collides(Rectangle rect)

{

if (rect.Left < 0 || rect.Right >= GameForm.MAXX || rect.Top < 0 || rect.Bottom >= GameForm.MAXY)

return true;

for (int i = 0; i < arrTank.Count; i++)

{

if (arrTank[i].CollidesWith(rect)) return true;

}

for (int i = 0; i < arrWall.Count; i++)

{

if (arrWall[i].CollidesWith(rect)) return true;

}

for (int i = 0; i < arrApple.Count; i++)

{

if (arrApple[i].CollidesWith(rect)) return true;

}

if (packMan != null && packMan.CollidesWith(rect)) return true;

return false;

}

/// <summary>

/// Уничтожение объектов

/// </summary>

public void Dispose()

{

for (int i = 0; i < arrThread.Count; i++)

{

arrThread[i].Abort();

}

packManThread.Abort();

}

/// <summary>

/// Подписка каждого элемента с каждым на пересечение

/// </summary>

public void SubscribePos()

{

for (int i = 0; i < arrTank.Count; i++)

{

for (int j = 0; j < arrTank.Count; j++)

{

if (i != j)

{

arrTank[i].CheckPosition += new EventHandler(arrTank[j].OnCheckPosition);

}

}

}

for (int i = 0; i < arrTank.Count; i++)

{

for (int j = 0; j < arrWall.Count; j++)

{

arrTank[i].CheckPosition += new EventHandler(arrWall[j].OnCheckPosition);

}

}

for (int j = 0; j < arrWall.Count; j++)

{

packMan.CheckPosition += new EventHandler(arrWall[j].OnCheckPosition);

}

for (int j = 0; j < arrTank.Count; j++)

{

packMan.CheckPosition += new EventHandler(arrTank[j].OnCheckPosition);

}

for (int j = 0; j < arrTank.Count; j++)

{

arrTank[j].CheckPosition += new EventHandler(packMan.OnCheckPosition);

}

for (int j = 0; j < arrApple.Count; j++)

{

packMan.CheckPosition += new EventHandler(arrApple[j].OnCheckPosition);

}

packMan.ReplaceNeeded += new EventHandler(packMan_ReplaceNeeded);

for (int j = 0; j < arrApple.Count; j++)

{

arrApple[j].ReplaceNeeded += new EventHandler(apple_ReplaceNeeded);

}

}

/// <summary>

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

/// </summary>

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

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

void apple_ReplaceNeeded(object sender, EventArgs e)

{

Rectangle rect2 = new Rectangle();

do

{

rect2 = new Rectangle(GameForm.rand.Next(0, GameForm.MAXX - (sender as MapObject).Width - 2), GameForm.rand.Next(GameForm.MAXY - (sender as MapObject).Height - 2), (new Apple()).Width, (new Apple()).Height);

} while (Collides(rect2));

(sender as Apple).Position = rect2.Location;

}

/// <summary>

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

/// </summary>

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

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

void packMan_ReplaceNeeded(object sender, EventArgs e)

{

Rectangle rect2 = new Rectangle();

do

{

rect2 = new Rectangle(GameForm.rand.Next(0, GameForm.MAXX - (sender as MapObject).Width - 2), GameForm.rand.Next(GameForm.MAXY - (sender as MapObject).Height - 2), (new PackMan()).Width, (new PackMan()).Height);

} while (Collides(rect2));

packMan.Position = rect2.Location;

}

/// <summary>

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

/// </summary>

private void UnSubscribePos()

{

for (int i = 0; i < arrTank.Count; i++)

{

for (int j = 0; j < arrTank.Count; j++)

{

if (i != j)

{

arrTank[i].CheckPosition -= new EventHandler(arrTank[i].OnCheckPosition);

}

}

}

}

}

}

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

namespace PackMan

{

class ViewGame:View<Game>

{

#region Создание представлений и их свойств

//Game model = new Game();

//internal Game Model

//{

// get { return model; }

// set { model = value; }

//}

/// <summary>

/// Спмсок представлений танков

/// </summary>

List<ViewTank> arrViewTank = new List<ViewTank>();

/// <summary>

/// Представление пакмена

/// </summary>

ViewPackMan viewPackMan;

/// <summary>

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

/// </summary>

List<ViewWall> viewWall = new List<ViewWall>();

/// <summary>

/// Список представлений яблок

/// </summary>

List<ViewApple> viewApple = new List<ViewApple>();

/// <summary>

/// Контроллер пакмена

/// </summary>

PackManController controller = new PackManController();

/// <summary>

/// Поле на котором происходит игра

/// </summary>

Panel panelMap;

/// <summary>

/// Очки

/// </summary>

Label point;

#endregion

/// <summary>

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

/// </summary>

/// <param name="panelMap"></param>

public ViewGame(Panel panelMap,Label point)

{

this.panelMap = panelMap;

this.point = point;

panelMap.Controls.Add(point);

}

#region Событие нажатие на клавишу

/// <summary>

/// Событие нажатия на клавишу

/// </summary>

private event KeyEventHandler keyPress;

/// <summary>

/// Обработчик нажатия на клавишу

/// </summary>

/// <param name="key">Клавиша,которая нажата</param>

public virtual void OnKeyPress(Keys key)

{

if (keyPress != null)

keyPress(viewPackMan.Model, new KeyEventArgs(key));

}

/// <summary>

/// Подписка на событие нажатие клавиши

/// </summary>

public void SubscribeKeyPress()

{

this.keyPress += new KeyEventHandler(controller.OnKeyPress);

controller.OnKeyPress(viewPackMan, new KeyEventArgs(Keys.Right));

}

/// <summary>

/// Отписка на событие нажатие клавиши

/// </summary>

public void UnsubscribeKeyPress()

{

this.keyPress -= new KeyEventHandler(controller.OnKeyPress);

}

#endregion

/// <summary>

/// Обновить.

/// </summary>

protected override void Update()

{

Refresh();

}

/// <summary>

/// Создание всех элементов игры,

/// связывание ссылок представлений объектоа с реальными объектами игры

/// </summary>

private void Refresh()

{

arrViewTank = new List<ViewTank>();

viewWall = new List<ViewWall>();

viewApple = new List<ViewApple>();

viewPackMan = new ViewPackMan(panelMap);

viewPackMan.Model = Model.PackMan;

viewPackMan.Model.MapSize = new Point(panelMap.Width, panelMap.Height);

viewPackMan.Subscribe();

for (int i = 0; i < Model.ArrTank.Count; i++)

{

ViewTank viewTanktemp = new ViewTank(panelMap);

viewTanktemp.Model = Model.ArrTank[i];

viewTanktemp.Model.MapSize = new Point(panelMap.Width, panelMap.Height);

viewTanktemp.Subscribe();

arrViewTank.Add(viewTanktemp);

}

for (int i = 0; i < Model.ArrWall.Count; i++)

{

ViewWall viewWalltemp = new ViewWall(panelMap);

viewWalltemp.Model = Model.ArrWall[i];

viewWalltemp.Model.MapSize = new Point(panelMap.Width, panelMap.Height);

viewWall.Add(viewWalltemp);

}

for (int i = 0; i < Model.ArrApple.Count; i++)

{

ViewApple viewAppletemp = new ViewApple(panelMap);

viewAppletemp.Model = Model.ArrApple[i];

viewAppletemp.Model.MapSize = new Point(panelMap.Width, panelMap.Height);

viewAppletemp.Subscribe();

viewApple.Add(viewAppletemp);

}

}

}

Следующая статья будет заключающей, где мы опишем контроллер игры и создадим модель и представление формы.

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

В прошлой статье были описаны модели основных сущностей нашей игры типа Pаckman. В этой статье мы поговорим о таком элементе паттерна MVC как “представление”.

Понятие “представления” в шаблоне Model-View-Controller

Представление – это абстрактный суперкласс View реализует общие поведения представления. Встроенный набор компонентов подкласса представления (например, StandardSystemView – стандартное окно и TextEditorView – текстовый редактор). Представления могут быть вложенными для разработки сложного пользовательского интерфейса. Например, StandardSystemView (т.е. окна) содержит компонент представления, называемые TextEditorView.

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

В обязанности Представления входит отображение данных полученных от Модели. Обычно Представление имеет свободный доступ к Модели и может брать из нее данные, однако это доступ только на чтение, ничего менять в Модели или даже просто вызывать методы, приводящие к изменению ее внутреннего состояния, Представлению позволять нельзя. В случае активной Модели, Представление может подписаться на события изменения Модели и перерисовываться, забрав измененные данные, при получении соответствующего оповещения. Для взаимодействия с Контроллером, представление, как правило, реализует некий интерфейс, известный Контроллеру, что позволяет менять представления независимо и иметь несколько представлений на Контроллер. Вообще, подмена или изменение Представления самая часто встречающаяся задача, по сути это и есть та причина, по которой придумывают различные паттерны разделения Модели и Представления.

Использование обобщенного MVC паттерна сводится к следующим шагам. Во-первых, разработчик определяет свойство модели – любой класс, представляющий данные предметной области и наполненный определенной бизнес-логикой приложения, так называемую модель предметной области (Domain Model). В дальнейшем данный класс будет подставляться в качестве параметра P класса Model – модель готова. Во-вторых, разработчик определяет представление, которое будет отображать данную модель. Класс представления наследуется от класса View, которому в качестве параметра P подставляется свойство модели, определенное на предыдущем шаге. Если необходимо работать с представлением предметной области, разработчик определяет представление, которое наследуется от класса ObjectView<P>. В качестве параметра P класса ObjectView подставляется тот же тип – свойство модели. Остальные задачи: выбор экранной формы отображения данных модели, инициализация ее, а также реакция экранной формы на изменение отображаемой модели полностью лежат на разработчике.

Свойство модели, которое необходимо модели, программист в любом случае формирует – это данные предметной области и логика разрабатываемого приложения. А предлагаемый framework со своей стороны по умолчанию побуждает разработчика выделять данные предметной области и по необходимости бизнес-логику приложения в отдельный компонент, чем по умолчанию уменьшает связность. С одной стороны, обобщенный MVC паттерн оставляет задачи создания визуальных форм представлений программисту, но, с другой стороны, обеспечивает двустороннюю связь отображения с данными приложения (отображение не только может изменять данные, но и обновляется при изменении данных) не обременяя разработчика тонкостями относительно сложной событийной модели MVC.

Приведём пример кода модели и его представления, суть которых описывалась выше.

/// <summary>

/// Класс модели

/// </summary>

/// <typeparam name="P">Тип модели</typeparam>

public class Model<P> where P : new()

{

private P property = new P();

/// <summary>

/// Свойство типа модели

/// </summary>

public virtual P Property

{

get

{

return property;

}

set

{

property = value;

}

}

}

/// <summary>

/// Класс представления модели

/// </summary>

/// <typeparam name="P">Тип модели</typeparam>

public abstract class View<P> where P : new()

{

private P model = new P();

/// <summary>

/// Свойство

/// </summary>

public P Model

{

get { return model; }

set

{

model = value;

Update();

}

}

/// <summary>

/// Обновить

/// </summary>

protected abstract void Update();

/// <summary>

/// Деструктор

/// </summary>

~View()

{

}

}

/// <summary>

/// Контроллер

/// </summary>

/// <typeparam name="P">свойство модели</typeparam>

public class Controller<P> where P: new() {

}

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

Теперь давайте напишем базовое представление уже для наших сущностей. Как и в проектировании моделей, мы создадим представление статического объекта, т.е. не двигающегося по полю игры и представление динамических объектов. Начнём с класса ViewMapObject, описывающего представление для статических объектов. В данном классе создаётся главное свойство типа PictureBox, содержащее в себе картинку объекта и конструктор, который размещает данную картинку на игровом поле. Также в этом классе осуществляется подписка на событие изменение координат и описан обработчик изменения координат, котором вызывается метод Show (рисование картинки объекта). Ещё описан виртуальный метод изменения картинки в соответствии с его направлением, который реализуем в более конкретных классах.

namespace PackMan

{

/// <summary>

/// Предсталение статического объекта

/// </summary>

class ViewMapObject:View<MapObject>

{

/// <summary>

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

/// </summary>

protected PictureBox picBox = new PictureBox();

/// <summary>

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

/// </summary>

/// <param name="map">Игровое поле</param>

public ViewMapObject(Panel map)

{

map.Controls.Add(picBox);

picBox.Height = Model.Height;

picBox.Width = Model.Width;

}

/// <summary>

/// Подписка на событие изменение координат

/// </summary>

public void Subscribe()

{

this.Model.PositionChanged += new EventHandler(OnPositionChanged);

OnPositionChanged(this, new EventArgs());

}

/// <summary>

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

/// </summary>

private void Unsubscribe()

{

this.Model.PositionChanged -= new EventHandler(OnPositionChanged);

}

/// <summary>

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

/// </summary>

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

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

private void OnPositionChanged(object sender, EventArgs e)

{

Show();

}

/// <summary>

/// Нарисовать картинку объекта

/// </summary>

private void Show()

{

SetImage(Model.Position);

}

delegate void SetImageCallback(Point p);

/// <summary>

/// Подвинуть картинку элемента в соответствии с его координатами

/// </summary>

/// <param name="p"></param>

private void SetImage(Point p)

{

if (this.picBox.InvokeRequired)

{

SetImageCallback d = new SetImageCallback(SetImage);

picBox.Invoke(d, new object[] { Model.Position });

}

else

{

ChangePicture();

this.picBox.Location = p;

}

}

/// <summary>

/// Изменить картинку в соответтствии с его направлением

/// </summary>

protected virtual void ChangePicture() { }

/// <summary>

/// Обновить.

/// </summary>

protected override void Update()

{

Show();

}

}

}

Теперь реализуем класс представление двигающегося объекта ViewDynamicMapObject. Класс очень похож на предыдущий но в шаблон View уже передаётся DynamicMapObject.

namespace PackMan

{

/// <summary>

/// Представление дви жущегося объекта

/// </summary>

class ViewDynamicMapObject:View<DynamicMapObject>

{

/// <summary>

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

/// </summary>

protected PictureBox picBox = new PictureBox();

/// <summary>

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

/// </summary>

/// <param name="map">Игровое поле</param>

public ViewDynamicMapObject(Panel map)

{

map.Controls.Add(picBox);

picBox.Height = Model.Height;

picBox.Width = Model.Width;

}

/// <summary>

/// Подписка на событие изменение координат

/// </summary>

public void Subscribe()

{

this.Model.PositionChanged += new EventHandler(OnPositionChanged);

OnPositionChanged(this, new EventArgs());

}

/// <summary>

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

/// </summary>

private void Unsubscribe()

{

this.Model.PositionChanged -= new EventHandler(OnPositionChanged);

}

/// <summary>

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

/// </summary>

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

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

private void OnPositionChanged(object sender, EventArgs e)

{

Show();

}

/// <summary>

/// Нарисовать картинку объекта

/// </summary>

private void Show()

{

SetImage(Model.Position);

}

delegate void SetImageCallback(Point p);

/// <summary>

/// Подвинуть картинку элемента в соответствии с его координатами

/// </summary>

/// <param name="p"></param>

private void SetImage(Point p)

{

if (this.picBox.InvokeRequired)

{

SetImageCallback d = new SetImageCallback(SetImage);

picBox.Invoke(d, new object[] { Model.Position });

}

else

{

ChangePicture();

this.picBox.Location = p;

}

}

/// <summary>

/// Изменить картинку в соответтствии с его направлением

/// </summary>

protected virtual void ChangePicture() { }

/// <summary>

/// Обновить.

/// </summary>

protected override void Update()

{

Show();

}

}

}

Сейчас мы уже можем описать конкретные классы представлений движущихся объектов, таких как пакмен и танк. Они практически одинаковые, разница заключается лишь в картинках, взятых из ресурсов приложения. Также в классах описано изменение картинки объекта при повороте модели.

/// <summary>

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

/// </summary>

class ViewPackMan:ViewDynamicMapObject

{

/// <summary>

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

/// </summary>

/// <param name="map">Поле игры</param>

public ViewPackMan(Panel map):base(map)

{

picBox.Image = Resources.PackManD;

}

/// <summary>

/// Смена картинки при смене направления движения

/// </summary>

protected override void ChangePicture()

{

switch (Model.DirectionNow)

{

case (int)Direction.Up:

{

picBox.Image = Resources.PackManU;

break;

}

case ((int)Direction.Down):

{

picBox.Image = Resources.PackManD;

break;

}

case (int)Direction.Right:

{

picBox.Image = Resources.PackManR;

break;

}

case (int)Direction.Left:

{

picBox.Image = Resources.PackManL;

break;

}

default:

break;

}

}

}

/// <summary>

/// Представление танка

/// </summary>

class ViewTank : ViewDynamicMapObject

{

/// <summary>

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

/// </summary>

/// <param name="map">Поле игры</param>

public ViewTank(Panel map):base(map)

{

picBox.Image = Resources.TankU;

}

/// <summary>

/// Смена картинки при смене направления движения

/// </summary>

protected override void ChangePicture()

{

switch (Model.DirectionNow)

{

case (int)Direction.Up:

{

picBox.Image = Resources.TankU;

break;

}

case ((int)Direction.Down):

{

picBox.Image = Resources.TankD;

break;

}

case (int)Direction.Right:

{

picBox.Image = Resources.TankR;

break;

}

case (int)Direction.Left:

{

picBox.Image = Resources.TankL;

break;

}

default:

break;

}

}

}

Более простыми являются представление яблока и стены так как они статические объекты. Разница заключается в том что яблоко появляется на поле при его создании (т.е. в конструкторе описано добавление картинки на игровое поле map.Controls.Add(picBox)), а стена добавляется на поле с помощью массива символов, который я опишу в следущих статьях.

/// <summary>

/// Представление яблока

/// </summary>

class ViewApple : ViewMapObject

{

/// <summary>

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

/// </summary>

/// <param name="map">Поле игры</param>

public ViewApple(Panel map):base(map)

{

picBox.Image = Resources.Apple;

map.Controls.Add(picBox);

}

}

/// <summary>

/// Класс представления статического объекта

/// </summary>

class ViewWall: ViewMapObject

{

/// <summary>

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

/// </summary>

/// <param name="map">Игровое поле</param>

public ViewWall(Panel map):base(map)

{

picBox.Image = Resources.Wall;;

}

}

Итак, у нас есть модели и представления всех сущностей игры, остаётся только написать самую главную часть модель и представление самой игры. А также контроллер, с помощью которого можно будет управлять пакмэном.

Реализация шаблона 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;

}

}

}

}

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

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

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

Для начала определимся, что такое паттерны программирования для чего они нужны, а потом подробнее рассмотрим паттерн Model-View-Controller.

Теория

Понятие шаблона проектирования

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

При программировании различных приложений время от времени задачи, возникающие перед программистом, повторяются. Есть некоторые типичные задачи проектирования, оптимальное решение для которых уже найдено. Эти шаблоны решений - и есть паттерны проектирования.

С помощью шаблонов проектирования описываются и формализуются типовые задачи и их решения. В результате опыт, который нарабатывается с большим трудом, становится доступным широкому сообществу программистов. Шаблоны это не просто какая-то теория, это в первую очередь практика, практика программистов, которые уже решали похожие задачи и нашли ценой проб и ошибок, размышлений и обмена мнениями то, как нужно проектировать приложение или его отдельные части в конкретном случае для поставленных требований. В английском языке есть такой термин “best practice”(перевод: “лучшие методы” или “наилучшие решения”) я считаю, что это выражение лучшим образом описывает суть паттернов. Знаменитый приверженец шаблонов Мартин Фаулер говорит, что он открывает шаблоны, а не создает их. Поэтому многие шаблоны у программистов вызывают чувство “дежа вю” — ведь часто бывает, что вы используете некоторые методы при проектировании своего приложения и просто не знаете, что они вынесены в шаблоны.

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

Вообще если обратиться к истории, то первые шаблоны проектирования для языка программирования Smalltalk представили в 1987 г. Кэнт Бэк и Вард Каннингем. Но это было только началом, настоящее признание в мире программирования они получили после публикации книги «Приемы объектно-ориентированного проектирования. Паттерны проектирования», написанной программистами Э. Гаммом, Р. Хелмом, Р. Джонсоном и Дж. Влиссидесом. В ней были представлены 23 шаблона, ставших сейчас основными. Данная работа дала толчок к изучению паттернов программистами. Издание «банды четырех» (так в шутку прозвали авторов книги) до сих пор остается одним из ИТ-бестселлеров, и его постоянно публикуют.

Краткий обзор основных паттернов

Рассмотрим кратко основные первые 23 паттерна, с которых все началось и которые сейчас должен знать каждый программист. Они разделены на три группы (для каждого паттерна приводится английское название из книги GoF и устоявшийся русский перевод). Краткое описание и разделение на группы описано в статье Олега Фёдорова в журнале PC Magazine.

«Порождающие шаблоны». В этой группе собраны паттерны, описывающие разные способы создания объектов. Прежде всего, это «Фабричный метод» (Factory Method), прием определения интерфейса создания объектов, при этом выбранный класс воплощается в подклассах. Шаблон «Абстрактная фабрика» (Abstract Factory) определяет интерфейс для создания семейств, связанных между собой или независимых объектов, конкретные классы которых неизвестны. С помощью шаблона «Строитель» (Builder) можно отделить процесс конструирования сложного объекта от его конкретного представления и при этом использовать один и тот же процесс для создания различных представлений. «Прототип» (Prototype) описывает виды разрабатываемых объектов с помощью прототипа и создает новые путем его копирования. Применение шаблона «Одиночка» (Singleton) гарантирует, что некоторый класс может иметь только один экземпляр (и предоставляет глобальную точку доступа к нему).

«Структурные паттерны». В этой группе собраны паттерны, которые позволяют менять структуру взаимодействия классов. «Адаптер» (Adapter) позволяет адаптировать интерфейс класса к конкретной ситуации, средствами шаблона «Мост» (Bridge) можно отделить интерфейс класса и его реализацию, «Компоновщик» (Composite) объединяет объекты в древовидную структуру для представления иерархии от частного к целому. Компоновщик позволяет клиентам единообразно обращаться к отдельным объектам и группам объектов. Паттерн «Оформитель» (Decorator, также известен какWrapper, «Оболочка») позволяет динамически добавлять новое поведение к объекту, «Фасад» (Facade)— скрыть сложность системы путем сведения всех возможных внешних вызовов к одному объекту, делегирующему их соответствующим объектам системы. Шаблон «Приспособленец» (Flyweight) используется для облегчения работы с большим числом мелких объектов, а «Заместитель» (Proxy)— контролировать доступ к объекту, перехватывая все вызовы к нему.

В группе «Паттерны поведения» собраны шаблоны, ответственные за реализацию поведения объектов. «Цепочка ответов» (Chain of Response) позволяет пропустить запрос через цепочку объектов, «Команда» (Command) инкапсулирует команду в объект, «Интерпретатор» (Interpreter) позволяет создать общее декларативное решение для часто изменяющихся условий задачи. В шаблоне «Итератор» (Iterator) организуется последовательный доступ к коллекции, «Посредник» (Mediator) определяет упрощенный механизм взаимодействия классов, «Напоминание» (Memento) задает принципы, позволяющие записывать и восстанавливать внутреннее состояние объекта. Средствами шаблона «Наблюдатель» (Observer) можно оповещать об изменениях множества объектов, «Состояние» (State)— менять поведение объекта при изменении его состояния. «Стратегия» (Strategy) инкапсулирует алгоритм внутри класса. Паттерн «Шаблонный метод» (Template Method) выделяет конкретные шаги в алгоритме и опирается на подклассы для их реализации. Средствами паттерна «Посетитель» (Visitor) в класс добавляются новые операции без его изменения.

Вот мы очень кратко ознакомились со всеми основными паттернами и определились с тем, что паттерны это очень полезная вещь, и каждый хороший программист должен знать и использовать шаблоны проектирования при разработке своих приложений. Для хорошего изучения паттернов программирования я советую прочитать книгу “Приемы объектно-ориентированного проектирования. Паттерны проектирования”, написанную Э. Гамма, Р. Хелм, Р. Джонсон, Дж. Влиссидес. В данной книге описываются простые и изящные решения типичных задач, возникающих в объектно-ориентированном проектировании. Авторы излагают принципы использования паттернов проектирования и приводят их каталог. Таким образом, книга одновременно решает две задачи. Во-первых, в ней демонстрируется роль паттернов в создании архитектуры сложных систем. Во-вторых, применяя содержащиеся в справочнике паттерны, вы сможете с легкостью разрабатывать собственные приложения. Издание предназначено как для профессиональных разработчиков, так и для программистов, осваивающих объектно-ориентированное проектирование. Как показывает практика, чтобы понять и осознать всё что написано в данной книге, прочитать её придётся не один раз. А для лучшего освоения нужно попробовать применить шаблоны проектирования на практике для решения хотя бы небольших задач. Так далее я попробую применить шаблон MVC (Model-View-Controller) при написании игры типа Packman. Определимся сначала с тем что нам нужно сделать. Т.е. приведу техническое задание.

Практика

Концептуальная модель

Игра состоит из поля с расставленными на нем стенами, на котором взаимодействуют несколько сущностей:

  1. Русский колобок. Начинает постоянное движение при старте игры и управляется пользователем с помощью клавиатуры. Игра заканчивается при столкновении колобка с вражеским танком.
  2. Вражеский танк. Может присутствовать в количестве нескольких штук (по умолчанию 5). Начинает постоянное движение при старте игры. На каждом шаге может изменить движение путем случайного выбора направления следующего шага. При столкновении двух танков каждый из них разворачивается на 180 градусов и продолжает движение
  3. Наливное яблоко. Может присутствовать в количестве нескольких штук (по умолчанию 5). Случайным образом расставляются в свободные для прохода клетки поля при старте игры. При наезде танка на яблоко ничего не происходит – танк проходит через клетку с яблоком и продолжает двигаться дальше. При столкновении с яблоком колобка - яблоко исчезает (съедается), общий счет игры увеличивается на 1 очко, и яблоко снова появляется в случайно выбранном месте на игровом поле.

Объекты при движении должны смещаться на пиксель для создания эффекта плавного перемещения.

Проектирование

1. Уровень представления данных и логики приложения

Приложению в качестве параметров запуска должны передаваться следующие параметры:

  1. Размеры игрового поля (ширина и высота)
  2. Количество танков на поле
  3. Количество яблок на поле
  4. Скорость передвижения объектов (задается для всех объектов сразу).

При запуске приложения должно показываться его окно, состоящее из следующих элементов:

  1. Игровое поле, на котором будут прорисовываться перечисленные концептуальной модели объекты.
  2. Кнопка New Game, при нажатии на которую будет начинаться новая игра.

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

clip_image002

Рис. 1. Пример интерфейса игры типа Packman

2. Паттерны

При проектировании приложения следует использовать паттерн MVC (Model View Controller).

Конструирование

Сущности колобка и танка должны представлять собой отдельные потоки и реализовываться в отдельных классах Packman и Tank.

Изображение сущностей должно быть реализовано с помощью классов PackmanView и TankView соответственно.

Должен быть класс PackmanController, являющийся контроллером для пользовательского окна, представляющий собой промежуточный уровень между презентационным и бизнес уровнями.

Дополнительные задания
  1. Реализовать «прозрачные стены» для игрового поля – т.е. когда танк подходит к границе поля, он не отходит в сторону, а выходит с противоположной стороны поля, то же самое происходит с колобком.
  2. Реализовать второе окно в качестве альтернативного view для игрового поля. Оно будет представлять из себя frame с таблицей со следующими полями:
    - название объекта
    - координаты в следующем виде: (X, Y)
  3. Сделать возможность расстрела колобком танков

В следующих статьях я предложу свою реализацию описанного задания, а также я подробно опишу паттерн проектирования MVC в классическом его понимании. А вы можете попробовать написать сами и потом сравнить с моим решением. Я не буду реализовывать дополнительное задание, и описывать это в своих статьях, так как целью решения этого задания является не создание интересной навороченной игры, а изучение и применение шаблона MVC, событийной модели приложений, применение принципов ООП, использование интерфейсов и шаблонных типов. Также нужно попытаться сделать приложение достаточно производительным, чтобы игра не тормозила на стандартных конфигурациях компьютеров, что легко может быть т.к. по заданию каждая сущность имеет свой поток. Также хочу отметить, что создавать приложение я буду на C# c использованием технологии Windows Form. Все исходники данной игры можно будет скачать по ссылке, приведённой в последней статье.