start rewriting collision section
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
parent
c7dd454c5d
commit
0f715d3451
|
@ -14,10 +14,26 @@ We don't want the behavior of any object to be directly tied to some state outsi
|
|||
|
||||
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 (axis-aligned bounding box) check will suffice. Essentially we just use non-rotating rectangles and check if they are overlapping.
|
||||
All of our objects are rectangles so a simple AABB (axis-aligned bounding box) 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.
|
||||
Hang on a sec though - this is a pretty standard problem right? Pretty much every game in existence uses collision detection. There's got to be something we can use...
|
||||
|
||||
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.
|
||||
Well, you're in luck, because I wrote a collision detection system called MoonTools.Bonk! https://moontools-docs.github.io/bonk/
|
||||
|
||||
Turns out that there is a library for AABB 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.
|
||||
Let's start by integrating it. We can add the framework as a *PackageReference* in PongFE.Framework.csproj:
|
||||
|
||||
```xml
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MoonTools.Structs" Version="3.0.1"/>
|
||||
<PackageReference Include="MoonTools.Bonk" Version="8.0.0"/>
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
and PongFE.Core.csproj:
|
||||
|
||||
```xml
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MoonTools.Structs" Version="3.0.1"/>
|
||||
<PackageReference Include="MoonTools.Bonk" Version="8.0.0"/>
|
||||
</ItemGroup>
|
||||
```
|
||||
|
|
|
@ -4,47 +4,54 @@ 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!
|
||||
Now we have a way to tell when objects are colliding, so let's put together our collision system.
|
||||
|
||||
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.
|
||||
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. It really does look bad.
|
||||
|
||||
Think about it... if you were an animator, and you sent an animation test to a director where two objects were overlapping each other for a frame, they would send it back. Games are largely an animation-based medium. So we should take this stuff seriously.
|
||||
|
||||
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.
|
||||
One possibility would be to give entities components that tell us what they are. For example, an entity that has BallComponent would react when it collides with an entity that has GoalComponent. I don't like this approach. The power of ECS is that it lets us attain high flexibility and reusability by defining entities by their _behavior_. If we are building our simulation based on object types, we might as well just do object-oriented programming. So we really should be asking what entities _do_, not what they _are_. A ball entity increases the score of one player and is destroyed when it collides with a goal.
|
||||
|
||||
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.
|
||||
I think a good way to achieve this goal is to break collision down into three overarching elements.
|
||||
|
||||
You might remember that we can only modify a component in one engine. So we're going to have to be deliberate about this.
|
||||
**Actors** do things to objects. For example, a **CanDamageComponent** would be an Actor that is capable of causing damage to other objects.
|
||||
|
||||
Here's what I'm thinking:
|
||||
**Receivers** receive actions. For example, a **CanBeDamagedComponent** could be damaged by a **CanDamageComponent** Actor.
|
||||
|
||||
**Responses** do something when a **Receiver** is hit by an **Actor**. For example, a **DieWhenDamagedComponent** would cause a **CanBeDamagedComponent** Receiver object to die when hit by a **CanDamageComponent** Actor.
|
||||
|
||||
All collision related behavior can be described by breaking it down into these three elements.
|
||||
|
||||
Here's the breakdown:
|
||||
|
||||
- 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
|
||||
- Calculate the position objects would potentially move to
|
||||
- Sweep from starting positions to ending positions, checking for solid collisions along the way
|
||||
- If collision with a solid is detected during the sweep, adjust movement
|
||||
- Update game state appropriately in response to what collided with what
|
||||
|
||||
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.
|
||||
Let's have the MotionEngine consolidate all the MotionMessages per-component, and send out an UpdatePositionMessage with that final delta after a sweep check.
|
||||
|
||||
Finally, a CollisionDispatchEngine figures out what two kinds of objects collided and emits a Message in response. We can then implement various collision resolution Engines that read each of those kinds of Messages. For example, a BallWallCollisionMessage would be handled by a BallWallCollisionEngine.
|
||||
Finally, a CollisionDispatchEngine figures out what two kinds of objects collided and emits a Message in response. We can then implement various collision resolution Engines that read each of those kinds of Messages. For example, a ScoreEngine would track **CanScore** Actors and **CanBeUsedToScore** Receivers, and perform behavior based on attached **Responses**, like an **IncreaseScoreResponseComponent**.
|
||||
|
||||
We can visualize the flow of data like so:
|
||||
|
||||
{{<mermaid align="center">}}
|
||||
graph TD;
|
||||
Engines(Various Engines) -->|MotionMessage| MotionEngine[MotionEngine]
|
||||
MotionEngine -->|CollisionCheckMessage| CollisionCheckEngine[CollisionCheckEngine]
|
||||
CollisionCheckEngine -->|CollisionMessage| CollisionDispatchEngine[CollisionDispatchEngine]
|
||||
CollisionDispatchEngine -->|BallWallCollisionMessage| BallWallCollisionEngine[BallWallCollisionEngine]
|
||||
CollisionDispatchEngine -->|BallPaddleCollisionMessage| BallPaddleCollisionEngine[BallPaddleCollisionEngine]
|
||||
BallWallCollisionEngine -->|UpdatePositionMessage| UpdatePositionEngine
|
||||
BallPaddleCollisionEngine -->|UpdatePositionMessage| UpdatePositionEngine
|
||||
MotionEngine -->|UpdatePositionMessage| UpdatePositionEngine
|
||||
Engines(Various Engines) -->|MotionMessages| MotionEngine[MotionEngine]
|
||||
MotionEngine -->|UpdatePositionMessages| UpdatePositionEngine
|
||||
MotionEngine -->|CollisionMessages| CollisionEngine
|
||||
CollisionEngine -->|ScoreMessages| ScoreEngine
|
||||
CollisionEngine -->|DamageMessages| DamageEngine
|
||||
{{< /mermaid >}}
|
||||
|
||||
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.
|
||||
Whew! That's kind of complicated! 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.
|
||||
|
||||
|
|
|
@ -1,96 +0,0 @@
|
|||
---
|
||||
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, let's download _bump.lua_ and place it in the lua-lib folder. This ensures that the library will be included in your game during the build process.
|
||||
|
||||
In the lua-lib folder, let's create a new file: **lua-lib/bump.d.ts**
|
||||
|
||||
Now 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*. We can also use *world:check* and *world:update* for finer control over the rectangles.
|
||||
|
||||
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;
|
||||
|
||||
remove(table: table): void;
|
||||
}
|
||||
```
|
||||
|
||||
In Lua, *table* is our generic object. All of our classes and other objects are translated to tables when using TSTL, so it's a convenient type for generic objects.
|
||||
|
||||
"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.
|
||||
|
||||
"remove" takes a table and removes it from the world. This is used when objects are destroyed and no longer exist in the game.
|
||||
|
||||
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.
|
Loading…
Reference in New Issue