goal collision and respawn
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
parent
6846df8424
commit
db68dfcc31
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
title: "Canvas Renderer"
|
||||
title: "Texture Renderer"
|
||||
date: 2019-05-23T11:29:24-07:00
|
||||
weight: 10
|
||||
---
|
|
@ -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>
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
|
||||
namespace PongFE.Components
|
||||
{
|
||||
public struct SpawnBallAfterDestroyComponent : IComponent
|
||||
{
|
||||
public float Seconds { get; }
|
||||
|
||||
public SpawnBallAfterDestroyComponent(float seconds)
|
||||
{
|
||||
Seconds = seconds;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Now let's create **PongFE/Messages/DestroyMessage.cs**:
|
||||
|
||||
```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
|
||||
...
|
||||
|
||||
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;
|
||||
}
|
||||
CheckBounce(message.EntityA, message.EntityB, message.HitOrientation);
|
||||
CheckBounce(message.EntityB, message.EntityA, message.HitOrientation);
|
||||
|
||||
CheckDestroy(message.EntityA, message.EntityB);
|
||||
CheckDestroy(message.EntityB, message.EntityA);
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
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);
|
||||
private void CheckDestroy(Entity a, Entity b)
|
||||
{
|
||||
if (HasComponent<CanDestroyComponent>(a))
|
||||
{
|
||||
if (HasComponent<CanBeDestroyedComponent>(b))
|
||||
{
|
||||
SendMessage(new DestroyMessage(b));
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
Don't forget to add **typeof(DestroyMessage)** to CollisionEngine's Sends.
|
||||
|
||||
{{% 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 %}}
|
||||
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; }
|
||||
|
||||
public GoalBoundarySpawnMessage(Position2D position, int width, int height)
|
||||
{
|
||||
Position = position;
|
||||
Width = width;
|
||||
Height = height;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
And a new GoalSpawner:
|
||||
And a file, **PongFE/Engines/Spawners/GoalBoundarySpawner.cs**:
|
||||
|
||||
```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";
|
||||
```cs
|
||||
using Encompass;
|
||||
using PongFE.Components;
|
||||
using PongFE.Messages;
|
||||
|
||||
export class GoalSpawner extends Spawner {
|
||||
public spawn_message_type = GoalSpawnMessage;
|
||||
namespace PongFE.Spawners
|
||||
{
|
||||
public class GoalBoundarySpawner : Spawner<GoalBoundarySpawnMessage>
|
||||
{
|
||||
protected override void Spawn(GoalBoundarySpawnMessage message)
|
||||
{
|
||||
var entity = CreateEntity();
|
||||
|
||||
private collision_world: World;
|
||||
|
||||
public initialize(collision_world: World) {
|
||||
this.collision_world = collision_world;
|
||||
AddComponent(entity, new PositionComponent(message.Position));
|
||||
AddComponent(entity, new CollisionComponent(new MoonTools.Bonk.Rectangle(0, 0, message.Width, message.Height)));
|
||||
AddComponent(entity, new CanDestroyComponent());
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
public spawn(message: GoalSpawnMessage) {
|
||||
const entity = this.create_entity();
|
||||
Let's add some components to **BallSpawner.cs**:
|
||||
|
||||
const boundaries = entity.add_component(BoundingBoxComponent);
|
||||
boundaries.width = message.width;
|
||||
boundaries.height = message.height;
|
||||
```cs
|
||||
AddComponent(ball, new CanBeDestroyedComponent());
|
||||
AddComponent(ball, new SpawnBallAfterDestroyComponent(0.5f));
|
||||
```
|
||||
|
||||
const position = entity.add_component(PositionComponent);
|
||||
position.x = message.x;
|
||||
position.y = message.y;
|
||||
Finally let's add our new engines and spawn messages to **PongFEGame.cs**:
|
||||
|
||||
const collision_types_component = entity.add_component(CollisionTypesComponent);
|
||||
collision_types_component.collision_types = [ CollisionType.goal ];
|
||||
```cs
|
||||
...
|
||||
|
||||
this.collision_world.add(
|
||||
entity,
|
||||
message.x - message.width * 0.5,
|
||||
message.y - message.height * 0.5,
|
||||
message.width,
|
||||
message.height
|
||||
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
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
Now let's work on a mechanism for respawning the ball.
|
||||
Now when the ball hits the left or right side of the play area, it will destroy and respawn. Great!
|
||||
|
|
Loading…
Reference in New Issue