Эволюция игрового фреймворка. Клиент 1. Компоненты

от автора

Пока что наше приложение состоит только из одного класса Dresser (не считая чисто формального класса Main). И уже одного этого оказалось достаточно для целого игрового жанра (до какой степени все же игроки бывают неприхотливы). Однако, для приличной игры этого маловато. Как минимум нужен еще экран меню, в котором мы могли бы выбирать игру.

Допустим, у нас есть несколько скинов для одевалки, и мы хотим для всех них использовать один экземпляр класса логики Dresser. При смене мувиклипа все ссылки на старый должны удаляться, а на новый — добавляться. Для этого код парсинга графики вынесем из конструктора в сеттер:

class Dresser {     //...     public var mc(default, set):MovieClip;     public function set_mc(value:MovieClip):MovieClip     {         if (mc != value)         {             // Unassign previous mc             if (mc != null)             {                 for (prevButton in prevButtons)                 {                     prevButton.removeEventListener(MouseEvent.CLICK, prevButton_clickHandler);                 }                 for (nextButton in nextButtons)                 {                     nextButton.removeEventListener(MouseEvent.CLICK, nextButton_clickHandler);                 }                 items = null;                 prevButtons = null;                 nextButtons = null;              }              mc = value;              // Assign new mc             if (mc != null)             {                 items = cast resolveNamePrefix(itemNamePrefix);                 prevButtons = cast resolveNamePrefix(prevButtonNamePrefix);                 nextButtons = cast resolveNamePrefix(nextButtonNamePrefix);                 for (item in items)                 {                     item.stop();                 }                 for (prevButton in prevButtons)                 {                     prevButton.buttonMode = true;                     prevButton.addEventListener(MouseEvent.CLICK, prevButton_clickHandler);                 }                 for (nextButton in nextButtons)                 {                     nextButton.buttonMode = true;                     nextButton.addEventListener(MouseEvent.CLICK, nextButton_clickHandler);                 }             }         }         return value;     }     public function new(?mc:MovieClip)     {         super();         this.mc = mc;     }     //... } 

Редкая игра может состоять только из одного класса. Мы же тут изобретаем общий подход для всех игр. Поэтому придумаем для примера функционал хотя бы еще на один класс. Два класса — это уже хоть какой-то материал для обобщения. Хм…, допустим, мы захотим двигать мышкой (dragging) одежду (создадим новый жанр — раздевалка). Не будем лепить новый код в классе Dresser, а сразу создадим отдельный класс Drag. Сделаем его по образцу Dresser, то есть с сеттером mc:

class Drag {     private var stage:Stage;     private var mouseDownX:Float;     private var mouseDownY:Float;     public var isDragging(default, null):Bool;      public var mc(default, set):MovieClip;     public function set_mc(value:MovieClip):MovieClip     {         if (mc != value)         {             // Unassign previous mc             if (mc != null)             {                 mc.removeEventListener(MouseEvent.MOUSE_DOWN, mc_mouseDownHandler);                 if (stage != null)                 {                     stage.removeEventListener(MouseEvent.MOUSE_UP, stage_mouseUpHandler);                     stage.removeEventListener(MouseEvent.MOUSE_MOVE, stage_mouseMoveHandler);                 }             }              mc = value;              // Assign new mc             if (mc != null)             {                 stage = mc.stage;                 mc.addEventListener(MouseEvent.MOUSE_DOWN, skin_mouseDownHandler);                 if (stage != null)                 {                     stage.addEventListener(MouseEvent.MOUSE_UP, stage_mouseUpHandler);                 }             }         }         return value;     }      public function new(?mc:MovieClip)     {         this.mc = mc;     }     private function skin_mouseDownHandler(event:MouseEvent):Void     {         if (stage != null)         {             isDragging = true;             mouseDownX = mc.parent.mouseX - mc.x;             mouseDownY = mc.parent.mouseY - mc.y;             stage.addEventListener(MouseEvent.MOUSE_MOVE, stage_mouseMoveHandler);         }     }     private function stage_mouseUpHandler(event:MouseEvent):Void     {         if (stage != null)         {             isDragging = false;             stage.removeEventListener(MouseEvent.MOUSE_MOVE, stage_mouseMoveHandler);         }     }     private function stage_mouseMoveHandler(event:MouseEvent):Void     {         mc.x = mc.parent.mouseX - mouseDownX;         mc.y = mc.parent.mouseY - mouseDownY;     } } class Dresser {     //...     private var drags:Array<Drag>;      public var mc(default, set):MovieClip;     public function set_mc(value:MovieClip):MovieClip     {         if (mc != value)         {             // Unassign previous mc             if (mc != null)             {                 for (drag in drags)                 {                     drag.mc = null;                 }                 drags = null;                 //...             }              mc = value;              // Assign new mc             if (mc != null)             {                 //...                 drags = [for (item in items) new Drag(item)];             }         }         return value;     }     //... } 

Теперь, если нам понадобится добавить перетаскивание любого объекта на экране, нам достаточно будет написать всего лишь одну строку: new Drag(some_mc);. Вот мы уже и подошли к повторному использованию кода!

Не нужно быть гением, чтобы увидеть, что оба наших классов имеют нечто общее. У обоих есть очень похожий сеттер — mc. В разных классах они делают разное, но шаблон явно прослеживается. Также оба класса являются оберткой вокруг графического объекта. Оберткой, которая добавляет логику к графике. Причем не только добавляет (dresser.mc = some_mc;), но и может очищать от нее (dresser.mc = null;).

Если нам понадобится реализовать кнопку, чекбокс, радио-кнопку, текстовую метку, список, скроллер, да что угодно работающее с графикой — удобнее всего их будет сделать точно так же. Вообще, все приложение целиком можно собрать из таких вот объектов-компонентов. Поэтому, почему бы нам не создать для всех них один общий базовый класс. Назовем его компонент (Component — от лат. componens «составляющий», род. пад. componentis). В программировании компонентами обычно называются классы, предназначенные для повторного использования и развёртывания. Именно так мы их и намереваемся использовать. Все сходится — компоненты.

Итак, произведем Extract class refactoring:

class Component {     public var mc(default, set):MovieClip;     // No need to override. Pattern: Template Method     public function set_mc(value:MovieClip):MovieClip     {         if (mc != value)         {             // Unassign previous mc             if (mc != null)             {                 unassignMC();             }              mc = value;              // Assign new mc             if (mc != null)             {                 assignMC();             }         }         return value;     }      public function new(?mc:MovieClip)     {         this.mc = mc;     }     // Override     private function assignMC():Void     {     }     // Override     private function unassignMC():Void     {     } } class Dresser extends Component {     private var drags:Array<Drag>;     //...     override private function assignMC():Void     {         super.assignMC();          items = cast resolveNamePrefix(itemNamePrefix);         prevButtons = cast resolveNamePrefix(prevButtonNamePrefix);         nextButtons = cast resolveNamePrefix(nextButtonNamePrefix);         for (item in items)         {             item.stop();         }         for (prevButton in prevButtons)         {             prevButton.buttonMode = true;             prevButton.addEventListener(MouseEvent.CLICK, prevButton_clickHandler);         }         for (nextButton in nextButtons)         {             nextButton.buttonMode = true;             nextButton.addEventListener(MouseEvent.CLICK, nextButton_clickHandler);         }         drags = [for (item in items) new Drag(item)];     }     override private function unassignMC():Void     {         for (prevButton in prevButtons)         {             prevButton.removeEventListener(MouseEvent.CLICK, prevButton_clickHandler);         }         for (nextButton in nextButtons)         {             nextButton.removeEventListener(MouseEvent.CLICK, nextButton_clickHandler);         }         for (drag in drags)         {             drag.mc = null;         }         items = null;         prevButtons = null;         nextButtons = null;         drags = null;          super.unassignMC();     }     //... } 

Сеттер мы сделали по паттерну шаблонный метод (Template method). Тогда нам не нужно при переопределении set_mc() делать каждый раз проверки if (mc != null). Во-первых, это скучная рутина — печатать одно и то же. Во-вторых, об этом нужно помнить. А идеальный метод не должен заставлять нас что-либо помнить, когда мы его переопределяем. Мы всегда должны в подклассе иметь возможность писать что-угодно, в том числе и — ничего. Всё, о чем не надо забывать, обязательно однажды будет забыто. Именно поэтому и были добавлены методы assignMC() и unassignMC(). Теперь переопределяться должны они, а не set_mc().

Однако, тут обнаруживается один недостаток. Не все графические объекты на экране могут быть мувиклипами (MovieClip). Бывают еще SimpleButton и Shape. MovieClip и SimpleButton имеют общим родительским классом InteractiveObject, а с Shape — аж только DisplayObject (поэтому Shape и не воспринимает события мышки). Значит, чтобы сделать класс Component по-настоящему универсальным, будем использовать в качестве базового класса для графики не MovieClip, а DisplayObject, а само свойство mc переименуем в более абстрактный skin:

class Component {     public var skin(default, set):DisplayObject;     // No need to override. Pattern: Template Method     public function set_skin(value:DisplayObject):DisplayObject     {         if (skin == value)         {             return value;         }         // Unassign previous skin         if (skin != null)         {             unassignSkin();         }          skin = value;         interactiveObject = Std.downcast(value, InteractiveObject);         simpleButton = Std.downcast(value, SimpleButton);         container = Std.downcast(value, DisplayObjectContainer);         sprite = Std.downcast(value, Sprite);         mc = Std.downcast(value, MovieClip);          // Assign new skin         if (skin != null)         {             assignSkin();         }         return value;     }     public var interactiveObject(default, null):InteractiveObject;     public var simpleButton(default, null):SimpleButton;     public var container(default, null):DisplayObjectContainer;     public var sprite(default, null):Sprite;     public var mc(default, null):MovieClip;      public function new(?skin:MovieClip)     {         this.skin = skin;     }     // Override     private function assignSkin():Void     {     }     // Override     private function unassignSkin():Void     {     } }

Чтобы не заниматься дополнительным приведением типов, если нам вдруг понадобятся свойства мувиклипа или кнопки, мы сделали все возможные приведения типов заранее и сохранили ссылки на них в свойствах: interactiveObject, simpleButton, container, sprite, mc.

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

Концепция компонентов позволяют нам писать любую логику отображения: от простых UI-элементов до сложных панелей, состоящих из других компонентов. Так, можно, как из кубиков, собрать что-угодно вплоть до целого приложения. Со временем у нас накопятся целые библиотеки компонентов, так что мы сможем собирать новые игры из готовых частей, не создавая ни одного нового класса. Представьте себе приложение состоящее из одного Main-класса, конфигурационного JSON- или YAML-файла и ассетов… Вот житуха! Но об этом пока еще рано говорить.

< Назад | Начало | Вперед >


ссылка на оригинал статьи https://habr.com/ru/post/675600/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *