more bounce tutorial

pull/1/head
Evan Hemsley 2019-05-28 21:05:36 -07:00
parent 1f10c364e5
commit d520606d75
9 changed files with 546 additions and 72 deletions

View File

@ -1,72 +0,0 @@
---
title: "Bouncing"
date: 2019-05-23T18:38:51-07:00
weight: 30
---
Let's make the ball bounce off the sides of the game window.
I know what you're thinking. "Let's just read the dimensions of the game window. When the ball goes past them, we know it should bounce!"
**NO.**
We don't want the behavior of any object to be directly tied to some state outside of the game simulation. That's just asking for trouble!!
Let's make a BoundariesComponent instead. In **game/components/boundaries.ts**
```ts
import { Component } from "encompass-ecs";
export class BoundariesComponent extends Component {
public left: number;
public top: number;
public right: number;
public bottom: number;
}
```
Now we have two options. We could put this on the ball or on a separate Entity. But let's think about it - the paddles need to respect the boundaries too, right? So let's make it a separate Entity.
{{% notice tip %}}
It's perfectly valid to create Entities for the purpose of only holding one Component. It's good architecture a lot of the time!
{{% /notice %}}
In **game/game.ts**:
```ts
const boundaries_entity = world_builder.create_entity();
const boundaries_component = boundaries_entity.add_component(BoundariesComponent);
boundaries_component.left = 0;
boundaries_component.top = 0;
boundaries_component.right = 1280;
boundaries_component.bottom = 720;
```
Now how do our boundaries actually work? You might recall that our MotionEngine updates the positions of objects directly. But that means it could leave something out of the boundary. Remember that we can't have two Engines mutate the same Component type.
There's something else going on here too: eventually we're gonna need the ball to bounce off of the paddles as well right? I think what we really need here is a collision system.
All of our objects are rectangles so I think a simple AABB check will suffice. Essentially we can use the four corners of our rectangles to see if they are overlapping.
Let's revisit our BoundariesComponent. I think we can use this for collision boundaries. And since we're specifying that everything will be a rectangle, we can simplify our BoundariesComponent to just have width and height values. In **games/components/boundaries.ts**
```ts
import { Component } from "encompass-ecs";
export class BoundariesComponent extends Component {
public width: number;
public height: number;
}
```
Let's write a CollisionCheckEngine. In **games/engines/collision.ts**
```ts
```
I think what we should do is have two new Engines. The first will be an Engine that takes a Message that reads a position and checks if it is outside of the boundaries. Then we can make a new Engine that finalizes the new value of the PositionComponent.
```ts
```

View File

@ -0,0 +1,23 @@
---
title: "Bouncing"
date: 2019-05-28T18:47:18-07:00
weight: 100
---
Let's make the ball bounce off the sides of the game window.
I know what you're thinking. "Let's just read the dimensions of the game window. When the ball goes past them, we know it should bounce!"
**NO.**
We don't want the behavior of any object to be directly tied to some state outside of the game simulation. That's just asking for trouble!! What if you want the game boundaries to be different from the window size later? What if a different device has different dimensions? The possibilities are endless!
There's something else going on here too: eventually we're gonna need the ball to bounce off of the paddles as well right? I think what we really need here is a collision system.
All of our objects are rectangles so I think a simple AABB check will suffice. Essentially we just use non-rotating rectangles and check if they are overlapping.
Hang on a sec though - this is a pretty standard problem right? Pretty much every game in existence uses collision detection.
LOVE provides a physics system under _love.physics_. But it's actually a fully featured physics simulator that attempts to behave realistically under physical constraints. It's a little heavy to use it just for simple collision detection and we'd probably have to rework all of our entities and components to integrate it properly at this point.
Turns out that there is a library for AABB (axis-aligned bounding box) collision that will work just fine for our purposes. It's called [bump.lua](https://github.com/kikito/bump.lua). Let's start by integrating it.

View File

