npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

gcs-arch

v0.2.3

Published

## Описание

Downloads

640

Readme

GCS - движок для создания игр и анимаций на канвасе

Описание

Движок основанный на реализации связки паттернов GameLoop, Update, Component. Не привязан к реализации рендеринга на канвасе, то есть рендерер необходимо реализовывать и подключать в проекте. Не зависит от размерности пространства, поэтому можно использовать как с pixi.js, так и с three.js. Реализация игры сводится к двум основным шагам

  • описание игровых объектов
  • описание взаимодействия этих объектов

Как устроен

Основные понятия

Игровой цикл (GameLoop) - основа для любой игры, так как в любой игре (кроме пошаговых, например, шахмат) существует множество игровых объектов и их взаимодействие на каждом кадре. Игровой жизненный цикл - Основной интерфейс для компонентов игрового движка, от которого они наследуются

export interface GameLifeCycle {
  start?(): void; // старт игрового цикла
  stop?(): void; // остановка игрового цикла (окончание)
  update?(delta: number): void; // обновление игрового цикла
  destroy?(): void; // деструктуризация игры
  pause?(): void; // пауза в игре
  resume?(): void; // возобновление игры
}

Метод Update - В игровом мире есть набор объектов. В каждом из объектов реализован метод Update, который симулирует поведение объекта каждый кадр. Каждый кадр игра обновляет объект из набора. Компонент Component - Класс для приватной логики или данных игрового объекта, например, позиционирование, отображение, управление. Отвечает принципу единой ответственности. Поддерживает методы жизненного цикла. Игровой объект (GameObject) - Класс-контейнер для игровых компонентов. Если у игровой сущности есть позиционирование в пространстве и визуальное отображение, то это Игровой объект. Поддерживает методы жизненного цикла. Игровой мир (GameWorld) - Класс-контейнер для игровых объектов. Поддерживает методы жизненного цикла. Игровые скрипты (GameScripts) - Нужны для описания общеигровой логики, например для коллизий или управления группой игровых объектов. Поддерживают методы жизненного цикла

Устройство GCSEngine

Класс GCSEngine - фасад движка, точка входа для всего доступного API. Доступное API:

  • добавление в мир игровых объектов
  • создание скриптов для игры
  • управление игровым циклом (поддержка методов GameLifeCycle)
  • управление игровым временем
  • доступ к рендереру и его контейнеру

Класс GameLifeCycle - единая точка управления всеми игровыми сущностями, которые поддерживают GameLifeCycle (то есть наследуются от этого интерфейса). Это могут быть и игровые объекты, и время, и отрисовщик, и прочее. Это позволяет на каждый кадр обновлять все компоненты игры и ставить их единовременно на паузу.

UML-Диаграммы

  • diagrams/GCSEngine.adoc - Общая схема устройства GCSEngine
  • diagrams/GameLoop.adoc - Схема для игрового цикла
  • diagrams/GameObject.adoc - Схема для игрового объекта
  • diagrams/SpecificGameObject.adoc - Схема для конкретного игрового объекта
  • diagrams/GameWorld.adoc - Схема для игрового мира
  • diagrams/GameScripts.adoc - Схема для игровых скриптов
  • diagrams/Renderer.adoc - Схема для отрисовщика

API

  • src/core/GCSEngine.ts - GCSEngine фасад для API движка

Основные сущности

  • src/gcsEngineFactory.ts - создание инстанса GCSEngine
  • src/di/di.container.ts - DI контейнер
  • src/components/index.ts - компоненты для игровых объектов
  • src/core/updatable-entities/GameObject.ts - игровой объект
  • src/core/updatable-entities/GameWorld.ts - игровой мир - контейнер для игровых объектов
  • src/core/updatable-entities/GameScripts.ts - игровые скрипты - набор исполняемых скриптов для общеигровой логики
  • src/core/updatable-entities/GameRenderer.ts - отрисовщик на канвасе

Алгоритм интеграции

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

  1. Создание рендерера и добавление игровых объектов в реальный рендерер
  2. Создание игровых объектов и их компонентов
  3. Создание игровых скриптов
  4. Старт игровой сцены/уровня

Пример интеграции на pixi.js

Пример реализации каждого из шагов, указанных выше, для pixi.js

1. Написание отрисовщика и компонентов отображения

Так как движок абстрактный и не знает о том, как рендерится игра, то нужно написать рендерер, который должен реализовывать интерфейс IRenderer. Пример такой реализации для pixi.js

import {Application, Container} from 'pixi.js';
import {IDimensions, IRenderer} from '@specials/gcs-engine';

export class PixiRenderer implements IRenderer {
  public app: Application;
  public rootContainer: Container;

  public create(containerNode: HTMLElement): void {
    const {width, height} = containerNode.getBoundingClientRect();

    this.app = new Application({
      width,
      height,
      antialias: true,
      resolution: this.getAppDPR(),
      autoStart: false,
    });
    this.app.ticker.autoStart = false;

    const canvas = this.app.view as HTMLCanvasElement;
    containerNode.appendChild(canvas);

    canvas.style.width = '100%';
    canvas.style.height = '100%';

    this.rootContainer = new Container();
    this.app.stage.addChild(this.rootContainer);
  }

