Игровой движок

August 15, 2022 · View on GitHub

UML диаграмма общих взаимосвязей

classDiagram
  direction RL

  class AbstractGameObject {
    <<abstract>>
    +position : Vector
    +velocity : Vector
    +width : number
    +height : number
    #debug : boolean
    -hasDelete: boolean

    +delete() void
    +init()* void
    +update(dt : number)* void
    #draw() void
    #debugDraw(color : string) void
  }

  class Vector {
    +x : number
    +y : number
  }

  class Player {
    -fireSound : Sound
    -killSound : Sound
    -idleSprite : Sprite
    -explosionSprite : Sprite

    +left() void
    +right() void
    +stop() void
    +fire() void
  }

  class Enemy {
    -fireSound : Sound
    -killSound : Sound
    -idleSprite : Sprite
    -explosionSprite : Sprite
    -dead : boolean

    +fire() void
  }

  class Swarm {
    -enemys : Enemy[0..*]
    +calcCollide() void
    +garbageCollector() void
  }

  class Projectile {
    -sprite : Sprite
  }

  class Sound {
    +name : string
    -buffer : AudioBuffer
    +play() void
  }

  class Sprite {
    +name : string
    +width : number
    +height : number
    +images : ImageBitmap[1..*]
    +draw() void
    +animate() void
    +animateOnce() boolean
  }

  class GameEngine {
    -objects: AbstractGameObject[0..*]

    +init() : Promise<boolean>
    +start() void
    +stop() void
    +addScore(score : number) void
    -gameLoop(nowTime : number) void
    -garbageCollector() void
  }

  GameEngine o-- "0..*" Swarm
  GameEngine o-- "1..2" Player
  GameEngine o-- "0..*" Projectile
  AbstractGameObject <|-- Enemy
  AbstractGameObject <|-- Player
  AbstractGameObject <|-- Swarm
  AbstractGameObject o-- Vector
  AbstractGameObject <|-- Projectile
  Swarm o-- "1..*" Enemy
  Enemy o-- Sprite
  Enemy o-- Sound
  Player o-- Sprite
  Player o-- Sound

Базовые классы

GameEngine

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

Аргументы конструктора тип TGameEngineOptions

ИмяОписание
ctx2D контекст на котором будет рисоваться игра
debugфлаг включения отладочной информации (default: false)
widthширина игрового поля
heightвысота игрового поля
onScoreUpdatecallback вызываемый в момент получения игроком очков
onGameOvercallback вызываемый при окончании игры

Пример инициализации и запуска игрового движка

const gameEngine = new GameEngine({
  /* Опции */
});
gameEngine.registerObject([
  /* Классы игровых объектов: Player и Swarm */
]);
const initResult = await gameEngine.init();
if (!initResult) throw new Error('Ошибка инициализации');
gameEngine.start();
  1. Создается instance игрового движка с указанными опциями
  2. Вызывается метод registerObject() в который передаются классы основных игровых объектов которые будут созданы автоматически в процессе инициализации
  3. Вызывается метод инициализации игрового движка init(), который возвращает Promise<boolean>
  4. При удачной инициализации для старта игрового процесса вызывается метод start()

AbstractGameObject

Абстрактный класс игрового объекта, содержит интерфейс необходимый для оперирования объектами в GameEngine

Аргументы конструктора тип TGameObjectOptions:

ИмяОписание
ctx2D контекст на котором будет рисоваться игра
debugфлаг включения отладочной информации (default: false), по умолчанию при вызове метода debugDraw(color) рисует полый прямоугольник цветом color, который игра интерпретирует как объект
positionкоординаты верхнего левого угла описывающего прямоугольника
velocityскорость движения по оси x и y соответственно
widthширина описывающего прямоугольника
heightвысота описывающего прямоугольника

Абстрактные методы для реализации в классах наследниках:

ИмяОписание
initдолжен содержать в себе всю асинхронную логику инициализации игрового объекта, в случае успешной инициализации возвращать true иначе false
updateдолжен обновлять состояние игрового объекта вызывается в начале расчета кадра отрисовки, на вход получает время прошедшее с прошлого кадра. Должен содержать всю игровую логику (перемещение и т.д.)
drawотрисовка игрового объекта, должен содержать всю логику работы с canvas

