4.4 KiB
title | date | weight |
---|---|---|
Design | 2019-05-28T17:27:59-07:00 | 400 |
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. 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.
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.
I think a good way to achieve this goal is to break collision down into three overarching elements.
Actors do things to objects. For example, a CanDamageComponent would be an Actor that is capable of causing damage to other objects.
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 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, and send out an UpdatePositionMessage with that final delta after a sweep check.
Finally, a CollisionEngine 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 receive ScoreMessages, and perform behavior based on attached Responses, like an IncreaseScoreResponseComponent.
We can visualize the flow of data like so:
{{}} graph TD; Engines(Various Engines) -->|MotionMessages| MotionEngine[MotionEngine] MotionEngine -->|UpdatePositionMessages| UpdatePositionEngine MotionEngine -->|CollisionMessages| CollisionEngine CollisionEngine -->|ScoreMessages| ScoreEngine CollisionEngine -->|DamageMessages| DamageEngine {{< /mermaid >}}
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.
Let's get started.