  public getAppDPR(): number {
    return window.devicePixelRatio;
  }

  public getFrameDimensions(): IDimensions {
    const dpr = this.getAppDPR();

    return {
      width: this.app.renderer.width / dpr,
      height: this.app.renderer.height / dpr,
    };
  }

  public stop(): void {
    this.app.stop();
  }

  public start(): void {
    this.app.start();
  }

  public pause(): void {
    this.app.stop();
    this.app.stage.eventMode = 'none';
  }

  public resume(): void {
    this.app.start();
    this.app.stage.eventMode = 'auto';
  }

  public destroy(): void {
    if (this.app) {
      this.app.destroy();
    }
  }
}

Затем нужно передать инстанс PixiRenderer при создании инстанса движка через DI

container
  .bind(DI_TOKENS.gcsEngine)
  .toInstance(() => gcsEngineFactory(container.get(DI_TOKENS.pixiRenderer)))
  .inSingletonScope();

Следующая задача заключается в том, чтобы связать игровые объекты из движка с реальным рендерером. В pixi.js у объектов есть позиция и отображение, то есть нужно связать их с игровым объектом. Как вариант, это можно сделать с помощью компонента ViewRendererComponent.

import {BaseComponent, GCSEngine, Transform2dComponent} from '@specials/gcs-engine';

import {ViewComponent} from './ViewComponent';

export class ViewRendererComponent extends BaseComponent {
  private transformComponent: Transform2dComponent;
  private viewComponent: ViewComponent;

  constructor(private readonly engine: GCSEngine) {
    super();
  }

  public create(): void {
    this.viewComponent = this.gameObject.getComponentByTag<ViewComponent>('ViewComponent')!;
    this.transformComponent = this.gameObject.getComponent<Transform2dComponent>(Transform2dComponent)!;

    // связываем позицию, скейл, и поворот объекта в pixi.js с позицией игрового объекта
    this.transformComponent.bindPosition = this.viewComponent.view.position;
    this.transformComponent.bindScale = this.viewComponent.view.scale;
    this.transformComponent.bindRotation = this.viewComponent.view.rotation;

    // при добавлении игрового объекта в мир, отображения для pixi.js так же добавится в его контейнер
    this.engine.renderer.container.addChild(this.viewComponent.view);
  }

  public destroy(): void {
    // удаление из контейнера pixi при удалении игрового объекта из мира
    this.engine.renderer.container.removeChild(this.viewComponent.view);
  }
}

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

import {BaseComponent, Transform2dComponent} from '@specials/gcs-engine';
import {Container} from 'pixi.js';

export abstract class ViewComponent<V extends Container = Container> extends BaseComponent {
  protected transformComponent: Transform2dComponent;
  public static componentName = 'ViewComponent';
  public name = ViewComponent.componentName;

  public view: V;

  public create(): void {
    this.transformComponent = this.gameObject.getComponent<Transform2dComponent>(Transform2dComponent)!;
    this.view = this.renderView();
  }

  protected abstract renderView(): V;
}

2. Пример написания игрового объекта

Игровой объект - это абстрактный контейнер для конкретных компонентов игрового объекта. Он должен собираться через набор компонентов. Для примера рассмотрим игровой объект "Шайба", которая может двигаться в пространстве. Для такого игрового объекта понадобятся следующие компоненты:

  • Transform2dComponent - позиционирование в двумерном пространстве
  • Movement2dComponent - движение в двумерном пространстве
  • PuckViewComponent - внешний вид фишки
  • ViewRendererComponent - добавление и удаление в отрисовщик

Компоненты Transform2dComponent и Movement2dComponent уже есть в библиотеке компонентов, поэтому просто импортируем их import {Movement2dComponent, Transform2dComponent} from '@specials/gcs-engine';

Компонент PuckViewComponent нужно написать, так как отображение обычно уникальное для каждого игрового объекта. ViewComponent был написан в пункте 1

export class PuckViewComponent extends ViewComponent<Graphics> {
  public shapeType: ViewComponentShapeType = ViewComponentShapeType.GraphicsCircle;

  public renderView(): Graphics {
    return new Graphics().beginFill(0x000000).drawCircle(0, 0, 3.5).endFill();
  }
}

Компонент ViewRendererComponent уже был написан в пункте 1, импортируем его

Теперь можно собрать игровой объект Puck. Для этого можно импортировать GameObject из библиотеки и наследоваться от него

export class Puck extends GameObject {
  constructor(
    public readonly transformComponent: Transform2dComponent,
    public readonly viewComponent: PuckViewComponent,
    public readonly movementComponent: Movement2dComponent,
    private readonly viewRendererComponent: ViewRendererComponent,
  ) {
    super();

    this.tag = 'puck';

    this.addComponent(
      this.transformComponent,
      this.viewComponent,
      this.movementComponent,
      this.viewRendererComponent,
    );
  }
}

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

