./exe/game/readme.txt
Here is a 2d game engine written in javascript using the HTML canvas interface.
The little robot can jet around, jump multiple times, and throw a ball.
Keyboard/Mouse controls:
wasd/arrows: move
mouse: throw ball
[ : shorter trail
] : longer trail
- : thinner trail
= : wider trail
. : display physics
h : toggle platforms/floor
g : toggle sprite visibility
As it is, this is not really a "game" yet, since there is not really any goal. Instead, it offers an engine that can be used as a foundation for a game. It is here to show you how you can start setting up keyboard controls, jumping, gravity, friction, collisions, item interactions, and animations - all the elements that are needed to start making a platformer game. There is quite a lot of code here, so be patient if you would like to understand this one. A lot of work goes into making the physics in a game feel right: is the movement smooth? do the collisions feel solid? is the jump satisfying? In a game it should feel fun just to move around, even if there is nothing to do.
advanced practice:
take any parts that you like from this code and combine them to make your own 2D game.
./exe/game/main.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>2d_engine</title>
<link rel="stylesheet" href="style.css">
</style>
</head>
<body>
<canvas id = "game_space"></canvas>
<img src = "caught_flying.png" alt="" id="caught_flying" />
<img src = "caught_standing.png" alt="" id="caught_standing" />
<img src = "robot_standing.png" alt="" id="robot_standing" />
<img src = "robot_flying.png" alt="" id="right_flying" />
<script src = "canvas.js"> </script>
<script src = "platforms.js"> </script>
<script src = "player.js"> </script>
<script src = "ball.js"> </script>
<script src = "game.js"> </script>
</body>
</html>
./exe/game/style.css
body {
background: #333540;
cursor: crosshair;
}
canvas {
cursor : url(crosshair.png),crosshair;
border-width : 10px;
border-style : solid;
border-color : #778;
background: #fff;
border-radius: 5px;
padding-left : 0 ;
padding-right : 0;
margin-left : auto;
margin-right : auto;
display : block;
max-width: 90vw;
max-height: 90vh;
}
img {
display: none;
}
./exe/game/game.js
let interval_id = start_animation();
function start_animation(){
let id = setInterval( () => {
clear();
if (platforms === platforms1)
move_platforms();
new_acc();
new_vel();
new_pos();
ball_vel();
ball_pos();
draw_ball();
draw_trail();
if (show_sprite)
draw_player();
info();
}, 20);
return id;
}
let show_sprite = true;
function inside_x(a ,b) { return (((a.x + a.w) > b.x) && (a.x < (b.x + b.w)));}
function inside_y(a ,b) { return (((a.y + a.h) > b.y) && (a.y < (b.y + b.h)));}
function on (a, b) { return ( Math.abs(a.y + a.h - b.y) <= (a.dy - b.dy)) }
function beside (a, b) { return ( Math.abs(a.x + a.w - b.x) <= (a.dx - b.dx)) }
function on_platform(me){
for (let i = 0; i < platforms.length; i++)
{
if ( me.dy >= 0 &&
(on(me, platforms[i])) &&
(inside_x(me, platforms[i])))
{
if (platforms[i] === me.on)
return;
me.on = platforms[i];
return;
}
}
// otherwise no platform was engaged
me.on = undefined;
}
./exe/game/platforms.js
class Platform {
constructor(name, w, h, x, y, dx, dy){
this.name = name;
this.w = w;
this.h = h;
this.x = x;
this.y = y;
this.dx = dx;
this.dy = dy;
}
move() {
if (this.x + this.w > canvas.width || this.x < 0)
this.dx *= -1;
this.x += this.dx;
this.y += this.dy;
this.x = Math.round(this.x);
this.y = Math.round(this.y);
}
draw() {
if (this === player.on){
ctx.fillStyle = '#abc';
ctx.fillRect(this.x, this.y, this.w, this.h);
ctx.fillStyle = '#fff';
ctx.fillText(this.name, this.x + this.w/2 - 5, this.y + 25);
} else {
ctx.fillStyle = '#468';
ctx.fillRect(this.x, this.y, this.w, this.h);
}
reset_background_fill();
}
}
//TODO make a platform yielding function
// to replace ones that go through floor
let platforms1 = [
new Platform("5",100,40,0,100,4,0),
new Platform("4",200,40,0,200,6,0),
new Platform("3",300,40,0,300,2,0),
new Platform("2",300,40,0,400,1,0),
new Platform("1",600,40,0,500,3,0),
new Platform("",canvas.width,40,0,canvas.height-10,0,0),
]
new Platform("c",canvas.width,40,0,-40,0,0),
new Platform("r",40,canvas.height,canvas.width,0,0,0),
new Platform("l",40,canvas.height,-40,0,0,0)
platforms2 = [
new Platform("",canvas.width,40,0,canvas.height-10,0,0)
]
platforms3 = [
];
let platforms = platforms1;
function move_platforms() {
platforms.forEach(platform => {
platform.move()
platform.draw()
});
reset_background_fill();
}
./exe/game/canvas.js
const canvas = document.querySelector('#game_space');
canvas.width = 1200;
canvas.height = 700;
const ctx = canvas.getContext('2d'); // essential line
const robot_standing = document.getElementById('robot_standing');
const robot_flying = document.getElementById('robot_flying');
const caught_standing = document.getElementById('caught_standing');
const caught_flying = document.getElementById('caught_flying');
ctx.font = "Bold " + 20 +"px Courier";
// need this because javascript % is
// wierd with negative numbers
Number.prototype.mod = function(n) {
return ((this%n)+n)%n;
};
window.oncontextmenu = function () {
return false;
}
let animations =
[
[caught_flying, caught_standing ],
[robot_flying, robot_standing ]
]
function reset_background_fill() {
//ctx.fillStyle = "#89a";
ctx.fillStyle = "#457";
}
reset_background_fill();
let show_info = 0;
function info() {
if (show_info){
ctx.fillStyle = '#000';
ctx.fillText('====BOT=====', 10, 20 );
ctx.fillText(' x : ' + player.x, 10, 40 );
ctx.fillText(' y : ' + player.y, 10, 60 );
ctx.fillText(' dx : ' + player.dx, 10, 80 );
ctx.fillText(' dy : ' + player.dy, 10, 100 );
ctx.fillText(' ddx : ' + player.ddx, 10, 120 );
ctx.fillText(' ddy : ' + player.ddy, 10, 140 );
ctx.fillText('=---BALL---=', 10, 160 );
ctx.fillText(' x : ' + ball.x, 10, 180 );
ctx.fillText(' y : ' + ball.y, 10, 200 );
ctx.fillText(' dx : ' + ball.dx, 10, 220 );
ctx.fillText(' dy : ' + ball.dy, 10, 240 );
ctx.fillText(' ddx : ' + ball.ddx, 10, 260 );
ctx.fillText(' ddy : ' + ball.ddy, 10, 280 );
ctx.fillText('============', 10, 300 );
reset_background_fill();
}
}
function clear() {
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
./exe/game/ball.js
const PI = 3.1415926535;
const ball = {
r: 8,
w: 8,
h: 8,
x: player.x,
y: player.y,
dx: player.dx,
dy: player.dy,
ddx: player.ddx,
ddy: player.ddy,
caught : 1,
on: undefined,
friction: 0.1,
max_vel: Infinity,
}
function follow_player(){
ball.x = player.x + 21;
ball.y = player.y - 6;
ball.dx = player.dx;
ball.dy = player.dy;
}
follow_player();
let MAX_TRAIL_RADIUS = ball.r * 8;
let TRAIL_LENGTH = 8;
const MAX_TRAIL_LENGTH = 500;
ball_trail = [];
canvas.onpointerdown = (event) => {
if (ball.caught === 1){
bb = canvas.getBoundingClientRect();
let x = (event.clientX-bb.left)*(canvas.width/bb.width);
let y = (event.clientY-bb.top)*(canvas.height/bb.height);
x += 10;
y += 10;
ball.dx = Math.round((x - ball.x)/10) + player.dx;
ball.dy = Math.round((y - ball.y)/10) + player.dy;;
while (inside_x(ball, player) && inside_y(ball, player)){
ball.x += ball.dx;
ball.y += ball.dy;
}
ball.caught = 0;
image = robot_standing;
}
};
function ball_vel() {
if (inside_x(ball, player) && inside_y(ball, player)){
ball.caught = 1;
image = caught_standing;
}
if (ball.caught)
return;
on_platform(ball);
ball.dx += ball.ddx;
if (ball.on){ //friction
if (ball.dx > ball.on.dx)
ball.dx -= ball.friction;
if (ball.dx < ball.on.dx)
ball.dx += ball.friction;
if (Math.abs(ball.dy) < 5){
ball.dy = 0;
} else {
ball.dy *= -0.8;
}
} else {
ball.dy += ball.ddy;
}
}
function correct_vel(){
if (ball.dx > ball.max_vel){
ball.dx = ball.max_vel;
}
if (ball.dx < -ball.max_vel){
ball.dx = -ball.max_vel;
}
if (ball.dy > ball.max_vel){
ball.dy = ball.max_vel;
}
if (ball.dy < -ball.max_vel){
ball.dy = -ball.max_vel;
}
ball.dx = Math.round(ball.dx);
ball.dy = Math.round(ball.dy);
}
function ball_pos(){
if (ball.on) {
ball.y = ball.on.y - ball.r;
}
if (ball.caught){
follow_player();
return;
} else {
touching_wall();
correct_vel();
ball.x += ball.dx;
ball.y += ball.dy;
}
}
function draw_trail(){
while (ball_trail.length > TRAIL_LENGTH){
ball_trail.shift();
}
ball_trail.push([ball.x, ball.y]);
for (let i = 0; i < ball_trail.length; i++){
//let float_amount = Math.round((ball_trail.length - i)/2);
let float_amount = 0;
ctx.globalAlpha = i/(ball_trail.length)/3;
draw_ball(ball_trail[i][0], ball_trail[i][1] - float_amount,
(MAX_TRAIL_RADIUS) - ((MAX_TRAIL_RADIUS - ball.r)/ball_trail.length)*i);
}
ctx.globalAlpha = 1;
}
function draw_ball(x = ball.x, y = ball.y, r = ball.r){
let mem = ctx.fillStyle;
ctx.fillStyle = "#ADE";
ctx.beginPath();
ctx.arc(x, y, r, 0, 2*PI, false);
ctx.fill();
ctx.fillStyle = "#336699";
ctx.beginPath();
ctx.arc(x, y, 2*r/3, 0, 2*PI, false);
ctx.fill();
ctx.fillStyle = mem;
}
function touching_wall(){
//console.log("collision check");
if (ball.x + 2*ball.r > canvas.width){
ball.x = canvas.width - 2*ball.r;
ball.dx *= -0.8;
}
if (ball.x < 0){
ball.x = 0 ;
ball.dx *= -0.8
}
if (ball.y + 2*ball.r > canvas.height){
ball.y = canvas.height - 2*ball.r;
ball.dy *= -0.8;
}
if (ball.y < 0){
ball.y = 0;
ball.dy *= -0.8;
}
}
./exe/game/player.js
const player = {
w : 40 ,
h : 40 ,
x : 1000 ,
y : 100 ,
dx : 0 ,
dy : 0 ,
ddx : 0 ,
ddy : 1 ,
dddx : 0 ,
dddy : 0 ,
on : undefined ,
jumped : 0 ,
friction : 0.5 ,
};
let image = robot_standing;
function new_acc() {
player.ddx += player.dddx;
player.ddy += player.dddy;
}
function new_vel() {
on_platform(player);
player.dx += player.ddx;
if (player.on){ //friction
if (player.dx > player.on.dx)
player.dx -= player.friction;
if (player.dx < player.on.dx)
player.dx += player.friction;
player.dy = player.on.dy;
} else {
player.dy += player.ddy;
}
}
function new_pos() {
if (player.on){
player.y = player.on.y - player.h;
}
player.x += Math.round(player.dx);
player.y += Math.round(player.dy);
player.x = player.x.mod(canvas.width);
player.y = player.y.mod(canvas.height);
}
function draw_player() {
ctx.drawImage(image, player.x, player.y, player.w, player.h);
}
// KEY LISTENERS
function keyDown(e) {
switch (e.key){
case ' ':
case 'w':
case 'ArrowUp':
if (player.jumped === 0){
player.on = undefined;
player.dy = -15;
player.jumped = 1;
}
break;
case 'a':
case 'ArrowLeft':
player.ddx = -1;
break;
case 'd':
case 'ArrowRight':
player.ddx = 1;
break;
case 'Escape':
clearInterval(interval_id);
break;
case 'h':
player.dddx = -1;
break;
case 'l':
player.dddx = 1;
break;
case 'p':
switch (platforms){
case platforms1:
platforms = platforms2;
break;
case platforms2:
platforms = platforms3;
break;
case platforms3:
platforms = platforms1;
break;
}
break;
case '.':
show_info ^= true;
break;
case '=':
MAX_TRAIL_RADIUS += 1;
break;
case '-':
MAX_TRAIL_RADIUS = Math.max(0, MAX_TRAIL_RADIUS - 1);
break;
case ']':
TRAIL_LENGTH = Math.min(MAX_TRAIL_LENGTH, TRAIL_LENGTH + 1);;
break;
case '[':
TRAIL_LENGTH = Math.max(0, TRAIL_LENGTH - 1);
break;
case 'g':
show_sprite ^= true;
break;
}
}
function keyUp(e) {
switch (e.key){
case ' ':
case 'w':
case 'ArrowUp':
player.jumped = 0;
break;
case 'a':
case 'ArrowLeft':
case 'd':
case 'ArrowRight':
player.ddx = 0;
break;
case 'h':
case 'l':
player.dddx = 0;
player.ddx = 0;
break;
}
}
//string: what kind of event listener
//function: what to do when the event happens
document.addEventListener('keydown' , keyDown);
document.addEventListener('keyup' , keyUp);