4
\$\begingroup\$

So I'm making a 2d shoot'em up game as a way of learning canvas, and larger-scale programming than what I'm accustomed to. I'm a purely front-end web dev, I can't say I'm very experienced in this.

Anyway the way I spawn my enemies is like this : at xyz gameTime, a spawner object is pushed into the game. This spawner object add various enemies in various configurations, using that type of interface, I hope the code is self explanatory:

state.enemies.push(enemies[this.enemyType].add({
  pos: [this.pos[0], this.pos[1]],
  angle: this.angle,
  rotation: this.enemyRotation,
  path: this.path
}));

But I'm not here for game design review so let me talk about what I want advice on.

basically you can see my enemy module here: https://github.com/acezard/aceforceone/blob/develop/src/entities/enemies.js

Let me explain it bit by bit. This is the global config object that manages only the default settings of every enemy type. That is, it doesn't contain any vector/rotation/position infos, because these settings are injected at enemy creation.

// Config Object
var enemyConfig = {
  redBomber: {
    url: 'assets/images/enemy-xs-1.svg',
    pos: [0, 0],
    size: [75, 53],
    speed: 100,
    hitpoints: 10,
    ROF: 100,
    score: 100,
    burst: {
      amount: 3,
      delay: 1000,
      counter: 3
    },
  },

  scout: {
    url: 'assets/images/scout.png',
    pos: [0, 0],
    size: [50, 44],
    speed: 500,
    hitpoints: 2,
    score: 50
  },

  rotatingPlat: {
    url: 'assets/images/platpart.png',
    pos: [0, 0],
    size: [150, 44],
    speed: 30,
    hitpoints: 100,
    ROF: 200,
    score: 250,
    burst: {
      amount: 1000,
      delay: 1000,
      counter: 1000
    },
  },

  rogueLeader: {
    url: 'assets/images/rogueleader.svg',
    pos: [0, 0],
    size: [200, 89],
    speed: 100,
    hitpoints: 60,
    score: 300,
    ROF: 500,
  },

  drone: {
    url: 'assets/images/drone.svg',
    pos: [0, 0],
    size: [50, 62],
    speed: 300,
    hitpoints: 5,
    score: 75
  },
};

