Игра космическая стрелялка в TypeScript


Недавно я начал изучать машинопись и создал небольшой космический шутер. Я хотел бы получить некоторую обратную связь на структуру моего кода. У меня есть некоторые классы как статические и экземплярные методы со ссылками назад и вперед, интересно, если я мог бы сделать это как-то чище.

Ссылка на полный код на GitHub
Игры: Светлячок (звук по умолчанию)

RenderObject.ТС

import { IRenderObect } from "../interfaces/IRenderObject";
import { Point } from "./Point";

const images: { [imagePath: string]: HTMLImageElement } = {};

let renderContext: CanvasRenderingContext2D;
const objectList: RenderObject[] = [];
export class RenderObject {
  public static setRenderContext(ctx: CanvasRenderingContext2D) {
    renderContext = ctx;
  }
  public static getObjectList() {
    return objectList as ReadonlyArray<RenderObject>;
  }
  public static setDimensionsForImage(imageSrc: string) {
    objectList
      .filter(o => o.image.src === imageSrc)
      .forEach(o => o.setDimensions());
  }
  public static displaceAll(pointA: Point, pointB: Point) {
    const displaceX = pointA.x - pointB.x;
    const displaceY = pointA.y - pointB.y;
    objectList.forEach(o => {
      o.center = new Point(o.center.x + displaceX, o.center.y + displaceY);
    });
  }
  public center: Point = new Point(0, 0);
  public angle: number;
  public width: number = 1;
  public height: number = 1;
  public image: HTMLImageElement;
  protected maxLifeSpan: number;
  private frameIndex: number = 0;
  private lifeSpan: number = 0;
  private ticksCurrentFrame: number = 0;

  constructor(private options: IRenderObect) {
    objectList.push(this);

    this.angle = options.angle || 0;
    this.maxLifeSpan = options.lifeSpan || Infinity;

    if (!images[options.imageSrc]) {
      this.image = new Image();
      this.image.src = options.imageSrc;
      this.image.onload = this.onImageLoad.bind(this);
      images[options.imageSrc] = this.image;
    } else {
      this.image = images[options.imageSrc];
      this.setDimensions();
    }

    if (this.options.sprite && !this.options.sprite.loop) {
      this.maxLifeSpan =
        this.options.sprite.frames * this.options.sprite.ticksPerFrame;
    }

    this.center = options.initialPosition;
  }

  public update() {
    if (this.lifeSpan > this.maxLifeSpan) {
      return this.destroy();
    }

    this.lifeSpan++;
    this.spriteActions();
    this.draw();
  }

  public destroy() {
    const index = objectList.indexOf(this);
    objectList.splice(index, 1);
  }

  protected draw() {
    const x = this.center.x;
    const y = this.center.y;
    const degrees = this.angle + 90;
    const angleInRadians = degrees * Math.PI / 180;

    renderContext.translate(x, y);
    renderContext.rotate(angleInRadians);

    if (this.options.sprite) {
      renderContext.drawImage(
        this.image,
        this.frameIndex * this.width,
        0,
        this.width,
        this.height,
        -this.width / 2,
        -this.height / 2,
        this.width,
        this.height
      );
    } else {
      renderContext.drawImage(this.image, -this.width / 2, -this.height / 2);
    }

    renderContext.rotate(-angleInRadians);
    renderContext.translate(-x, -y);
  }

  protected onImageLoad() {
    RenderObject.setDimensionsForImage(this.image.src);
  }

  private setDimensions() {
    this.width = this.options.sprite
      ? this.image.width / this.options.sprite.frames
      : this.image.width;
    this.height = this.image.height;
  }

  private spriteActions() {
    if (!this.options.sprite) {
      return;
    }

    this.ticksCurrentFrame++;

    if (this.ticksCurrentFrame > this.options.sprite.ticksPerFrame) {
      this.ticksCurrentFrame = 0;
      if (this.frameIndex < this.options.sprite.frames - 1) {
        this.frameIndex++;
      } else {
        this.frameIndex = 0;
      }
    }
  }
}

Лица.ТС

import * as Calculations from "../Calculations";
import { IDirection } from "../interfaces/IDirection";
import { IStatus } from "../interfaces/IStatus";
import { Point } from "./Point";
import { RenderObject } from "./RenderObject";

const entityList: Entity[] = [];
export class Entity extends RenderObject {
  public static getEntityList() {
    return entityList as ReadonlyArray<Entity>;
  }

  public static testCollision(entity1: Entity, entity2: Entity) {
    return entity1.isCollidingWith(entity2);
  }

  public health = Infinity;
  public maxHealth = Infinity;
  public speedX = 0;
  public speedY = 0;
  public impactDamage = 1;
  public status: IStatus = {};
  public faction?: string;
  public owner?: Entity;
  public acceleration = 0;
  protected turnSpeed = 0;
  protected initialized = false;

