Physics
Physics simulation can make games much more engaging and realistic. Easel makes this easy with its built-in physics engine.
pub tangible category Category:Ship
pub game fn World.Main() {
SpawnEachPlayer owner {
Subspawn ship {
use body=ship, radius=1, shape=Equilateral(numPoints=3)
Body(pos=20*RandomVector)
PolygonCollider(category=Category:Ship)
PolygonSprite(color=#0f0)
}
on BeforeCollide that {
Print { "Hero collided!" }
}
}
}
Bodies
A body represents a physical object that has a position and velocity, amongst other properties. Give your entity a body with the Body function:
Subspawn ship {
Body(pos=20*RandomVector)
}
You can now get or set various physical properties on the body:
- Pos is the body's position in the world.
- Heading is the body's orientation.
- Velocity is the body's velocity.
- TurnRate is the body's rate of rotation.
Subspawn ship {
use body=ship
Body(pos=@(10,10))
Velocity = @(20,20)
}
Colliders
By itself, a body does nothing. You need to attach a collider to it to make it interact with the world, using the PolygonCollider function. A collider gives the body a physical shape and mass. This allows the physics engine to calculate when two bodies collide with each other, and how their velocities should change as a result of the collision.
Subspawn ship {
use body=ship
Body(pos=20*RandomVector)
PolygonCollider(category=Category:Ship, shape=Equilateral(radius=1, numPoints=3))
}
A PolygonCollider requires a few parameters to be specified:
bodyspecifies which Body the collider is attached to.categoryspecifies which Category the collider belongs to. This is used for collision filtering and querying.shapespecifies the shape of the collider, for example a Circle. See Polygons for all the different shapes you can use.
Other body attachments
A body can have many attachments other than just colliders. For example:
- Graphical attachments like a PolygonSprite or Camera. See Graphics for more details.
- Sound attachments like Sing or Hear. See Audio for more details.
Subspawn ship {
use body=ship, radius=1, shape=Equilateral(numPoints=3)
Body(pos=20*RandomVector)
PolygonCollider(category=Category:Ship)
PolygonSprite(color=#0f0)
Hear(@ship-launch.esfx)
on Paint {
Spark(color=#f80)
}
}
Detecting collisions
The physics engine can report when two colliders collide with each other.
BeforeCollide is triggered when a collider is begins contact with another collider,
and AfterCollide is triggered when a collider ends contact with another collider.
You would typically use an on block to listen for these events.
on BeforeCollide that {
if that.Category.Overlaps(Category.Unit) {
that.Health -= 10
}
}
Parent-child collisions
THere is a common issue where child colliders should not collide with their parents. For example, when a ship spawns a plasma bolt, the plasma bolt should not collide with the ship that spawned it, at least not for the first few moments after it is spawned. Otherwise, the plasma bolt might damage its own ship.
The parent parameter of the PolygonCollider
function can be used to specify a parent body for the collider.
The collider will keep ignoring collisions with the parent until it has been separated from the parent.
It will also ignore collisions with any other colliders that have the same parent until separated from them.
This is useful, for example, if you are firing multiple projectiles at once from the same ship.
Motion
There are two main ways to move a body around the world, we will call them walking and flying. Walking involves taking one step at a time. The velocity only lasts for one tick, and then it is reset to zero. Flying involves gaining velocity that continues to affect the body until it is changed by another force or by friction.
Flying
Flying is the most straightforward way to move a body around the world. Simply modify the Velocity property of the body directly to make it fly in a certain direction.
Subspawn ship {
use body=ship, radius=1, shape=Equilateral(numPoints=3)
Body(pos=20*RandomVector)
PolygonCollider(category=Category:Ship)
PolygonSprite(color=#0f0)
on BeforePhysics {
// Accelerate to wards the pointer
Velocity += 0.1 * (Pointer - Pos).Direction
}
}
BeforePhysics occurs just before the physics simulation step of each tick,
which means that any changes to velocity made in this block will be taken into account during this tick's physics simulation.
It is a good idea to use BeforePhysics any time you want to affect a body's motion.
Walking
Let's say you have a character that you want to take a step forward. One way to achieve this is to simply give it a new Pos, a short distance in front of its current position. However, if you do that, the character will just teleport to the new position, sometimes tunnelling into walls or other characters in the process, which can cause unrealistic collisions that involve being ejected out of walls at high speeds.
Call the ForcefulStep to move the body to another position by applying a one-off force during the next physics simulation. Similarly, call ForcefulTurn to change the body's heading by applying a one-off torque during the next physics simulation.
If, in the process of taking the step, the body collides with another body, and the collision will be simulated in a realistic manner. This might mean the body bounces back, or it might push the other body out of the way.
Subspawn hero {
use body=hero, radius=1, shape=Circle
Body(pos=20*RandomVector)
PolygonCollider(category=Category:Hero)
PolygonSprite(color=#0f0)
on ButtonDown(ArrowRight) {
// Take on step to the right
ForcefulStep(@(10, 0))
}
}
This technique makes a body realistically push other bodies when it is walking into them, for example a hero pushing into a boulder or into another hero.
When a body using ForcefulStep walks into a wall, they will bounce off.
If you do not like this, consider using DecaySpeed to dampen the bounce back.
You can even use DecaySpeed(1) to eliminate all speed except for the walking force itself.
Decay and friction
All bodies exist in a frictionless world where they will keep moving forever unless acted on by a force. This can mean that, after a collision, a body might keep sliding forever or keep spinning forever.
One way to solve this is to give colliders some friction.
The PolygonCollider function has a friction parameter
that you can set to a value between 0 and 1.
This works well if you are making a game with Gravity,
and your players generally walk on land,
Give the ground collider some friction and they will slow down naturally when they walk on it.
If you are making a top-down game, then the DecaySpeed and DecayTurnRate functions can be used to make bodies slow down over time even when they are not in contact with any other bodies.
Continuous collision detection
The physics engine simulates collisions in discrete time steps, which means that if a body is moving very fast,
it can sometimes tunnel through other bodies without the physics engine detecting the collision.
To avoid this, enable continuous collision detection on your fast-moving bodies by setting the bullet=true
on your Body function.
The physics engine will then sweep the body along its path during each simulation step to check for collisions, which is more computationally expensive, but will prevent tunnelling. Avoid this for slow-moving bodies as it can be unnecessarily expensive.
Querying
Querying searches all colliders in the physics engine for the ones that meet certain conditions:
- QueryNearest finds the nearest collider to a point.
- QueryWithinRadius finds all colliders within a certain radius of a point.
- QueryOverlapping finds all colliders that overlap with a given collider.
- QueryNearestAlongRay finds the nearest collider along a ray.
Querying is performed on colliders not bodies.
That means if a body has multiple colliders, it will be returned multiple times in the results.
If this is an issue for you, consider putting the main collider in one category,
and then all auxiliary colliders in another category,
then using the filter parameter to only return colliders in the main category when you query.
Cached spatial index
Querying is done using a spatial index which is only updated during the physics simulation step of each tick. That means if a query is performed, and then body positions are changed manually, the next query will run on a stale spatial index and so results might not be accurate. You can force the spatial index to be recalculated by calling ResetSpatialQueryIndex.
When a new collider is created, it is automatically added to the spatial index,
so you do not need to call ResetSpatialQueryIndex after creating a new collider.
Try to avoid calling ResetSpatialQueryIndex where possible as it is an expensive operation! Consider waiting until the next tick to perform your query when the physics simulation will automatically update the spatial index for you.
Timescale
Normally, the physics simulation moves forward at the same rate as the game ticks,
but you can change this using the PhysicsTimescale property
to create various effects.
A value of 1 is normal speed, 0.5 for half speed, 2 for double speed,
and 0 to pause the physics simulation entirely.