Destructible Terrain and Pixel based Physics
In this post here I had faithfully followed and recreated in Phaser 3, the amazing tutorial by Richard Davey that implemented the basic mechanisms of a worms / tanks like game in Phaser 2. The focus of this implementation was the use of the canvas texture class to create a destructible terrain. In particular, the interesting aspect of it was the use of getpixel method to detect where land exists or whether a hole had been blown out of the terrain; in effect pixel based collision detection.
In the original implementation, the code went so far as destroying the bomb as soon as it touched the terrain and the exploding debris effect was created using the particle emitter, which cared nothing about the terrain.
I wondered how computationally intensive it might be to create a simple "physics engine" so that that the bomb could bounce off the non-flat terrain in a "realistic" manner.
I have covered the basic aspects of collision response of circles colliding against other circles here and here. In this post we are simply aiming to have the "bomb" (a circle) bounce of the stationary ground. So the basic steps in getting the bomb to bounce off the ground are:
detect collision (we can already do this)
get the velocity component along the collision normal
subtract x2 velocity-along-collision-normal to the current velocity
And that is it.
How to calculate the collision normal?
The tricky part is, how do you get the collision normal? There are various ways of doing this:
Intuitively most obvious way is to build the "surface normal" information when the terrain is created, say for each pixel along the x-axis. However, as this is a destructible terrain, we would need to recalculate the surface normal each time there is an explosion - this is tricky.
A surprisingly simple but remarkably effective method is to search for the pixels around the edge of the circle, accumulate the vector from the center of the circle to points of contact, average it, and then finally normalize it. This method is explained very well in this video here (I have also borrowed quite heavily from the video example code to build this Phaser example code).
Once we have figured out how to get the collision normal, you can do all sorts of physics-like things.
In this example, I have created a Ball class (which extends Phaser.GameObjects.Sprite - so that I don't have to worry about rendering) - the beginning of the constructor has the bits important to the physics behaviour.
class Ball extends Phaser.GameObjects.Sprite {
constructor(scene, x, y, texture, options) {
super(scene, x, y, texture);
this.scene = scene;
this.scene.add.existing(this);
this.canvas = scene.canvas;
this.radius = this.displayHeight / 2;
this.velocity = new Phaser.Math.Vector2();
this.acceleration = new Phaser.Math.Vector2();
this.newPos = new Phaser.Math.Vector2(x,y);
this.friction = 0.9; // arbitrary factor to dampen the post-collision velocity
this.angle = 0;
....remainder is a lot of properties to control behavior of object
This Ball class has the usual update method to integrate velocity and position. Note, we do not immediately update the actual positional properties of the sprite object; rather we hold the position in a variable called "newPos". If we detect a collision at newPos, we do not actually move the object to the new position, instead making the object "bounce". This helps us avoid having to deal with positional correction/resolution which in the case of destructible terrain is quite complicated - so let's simply avoid it.
update(dt) {
this.velocity.add(this.world.gravity.clone().scale(dt));
// calculate the new position based on the velocity (but the actual position is not yet set to the new position)
this.newPos.add(this.velocity.clone().scale(dt));
}
Collision detection and finding the Contact Normal
And this is the heart of this example.
The red highlighted code is the bit that searches 8 points (8 being an arbitrary number - you can search for more points if you want to increase accuracy) along the semi-circle in the direction that the ball is moving in (ie don't bother checking for collisions "behind" the object).
Each time a particular point along the circumference detects contact with the ground (and as soon as there is one contact point detected, the collision flag is set to true), the vector pointing from the circumference point to the center of the circle is "accumulated" in the response vector.
Once all the 8 points have been searched through, then the response vector is normalized, giving you the collision normal vector.
detectCollision() {
// collision check with the ground
this.collision = false;
this.response.reset(); // reset the response vector (will hold the collision normal)
this.angle = this.velocity.angle(); // get the heading of the object
// iterate through semicircle of object radius rotated to direction of velocity
for (let r = this.angle - Math.PI / 2; r < this.angle + Math.PI / 2; r += Math.PI / 8) {
// calculate the test points on the circumference of object
this.sensor.setToPolar(r, this.radius).add(this.newPos)
// test if the test point is colliding with the ground
const rgba = this.canvas.getPixel(this.sensor.x, this.sensor.y);
if (rgba.a > 0) {
// accumulate collision points to give resopnse vector, which is effectively normal to the area of contact
this.response.add(this.newPos.clone().subtract(this.sensor));
this.collision = true;
}
}
this.response.normalize(); // normalise the response vector
}
Collision Response
Once collision has been detected and the collision normal obtained, the rest is easy.
The code to determine the "reponse" after collision is as shown below. It is quite long but the "physics" bit is just the red highlighted part. It is simply doing what was described above; namely, calculate the velocity along the normal and subtract x2 that from the existing velocity (+ I have added a restitution element to dampen the bounce).
collisionResponse() {
if (this.collision) {
// Calculate reflection vector of objects velocity vector, using response vector as normal
const vn = this.velocity.dot(this.response); // magnitude of the velocity along the normal
// Use friction coefficient to dampen response (approximating energy loss)
this.velocity.subtract(this.response.scale(2 * vn)).scale(this.friction);
//Some objects will "die" after several bounces
if ( this.bounceBeforeDeath > 0) {
this.bounceBeforeDeath--;
this.dead = (this.bounceBeforeDeath == 0);
if (this.dead) {
if (this.explode) {
this.boom(this.x, this.y, this.explosionSize);
};
this.destroy();
}
}
} else
{
// No collision so update objects position
this.setPosition(this.newPos.x, this.newPos.y);
}
}
Bavioural properties of the Ball class
I have added the following "minimal" set of properties to control the behaviour of the "ball".
this.bounceBeforeDeath: number of times the ball will bounce before it disappears (or explodes). If set to -1 (default), it will keep bouncing forever.
this.explode: if set to true, the ball will "explode" emitting n-number of other ball objects.
this.explosionSize: this is the "radius" of the hole to be blown out of the ground when the ball explodes
this.explosionDebris: this is the number of "debris" ball objects that are emitted when the original ball object explodes
Adding the ball object to the World
To make the ball object come alive, you must (i) instantiate it, and (ii) added to the world, like so. The second and third parameters are the x and y coordinates at which the ball will be instantiated it.
this.ball = new Ball(this, 300, 30, "bomb", {
bounceBeforeDeath: 5,
explode: true
});
this.world.add(this.ball);
Crudeness of implementation
The demo code (accessible at Codepen below) is only designed to illustrate how the collision normal may be obtained in a pixel based way. It is short and simple. Even so, it does illustrate how effective the simple algorithm is. However, do note that the way I have implemented boundary collision & response is very crude and sometimes the ball "disappears" when it hits the wall and the ground at the same time.
The CodePen is available below for your perusal and comment.
コメント