Next we have a global enemy constructor, managing every enemy type as a base. It contains a mix of the default settings shown above, and the what I call active settings, that is, arguments different for every enemy entity even if they are of the same type (like position). This constructor also have the basic methods and a blank shoot method, because each enemy type has his own implementation (doesn't seem right?).

// Base enemy constructor
var EnemyEntity = function(settingsDefault, settingsActive) {
  // Default
  this.active = true;
  this.speed = settingsDefault.speed;
  this.hitpoints = settingsDefault.hitpoints;
  this.lastFire = Date.now();
  this.score = settingsDefault.score;
  this.ROF = settingsDefault.ROF || null;
  this.maxHitpoints = settingsDefault.hitpoints;
  this.rotating = settingsDefault.rotating || null;
  if (settingsDefault.burst) {
    this.burst = {
      amount: settingsDefault.burst.amount,
      delay: settingsDefault.burst.delay,
      counter: settingsDefault.burst.counter
    };
  }
  this.sprite = new Sprite({
    url: settingsDefault.url,
    pos: settingsDefault.pos,
    size: settingsDefault.size,
    rotated: true
  });

  // Active
  this.angle = settingsActive.angle;
  this.pos = settingsActive.pos;
  this.radians = settingsActive.angle * Math.PI / 180;
  this.vector = [Math.cos(this.radians) * this.speed, Math.sin(this.radians) * this.speed];
  this.rotation = settingsActive.rotation || 0;
  this.path = settingsActive.path || null;
};

// Update method
EnemyEntity.prototype.shoot = function() {};

EnemyEntity.prototype.update = function(dt) {
  if (this.outOfBounds()) {
    this.active = false;
    return;
  }

  if (this.path == "angular") {
    paths.angular(this);
  }

  this.pos[0] += this.vector[0] * dt;
  this.pos[1] += this.vector[1] * dt;
};

EnemyEntity.prototype.outOfBounds = function() {
  return this.pos[1] > canvas.height || this.pos[0] < 0 || this.pos[0] > canvas.width;
};

// Draw method
EnemyEntity.prototype.render = function() {
    canvas.ctx.save();
    canvas.ctx.translate(this.pos[0], this.pos[1]);
    canvas.ctx.translate(this.sprite.size[0] / 2, this.sprite.size[1] / 2);
    canvas.ctx.rotate(Math.PI / 180 * this.rotation);
    this.sprite.render(canvas.ctx);
    canvas.ctx.restore();
};

This part is another constructor, used to create enemy factories. Basically its only role is to share an ADD method, used by every enemy factory.

// Factory constructor
var EnemyFactory = function() {};

EnemyFactory.prototype.add = function(settings) { 
  return new this.type(settings);
};

Now this is an example of what a specific enemy factory looks like. I put two so you can see how much WET code this is. Basically, we first create a constructor, by calling the EnemyEntity constructor with this, and with the active settings. We also get enemyentity methods with calling it as a prototype. The shoot function is different for each enemy.

Then we have the enemy factory per se, basically it's constructed from the EnemyFactory constructor, and giving the specific Enemy constructor as a type, so we then can use myEnemy.add(activeSettings);

// RedBomber
var RedBomber = function(settings) {
  EnemyEntity.call(this, enemyConfig.redBomber, settings);
};

RedBomber.prototype = Object.create(EnemyEntity.prototype);

RedBomber.prototype.shoot = function() {
  var now = Date.now();

  // If the enemy can shoot
  if (this.pos[1] > 0 && this.burst.counter && now - this.lastFire > this.ROF) {
    var x = this.pos[0] + this.sprite.size[0] / 2;
    var y = this.pos[1] + this.sprite.size[1] / 2;

    state.ebullets.push(weapons.red.addMissile({x: x, y: y, angle: 90}));
    state.ebullets.push(weapons.red.addMissile({x: x, y: y, angle: 80}));
    state.ebullets.push(weapons.red.addMissile({x: x, y: y, angle: 100}));

    this.burst.counter--;
    this.lastFire = now;
    return;
  } else

  if (!this.burst.counter && now - this.lastFire > this.burst.delay) {
    this.burst.counter = this.burst.amount;
  }
};

function RedBomberFactory () {};
RedBomberFactory.prototype = new EnemyFactory();
RedBomberFactory.prototype.type = RedBomber;

var redBomber = new RedBomberFactory();

// Rogue Leader
var RogueLeader = function(settings) {
  EnemyEntity.call(this, enemyConfig.rogueLeader, settings);
};

RogueLeader.prototype = Object.create(EnemyEntity.prototype);

RogueLeader.prototype.shoot = function() {
  var now = Date.now();

  // If the enemy can shoot
  if (this.pos[1] > 0 && now - this.lastFire > this.ROF) {
    var x = this.pos[0];
    var y = this.pos[1];

    state.ebullets.push(weapons.redRay.addMissile({x: x + this.sprite.size[0] * 0.3, y: y + this.sprite.size[1] * 0.6, angle: this.rotation - 90}));
    state.ebullets.push(weapons.redRay.addMissile({x: x + this.sprite.size[0] * 0.7, y: y + this.sprite.size[1] * 0.6, angle: this.rotation - 90}));

    this.lastFire = now;
  }
};

function RogueLeaderFactory () {};
RogueLeaderFactory.prototype = new EnemyFactory();
RogueLeaderFactory.prototype.type = RogueLeader;

var rogueLeader = new RogueLeaderFactory();

Then we exports our factory functions

module.exports = {
  redBomber: redBomber,
  scout: scout,
  rotatingPlat: rotatingPlat,
  rogueLeader: rogueLeader,
  drone: drone
};

What's the problem then ? As you can see there is a lot of repetition in my code, every new enemy type adds a LOT of code, but I only really need the shoot method. Though I'm not sure right now how I could manage that better and ask for your advice. Any other consideration of the code is of course much appreciated

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.