Расчет столкновений

Pure function isRectCollide проверяет пересекаются ли между собой да игровых объекта

/* ... */
if (
    obj1.position.x + obj1.width >= obj2.position.x &&
    obj1.position.x <= obj2.position.x + obj2.width &&
    obj1.position.y + obj1.height >= obj2.position.y &&
    obj1.position.y <= obj2.position.y + obj2.width
  )
/* ... */

rectCollide

Sound

Класс работы со звуками, использует AudioContext в конструктор передается instance AudioContext и AudioBuffer который содержит в себе звуковую дорожку. Метод play() однократно воспроизводит звуковую дорожку.

Пример использования:

const sound = new Sound(/* AudioContext */, /* AudioBuffer */);
sound.play();

Sprite

Класс работы с изображениями, использует ImageBitmap.

Аргументы конструктора тип TSpriteOptions:

ИмяОписание
nameимя спрайта
imagesмассив ImageBitmap содержащий кадры анимации если таковая имеется, иначе массив состоит из единственного элемента
animationнеобязательный параметр объект описывающий анимацию, состоит из полей frames - количество кадров в анимации и duration - длительность анимации

Для рисования статичного спрайта используется метод draw(ctx, x, y), где ctx - 2D контекст, x и y - координаты верхней левой точки куда будет выводится изображение, также можно передать 4-ый необязательный параметр frame в котором можно указать номер кадра для отрисовки

Для рисования анимации используется метод animate(ctx, x, y, dt),где ctx - 2D контекст, x и y - координаты верхней левой точки куда будет выводится изображение, dt - время прошедшее с предыдущего кадра. Также для проигрывания анимации один раз может использоваться метод animateOnce(ctx, x, y, dt)

Пример использования:

const sprite = new Sprite({
  /* Опции */
});

// Статическая отрисовка на 2D контексте ctx по координатам x: 10, y: 10
sprite.draw(ctx, 10, 10);

// Рисования анимации на 2D контекста ctx по координатам x: 42, y: 24
sprite.animate(ctx, 42, 24, dt);

// Рисования анимации один раз на 2D контекста ctx по координатам x: 42, y: 24
sprite.animateOnce(ctx, 42, 24, dt);

Note: Методы draw(), animate(), animateOnce() должы использоваться внутри метода update() игровых объектов.

Vector

Класс для хранения и работы с векторной информацией. Хранит в себе две координаты по X и Y осям соответственно. конструктор принимает x и y координаты.

Поддерживаемые методы:

МетодОписание
v1.subtract(v2)вычитает из элементов v1 элементы v2, возвращает новый вектор с результатом
v1.subtract(x1, y1)вычитает из элементов v1 переданные аргументы, соответственно, возвращает новый вектор с результатомd
v1.add(v2)складывает одноименные элементы v1 и v1, возвращает новый вектор с результатом
v1.add(x1, x2)складывает элементы v1 и переданные аргументы соответственно, возвращает новый вектор с результатом
v1.length()вычисляет длину вектора
v1.dor(v2)вычисляет скалярное произведение векторов
v1.normalize()приводит вектор к нормализованному виду

Пример использования:

const v1 = new Vector(2, 3);
const v2 = new Vector(3, 2);

const res = v1.add(v2); // res = {x: 5, y: 5}

Классы игровых объектов

Player

Описывает логику работы с объектом игрока, наследуется от класса AbstractGameObject, расширяет его следующими методами:

МетодОписание
left()двигает игрока влево по игровому полю
right()двигает игрока вправо по игровому полю
stop()останавливает движение игрока
fire()производит выстрел игрока

Переопределен метод delete базового класса для проигрывания звука взрыва игрока

Аргументом конструктора является тип TPlayerOptions который добавляет в тип TGameObjectOptions свойство onFire которое является callback функции вызываемой в момент произведения выстрела игрока, на вход получает объект пули игрока.

Swarm

Описывает логику работы с объектом роя, наследуется от класса AbstractGameObject, расширяет его следующими методами:

МетодОписание
calcCollide()Проверяет факт столкновения пули и каждого врага внутри роя
garbageCollector()Удаляет врагов внутри роя, которые считаются убитыми
revertVelocityX()Изменяет скорость движение роя по оси X на противоположную

Аргументом конструктора является тип TSwarmOptions который добавляет в тип TGameObjectOptions свойство onFire которое является callback функции вызываемой в момент произведения выстрела одного из врагов внутри роя, на вход получает объект пули игрока и свойство formation которое описывает построение врагов внутри роя.

Пример свойства formation:

[
  [0, 0, 0, 0, 3, 3, 3, 3, 0, 0, 0, 0],
  [0, 0, 0, 2, 2, 2, 2, 2, 2, 0, 0, 0],
  [0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0],
  [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
  [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
];

Цифры внутри массива, тип врага, используемый в класса Enemy ыдля выбора спрайта и логики поведения врага

Enemy

Описывает логику работы с объектом врага, наследуется от класса AbstractGameObject, расширяет его следующими методами:

МетодОписание
fire()производит выстрел врага
isDeadгеттер, возвращает true если игрок уничтожил этого врага

Переопределен метод delete базового класса для проигрывания звука взрыва врага

Аргументом конструктора является тип TEnemyOptions который добавляет в тип TGameObjectOptions свойство onFire которое является callback функции вызываемой в момент произведения выстрела врага и свойство type которое определяет тип врага используемый для выбора спрайта и логики поведения врага.

Projectile

Описывает объект пули, наследуется от класса AbstractGameObject. Аргументом конструктора является тип TProjectileOptions который добавляет в тип TGameObjectOptions свойство type которое является типом пули (ProjectileType.player или ProjectileType.enemy) и свойство sprite которое определяет спрайт пули.

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

SoundMap

Используется для загрузки и хранения всех звуков игры.

Аргументом конструктора является объект типа TAudioOptions:

СвойствоОписание
soundМассив объектов содержащих поля: name - имя звука и url - адрес загрузки для каждого звука в игре

Имеет один публичный метод getSoundByName(name) возвращающий экземпляр класса Sound

Пример использования:

const soundMap = new SoundMap({
  sounds: [
    {
      name: 'fire',
      url: 'https://galaga.com/assets/sound/fire.mp3',
    },
    {
      name: 'kill',
      url: 'https://galaga.com/assets/sound/kill.mp3',
    },
  ],
});

const sound = soundMap.getSoundByName('fire');

SpriteMap

Используется для загрузки и нарезки каты спрайтов на отдельные изображения.

Аргументом конструктора является объект типа TSpriteMapOptions:

СвойствоОписание
urlадрес для загрузки общей карты спрайтов
spriteSizeразмер спрайта в карте ('16', '32', '48', '64')
rowsколичество строк спрайтов в карте
colsколичество колонок в карте
spritesмассив описания спрайтов

Описание спрайтов представляет собой объект типа TSpriteInfo:

СвойствоОписание
rowномер колонки в карте или диапазон строк
colномер строки в карте или диапазон колонок
nameимя спрайта
scaleнеобязательный параметр масштаб отрисовки спрайта
animationнеобязательный параметр объект описывающий анимацию, состоит из полей frames - количество кадров в анимации и duration - длительность анимации

Имеет один публичный метод getSpriteByName(name) возвращающий экземпляр класса Sound

Пример:

const spriteMap = new SpriteMap({
  url: 'http://galaga.com/assets/sprite-map.png',
  spriteSize: '16',
  rows: 7,
  cols: 12,
  sprites: [
    {
      name: 'player',
      row: 0,
      col: 0,
      scale: 3, // увеличить спрайт в 3 раза
    },
    // получает объединенный спрайт состоящий из 4 квадратов 16x16
    // по координатам  0 и 1 строки, 1 и 2 столбцы. И так как указано количество кадров 4
    // повторяет операцию со смещением окна вырезания на ширину получившегося прямоугольника
    {
      name: 'player-explosion',
      row: [0, 1],
      col: [1, 2],
      scale: 3,
      animation: { frames: 4, duration: 2000 },
    },
  ],
});

const playerSprite = spriteMap.getSpriteByName('player');