  constructor(imageSrc: string, initialPosition: Point) {
    super({ imageSrc, initialPosition });
    entityList.push(this);
  }

  public update() {
    if (!this.initialized) {
      this.init();
    }

    if (this.health <= 0) {
      return this.destroy();
    }

    this.processStatus();

    super.update();
  }

  public destroy() {
    const index = entityList.indexOf(this);
    entityList.splice(index, 1);
    super.destroy();
  }

  public isCollidingWith(entity: Entity) {
    return !(
      this === entity ||
      this.owner === entity ||
      this === entity.owner ||
      this.owner === entity.owner ||
      this.center.x + this.width / 2 < entity.center.x - entity.width / 2 ||
      this.center.y + this.height / 2 < entity.center.y - entity.height / 2 ||
      this.center.x - this.width / 2 > entity.center.x + entity.width / 2 ||
      this.center.y - this.height / 2 > entity.center.y + entity.height / 2
    );
  }

  public init() {
    if (!this.initialized) {
      this.initialized = true;
      this.maxHealth = this.health;
    }
  }

  protected move(directions: IDirection) {
    // Angle 0 is X-axis, direction is in radians.
    const angle = this.angle * (Math.PI / 180);

    const forward = directions.forward ? 1 : 0;
    const back = directions.back ? -0.4 : 0;
    const left = directions.left ? 0.5 : 0;
    const right = directions.right ? -0.5 : 0;

    // Forward and backward.
    this.speedX =
      this.speedX + (forward + back) * this.acceleration * Math.cos(angle);
    this.speedY =
      this.speedY + (forward + back) * this.acceleration * Math.sin(angle);

    // Left and right.
    this.speedX =
      this.speedX +
      (left + right) * this.acceleration * Math.cos(angle - Math.PI / 2);
    this.speedY =
      this.speedY +
      (left + right) * this.acceleration * Math.sin(angle - Math.PI / 2);

    // Friction.
    this.speedX *= 0.97;
    this.speedY *= 0.97;

    this.center = new Point(
      this.center.x + this.speedX,
      this.center.y + this.speedY
    );
  }

  protected turn(point: Point) {
    const targetAngle = Calculations.getAngle(this.center, point);
    const turnDegrees =
      Calculations.mod(targetAngle - this.angle + 180, 360) - 180;

    if (turnDegrees > -4 && turnDegrees < 4) {
      this.angle = targetAngle;
    } else if (turnDegrees < 0) {
      this.angle -= this.turnSpeed;
    } else {
      this.angle += this.turnSpeed;
    }
  }

  private processStatus() {
    if (this.status.firing) {
      this.status.firing--;
      // tslint:disable-next-line:no-unused-expression
      new RenderObject({
        angle: this.angle,
        imageSrc: "images/objects/GunFlare.png",
        initialPosition: this.center,
        lifeSpan: 1
      });
    }

    if (this.status.takingFire) {
      this.status.takingFire--;
      // tslint:disable-next-line:no-unused-expression
      new RenderObject({
        angle: this.angle,
        imageSrc: "images/objects/BulletImpact.png",
        initialPosition: this.center,
        lifeSpan: 1
      });
    }
  }
}

Корабль.ТС

import * as Calculations from "../Calculations";
import { IDirection } from "../interfaces/IDirection";
import { Entity } from "./Entity";
import { ExplosionSprite } from "./Explosion";
import { doubleLaserShot } from "./Laser";
import { Point } from "./Point";
import { Projectile } from "./Projectile";
import { SoundPool } from "./SoundPool";

const laserSound = new SoundPool("sound/effects/laser.wav", 0.02, 200);

export class Ship extends Entity {
  public health = 50;
  public shield = 0;
  public cooldown = 0;
  public cooldownTime = 20;
  public acceleration = 0.3;
  public inaccuracy = 100;
  public turnSpeed = 3;

  constructor(imageSrc: string, initialPosition: Point) {
    super(imageSrc, initialPosition);
  }

  public canFire() {
    return this.cooldown === 0;
  }

  public update() {
    if (this.cooldown > 0) {
      this.cooldown--;
    }
    super.update();
  }

  public destroy() {
    // tslint:disable-next-line:no-unused-expression
    new ExplosionSprite(this.center);
    super.destroy();
  }

  protected firePrimary() {
    if (!this.canFire()) {
      return;
    }
    this.status.firing = 1;
    doubleLaserShot(this);
  }

