goal collision and respawn
continuous-integration/drone/push Build is passing Details

main
Evan Hemsley 2020-07-15 15:37:38 -07:00
parent 6846df8424
commit db68dfcc31
4 changed files with 232 additions and 111 deletions

View File

@ -1,5 +1,5 @@
---
title: "Canvas Renderer"
title: "Texture Renderer"
date: 2019-05-23T11:29:24-07:00
weight: 10
---

View File

@ -192,6 +192,8 @@ Finally, let's send a message to spawn the computer-controlled paddle.
```
*Insert video here*
<video width="75%" autoplay="autoplay" muted="muted" loop="loop" style="display: block; margin: 0 auto;">
<source src="/images/computer.webm" type="video/webm">
</video>

View File

@ -13,4 +13,4 @@ In Pong, your opponent scores a point by getting the ball behind your paddle. Th
- The ball position and velocity needs to be reset on a "serve".
- The score of each player needs to be tracked.
Here's what I'm thinking: first let's sort out collision detection with the goal and resetting the ball. Then we can set up a scoring mechanism.
First, let's sort out collision detection with the goal and resetting the ball. Then we can set up a scoring mechanism.

View File

@ -4,141 +4,260 @@ date: 2019-06-03T12:39:11-07:00
weight: 10
---
First we will need a new collision type for goals.
A goal destroys a ball, and then spawns a ball. So let's set up a new actor-receiver-response set of components to model this behavior.
```ts
export enum CollisionType {
ball,
goal,
paddle,
wall,
Our Actor: **PongFE/Components/CanDestroyComponent.cs**:
```cs
using Encompass;
namespace PongFE.Components
{
public struct CanDestroyComponent : IComponent { }
}
```
And a BallGoalCollisionMessage:
Our Receiver: **PongFE/Components/CanBeDestroyedComponent.cs**:
```ts
import { Entity, Message } from "encompass-ecs";
```cs
using Encompass;
export class BallGoalCollisionMessage extends Message {
public ball_entity: Entity;
public goal_entity: Entity;
namespace PongFE.Components
{
public struct CanBeDestroyedComponent : IComponent { }
}
```
And some dispatch logic in the CollisionDispatchEngine:
Our Response: **PongFE/Components/SpawnBallAfterDestroyComponent.cs**:
```ts
...
```cs
using Encompass;
switch (collision_message.collision_type_one) {
case CollisionType.ball:
switch (collision_message.collision_type_two) {
case CollisionType.goal: {
const message = this.emit_message(BallGoalCollisionMessage);
message.ball_entity = collision_message.entity_one;
message.goal_entity = collision_message.entity_two;
break;
}
namespace PongFE.Components
{
public struct SpawnBallAfterDestroyComponent : IComponent
{
public float Seconds { get; }
...
```
Don't forget to declare the BallGoalCollisionMessage in @Emits!
Let's create our BallGoalCollisionEngine:
```ts
import { Engine, Mutates, Reads } from "encompass-ecs";
import { ScoreComponent } from "game/components/score";
import { BallGoalCollisionMessage } from "game/messages/collisions/ball_goal";
import { World } from "lua-lib/bump";
@Reads(BallGoalCollisionMessage)
@Mutates(ScoreComponent)
export class BallGoalCollisionEngine extends Engine {
private collision_world: World;
public initialize(collision_world: World) {
this.collision_world = collision_world;
}
public update() {
for (const message of this.read_messages(BallGoalCollisionMessage).values()) {
message.ball_entity.destroy();
this.collision_world.remove(message.ball_entity);
public SpawnBallAfterDestroyComponent(float seconds)
{
Seconds = seconds;
}
}
}
```
When we detect a collision between a ball and a goal, we destroy the ball entity and, importantly, remember to remove its rectangle from the collision world.
Now let's create **PongFE/Messages/DestroyMessage.cs**:
{{% notice tip %}}
It is generally much more performance-efficient to deactivate entities instead of destroying and respawn them, but it's slightly more complicated to do so. In our case, it will be fine to destroy and respawn.
{{% /notice %}}
```cs
using Encompass;
namespace PongFE.Messages
{
public struct DestroyMessage : IMessage
{
public Entity Entity { get; }
public DestroyMessage(Entity entity)
{
Entity = entity;
}
}
}
```
And **PongFE/Engines/DestroyEngine.cs**:
```cs
using Encompass;
using PongFE.Components;
using PongFE.Messages;
namespace PongFE.Engines
{
[Reads(typeof(SpawnBallAfterDestroyComponent))]
[Receives(typeof(DestroyMessage))]
[Sends(typeof(BallSpawnMessage))]
public class DestroyEngine : Engine
{
public override void Update(double dt)
{
foreach (ref readonly var message in ReadMessages<DestroyMessage>())
{
if (HasComponent<SpawnBallAfterDestroyComponent>(message.Entity))
{
ref readonly var respawnComponent = ref GetComponent<SpawnBallAfterDestroyComponent>(message.Entity);
SendMessage(new BallSpawnMessage(
new MoonTools.Structs.Position2D(640, 360),
new System.Numerics.Vector2(-200, 100),
16,
16
));
}
Destroy(message.Entity);
}
}
}
}
```
In **CollisionEngine.cs**:
```cs
...
CheckBounce(message.EntityA, message.EntityB, message.HitOrientation);
CheckBounce(message.EntityB, message.EntityA, message.HitOrientation);
CheckDestroy(message.EntityA, message.EntityB);
CheckDestroy(message.EntityB, message.EntityA);
...
private void CheckDestroy(Entity a, Entity b)
{
if (HasComponent<CanDestroyComponent>(a))
{
if (HasComponent<CanBeDestroyedComponent>(b))
{
SendMessage(new DestroyMessage(b));
}
}
}
```
Don't forget to add **typeof(DestroyMessage)** to CollisionEngine's Sends.
Now let's create **PongFE/Engines/DestroyEngine.cs**:
```cs
using Encompass;
using PongFE.Components;
using PongFE.Messages;
namespace PongFE.Engines
{
[Reads(typeof(SpawnBallAfterDestroyComponent))]
[Receives(typeof(DestroyMessage))]
[Sends(typeof(BallSpawnMessage))]
public class DestroyEngine : Engine
{
public override void Update(double dt)
{
foreach (ref readonly var message in ReadMessages<DestroyMessage>())
{
if (HasComponent<SpawnBallAfterDestroyComponent>(message.Entity))
{
ref readonly var respawnComponent = ref GetComponent<SpawnBallAfterDestroyComponent>(message.Entity);
SendMessage(
new BallSpawnMessage(
new MoonTools.Structs.Position2D(640, 360),
new System.Numerics.Vector2(-200, 100),
16,
16
),
respawnComponent.Seconds
);
}
Destroy(message.Entity);
}
}
}
}
```
Notice that **SendMessage** has an extra argument here. What's that about? If you pass a numeric value along with **SendMessage**, the message will be delayed, and send after the given number of seconds has elapsed. This can be very convenient for producing timing-based behaviors. Be warned however, that there is no way to cancel a delayed message send. So if you need to be able to cancel a timed action, it's probably better to set up a component/engine structure that keeps track of the time.
We'll need a way to create our goal entities now.
Let's have a new GoalSpawnMessage:
Create a file, **PongFE/Messages/GoalBoundarySpawnMessage.cs**:
```ts
import { Message } from "encompass-ecs";
```cs
using Encompass;
using MoonTools.Structs;
export class GoalSpawnMessage extends Message {
public x: number;
public y: number;
public width: number;
public height: number;
public player_one: boolean;
}
```
namespace PongFE.Messages
{
public struct GoalBoundarySpawnMessage : IMessage
{
public Position2D Position { get; }
public int Width { get; }
public int Height { get; }
And a new GoalSpawner:
```ts
import { Spawner } from "encompass-ecs";
import { BoundingBoxComponent } from "game/components/bounding_box";
import { CollisionType, CollisionTypesComponent } from "game/components/collision_types";
import { PositionComponent } from "game/components/position";
import { ScoreComponent } from "game/components/score";
import { GoalSpawnMessage } from "game/messages/goal_spawn";
import { World } from "lua-lib/bump";
export class GoalSpawner extends Spawner {
public spawn_message_type = GoalSpawnMessage;
private collision_world: World;
public initialize(collision_world: World) {
this.collision_world = collision_world;
}
public spawn(message: GoalSpawnMessage) {
const entity = this.create_entity();
const boundaries = entity.add_component(BoundingBoxComponent);
boundaries.width = message.width;
boundaries.height = message.height;
const position = entity.add_component(PositionComponent);
position.x = message.x;
position.y = message.y;
const collision_types_component = entity.add_component(CollisionTypesComponent);
collision_types_component.collision_types = [ CollisionType.goal ];
this.collision_world.add(
entity,
message.x - message.width * 0.5,
message.y - message.height * 0.5,
message.width,
message.height
);
public GoalBoundarySpawnMessage(Position2D position, int width, int height)
{
Position = position;
Width = width;
Height = height;
}
}
}
```
Notice this is very similar to our GameBoundarySpawner. I'm not thrilled about that, so we could maybe consolidate it later. Let's leave it for now because we're not quite done with this feature.
And a file, **PongFE/Engines/Spawners/GoalBoundarySpawner.cs**:
Now let's work on a mechanism for respawning the ball.
```cs
using Encompass;
using PongFE.Components;
using PongFE.Messages;
namespace PongFE.Spawners
{
public class GoalBoundarySpawner : Spawner<GoalBoundarySpawnMessage>
{
protected override void Spawn(GoalBoundarySpawnMessage message)
{
var entity = CreateEntity();
AddComponent(entity, new PositionComponent(message.Position));
AddComponent(entity, new CollisionComponent(new MoonTools.Bonk.Rectangle(0, 0, message.Width, message.Height)));
AddComponent(entity, new CanDestroyComponent());
}
}
}
```
Let's add some components to **BallSpawner.cs**:
```cs
AddComponent(ball, new CanBeDestroyedComponent());
AddComponent(ball, new SpawnBallAfterDestroyComponent(0.5f));
```
Finally let's add our new engines and spawn messages to **PongFEGame.cs**:
```cs
...
WorldBuilder.AddEngine(new DestroyEngine());
...
WorldBuilder.AddEngine(new GoalBoundarySpawner());
...
// right boundary
WorldBuilder.SendMessage(
new GoalBoundarySpawnMessage(
new MoonTools.Structs.Position2D(1280, 0),
6,
720
)
);
// left boundary
WorldBuilder.SendMessage(
new GoalBoundarySpawnMessage(
new MoonTools.Structs.Position2D(-6, 0),
6,
720
)
);
```
Now when the ball hits the left or right side of the play area, it will destroy and respawn. Great!