С примером рендеринга игровой сцены

Возможность TypeScript определять поведение, используя несколько интерфейсов — очень мощная штука. Эта возможность предоставляет абстракцию, взаимодействие только через интерфейсы без использования классов.
Способность реализовывать несколько интерфесов решает некоторые сложности наследования, которые возникали с использованием обычных классов.
Интерфейсы также определяют полиморфизм, дают возможность различным классам определять поведение, не связанное с реализацией интерфеса. Классы, которые реализуют один и тот же интерфейс, могут заменять друг друга.
Able интерфейсы
Условности .NET Framework’а об able интерфейсах всегда ассоциируются с вещами по типу IEquatable или IComparable, которые обозначают действие интерфейса прилагательным с суффиксом «мый».
Например, у вас есть приложение для рисования, состоящее из некоторых сущностей, которые реализуют собственные операции рендера.
Эта функция render() будет существовать в IRenderable интерфейсе:
export interface IRenderable { render(): void; }
Несколько классов, например Circle и Rectangle , могут реализовать этот интерфейс:
export class Circle implements IRenderable { render(): void { console.log("Circle rendering code here...") } } export class Rectangle implements IRenderable { render(): void { console.log("Rectangle rendering code here...") } }
Удобство состоит в том, чтобы не использовать в качестве типа конкретные классы, а использовать интерфейс. Например, вы хотите создать какую-нибудь фигуру:
const shape: IRenderable = new Circle(); shape.render();
Или изменить тип фигуры на другой класс, который реализует тот же интерфейс:
let shape: IRenderable; shape = new Circle(); shape.render(); shape = new Rectangle(); shape.render();
Практический пример — добавление фигур в коллекцию, которая потом будет обработана для вывода на канвас.
const shapes: IRenderable[] = [ new Circle(), new Rectangle() ]; for (let shape of shapes) { shape.render(); }
Полный пример с использованием библиотеки PIXI:
export interface IRenderable { render(graphics: PIXI.Graphics): void; } export class Circle implements IRenderable { public radius: number = 100; render(graphics: PIXI.Graphics): void { graphics.drawCircle(0, 0, this.radius); } } export class Rectangle implements IRenderable { public width: number = 100; public height: number = 100; render(graphics: PIXI.Graphics): void { graphics.drawRect(0, 0, this.width, this.height); } } // Определение фигур и контекста для графики const shapes: IRenderable[] = [new Circle(), new Rectangle()]; const graphics = new PIXI.Graphics(); for (let shape of shapes) { shape.render(graphics); }
Создание интерфейсов
Допустим, вы хотите создать игровую сцену, используя движок для рендера — давайте посмотрим на встроенный в PIXI движок и на собственный.
Наш интерфейс для игровой сцены будет содержать движок обощенного типа (generic type):
export interface IGameScene<T> { engine: T; }
В интерфейсе движка будет функция start() , которая будет запускать процесс рендера:
export interface IEngine { start(): void; }
Первый движок будет использовать обычный Ticker движок, который есть в PIXI. Назовем этот движок TickerEngine , он будет реализовывать IEngine интерфейс:
export class TickerEngine implements IEngine { start(): void { console.log("Starting ticker engine..."); const renderer = new PIXI.Renderer(); const scene = new PIXI.Container(); const ticker = new PIXI.Ticker(); ticker.add(() => { console.log("ticker frame handler"); renderer.render(scene); }, PIXI.UPDATE_PRIORITY.LOW); ticker.start(); } }
Второй движок будет использовать пользовательский обработчик. Назовем этот движок LoopEngine , который также будет реализовывать IEngine интерфейс:
export class LoopEngine implements IEngine { renderer = new PIXI.Renderer(); scene = new PIXI.Container(); start(): void { console.log("Starting loop engine..."); requestAnimationFrame(this.frameHandler); } private frameHandler = () => { console.log("loop frame handler"); this.renderer.render(this.scene); requestAnimationFrame(this.frameHandler); }; }
Класс игровой сцены будет реализовывать IGameScene интерфейс типа IEngine. Это позволит нам указывать конкретный движок, который мы будем использовать и автоматически запускать в конструкторе:
export class Scene implements IGameScene<IEngine> { engine: IEngine; constructor(engine: IEngine) { this.engine = engine; this.engine.start(); } }
Теперь создадим экзепляр игровой сцены, используя TickerEngine :
Сделаем тоже самое, но теперь с LoopEngine :
Этот подход позволяет нам создать похожее поведение для разных сущностей.
Дальнейшая инкапсуляция
Допустим, вы оцениваете два физических движка для игры и хотите менять их. В обработчике анимаций в вашем приложении вы хотите использовать функцию step(), чтобы вызывать вычисления движка по одному. Начнем с определения интерфейса:
export interface IPhysicsEngine { step(): void; }
Скажем, вы нашли библиотеку физического движка Box 2D, который взаимодействует с вашим графическим фреймворком. Создадим вокруг него обертку, вызывающую следующее вычисление:
export class Box2DPhysicsEngine implements IPhysicsEngine { step(): void { // Какая-то реализация движка Box 2D } }
Возможно, вы хотите сравнить Box 2D с Planck. Создадим обертку вокруг этого движка:
export class PlanckPhysicsEngine implements IPhysicsEngine { step(): void { // Какая-то реализация движка Planck } }
В классе игры создадим экземпляр физического движка. В обработчике будем ссылаться на экземпляр и вызывать метод step() :
export class GameScene { private physicsEngine: IPhysicsEngine; constructor(physicsEngine: IPhysicsEngine) { this.physicsEngine = physicsEngine; } private frameHandler = () => { this.physicsEngine.step(); requestAnimationFrame(this.frameHandler); }; }
Попробуем создать игру с помощью движка Box 2D:
const game = new GameScene(new Box2DPhysicsEngine());
А теперь с помощью Planck:
const game = new GameScene(new PlanckPhysicsEngine());
Конечно, смена основной функциональности может быть и сложной, и невозможной¹, это зависит от архитектуры и компонентов. Может быть интересно исследовать различные паттерны, используя обобщенные типы, интерфейсы и инкапсуляцию.
Упрощение
Описание связей с помощью типов упрощает ваш код, требует лишь один интерфейс, который будет определять что-то общее.
Рассмотрим игру в космическом стиле с многоугольниками, летающими в открытом пространстве, у которых есть свои координаты и вращение. Также, вероятно, количество сторон многоугольника уменьшается после попадания в него.
Будем использовать тот же интерфейс IRenderable :
export interface IRenderable { render(graphics: PIXI.Graphics): void; }
Наши фигуры будут иметь две координаты, угол вращения, а также количество сторон и радиус. Определим для этого три разных интерфейса:
export interface IPosition { x: number; y: number; } export interface IRotation { angle: number; } export interface IPolygon { sides: number; radius: number; }
Класс фигуры будет реализовывать IPosition и IRotation интерфейсы без знания того, как потом эту фигуру обработают:
export class Shape implements IPosition, IRotation { x: number = 0; y: number = 0; angle: number = 0; }
Конкретный класс многоугольника унаследует класс фигуры, реализует интерфейсы, который реализует класс фигуры, а помимо этого еще и интерфейсы IPolygon и IRenderable :
export class Polygon extends Shape implements IPolygon, IRenderable { sides: number; radius: number; constructor(sides: number, radius: number) { super(); this.sides = sides; this.radius = radius; } render(graphics: PIXI.Graphics): void { let step = (Math.PI * 2) / this.sides; let start = (this.angle / 180) * Math.PI; let n, dx, dy; graphics.moveTo( this.x + Math.cos(start) * this.radius, this.y - Math.sin(start) * this.radius ); for (n = 1; n <= this.sides; ++n) { dx = this.x + Math.cos(start + step * n) * this.radius; dy = this.y - Math.sin(start + step * n) * this.radius; graphics.lineTo(dx, dy); } } }
Все многоугольники могут быть созданы как:
const triangle = new Polygon(3, 100); const square = new Polygon(4, 100); const pentagon = new Polygon(5, 100); const hexagon = new Polygon(6, 100);
Или определены как отдельные классы:
export class Triangle extends Polygon { constructor(radius: number) { super(3, radius); } } export class Square extends Polygon { constructor(radius: number) { super(4, radius); } } export class Pentagon extends Polygon { constructor(radius: number) { super(5, radius); } }
Сейчас, когда мы определили все эти интерфейсы, мы может забыть обо всех сложностях и сфокусироваться на других задачах.
Для нашего рендерного обработчика важно только то, чтобы все фигуры реализовывали интерфейс IRenderable :
const shapes: IRenderable[] = [triangle, square, pentagon, hexagon]; const graphics = new PIXI.Graphics(); for (let shape of shapes) { shape.render(graphics); }
Возможно, в тестах на попадание в многоугольники, нам нужно будет считать площадь этого многоугольника. Для этого мы может использовать интерфейс IPolygon , чтобы получить доступ к необходимым полям raduis и sides :
/** Подсчет площади многоульника */ export const area = (polygon: IPolygon): number => { const r = polygon.radius; const n = polygon.sides; return (n * Math.pow(r, 2)) / (4 * Math.tan(Math.PI / n)); }; const polygons: IPolygon[] = [ new Triangle(100), new Square(100), new Pentagon(100) ]; for (let polygon of polygons) { console.log(`Area: ${area(polygon)}`); }
Возможно, каждый раз, когда в многоугольник попадают, количество сторон будет уменьшаться до того момента, пока фигура не будет уничтожена:
const hit = (shape: IPolygon) => { shape.sides -= 1; if (shape.sides < 3) { console.log("Enemy destroyed!"); } };
Возможно, при клике вам нужно будет получить доступ к координатам фигуры, для этого можно использовать IPosition интерфейс:
export const onMouseDown = (position: IPosition) => { console.log(position.x, position.y); };
Координаты мыши имеют схожую с IPosition интерфейсом структуру и этим можно пользоваться.
Интерфейсы помогают разделить логику и сделать код более понятным.
¹ нет ничего невозможного, но есть то, что не стоит своих вложений.
Все проблемы в сфере компьютерных наук могут быть решены другим уровнем абстракции. — Дэвид Уиллер
ссылка на оригинал статьи https://habr.com/ru/post/649933/
Добавить комментарий