  protected fireSecondary() {
    if (!this.canFire()) {
      return;
    }
    this.status.firing = 1;

    // const offset = this.width / 2.4;
    // tslint:disable-next-line:no-unused-expression
    new Projectile(this, "dumbFire");

    this.cooldown = this.cooldownTime;
    laserSound.play();
  }
}

PlayerShip.ТС

import { MouseButtonMap } from "../enums/MouseButtonMap";
import { IDirection } from "../interfaces/IDirection";
import { InputController } from "./InputController";
import { Point } from "./Point";
import { RenderObject } from "./RenderObject";
import { Ship } from "./Ship";

export class PlayerShip extends Ship {
  public health = 500;
  public shield = 0;
  public acceleration = 0.8;
  public cooldownTime = 5;
  public inaccuracy = 0;
  public turnSpeed = 6;
  public alive = true;

  constructor() {
    super(
      "images/objects/Firefly.png",
      new Point(window.innerWidth / 2 - 40, window.innerHeight / 2 - 40)
    );
  }

  public update() {
    this.turn(InputController.getMousePosition());

    const keysDown = InputController.getKeysDown();
    const directions: IDirection = {
      back: keysDown.ArrowDown || keysDown.s,
      forward: keysDown.ArrowUp || keysDown.w,
      left: keysDown.ArrowLeft || keysDown.a,
      right: keysDown.ArrowRight || keysDown.d
    };
    const currentPosition = this.center;
    this.move(directions);
    const newPosition = this.center;
    RenderObject.displaceAll(currentPosition, newPosition);

    if (InputController.getMouseDownButtons()[MouseButtonMap.LEFT]) {
      this.firePrimary();
    }

    if (keysDown[" "]) {
      this.fireSecondary();
    }

    super.update();
  }

  public destroy() {
    this.alive = false;
    super.destroy();
  }
}

EnemyShip.ТС

import * as Calculations from "../Calculations";
import { IDirection } from "../interfaces/IDirection";
import { Entity } from "./Entity";
import { PlayerShip } from "./PlayerShip";
import { Point } from './Point';
import { Ship } from "./Ship";

export class EnemyShip extends Ship {
  public health = 50;
  public shield = 0;

  constructor(imageSrc = "images/objects/enemy1.png", initialPosition: Point) {
    super(imageSrc, initialPosition);
  }

  public update() {
    this.actions();
    super.update();
  }

  private findTarget() {
    return Entity.getEntityList().find(p => p instanceof PlayerShip);
  }

  private actions() {
    if (this.findTarget()) {
      return this.attack(this.findTarget() as PlayerShip);
    }
  }

  private attack(target: Entity) {
    this.turn(target.center);

    const playerFacing = Calculations.isFacing(target, this);
    let directions: IDirection = {};
    if (playerFacing) {
      directions = {
        forward: true,
        left: playerFacing > 0,
        right: playerFacing < 0
      };
    } else if (Calculations.lineDistance(this.center, target.center) < 300) {
      directions = {
        back: true
      };
    }
    this.move(directions);

    if (Calculations.chance(0.5)) {
      this.firePrimary();
    }

    if (Calculations.chance(0.5)) {
      this.fireSecondary();
    }
  }
}

Снаряда.ТС

import { getRandomInt } from "../Calculations";
import { Entity } from "./Entity";
import { ExplosionSprite } from "./Explosion";
import { Point } from "./Point";

interface IProjectileType {
  impactDamage: number;
  acceleration: number;
  maxLifeSpan: number;
  turnSpeed?: number;
}

const projectileTypes: { [key: string]: IProjectileType } = {
  dumbFire: {
    acceleration: 0.1,
    impactDamage: 10,
    maxLifeSpan: 100
  },
  homing: {
    acceleration: 0.1,
    impactDamage: 10,
    maxLifeSpan: 100,
    turnSpeed: 0.8
  }
};

const image = "images/objects/bolt1.png";
export class Projectile extends Entity {
  public target?: Entity;
  constructor(owner: Entity, type: string, target?: Entity) {
    super(image, owner.center);

    const properties = projectileTypes[type];

    this.owner = owner;
    this.target = target;

    this.speedX = owner.speedX;
    this.speedY = owner.speedY;
    this.angle = owner.angle;

    this.health = 1;
    this.impactDamage = properties.impactDamage;
    this.acceleration = owner.acceleration + properties.acceleration;
    this.maxLifeSpan = properties.maxLifeSpan;
    this.turnSpeed = properties.turnSpeed || 0;
  }

  public update() {
    if (this.target) {
      super.turn(this.target.center);
    }
    super.move({ forward: true });
    super.update();
  }

  public destroy() {
    // tslint:disable-next-line:no-unused-expression
    new ExplosionSprite(this.center);
    super.destroy();
  }
}


162
2
задан 25 марта 2018 в 03:03 Источник Поделиться
Комментарии