Talking about Serious Scramblers game, Game development, HTML5, Javascript, Phaser and TypeScript.
In this new part of Serious Scramblers series we are going to try to solve a problem affecting all physics engines, like Phaser’s Arcade, which do not feature Continuous Collision Detection.
What s Continuous Collision Detection? The purpose of Continuous Collision Detection (CCD) is to detect collisions of fast moving bodies that would otherwise be missed.
In our case, if the player falls down too fast, without CCD we’ll see the player passing through platforms. This glitch is called “tunnelling”, like you can see in this animation:
In this case the hero falls too fast for the physics engine to detect the collision with some platforms, and we have a tunnelling effect.
Some physics engines like Box2D feature CCD, using “bullets” in the specific Box2D case, but Arcade Physics does not.
How can we deal with that? There are several ways, which can be more or less complex, but the cheapest one I found is this one:
We assist to tunnelling because the platforms are too tiny to detect a fast body approaching them.
So let’s just create bigger platforms. But what if we want tiny platforms? Then create tiny platform with a big body, like in this example:
Tap and hold left or right to move the character left or right. Once you move, platforms will scroll up. Reach the top of the stage, and it’s game over.
Fall from platform to platform without falling too down, if you reach the bottom of the stage, it’s game over.
As you can see from the debug draw, platform bodies, in purple, are way taller than platform themselves. This will prevent tunnelling: they are simply too big for the player to pass through them.
Once player bottom coordinate is higher than platform top coordinate, which means player is below a platform, we reset platform body to match platform actual size.
And we can have something working like CCD.
Let’s have a look at the 6 TypeScript files and the HTML file used in this prototype. I also highlighted the new lines of code added to the source code of the previous step.
index.html
The webpage which hosts the game, just the bare bones of HTML and main.ts
is called.
Also look at the thegame
div, this is where the game will run.
<!DOCTYPE html>
<html>
<head>
<style type = "text/css">
body {
background: #000000;
padding: 0px;
margin: 0px;
}
</style>
<script src = "scripts/main.ts"></script>
</head>
<body>
<div id = "thegame"></div>
</body>
</html>
main.ts
The main TypeScript file, the one called by index.html
.
Here we import most of the game libraries and define both Scale Manager object and Physics object.
Here we also initialize the game itself.
In this file I added debug draw to Arcade Physics, to let the engine draw bodies.
// MAIN GAME FILE
// modules to import
import Phaser from 'phaser';
import { PreloadAssets } from './preloadAssets';
import { PlayGame} from './playGame';
import { GameOptions } from './gameOptions';
// object to initialize the Scale Manager
const scaleObject: Phaser.Types.Core.ScaleConfig = {
mode: Phaser.Scale.FIT,
autoCenter: Phaser.Scale.CENTER_BOTH,
parent: 'thegame',
width: 750,
height: 1334
}
// obhect to initialize Arcade physics
const physicsObject: Phaser.Types.Core.PhysicsConfig = {
default: 'arcade',
arcade: {
gravity: {
y: GameOptions.gameGravity
},
debug: true
}
}
// game configuration object
const configObject: Phaser.Types.Core.GameConfig = {
type: Phaser.AUTO,
backgroundColor:0x444444,
scale: scaleObject,
scene: [PreloadAssets, PlayGame],
physics: physicsObject
}
// the game itself
new Phaser.Game(configObject);
preloadAssets.ts
Class to preload all assets used in the game.
// CLASS TO PRELOAD ASSETS
// this class extends Scene class
export class PreloadAssets extends Phaser.Scene {
// constructor
constructor() {
super({
key: 'PreloadAssets'
});
}
// preloading assets, the good old way
preload(): void {
this.load.image('hero', 'assets/hero.png');
this.load.image('platform', 'assets/platform.png');
}
// method to be called once the instance has been created
create(): void {
// call PlayGame class
this.scene.start('PlayGame');
}
}
gameOptions.ts
Game options which can be changed to tune the gameplay are stored in a separate module, ready to be reused.
I increased game gravity to have the player to fall down very fast, to test tunnelling.
// CONFIGURABLE GAME OPTIONS
export const GameOptions = {
// first platform vertical position. 0 = top of the screen, 1 = bottom of the screen
firstPlatformPosition: 1 / 10,
// game gravity, which only affects the hero
gameGravity: 11200,
// hero speed, in pixels per second
heroSpeed: 300,
// platform speed, in pixels per second
platformSpeed: 190,
// platform length range, in pixels
platformLengthRange: [50, 150],
// platform horizontal distance range from the center of the stage, in pixels
platformHorizontalDistanceRange: [0, 250],
// platform vertical distance range, in pixels
platformVerticalDistanceRange: [150, 300]
}
playGame.ts
The game itself, the biggest class, game logic is stored here, including our creative way to have CCD, changing platforms body size:
// THE GAME ITSELF
// modules to import
import { GameOptions } from './gameOptions';
import PlayerSprite from './playerSprite';
import PlatformSprite from './platformSprite';
// this class extends Scene class
export class PlayGame extends Phaser.Scene {
// group which will contain all platforms
platformGroup: Phaser.Physics.Arcade.Group;
// the hero of the game
hero: PlayerSprite;
// here we store game width once for all
gameWidth: number;
// here we store game height once for all
gameHeight: number;
// constructor
constructor() {
super({
key: 'PlayGame'
});
}
// method to be called once the class has been created
create(): void {
// save game width value
this.gameWidth = this.game.config.width as number;
// save game height value
this.gameHeight = this.game.config.height as number;
// create a new physics group
this.platformGroup = this.physics.add.group();
// create starting platform
let platform: PlatformSprite = new PlatformSprite(this, this.gameWidth / 2, this.gameHeight * GameOptions.firstPlatformPosition, "platform");
// add platform to platform group
platform.addToGroup(this.platformGroup);
// we are going to create 10 more platforms which we'll reuse to save resources
for(let i = 0; i < 10; i ++) {
// platform creation, as a member of platformGroup physics group
let platform = new PlatformSprite(this, 0, 0, "platform");
// add platform to platform group
platform.addToGroup(this.platformGroup);
// position the platform
this.positionPlatform(platform);
}
// add the hero
this.hero = new PlayerSprite(this, this.gameWidth / 2, 0, "hero");
// input listener to move the hero
this.input.on("pointerdown", this.moveHero, this);
// input listener to stop the hero
this.input.on("pointerup", this.stopHero, this);
}
// method to position a platform
positionPlatform(platform: PlatformSprite): void {
// vertical position
platform.y = this.getLowestPlatform() + this.randomValue(GameOptions.platformVerticalDistanceRange);
// horizontal position
platform.x = this.gameWidth / 2 + this.randomValue(GameOptions.platformHorizontalDistanceRange) * Phaser.Math.RND.sign();
// platform width
platform.displayWidth = this.randomValue(GameOptions.platformLengthRange);
// set platform bounding box size to make it way taller than platform sprite
platform.setBodySize(platform.displayWidth / platform.scaleX, 400, false);
// the platform is higher
platform.isHigher = true;
}
// method to get the lowest platform, returns the position of the lowest platform, in pixels
getLowestPlatform(): number {
// lowest platform value is initially set to zero
let lowestPlatform = 0;
// get all platforms
let platforms: PlatformSprite[] = this.platformGroup.getChildren() as PlatformSprite[];
// loop through all platforms
for (let platform of platforms) {
// get the highest value between lowestPlatform and platform y coordinate
lowestPlatform = Math.max(lowestPlatform, platform.y);
};
// return lowest platform coordinate
return lowestPlatform;
}
// method to toss a random value between two elements in an array
randomValue(a: number[]): number {
// return a random integer between the first and the second item of the array
return Phaser.Math.Between(a[0], a[1]);
}
// method to move the hero
moveHero(e: Phaser.Input.Pointer): void {
// set hero velocity according to input horizontal coordinate
this.hero.setVelocityX(GameOptions.heroSpeed * ((e.x > this.gameWidth / 2) ? 1 : -1));
// is it the first move?
if(this.hero.firstMove) {
// it's no longer the first move
this.hero.firstMove = false;
// move platform group
this.platformGroup.setVelocityY(-GameOptions.platformSpeed);
}
}
// method to stop the hero
stopHero(): void {
// ... just stop the hero :)
this.hero.setVelocityX(0);
}
// method to be executed at each frame
update(): void {
// handle collision between ball and platforms
this.physics.world.collide(this.platformGroup, this.hero);
// get hero lower bounds
let heroLowerBounds: number = this.hero.getBounds().bottom;
// get all platforms
let platforms: PlatformSprite[] = this.platformGroup.getChildren() as PlatformSprite[];
// loop through all platforms
for (let platform of platforms) {
// get platform bounds
let platformBounds: Phaser.Geom.Rectangle = platform.getBounds();
// if hero bottom side is below platform top side and platform size is higher...
if (platformBounds.top < heroLowerBounds && platform.isHigher) {
// reset platform body size
platform.setBodySize(platform.displayWidth / platform.scaleX, platform.displayHeight, false);
// platform is no longer higher
platform.isHigher = false;
}
// if a platform leaves the stage to the upper side...
if (platformBounds.bottom < 0) {
// ... recycle the platform
this.positionPlatform(platform);
}
}
// if the hero falls down or leaves the stage from the top...
if(this.hero.y > this.gameHeight || this.hero.y < 0) {
// restart the scene
this.scene.start("PlayGame");
}
}
}
playerSprite.ts
Class to define the player Sprite, the main actor of the game, the one players control.
// PLAYER SPRITE CLASS
// player sprite extends Arcade Sprite class
export default class PlayerSprite extends Phaser.Physics.Arcade.Sprite {
// is the first time player is moving
firstMove: Boolean = true;
// constructor
constructor(scene: Phaser.Scene, x: number, y: number, key: string) {
super(scene, x, y, key);
// add the player to the scnee
scene.add.existing(this);
// add physics body to platform
scene.physics.add.existing(this);
}
}
platformSprite.ts
Class to define the platforms, we added a property to see if the platform body is higher than actual platform size.
// PLATFORM SPRITE CLASS
// platform sprite extends RenderTexture class
export default class PlatformSprite extends Phaser.Physics.Arcade.Sprite {
// platform physics body
body: Phaser.Physics.Arcade.Body;
// is the platform body higher?
isHigher: Boolean = false;
// constructor
constructor(scene: Phaser.Scene, x: number, y: number, key: string) {
super(scene, x, y, key);
// add the platform to the scnee
scene.add.existing(this);
// add physics body to platform
scene.physics.add.existing(this);
}
// method to add the platform to a group and set physics properties
addToGroup(group: Phaser.Physics.Arcade.Group) {
// add the platform to group
group.add(this);
// platform body does not react to collisions
this.body.setImmovable(true);
// platform body is not affected by gravity
this.body.setAllowGravity(false);
}
}
And in a few lines we added something working like CCD, and finally we are ready to add multiple camera, enemies and stuff to collect. Meanwhile, download the source code of the entire project.
Never miss an update! Subscribe, and I will bother you by email only when a new game or full source code comes out.