Space shooter game in TypeScript

Clash Royale CLAN TAG#URR8PPP
.everyoneloves__top-leaderboard:empty,.everyoneloves__mid-leaderboard:empty margin-bottom:0;
up vote
2
down vote
favorite
I recently started learning TypeScript and created a small space shooter. I would like to get some feedback on the structure of my code.
I have some classes with both static and instance methods with references back and forth and wonder if I could have done this cleaner somehow.
Link to full code: Github
Game: Firefly (has sound on by default)
RenderObject.ts
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) 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;
Entity.ts
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)
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
);
Ship.ts
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.ts
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 = keysDown.w,
left: keysDown.ArrowLeft ;
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.ts
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();
Projectile.ts
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)
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();
object-oriented game typescript
add a comment |Â
up vote
2
down vote
favorite
I recently started learning TypeScript and created a small space shooter. I would like to get some feedback on the structure of my code.
I have some classes with both static and instance methods with references back and forth and wonder if I could have done this cleaner somehow.
Link to full code: Github
Game: Firefly (has sound on by default)
RenderObject.ts
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) 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;
Entity.ts
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)
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
);
Ship.ts
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.ts
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 = keysDown.w,
left: keysDown.ArrowLeft ;
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.ts
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();
Projectile.ts
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)
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();
object-oriented game typescript
add a comment |Â
up vote
2
down vote
favorite
up vote
2
down vote
favorite
I recently started learning TypeScript and created a small space shooter. I would like to get some feedback on the structure of my code.
I have some classes with both static and instance methods with references back and forth and wonder if I could have done this cleaner somehow.
Link to full code: Github
Game: Firefly (has sound on by default)
RenderObject.ts
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) 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;
Entity.ts
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)
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
);
Ship.ts
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.ts
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 = keysDown.w,
left: keysDown.ArrowLeft ;
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.ts
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();
Projectile.ts
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)
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();
object-oriented game typescript
I recently started learning TypeScript and created a small space shooter. I would like to get some feedback on the structure of my code.
I have some classes with both static and instance methods with references back and forth and wonder if I could have done this cleaner somehow.
Link to full code: Github
Game: Firefly (has sound on by default)
RenderObject.ts
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) 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;
Entity.ts
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)
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
);
Ship.ts
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.ts
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 = keysDown.w,
left: keysDown.ArrowLeft ;
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.ts
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();
Projectile.ts
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)
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();
object-oriented game typescript
edited Mar 25 at 16:55
200_success
123k14142399
123k14142399
asked Mar 25 at 15:30
Jonathan
287113
287113
add a comment |Â
add a comment |Â
active
oldest
votes
active
oldest
votes
active
oldest
votes
active
oldest
votes
active
oldest
votes
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
StackExchange.ready(
function ()
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f190442%2fspace-shooter-game-in-typescript%23new-answer', 'question_page');
);
Post as a guest
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password