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