@ -0,0 +1,128 @@
---
title: "Collision Checking"
date: 2019-05-28T18:51:15-07:00
weight: 600
---
In **game/engines/collision_message.ts**:
```ts
import { Entity, Message } from "encompass-ecs";
import { CollisionType } from "game/components/collision_types";
import { Collision } from "lua-lib/bump";
export class CollisionMessage extends Message {
public entity_one: Entity;
public entity_two: Entity;
public collision_type_one: CollisionType;
public collision_type_two: CollisionType;
public entity_one_new_x: number;
public entity_one_new_y: number;
public entity_two_new_x: number;
public entity_two_new_y: number;
public collision_data: Collision;
}
```
Let's break down what we want collision detection to actually do.
First, we tell the Collision World about the current positions of the objects. Next we check each object for collisions by using the "check" method, which takes the proposed new position of the object and gives us collision information in return.
For every collision that we find, we create a CollisionMessage for it. Why are we comparing the collision types? Let's say we want to have a BallWallCollisionMessage. Obviously we will want to know which entity in the collision represents the ball and which one represents the wall. So we just sort them at this step for convenience.
In **game/engines/collision_check.ts**:
```ts
import { Emits, Engine, Entity, Reads } from "encompass-ecs";
import { BoundariesComponent } from "game/components/boundaries";
import { CollisionTypesComponent } from "game/components/collision_types";
import { PositionComponent } from "game/components/position";
import { CollisionMessage } from "game/messages/collision";
import { CollisionCheckMessage } from "game/messages/collision_check";
import { World } from "lua-lib/bump";
@Reads(CollisionCheckMessage)
@Emits(CollisionMessage)
export class CollisionCheckEngine extends Engine {
private collision_world: World;
public initialize(collision_world: World) {
this.collision_world = collision_world;
}
public update() {
const collision_check_messages = this.read_messages(CollisionCheckMessage);
// update all positions in collision world
for (const message of collision_check_messages.values()) {
const entity = message.entity;
const position = entity.get_component(PositionComponent);
const boundaries = entity.get_component(BoundariesComponent);
this.collision_world.update(
message.entity,
position.x - boundaries.width * 0.5,
position.y - boundaries.height * 0.5
);
}
// perform collision checks with new positions
for (const message of collision_check_messages.values()) {
const entity = message.entity;
const position = entity.get_component(PositionComponent);
const boundaries = entity.get_component(BoundariesComponent);
const x = position.x + message.x_delta;
const y = position.y + message.y_delta;
const [new_x, new_y, cols, len] = this.collision_world.check(
entity,
x - boundaries.width * 0.5,
y - boundaries.height * 0.5,
() => "touch"
);
for (const col of cols) {
const other = col.other as Entity;
const other_position = other.get_component(PositionComponent);
for (const collision_type_one of entity.get_component(CollisionTypesComponent)!.collision_types) {
for (const collision_type_two of other.get_component(CollisionTypesComponent)!.collision_types) {
const collision_message = this.emit_message(CollisionMessage);
if (collision_type_one < collision_type_two) {
collision_message.entity_one = entity;
collision_message.entity_two = other;
collision_message.collision_type_one = collision_type_one;
collision_message.collision_type_two = collision_type_two;
collision_message.entity_one_new_x = x;
collision_message.entity_one_new_y = y;
collision_message.entity_two_new_x = other_position.x;
collision_message.entity_two_new_y = other_position.y;
collision_message.collision_data = col;
} else {
collision_message.entity_one = other;
collision_message.entity_two = entity;
collision_message.collision_type_one = collision_type_two;
collision_message.collision_type_two = collision_type_one;
collision_message.entity_one_new_x = other_position.x;
collision_message.entity_one_new_y = other_position.y;
collision_message.entity_two_new_x = x;
collision_message.entity_two_new_y = y;
collision_message.collision_data = col;
}
}
}
}
}
}
}
```
The "initialize" method gives the Engine a reference to the Collision World that needs to be shared by everything that deals with collision. Let's make sure to call the "initialize" method from **game.ts**:
```ts
const collision_world = CollisionWorld.newWorld(32);
...
world_builder.add_engine(CollisionCheckEngine).initialize(collision_world);
```

