В первой части мы начали моделирование игры в шашки с помощью Alan.Platform. Мы создали библиотеку элементов, к которой добавили один элемент, оператор, управляющий расположением шашек. Также с помощью конструктора мы создали две шашки, расположенные по углам платформы. Все это можно было лицезреть в консоли, в виде текста, который был любезно составлен ObjectDumper'ом.
Как бы ни был хорош ObjectDumper — нашему мозгу трудно разглядеть доску для игры в шашки среди пар ключ-значение. Поэтому нужно создать графическое представление для модели. Этим мы в ближайшее время и займемся.
От графического интерфейса нам нужно две вещи. Первая — возможность увидеть в окне объекты созданного мира, узнать их свойства. Вторая — возможность подействовать на эти объекты, изменить их свойста. Так уж вышло, что от модели организма требуются те же самые возможности — она должна уметь «почувствовать» свойства окружающих объектов, а также уметь совершать над ними действия, изменяющие эти свойства.
Это значит, что создание графического интерфейса будет похоже на моделирование организма. И не просто похоже — будут использоваться одни и те же классы и принципы. Проще всего это объяснить с помощью следующей диаграммы:
Стрелки указывают, в каком направлении движется информация. Как я и говорил в первой части — информация ходит по кругу. Оператор и мозг взаимодействуют друг с другом. При этом изменяются состояния мира (Properties) и мозга (Memory).
Действия и сенсоры в данном случае являются посредниками этого взаимодействия, медиаторами. Их основная задача — преобразование информации из формы, понятной одной стороне, в форму, понятную другой. Для оператора — это наборы свойств, а для мозга — массив значений.
Действия, сенсоры и мозг вместе являются одной сущностью — организмом. В Alan.Platform организм реализован как производный от компонента класс — клиент. К нему добавилась возможность содержать набор сенсоров, действий и мозг.
Таким образом для системы наш интерфейс является обычным организмом, который имеет определенную форму, располажение и другие свойства, которые есть у объектов модели. Он сам является одним из этих объектов. А мы можем все видеть его «глазами» и взаимодействовать с системой его «руками».
Итак, из каких частей будет состоять наш организм? Нужен один сенсор, который будет подключен к созданному ранее оператору CellBoard и который сможет видеть расположение шашек и преобразовывать эту информацию во что-то, пригодное для отображения в окне. Также нужно одно действие, которое будет передвигать шашки по платформе. И наконец нужен мозг, который будет управлять всем этим. Он будет отвчать за отрисовку шашек и обработку действий пользователя.
Звучит довольно сложно, но подходящий мозг уже есть в составе Alan.Platform. Он использует WPF для отрисовки объектов. Более того — он сам унаследован от FrameworkElement, так что его можно помещать прямо на окно.
Вооружившить теорией, можно плавно переходить к практической части. Скопируем в созданное ранее решение проекты Platform.Explorer и ElementsLibSample из Alan.Platform. Сделаем Platform.Explorer запускаемым по умолчанию проектом и добавим к нему ссылку на созданную ранее библиотеку элементов, содержащую CellBoard.
Начнем с сенсора. Добавим к нашей библиотеке элементов новый класс UISensor.
В обычной ситуации ChannelsCount указывает количество значений, которое передается в мозг. Но наш интерфейсный мозг не совсем обычный и не принимает значений. Чтобы выжать хоть какую-то пользу из атрибута, в его конструктор передан масштаб. По соглашению, все значения свойств находятся в пределах от 0 до 1. Таким образом наша доска в клеточку имеет размеры 1х1. Используя масштаб, доска будет иметь размер 300х300 юнитов при отрисовке в окне.
Далее у нас идут реализации абстрактных методов. Метод Update объявлен в абстрактном классе Mediator, от которого унаследованы Sensor и Action. Он помогает реализовать «поле зрения». Для сенсора — это набор объектов, свойства которых он может увидеть. Для действия — набор объектов, свойства которых оно может изменить. Эти объекты хранятся в защищенном поле VisibleElements. Так как нам нужно видеть все объекты, то реализация метода Update примет вид:
Метод Transmit объявлен в классе Sensor. Его задача — передать в мозг информацию о свойствах объектов, предварительно преобразовав ее в понятную мозгу форму. Этот метод вызывается оператором, когда значения свойств изменяются. Поэтому у мозга всегда актуальная информация о состоянии мира.
Наш мозг ожидает получить информацию о том, как следует отрисовывать объекты. Этот механизм использует RenderInstructions — простую обертку для DrawingVisual. В мозге определены три метода, которые принимают инструкции:
Однако, все это не так важно. Главное понять, что после вызова любого из трех методов мы сразу же увидим результат на экране.
С аргументами, я думаю, должно быть все понятно. Shapes — это перечислимый тип, который пока содержит только две формы — эллипс и прямоугольник. Размеры есть, заливка есть, но нет самих координат. Не хотелось бы при каждом перемещении шашки открывать и закрывать DrawingContext. Поэтому перемещения будут реализованы с помощью TranslateTransform. Изначально центры всех шашек будут находиться в точке (0,0), откуда они будут транслироваться по месту назначения. Для смещения шашки на экране нужно будет просто изменить значения свойств X и Y соответствующего объекта TranslateTransform. Так как эти свойства являются DependencyProperty, для их изменения можно использовать анимацию.
С отрисовкой вроде бы разобрались, можно приступать к ее реализации. В методе Transmit мы дожны будем изменять свойства TranslateTransform. Но прежде чем их изменять, нужно добавить эту трансформацию к RenderInstructions. Туда же нужно добавить и все остальные инструкции. Это нужно сделать один раз после запуска программы.
Наиболее подходящим для этого местом является метод ConnectTo, объявленный в интерфейсе IConnectable, который реализуют все элементы. Он вызывается сразу после запуска программы и помогает связывать элементы. Поэтому он является удачным местом для инициализации.
Добавляем к нашей библиотеке элементов ссылки на PresentationCore, PresentationFramework, WindowsBase и проект ElementsLibSample.
BaseUIBrain поддерживает выделение объектов. Если нажать на любую из шашек, внизу можно увидеть значения всех ее свойств (значения округлены). Немного подправив XAML и world.xml, можно добиться следующего результата:
Итак, сенсор есть, мозг есть. Осталось действие. Добавим новый класс MoveChecker к нашей библиотеке элементов.
Для нашего действия будет достаточно трех аргументов. В первом будет id шашки, которую нужно переместить, во втором — значение, на которое нужно изменить координату «X», а в третьем — значение, на которое нужно изменить координату «Y».
Та часть Alan.Platform, которая отвечает за изменение свойств, тесно связана с внутренним временем платформы, о котором я еще не упоминал. Внутреннее время реализовано в виде тактов. Центром управления временем является статический класс Platform.Core.Concrete.Time.
Идея заключается в том, что существуют некоторые объекты, состояние которых может зависеть от времени. Эти объекты регистрируются в классе Time. Затем, когда пользователь вызывает метод Time.Tick(), аналогичный метод вызывается у всех зарегистрированных объектов. Разработчик может переопределить этот метод. Эти объекты называются TimeObject, и PropertySet является одним из них.
PropertySet позволяет планировать изменения свойств следующим образом:
Причина, по которой используется дельта, а не новое значение свойства, проста — на один такт может быть запланировано несколько дельт. И не зависимо от того, в каком парядке они будут применены, конечный результат останется тем же (простое правило арифметики).
Теперь можно приступать к реализации DoAction:
Обработчик UIBrain_KeyDown очень простой:
Метод Tick вернет управление только тогда, когда все его подписчики обработают это событие. Т. е. тогда, когда изменения свойств, запланированные на следующий такт, будут применены. В самом конце вызывается метод OnChanged, который обновляет информацию о свойствах внизу окна. Такт будет отсчитан при нажатии на любую из клавиш.
UIBrain готов. Осталось только указать его в world.xml вместо BaseUIBrain и можно запускать… Запуск не удается, так как мы забыли реализовать CellBoard.ValidatePropertySet. Этот метод вызывается сразу после изменения значений свойств в наборе. Именно в нем реализуются законы мира. Вот, как выглядит его объявление в классе Operator:
Повторный запуск не удается, так как мы забыли реализовать еще и UISensor.Transmit. Здесь мы должны изменить свойства объектов TranslateTransform.
Конечно, есть некоторые недочеты. Например, нельзя удалить сбитую шашку, можно только переместить ее за пределы окна. Удаление и добавление объектов пока не поддерживаются в Alan.Platform. Шашками можно ходить как вперед, так и назад и даже ставить одну на другую. В принципе это исправить можно — достаточно добавить соответствующий код в CellBoard.ValidatePropertySet, который забракует подобные состояния шашек. Также можно добавить цвета шашкам, а еще — добавить состояние «дамка-не дамка». Все это можно сделать, добавив еще один оператор и подключив к нему свои сенсор и действие.
На этом туториал заканчивается. В нем мы создали модель мира, населенного 24-мя шашками, одной из которых является организм-интерфейс. Архив с конечным результатом можно скачать здесь.
В целом модель не обязательно должна быть привязана к WPF. Вполне можно по аналогии создать новый Command Line Interface в виде организма и добавить его к остальным шашкам. Можно вообще не добавлять организмы в модель, но тогда ее состояние никто не увидит и никто не изменит.
В самом конце хотелось бы ответить на вопрос — что же такое Alan.Platform? Это то, что позволяет вам забыть о служебном коде, о создании и связывании объектов, о том, чтобы поддерживать их в актуальном состоянии и т. п. Можно просто сесть и заняться моделированием.
P. S. Может показаться, что подобная идея создания объектов во время выполнения избыточная, сложная и неудобная. Куда проще и понятнее было бы создавать строго типизированные объекты со свойствами на подобие того, как это деляется в ORM системах. С одной стороны это упростит создание объектов, так как можно будет использовать все прелести ООП. С другой — это усложнит создание законов мира и взаимодействие мозга с объектами.
Желающих поучаствовать в проекте или использовать его для создания чего-нибудь просьба обращаться в ЛС или стучаться в jabber:
openminded@xdsl.by
Как бы ни был хорош ObjectDumper — нашему мозгу трудно разглядеть доску для игры в шашки среди пар ключ-значение. Поэтому нужно создать графическое представление для модели. Этим мы в ближайшее время и займемся.
От графического интерфейса нам нужно две вещи. Первая — возможность увидеть в окне объекты созданного мира, узнать их свойства. Вторая — возможность подействовать на эти объекты, изменить их свойста. Так уж вышло, что от модели организма требуются те же самые возможности — она должна уметь «почувствовать» свойства окружающих объектов, а также уметь совершать над ними действия, изменяющие эти свойства.
Это значит, что создание графического интерфейса будет похоже на моделирование организма. И не просто похоже — будут использоваться одни и те же классы и принципы. Проще всего это объяснить с помощью следующей диаграммы:
Стрелки указывают, в каком направлении движется информация. Как я и говорил в первой части — информация ходит по кругу. Оператор и мозг взаимодействуют друг с другом. При этом изменяются состояния мира (Properties) и мозга (Memory).
Действия и сенсоры в данном случае являются посредниками этого взаимодействия, медиаторами. Их основная задача — преобразование информации из формы, понятной одной стороне, в форму, понятную другой. Для оператора — это наборы свойств, а для мозга — массив значений.
Действия, сенсоры и мозг вместе являются одной сущностью — организмом. В Alan.Platform организм реализован как производный от компонента класс — клиент. К нему добавилась возможность содержать набор сенсоров, действий и мозг.
Таким образом для системы наш интерфейс является обычным организмом, который имеет определенную форму, располажение и другие свойства, которые есть у объектов модели. Он сам является одним из этих объектов. А мы можем все видеть его «глазами» и взаимодействовать с системой его «руками».
Итак, из каких частей будет состоять наш организм? Нужен один сенсор, который будет подключен к созданному ранее оператору CellBoard и который сможет видеть расположение шашек и преобразовывать эту информацию во что-то, пригодное для отображения в окне. Также нужно одно действие, которое будет передвигать шашки по платформе. И наконец нужен мозг, который будет управлять всем этим. Он будет отвчать за отрисовку шашек и обработку действий пользователя.
Звучит довольно сложно, но подходящий мозг уже есть в составе Alan.Platform. Он использует WPF для отрисовки объектов. Более того — он сам унаследован от FrameworkElement, так что его можно помещать прямо на окно.
Tutorial[«Part 2»]
Вооружившить теорией, можно плавно переходить к практической части. Скопируем в созданное ранее решение проекты Platform.Explorer и ElementsLibSample из Alan.Platform. Сделаем Platform.Explorer запускаемым по умолчанию проектом и добавим к нему ссылку на созданную ранее библиотеку элементов, содержащую CellBoard.
Начнем с сенсора. Добавим к нашей библиотеке элементов новый класс UISensor.
using System;
using System.Collections.Generic;
using Platform.Core.Concrete;
using Platform.Core.Elements;
namespace Checkers
{
[AssociatedOperator("Checkers.CellBoard")]
[ChannelsCount(300)]
public class UISensor : Sensor
{
public override void Update(IEnumerable<PropertySet> elements)
{
throw new NotImplementedException();
}
public override void Transmit()
{
throw new NotImplementedException();
}
}
}
AssociatedOperator указывает, с каким оператором умеет работать сенсор. В конструктор передается полное имя типа оператора. Каждый сенсор и действие должны иметь такой атрибут.В обычной ситуации ChannelsCount указывает количество значений, которое передается в мозг. Но наш интерфейсный мозг не совсем обычный и не принимает значений. Чтобы выжать хоть какую-то пользу из атрибута, в его конструктор передан масштаб. По соглашению, все значения свойств находятся в пределах от 0 до 1. Таким образом наша доска в клеточку имеет размеры 1х1. Используя масштаб, доска будет иметь размер 300х300 юнитов при отрисовке в окне.
Далее у нас идут реализации абстрактных методов. Метод Update объявлен в абстрактном классе Mediator, от которого унаследованы Sensor и Action. Он помогает реализовать «поле зрения». Для сенсора — это набор объектов, свойства которых он может увидеть. Для действия — набор объектов, свойства которых оно может изменить. Эти объекты хранятся в защищенном поле VisibleElements. Так как нам нужно видеть все объекты, то реализация метода Update примет вид:
public override void Update(IEnumerable<PropertySet> elements)
{
this.VisibleElements = elements;
}
Этот метод вызывается оператором каждый раз, когда поле зрения может измениться, т. е. тогда, когда перемещается один из объектов, а также почти сразу после запуска программы. В метод передаются все наборы свойств, которые есть у оператора, чтобы сенсор выбрал из них те, которые он «видит».Метод Transmit объявлен в классе Sensor. Его задача — передать в мозг информацию о свойствах объектов, предварительно преобразовав ее в понятную мозгу форму. Этот метод вызывается оператором, когда значения свойств изменяются. Поэтому у мозга всегда актуальная информация о состоянии мира.
Наш мозг ожидает получить информацию о том, как следует отрисовывать объекты. Этот механизм использует RenderInstructions — простую обертку для DrawingVisual. В мозге определены три метода, которые принимают инструкции:
public void SetShape(int id, Shapes shape, double height, double width);
public void SetBrush(int id, Brush brush);
public void SetTransform(int id, Transform transform);
Id — это идентификатор компонента, для которого указывается инструкция. В мозге с каждым id связан объект RenderInstructions. Вызывая один из этих методов, мы меняем свойства этого объекта и заставляем его перерисоваться. Для этого внутри RenderInstructions вызывается метод RenderOpen() соответствующего объекта DrawingVisual. Сотавляется новый контекст для отрисовки объекта, а затем WPF сама позаботится о том, чтобы обновить содержимое окна.Однако, все это не так важно. Главное понять, что после вызова любого из трех методов мы сразу же увидим результат на экране.
С аргументами, я думаю, должно быть все понятно. Shapes — это перечислимый тип, который пока содержит только две формы — эллипс и прямоугольник. Размеры есть, заливка есть, но нет самих координат. Не хотелось бы при каждом перемещении шашки открывать и закрывать DrawingContext. Поэтому перемещения будут реализованы с помощью TranslateTransform. Изначально центры всех шашек будут находиться в точке (0,0), откуда они будут транслироваться по месту назначения. Для смещения шашки на экране нужно будет просто изменить значения свойств X и Y соответствующего объекта TranslateTransform. Так как эти свойства являются DependencyProperty, для их изменения можно использовать анимацию.
С отрисовкой вроде бы разобрались, можно приступать к ее реализации. В методе Transmit мы дожны будем изменять свойства TranslateTransform. Но прежде чем их изменять, нужно добавить эту трансформацию к RenderInstructions. Туда же нужно добавить и все остальные инструкции. Это нужно сделать один раз после запуска программы.
Наиболее подходящим для этого местом является метод ConnectTo, объявленный в интерфейсе IConnectable, который реализуют все элементы. Он вызывается сразу после запуска программы и помогает связывать элементы. Поэтому он является удачным местом для инициализации.
Добавляем к нашей библиотеке элементов ссылки на PresentationCore, PresentationFramework, WindowsBase и проект ElementsLibSample.
BaseUIBrain brain;
int scale;
public override void ConnectTo(Component parent)
{
base.ConnectTo(parent);
this.brain = this.ConnectedBrain as BaseUIBrain;
this.scale = Sensor.GetChannelsCount(this.GetType());
this.brain.Scale = scale; // Меняет размер окна.
var checkerBrush = Brushes.BurlyWood;
double diameter = 0.12 * scale; // Диаметр шашки.
// К этому моменту метод Update уже вызван, так что поле
// VisibleElements проинициализировано.
foreach (var checker in this.VisibleElements)
{
// Координаты центра шашек.
double x = checker["X"].Value * scale;
double y = checker["Y"].Value * scale;
var translate = new TranslateTransform(x, y);
brain.SetShape(checker.Id, Shapes.Ellipse, diameter, diameter);
brain.SetBrush(checker.Id, checkerBrush);
brain.SetTransform(checker.Id, translate);
}
}
Этого уже достаточно, чтобы увидеть на экране заветные кружочки. Platform.Explorer хранит конфигурацию модели в файле world.xml. Откроем его и заменим его содержимое на следующее:<?xml version="1.0" ?>
<component xmlns="http://alan.codeplex.com/constructor/world">
<operator name="Checkers.CellBoard" />
<component>
<propertySet name="Checker" operator="Checkers.CellBoard">
<property name="X" value="0.0625" />
<property name="Y" value="0.6875" />
</propertySet>
</component>
<component>
<propertySet name="Checker" operator="Checkers.CellBoard">
<property name="X" value="0.0625" />
<property name="Y" value="0.9375" />
</propertySet>
</component>
<client>
<propertySet name="Checker" operator="Checkers.CellBoard">
<property name="X" value="0.1875" />
<property name="Y" value="0.8125" />
</propertySet>
<sensor name="Checkers.UISensor" />
<brain name="ElementsLibSample.UIElements.BaseUIBrain" />
</client>
</component>
Таким образом мы создаем три шашки, одной из которых является наш организм-интерфейс. Теперь можно запускать. Вот, что должно получиться в итоге:BaseUIBrain поддерживает выделение объектов. Если нажать на любую из шашек, внизу можно увидеть значения всех ее свойств (значения округлены). Немного подправив XAML и world.xml, можно добиться следующего результата:
Итак, сенсор есть, мозг есть. Осталось действие. Добавим новый класс MoveChecker к нашей библиотеке элементов.
using System;
using System.Linq;
using System.Collections.Generic;
using Platform.Core.Concrete;
namespace Checkers
{
[AssociatedOperator("Checkers.CellBoard")]
[ChannelsCount(3)]
public class MoveChecker : Platform.Core.Elements.Action
{
public override void Update(IEnumerable<PropertySet> elements)
{
this.VisibleElements = elements;
}
public override void DoAction(params double[] args)
{
throw new NotImplementedException();
}
}
}
Класс Action очень похож на Sensor, только вместо метода Transmit у него есть DoAction, который принимает от мозга массив значений double. Если сенсоры брали свойства объектов и из них составляли набор данных, то у действий обратная задача — они должны, используя набор данных, найти нужные объекты и изменить их свойства.Для нашего действия будет достаточно трех аргументов. В первом будет id шашки, которую нужно переместить, во втором — значение, на которое нужно изменить координату «X», а в третьем — значение, на которое нужно изменить координату «Y».
Та часть Alan.Platform, которая отвечает за изменение свойств, тесно связана с внутренним временем платформы, о котором я еще не упоминал. Внутреннее время реализовано в виде тактов. Центром управления временем является статический класс Platform.Core.Concrete.Time.
Идея заключается в том, что существуют некоторые объекты, состояние которых может зависеть от времени. Эти объекты регистрируются в классе Time. Затем, когда пользователь вызывает метод Time.Tick(), аналогичный метод вызывается у всех зарегистрированных объектов. Разработчик может переопределить этот метод. Эти объекты называются TimeObject, и PropertySet является одним из них.
PropertySet позволяет планировать изменения свойств следующим образом:
propertySet["PropertyName"][ticks] = delta;
Где ticks — количество тактов, через которое к свойству следует добавить значение, а delta — это и есть то самое значение. Например:checker["Y"][1] = 0.125;
Эта строчка означает, что на следующий такт к свойству «Y» нужно добавить 0.125. Минимальное число тактов, на которое можно запланировать изменение — 1, максимальное — 10 (пока что). Т. е. мгновенно изменить значение нельзя.Причина, по которой используется дельта, а не новое значение свойства, проста — на один такт может быть запланировано несколько дельт. И не зависимо от того, в каком парядке они будут применены, конечный результат останется тем же (простое правило арифметики).
Теперь можно приступать к реализации DoAction:
public override void DoAction(params double[] args)
{
var checker = this.VisibleElements.First(x => x.Id == args[0]);
checker["X"][1] = args[1];
checker["Y"][1] = args[2];
}
На этом наше действие готово и его можно добавлять в world.xml:...
</propertySet>
<action name="Checkers.MoveChecker" />
<sensor name="Checkers.UISensor" />
<brain name="ElementsLibSample.UIElements.BaseUIBrain" />
</client>
...
Если сейчас запустить Platform.Explorer, то никаких изменений мы не увидим, так как метод DoAction у нас нигде не вызывается. Исправляем это недоразумение. Для этого нужно к библиотеке элементов добавить новый класс UIBrain, производный от BaseUIBrain, который мы до этого использовали.using ElementsLibSample.UIElements;
namespace Checkers
{
public class UIBrain : BaseUIBrain
{
}
}
Нам нужно, чтобы в ответ на какое-то действие пользователя UIBrain вызывал метод DoAction с нужными параметрами. Проще всего повесить это на событие KeyDown — нажатие клавиши на клавиатуре. Для начала добавим обработчик для этого события. Это можно сделать в методе ConnectTo:public override void ConnectTo(Decorator parent)
{
base.ConnectTo(parent);
this.KeyDown += UIBrain_KeyDown;
}
Этот хитрый метод ConnectTo определен в BaseUIBrain, а не в IConnectable. У BaseUIBrain два родителя. Один — это клиент из дерева модели мира, а другой — Border из мира WPF. Так или иначе инициализацию UIBrain можно провести в этом методе.Обработчик UIBrain_KeyDown очень простой:
void UIBrain_KeyDown(object sender, KeyEventArgs e)
{
if (this.selectedId != 0)
{
var moveChecker = this.actions["Checkers.MoveChecker"];
switch(e.Key)
{
case Key.Q:
moveChecker.DoAction(selectedId, -0.125, -0.125);
break;
case Key.W:
moveChecker.DoAction(selectedId, 0.125, -0.125);
break;
case Key.S:
moveChecker.DoAction(selectedId, 0.125, 0.125);
break;
case Key.A:
moveChecker.DoAction(selectedId, -0.125, 0.125);
break;
}
Time.Tick();
OnChanged(selectedId);
}
}
Вначале мы проверяем, выделена ли какая-нибудь шашка. Если да, то selectedId будет содержать ее индекс. Далее находим наше действие. Затем анализируем, какая из клавиш была нажата. Если это 'Q' — перемещаем выделенную шашку вверх-влево, если это 'W' — вверх-вправо, 'S' — вниз-вправо, 'A' — вниз-влево. После этого отсчитываем один такт.Метод Tick вернет управление только тогда, когда все его подписчики обработают это событие. Т. е. тогда, когда изменения свойств, запланированные на следующий такт, будут применены. В самом конце вызывается метод OnChanged, который обновляет информацию о свойствах внизу окна. Такт будет отсчитан при нажатии на любую из клавиш.
UIBrain готов. Осталось только указать его в world.xml вместо BaseUIBrain и можно запускать… Запуск не удается, так как мы забыли реализовать CellBoard.ValidatePropertySet. Этот метод вызывается сразу после изменения значений свойств в наборе. Именно в нем реализуются законы мира. Вот, как выглядит его объявление в классе Operator:
public abstract bool ValidatePropertySet(PropertySet ps);
Единственный параметр — это набор свойств, у которого нужно провереть новое состояние и вернуть результат — является ли это состояние допустимым. Если нет, то произойдет откат к предыдущему состоянию, т. е. запланированные изменения свойств не будут применены. Так как у шашек довольно много законов, пока поставим на их месте заглушку «return true» и попробуем запустить программу снова…Повторный запуск не удается, так как мы забыли реализовать еще и UISensor.Transmit. Здесь мы должны изменить свойства объектов TranslateTransform.
public override void Transmit()
{
foreach (var element in VisibleElements)
{
var translate = brain.GetTransform<TranslateTransform>(
element.Id);
translate.X = element["X"].Value * scale;
translate.Y = element["Y"].Value * scale;
}
}
Ну теперь точно запустится! Постучав по дереву, нажимаем Run… Уррраа! Если вас не смущает то, что все шашки одного цвета — можете сыграть патрию-другую.Конечно, есть некоторые недочеты. Например, нельзя удалить сбитую шашку, можно только переместить ее за пределы окна. Удаление и добавление объектов пока не поддерживаются в Alan.Platform. Шашками можно ходить как вперед, так и назад и даже ставить одну на другую. В принципе это исправить можно — достаточно добавить соответствующий код в CellBoard.ValidatePropertySet, который забракует подобные состояния шашек. Также можно добавить цвета шашкам, а еще — добавить состояние «дамка-не дамка». Все это можно сделать, добавив еще один оператор и подключив к нему свои сенсор и действие.
На этом туториал заканчивается. В нем мы создали модель мира, населенного 24-мя шашками, одной из которых является организм-интерфейс. Архив с конечным результатом можно скачать здесь.
В целом модель не обязательно должна быть привязана к WPF. Вполне можно по аналогии создать новый Command Line Interface в виде организма и добавить его к остальным шашкам. Можно вообще не добавлять организмы в модель, но тогда ее состояние никто не увидит и никто не изменит.
В самом конце хотелось бы ответить на вопрос — что же такое Alan.Platform? Это то, что позволяет вам забыть о служебном коде, о создании и связывании объектов, о том, чтобы поддерживать их в актуальном состоянии и т. п. Можно просто сесть и заняться моделированием.
P. S. Может показаться, что подобная идея создания объектов во время выполнения избыточная, сложная и неудобная. Куда проще и понятнее было бы создавать строго типизированные объекты со свойствами на подобие того, как это деляется в ORM системах. С одной стороны это упростит создание объектов, так как можно будет использовать все прелести ООП. С другой — это усложнит создание законов мира и взаимодействие мозга с объектами.
- Аналог оператора будет невозможно создать, даже с использованием Reflection API.
- Законы мира придется встраивать в сами объекты.
- Дерево наследования будет постоянно разрастаться.
- Композиция может усложнить процесс создания объектов.
- Аналогам сенсоров и действий придется либо знать обо всех типах объектов, либо использовать Reflection API.
Желающих поучаствовать в проекте или использовать его для создания чего-нибудь просьба обращаться в ЛС или стучаться в jabber:
openminded@xdsl.by