Multiple Entities
Contrary to all other textbooks, this player does not have to be a class or an instance of a Player class. It just needs to have data associated with it. If the player is a one off it can be a simple object. If you want to create many similar objects, a factory function works well and is faster to computer than a class.
const createPlayer = (x, y) => {
return {
x: (x || CANVAS.width / 2),
y: (y || CANVAS.height / 2),
radius: 30,
turnDirection: 0,
walkDirection: 0,
rotationAngle: (Math.PI / 2),
moveSpeed: .3,
rotationSpeed: .2 * (Math.PI / 180)
}
}
let HERO = createPlayer(1, 2)
The Shallow clone only works for objects with primatives as properties.
Attentive readers might still want the OO structure, because of the common pattern of looping through all entities and calling individual entity's update methods, like Unity patterns. An OO implementation might look like this.
class Player {
constructor(x, y) {
this.x = x || CANVAS.width / 2;
this.y = y || CANVAS.height / 2;
this.radius = 30;
this.turnDirection = 0;
this.walkDirection = 0;
this.rotationAngle = Math.PI / 2;
this.moveSpeed = .3;
this.rotationSpeed = .2 * (Math.PI / 180);
}
update() {
this.rotationAngle += this.turnDirection * this.rotationSpeed;
var moveStep = this.walkDirection * this.moveSpeed;
this.x += Math.cos(this.rotationAngle) * moveStep;
this.y += Math.sin(this.rotationAngle) * moveStep;
}
render() {
GRAPHICS.circle('red', this.x, this.y, this.radius);
GRAPHICS.line('black', this.x, this.y,
this.x + Math.cos(this.rotationAngle) * 30,
this.y + Math.sin(this.rotationAngle) * 30)
}
}
So if we wanted to embrace a more C++ style, we could instantiate the constructor.
let ENTITIES = []
let HERO = new Player(1, 2);
ENTITIES.push(HERO)
With a factory function, the update still happens in a loop, but the loop can be made faster.
Instead of:
const loop = () => {
for (var i = 0; i < ENTITIES.length; i++) {
let hero = ENTITIES[i]
hero.update()
hero.render()
}
}
We would have an update and render function that accepts an object and changes that object. That's 1 function, not 1 for each class of entity. It's up to you how you want to set up your code. But I've decided if i don't need class heirarchy then I won't use classes. So here is our current loop.
let ENTITIES = [], HERO;
const init = () => {
HERO = createPlayer(1, 2);
ENTITIES.push(HERO)
}
const loop = () => {
for (var i = 0; i < ENTITIES.length; i++) {
let hero = ENTITIES[i]
update(hero)
render(hero)
}
}
const update = ent => {
ent.rotationAngle += ent.turnDirection * ent.rotationSpeed;
var moveStep = ent.walkDirection * ent.moveSpeed;
ent.x += Math.cos(ent.rotationAngle) * moveStep;
ent.y += Math.sin(ent.rotationAngle) * moveStep;
}
const render = ent => {
circle('red', ent.x, ent.y, ent.radius);
line('black', ent.x, ent.y,
ent.x + Math.cos(ent.rotationAngle) * 30,
ent.y + Math.sin(ent.rotationAngle) * 30)
}
const line = (color, x1, y1, x2, y2) => {
CONTEXT.strokeStyle = color;
CONTEXT.beginPath();
CONTEXT.moveTo(x1, y1);
CONTEXT.lineTo(x2, y2);
CONTEXT.stroke();
}
const circle = (color, x, y, radius) => {
CONTEXT.fillStyle = color;
CONTEXT.beginPath();
CONTEXT.arc(x, y, radius, 0, 2 * Math.PI);
CONTEXT.fill();
}
const move = () => {
if (KEYS_PRESSED["ArrowUp"] && !HERO.jumping) {
HERO.walkDirection += .3;
}
else if (KEYS_PRESSED["ArrowDown"]) {
HERO.walkDirection -= .3;
}
else {
HERO.walkDirection *= 0.99;
}
if (KEYS_PRESSED["ArrowLeft"]) {
HERO.turnDirection -= 1;
}
else if (KEYS_PRESSED["ArrowRight"]) {
HERO.turnDirection += 1;
}
else {
HERO.turnDirection *= .5
}
}
We've got a Hero circle and he can now turn like a tank shooter. This could function as the game's map if we added a 3D first person camera. But we want a little more to play with than just an empty screen. We need some walls to collide off of. So let's look at what level data is and how we load different levels for the same hero in the next article.
The End