View File

@ -0,0 +1,65 @@
---
title: "Collision Dispatch"
date: 2019-05-28T19:06:03-07:00
weight: 700
---
Let's make the CollisionDispatchEngine.
In **games/engines/collision_dispatch.ts**:
```ts
import { Emits, Engine, Reads } from "encompass-ecs";
import { CollisionType } from "game/components/collision_types";
import { CollisionMessage } from "game/messages/collision";
import { BallPaddleCollisionMessage } from "game/messages/collisions/ball_paddle";
import { BallWallCollisionMessage } from "game/messages/collisions/ball_wall";
import { PaddleWallCollisionMessage } from "game/messages/collisions/paddle_wall";
@Reads(CollisionMessage)
@Emits(BallPaddleCollisionMessage, BallWallCollisionMessage, PaddleWallCollisionMessage)
export class CollisionDispatchEngine extends Engine {
public update() {
const collision_messages = this.read_messages(CollisionMessage);
for (const collision_message of collision_messages.values()) {
switch (collision_message.collision_type_one) {
case CollisionType.ball:
switch (collision_message.collision_type_two) {
case CollisionType.paddle: {
// an exercise for the reader ;)
}
case CollisionType.wall: {
const message = this.emit_message(BallWallCollisionMessage);
message.ball_entity = collision_message.entity_one;
message.wall_entity = collision_message.entity_two;
message.ball_new_x = collision_message.entity_one_new_x;
message.ball_new_y = collision_message.entity_one_new_y;
message.normal = collision_message.collision_data.normal;
message.touch = collision_message.collision_data.touch;
break;
}
}
break;
case CollisionType.paddle: {
switch (collision_message.collision_type_two) {
case CollisionType.wall: {
// another exercise for the reader ;)
}
}
}
}
}
}
}
```
Now we are emitting a BallWallCollisionMessage every time a ball collides with a wall. Next we need to resolve the collision.
{{% notice notice %}}
Clever readers have probably noticed that this is a bit of an awkward structure. For our game, we only have three types of colliding entities we care about, so some switch statements work fine. What about a game with 20 different kinds of colliding entities? 100? We'd probably want a much more generic structure or this Engine's complexity would get out of hand.
What you really want to do, fundamentally, is map two collision types, independent of order, to a message emitting function. You'll probably need to implement a custom data structure to do this cleanly. It's very much outside of the scope of this tutorial for me to do this, but I wish you luck!
{{% /notice %}}

View File