Второй вариант создания игрового объекта через класс-фабрику игровых объектов. Это правильный вариант, но не очень удобный,так как чтобы получить компонент игрового объекта, это нужно делать через метод getComponent, который может вернуть undefined, из-за чего придется делать множество проверок, что ослабить читабельность.

export class ProjectGameObjectFactory {
  constructor(
    private readonly gameObjectFactory: () => GameObject,
    private readonly transformComponentFactory: () => Transform2dComponent,
    private readonly puckViewComponentFactory: () => PuckViewComponent,
    private readonly movementComponentFactory: () => Movement2dComponent,
    private readonly viewRendererComponentFactory: () => ViewRendererComponent,
  ) {}

  public createPuck(): GameObject {
    const puck = this.gameObjectFactory();
    puck.tag = 'puck';
    puck.addComponent(
      this.transformComponentFactory(),
      this.puckViewComponentFactory(),
      this.movementComponentFactory(),
      this.viewRendererComponentFactory(),
    );

    return puck;
  }
}

Теперь нужно провести настройку движения, это обычно делается в классе Spawner (реализовывается отдельно в проекте, не входит в библиотеку)

export class GameObjectsSpawner {
  constructor(
    private readonly puckFactory: () => Puck,
    private readonly engine: GCSEngine,
  ) {}

  public spawnPuck(directionAngleInRad: number, velocity: number): Puck {
    const puck = this.puckFactory();
    this.engine.world.addGameObject(puck);

    puck.movementComponent.velocity = velocity;
    puck.movementComponent.directionFromAngle = directionAngleInRad;

    return puck;
  }
}

3. Пример скрипта

Игровой объект есть, теперь можно написать какой-нибудь скрипт по обработке общеигровой логике. Скрипт поддерживает все методы игрового цикла, поэтому с помощью него можно написать инициализацию игрового мира (то есть настроить уровень игры), а также обработку логики на каждый кадр Обычно в каждой игре есть инициализация уровня и коллизии в нем, это можно сделать с помощью скриптов. Рассмотрим примеры реализации таких скриптов

** Инициализация уровня **


export class StartUpScript implements IGameScript {
  constructor(
    private readonly spawner: GameObjectsSpawner,
    private readonly engine: GCSEngine,
  ) {}

  public create(): void {
    const world = this.engine.world;
    this.spawner.spawnPuck(Math.PI / 2, 5);
    this.spawner.spawnPuck(3 * Math.PI / 2, 5);
    this.spawner.spawnPuck(-3 * Math.PI / 2, 5);
  }
}

** Коллизии **

export class CollisionsScript implements IGameScript {
  private gameFieldBounds: Rectangle;

  constructor(private readonly engine: GCSEngine) {}

  public create(): void {
    const {width, height} = this.engine.renderer.dimensions;
    this.gameFieldBounds = new Rectangle(0, 0, width, height);
  }

  public update(): void {
    const puckList = this.engine.world.getGameObjectList<Puck>(Puck);
    // либо const puckList = this.engine.world.getGameObjectListByTag<Puck>('puck');

    puckList.forEach(p => this.checkGameObjectInBounds(p))
  }

  // шайбы меняют направление на противоположное, если выходят за пределы игрового поля
  private checkGameObjectInBounds(puck: Puck): void {
    const halfWidth = puck.viewComponent.halfWidth;
    const halfHeight = puck.viewComponent.halfHeight;

    const x = puck.transformComponent.x;
    const y = puck.transformComponent.y;

    // у объекта центр координат совпадает с центром
    if (x - halfWidth <= this.gameFieldBounds.x && y + halfHeight < this.gameFieldBounds.height) {
      puck.movementComponent.dirX *= -1;
    }
    else if (x + halfWidth >= this.gameFieldBounds.width && y + halfHeight < this.gameFieldBounds.height) {
      puck.movementComponent.dirX *= -1;
    }
    else if (y - halfWidth <= this.gameFieldBounds.y) {
      puck.movementComponent.dirY *= -1;
    }
    else if (y + halfWidth >= this.gameFieldBounds.y) {
      puck.movementComponent.dirY *= -1;
    }
  }
}

4. Старт игровой сцены/уровня

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

export class TimeAccuracyChallengeScene {
  constructor(
    private readonly engine: GCSEngine,
    private readonly startUpScript: StartUpScript,
    private readonly collisionsScript: CollisionsScript,
  ) {}

  public create(container: HTMLDivElement): void {
    this.engine.renderer.create(container);

    this.engine.scripts.addScript(
      this.startUpScript,
      this.collisionsScript,
    );

    // стартуем игровой цикл
    this.engine.start();
  }
}

И компонент на React, который использует эту сцену для добавления канваса в html-контейнер

export function GameContainer(): JSX.Element {
  const game = useGame(); // TimeAccuracyChallengeScene
  const gameContainerRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (gameContainerRef.current) {
      game.create(gameContainerRef.current)
      return game.destroy
    }
  });

  return <div ref={gameContainerRef} style={{width: window.innerWidth, height: window.innerHeight}} />;
}