@ -0,0 +1,90 @@
---
title: "Collision Resolution"
date: 2019-05-28T20:39:54-07:00
weight: 800
---
What do we want to actually happen when a ball collides with a wall?
Obviously the wall doesn't do anything. It just sits there. That's easy!
The ball needs to bounce off of the wall. We can calculate exactly where it should end up by adding distance along the collision normal equal to twice the difference between the proposed location of the ball and where it touched the wall.
{{% notice tip %}}
**What the heck is a collision normal?**
You can think of the collision normal as just an arrow pointing away from the wall. If you want more details about this, check out the [bump.lua README](https://github.com/kikito/bump.lua/blob/master/README.md). It has illustrations of collisions and the normal vectors they create.
{{% /notice %}}
In **game/engines/collision/ball_wall.ts**:
```ts
import { Emits, Engine, Reads } from "encompass-ecs";
import { BoundariesComponent } from "game/components/boundaries";
import { PositionComponent } from "game/components/position";
import { VelocityComponent } from "game/components/velocity";
import { BallWallCollisionMessage } from "game/messages/collisions/ball_wall";
import { UpdatePositionMessage } from "game/messages/update_position";
import { UpdateVelocityMessage } from "game/messages/update_velocity";
@Reads(BallWallCollisionMessage)
@Emits(UpdatePositionMessage, UpdateVelocityMessage)
export class BallWallCollisionEngine extends Engine {
public update() {
for (const message of this.read_messages(BallWallCollisionMessage).values()) {
const ball_position = message.ball_entity.get_component(PositionComponent);
const ball_velocity = message.ball_entity.get_component(VelocityComponent);
const ball_boundaries = message.ball_entity.get_component(BoundariesComponent);
const velocity_message = this.emit_component_message(UpdateVelocityMessage, ball_velocity);
velocity_message.x_delta = 2 * message.normal.x * Math.abs(ball_velocity.x);
velocity_message.y_delta = 2 * message.normal.y * Math.abs(ball_velocity.y);
// calculate bounce, remembering to re-transform coordinates to origin space
const y_distance = Math.abs(message.ball_new_y - (message.touch.y + ball_boundaries.height * 0.5));
const x_distance = Math.abs(message.ball_new_x - (message.touch.x + ball_boundaries.width * 0.5));
const position_message = this.emit_component_message(UpdatePositionMessage, ball_position);
position_message.x_delta = 2 * message.normal.x * x_distance;
position_message.y_delta = 2 * message.normal.y * y_distance;
}
}
}
```
Notice that we also want to update the velocity when the ball bounces. Let's create that UpdateVelocity behavior.
In **game/messages/update_velocity.ts**:
```ts
import { ComponentMessage, Message } from "encompass-ecs";
import { VelocityComponent } from "game/components/velocity";
export class UpdateVelocityMessage extends Message implements ComponentMessage {
public component: Readonly<VelocityComponent>;
public x_delta: number;
public y_delta: number;
}
```
In **game/engines/update_velocity.ts**:
```ts
import { ComponentModifier, Mutates, Reads } from "encompass-ecs";
import { VelocityComponent } from "game/components/velocity";
import { UpdateVelocityMessage } from "game/messages/update_velocity";
import { GCOptimizedSet } from "tstl-gc-optimized-collections";
@Reads(UpdateVelocityMessage)
@Mutates(VelocityComponent)
export class UpdateVelocityEngine extends ComponentModifier {
public component_message_type = UpdateVelocityMessage;
public modify(component: VelocityComponent, messages: GCOptimizedSet<UpdateVelocityMessage>) {
for (const message of messages.entries()) {
component.x += message.x_delta;
component.y += message.y_delta;
}
}
}
```

View File

@ -0,0 +1,39 @@
---
title: "Design"
date: 2019-05-28T17:27:59-07:00
weight: 400
---
Now we have a way to tell when objects are colliding. Let's make something happen as a result!
First, let's think about the structure of a collision system, and how we want it to resolve collisions.
One thing we really don't want is for collision to resolve late. For example, a frame finishing with two solid objects lodged inside each other, and then fixing itself on the next frame. Even if it's only for a frame, it's enough for players to detect some awkwardness.
We also need objects to behave differently based on _what_ they collide with. For example, a ball colliding with the top boundary is going to react differently than if it collides with a goal boundary.
For this reason, I think it makes sense to define each object as having a CollisionType; in our game a colliding object is always either a ball, a wall, or a paddle. We can implement different behaviors for a ball-wall, ball-paddle, or paddle-wall collision.
You might think it would be better to determine collision behavior by placing collision behavior components on the entities. This feels awkward to me, but I could definitely be wrong about this. If you want to try doing things that way, the power is yours! Just make sure you have a good justification for it.
You might remember that we can only modify a component in one engine. So we're going to have to be deliberate about this.
Here's what I'm thinking:
- Tell things to move
- Calculate the position they would potentially move to
- Use that position to check collision
- Do stuff in response to collision, including telling things to move again
- Resolve all the movements
We already have MotionMessages. Let's keep those, but redirect them slightly. We can't read MotionMessages and then Emit them again later down the line: that would cause a cycle. So let's have a new UpdatePositionMessage.
Let's have the MotionEngine consolidate all the MotionMessages per-component, send out an UpdatePositionMessage with that final delta, but then also send out a CollisionCheckMessage for everything that has a BoundingBoxComponent.
Finally, a CollisionResolveEngine figures out what two kinds of objects collided and emits a Message in response. We can then implement Engines that read each of those kinds of Messages.
Whew! That's a lot! You might be wondering why we're doing all this work just to get some objects reacting to each other.
The thing is: this kind of thing is the backbone of many game designs. So of course it's a bit complex! The point is that there's no point obscuring the complexity of such a system or putting off thinking about it until later. If we build our project on a shoddy foundation we will surely have problems later. Let's get a robust system in there so we don't have to fundamentally reorganize our program at a later time, when it will be more frustrating and have more potential to break things.
Let's get started.

View File

@ -0,0 +1,92 @@
---
title: "Library Integration"
date: 2019-05-27T13:19:31-07:00
weight: 5
---
So we've found a library in our target language that implements some feature we want. In our case it's the [bump.lua](https://github.com/kikito/bump.lua) library that provides AABB collision detection.
Now we need to declare that library to TypeScript so we can use it in our game code.
If you'd like a very detailed description of declaration files and how they work, I recommend perusing the [official documentation](https://www.typescriptlang.org/docs/handbook/declaration-files/introduction.html). But I will give a specific use-case walkthrough here.
First, we need to find out which functions of the library we are actually going to use. If we look over the bump.lua documentation, we see that the library asks us to initialize a "world" using *bump.newWorld*, add rectangles to the world with *world:add*, and declare their movements using *world:move*.
First, let's download _bump.lua_ and place it in the lua-lib folder.
In the lua-lib folder, let's create a new file: **lua-lib/bump.d.ts**
First things first, the newWorld function.
```ts
export function newWorld(this: void, cell_size?: number): World;
```
{{% notice notice %}}
Why "this: void"?
In Lua, there are two ways to call a function: with the dot . syntax or the colon : syntax. The colon is used as shorthand for functions that need to have the `self` argument passed to the function being called.
Since _newWorld_ is called at the library level, "this: void" tells TypeScriptToLua that this function does not need `self` and should be called with the dot syntax.
For more information, check out the [TypeScriptToLua documentation](https://github.com/TypeScriptToLua/TypeScriptToLua/wiki/Functions-and-the-%60self%60-Parameter)
{{% /notice %}}
We'll need to declare the *World* interface as well, with its "add" and "move" functions.
```ts
export interface World {
add(table: table, x: number, y: number, width: number, height: number): void;
/** @tupleReturn */
check(table: table, x: number, y: number, filter?: (item: table, other: table) => CollisionType): [number, number, Collision[], number];
/** @tupleReturn */
move(table: table, x: number, y: number): [number, number, Collision[], number];
update(table: table, x: number, y: number, width?: number, height?: number): void;
}
```
In Lua, *table* is our generic object. All of our classes and other objects are translated to tables when using TSTL.
"add" is a pretty straightforward function: it takes a position and a width and height and adds that rectangle to the World.
"check" and "move" are a bit more involved. See the return type there? That means that the function returns multiple variables. This is called a "tuple return" in Lua. _@tupleReturn_ tells the TSTL transpiler that the function returns a tuple so it can translate the call directly. We'll talk more about this in a second.
"check" and "move" return four variables. The first two are _actualX_ and _actualY_, which are the new positions after the move. The next is _cols_, which is a list of collisions detected during the move. The final is _len_, which is the total amount of collisions detected.
Inspecting the contents of _cols_ in the bump.lua documentation gives us the following types and interfaces:
```ts
export type CollisionType = "touch" | "cross" | "slide" | "bounce";
export interface Vector {
x: number,
y: number
}
export interface Rect {
x: number,
y: number,
w: number,
h: number
}
export interface Collision {
item: table,
other: table,
type: CollisionType,
overlaps: boolean,
ti: number,
move: Vector,
normal: Vector,
touch: Vector,
itemRect: Rect,
otherRect: Rect
}
```
Finally, "update" tells the World about the new position of an object. It's useful if we are using "check" instead of "move".
With that out of the way, we can integrate the collision detection functionality into our project.

View File

@ -0,0 +1,105 @@
---
title: "Motion Engine: The Revenge"
date: 2019-05-28T18:01:49-07:00
weight: 500
---
In **game/messages/collision_check.ts**:
```ts
import { Entity, Message } from "encompass-ecs";
export class CollisionCheckMessage extends Message {
public entity: Entity;
public x_delta: number;
public y_delta: number;
}
```
In **game/messages/update_position.ts**:
```ts
import { ComponentMessage, Message } from "encompass-ecs";
import { PositionComponent } from "game/components/position";
export class UpdatePositionMessage extends Message implements ComponentMessage {
public component: Readonly<PositionComponent>;
public x_delta: number;
public y_delta: number;
}
```
Let's rewrite our MotionEngine.
We associate MotionMessages with their PositionComponents. We consolidate them to get an "x_delta" and a "y_delta". We create an UpdatePositionMessage containing these values. Next, we create CollisionCheckMessages containing the delta values if the PositionComponent's entity has a BoundingBoxComponent.
Finally, we go over all BoundariesComponents that didn't have MotionMessages associated with them and create CollisionCheckMessages for those too. Otherwise things that didn't move wouldn't be collision checked, and that would not be correct.
```ts
import { Emits, Engine, Reads } from "encompass-ecs";
import { BoundariesComponent } from "game/components/boundaries";
import { PositionComponent } from "game/components/position";
import { CollisionCheckMessage } from "game/messages/collision_check";
import { MotionMessage } from "game/messages/component/motion";
import { UpdatePositionMessage } from "game/messages/update_position";
import { GCOptimizedList, GCOptimizedSet } from "tstl-gc-optimized-collections";
@Reads(MotionMessage)
@Emits(UpdatePositionMessage, CollisionCheckMessage)
export class MotionEngine extends Engine {
private component_to_message = new Map<PositionComponent, GCOptimizedList<MotionMessage>>();
private boundaries_set = new GCOptimizedSet<BoundariesComponent>();
public update(dt: number) {
const motion_messages = this.read_messages(MotionMessage);
for (const message of motion_messages.values()) {
this.register_message(message);
}
for (const [position_component, messages] of this.component_to_message.entries()) {
const entity = this.get_entity(position_component.entity_id)!;
let x_delta = 0;
let y_delta = 0;
for (const message of messages.values()) {
x_delta += message.x * dt;
y_delta += message.y * dt;
}
const update_position_message = this.emit_component_message(UpdatePositionMessage, position_component);
update_position_message.x_delta = x_delta;
update_position_message.y_delta = y_delta;
if (entity.has_component(BoundariesComponent)) {
const collision_check_message = this.emit_message(CollisionCheckMessage);
collision_check_message.entity = entity;
collision_check_message.x_delta = x_delta;
collision_check_message.y_delta = y_delta;
this.boundaries_set.add(entity.get_component(BoundariesComponent));
}
}
for (const component of this.read_components(BoundariesComponent).values()) {
if (!this.boundaries_set.has(component)) {
const collision_check_message = this.emit_message(CollisionCheckMessage);
collision_check_message.entity = this.get_entity(component.entity_id)!;
collision_check_message.x_delta = 0;
collision_check_message.y_delta = 0;
}
}
this.component_to_message.clear();
}
private register_message(message: MotionMessage) {
if (!this.component_to_message.has(message.component)) {
this.component_to_message.set(message.component, new GCOptimizedList<MotionMessage>());
}
this.component_to_message.get(message.component)!.add(message);
}
}
```
Now let's detect collisions.

View File

@ -11,3 +11,7 @@ Everyone has played, or at least heard of, Pong. Right? Right...
Pong was one of the first video games ever created and as such, it is extremely simple. We're introducing a lot of new concepts with Encompass and the Hyper ECS architecture, so I think it's a good choice to try re-implementing the very simple Pong in Encompass as an example.
We'll be developing this with the Encompass/LOVE starter pack. Go ahead and [set that up](/getting_started/case_study_love/) if you haven't already so you can follow along. And please do follow along - you can do it!
{{% notice tip %}}
I recommend following along with the tutorial by actually typing out the code rather than cut-and-pasting. You'll be able to follow the structure of what's happening much better. Think of it like taking notes.
{{% /notice %}}