computer control
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
parent
a996d03c31
commit
6846df8424
|
@ -1,106 +0,0 @@
|
||||||
---
|
|
||||||
title: "Putting It All Together"
|
|
||||||
date: 2019-05-28T21:22:44-07:00
|
|
||||||
weight: 1000
|
|
||||||
---
|
|
||||||
|
|
||||||
Finally, we need to set up our initial game state with our spawn messages, and make sure we added and initialized all of our required Engines.
|
|
||||||
|
|
||||||
Our load method in **game/game.ts** should look something like this:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
public load() {
|
|
||||||
this.canvas = love.graphics.newCanvas();
|
|
||||||
|
|
||||||
const collision_world = CollisionWorld.newWorld(32);
|
|
||||||
|
|
||||||
const world_builder = new WorldBuilder();
|
|
||||||
|
|
||||||
// ADD YOUR ENGINES HERE...
|
|
||||||
world_builder.add_engine(BallSpawner).initialize(collision_world);
|
|
||||||
world_builder.add_engine(GameBoundarySpawner).initialize(collision_world);
|
|
||||||
world_builder.add_engine(PaddleSpawner).initialize(collision_world);
|
|
||||||
|
|
||||||
world_builder.add_engine(InputEngine);
|
|
||||||
world_builder.add_engine(PaddleMovementEngine);
|
|
||||||
world_builder.add_engine(MotionEngine);
|
|
||||||
world_builder.add_engine(VelocityEngine);
|
|
||||||
|
|
||||||
world_builder.add_engine(CollisionCheckEngine).initialize(collision_world);
|
|
||||||
world_builder.add_engine(CollisionDispatchEngine);
|
|
||||||
world_builder.add_engine(BallWallCollisionEngine);
|
|
||||||
world_builder.add_engine(BallPaddleCollisionEngine);
|
|
||||||
|
|
||||||
world_builder.add_engine(UpdatePositionEngine);
|
|
||||||
world_builder.add_engine(UpdateVelocityEngine);
|
|
||||||
|
|
||||||
// ADD YOUR RENDERERS HERE...
|
|
||||||
world_builder.add_renderer(CanvasRenderer);
|
|
||||||
|
|
||||||
// ADD YOUR STARTING ENTITIES HERE...
|
|
||||||
|
|
||||||
const play_area_width = 1280;
|
|
||||||
const play_area_height = 720;
|
|
||||||
|
|
||||||
const boundary_width = 30;
|
|
||||||
|
|
||||||
const paddle_width = 20;
|
|
||||||
const paddle_height = 120;
|
|
||||||
const paddle_spacing = 40;
|
|
||||||
const paddle_speed = 400;
|
|
||||||
|
|
||||||
const ball_size = 16;
|
|
||||||
|
|
||||||
const paddle_spawn_message = world_builder.emit_message(PaddleSpawnMessage);
|
|
||||||
paddle_spawn_message.x = paddle_spacing;
|
|
||||||
paddle_spawn_message.y = play_area_height * 0.5;
|
|
||||||
paddle_spawn_message.width = paddle_width;
|
|
||||||
paddle_spawn_message.height = paddle_height;
|
|
||||||
paddle_spawn_message.move_speed = paddle_speed;
|
|
||||||
|
|
||||||
const ball_spawn_message = world_builder.emit_message(BallSpawnMessage);
|
|
||||||
ball_spawn_message.x = play_area_width * 0.5;
|
|
||||||
ball_spawn_message.y = play_area_height * 0.5;
|
|
||||||
ball_spawn_message.size = ball_size;
|
|
||||||
ball_spawn_message.x_velocity = 200;
|
|
||||||
ball_spawn_message.y_velocity = -400;
|
|
||||||
|
|
||||||
const top_wall_spawn_message = world_builder.emit_message(GameBoundarySpawnMessage);
|
|
||||||
top_wall_spawn_message.x = play_area_width * 0.5;
|
|
||||||
top_wall_spawn_message.y = -boundary_width * 0.5;
|
|
||||||
top_wall_spawn_message.width = play_area_width;
|
|
||||||
top_wall_spawn_message.height = boundary_width;
|
|
||||||
|
|
||||||
const right_wall_spawn_message = world_builder.emit_message(GameBoundarySpawnMessage);
|
|
||||||
right_wall_spawn_message.x = play_area_width + boundary_width * 0.5;
|
|
||||||
right_wall_spawn_message.y = play_area_height * 0.5;
|
|
||||||
right_wall_spawn_message.width = boundary_width;
|
|
||||||
right_wall_spawn_message.height = play_area_height;
|
|
||||||
|
|
||||||
const bottom_wall_spawn_message = world_builder.emit_message(GameBoundarySpawnMessage);
|
|
||||||
bottom_wall_spawn_message.x = play_area_width * 0.5;
|
|
||||||
bottom_wall_spawn_message.y = boundary_width * 0.5 + play_area_height;
|
|
||||||
bottom_wall_spawn_message.width = play_area_width;
|
|
||||||
bottom_wall_spawn_message.height = boundary_width;
|
|
||||||
|
|
||||||
const left_wall_spawn_message = world_builder.emit_message(GameBoundarySpawnMessage);
|
|
||||||
left_wall_spawn_message.x = -boundary_width * 0.5;
|
|
||||||
left_wall_spawn_message.y = play_area_height * 0.5;
|
|
||||||
left_wall_spawn_message.width = boundary_width;
|
|
||||||
left_wall_spawn_message.height = play_area_height;
|
|
||||||
|
|
||||||
this.world = world_builder.build();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Let's try it!
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm run love
|
|
||||||
```
|
|
||||||
|
|
||||||
<video width="75%" autoplay="autoplay" muted="muted" loop="loop" style="display: block; margin: 0 auto;">
|
|
||||||
<source src="/images/bouncing.webm" type="video/webm">
|
|
||||||
</video>
|
|
||||||
|
|
||||||
All our hard work paid off. Look at that! *chef kiss*
|
|
|
@ -0,0 +1,351 @@
|
||||||
|
---
|
||||||
|
title: "Revisiting Spawners"
|
||||||
|
date: 2019-05-28T21:22:44-07:00
|
||||||
|
weight: 1000
|
||||||
|
---
|
||||||
|
|
||||||
|
At this point, our LoadContent method in **PongFEGame.cs** should look like this:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
protected override void LoadContent()
|
||||||
|
{
|
||||||
|
SpriteBatch = new SpriteBatch(GraphicsDevice);
|
||||||
|
|
||||||
|
WhitePixel = new Texture2D(GraphicsDevice, 1, 1);
|
||||||
|
WhitePixel.SetData(new Color[] { Color.White });
|
||||||
|
|
||||||
|
PaddleTexture = new RenderTarget2D(GraphicsDevice, 20, 80);
|
||||||
|
GraphicsDevice.SetRenderTarget(PaddleTexture);
|
||||||
|
SpriteBatch.Begin();
|
||||||
|
SpriteBatch.Draw(WhitePixel, new Rectangle(0, 0, 20, 80), Color.White);
|
||||||
|
SpriteBatch.End();
|
||||||
|
|
||||||
|
BallTexture = new RenderTarget2D(GraphicsDevice, 16, 16);
|
||||||
|
GraphicsDevice.SetRenderTarget(BallTexture);
|
||||||
|
SpriteBatch.Begin();
|
||||||
|
SpriteBatch.Draw(WhitePixel, new Rectangle(0, 0, 16, 16), Color.White);
|
||||||
|
SpriteBatch.End();
|
||||||
|
GraphicsDevice.SetRenderTarget(null);
|
||||||
|
|
||||||
|
WorldBuilder.AddEngine(new InputEngine());
|
||||||
|
WorldBuilder.AddEngine(new PaddleMovementEngine());
|
||||||
|
WorldBuilder.AddEngine(new VelocityEngine());
|
||||||
|
WorldBuilder.AddEngine(new MotionEngine());
|
||||||
|
WorldBuilder.AddEngine(new CollisionEngine());
|
||||||
|
WorldBuilder.AddEngine(new BounceEngine());
|
||||||
|
WorldBuilder.AddEngine(new UpdatePositionEngine());
|
||||||
|
WorldBuilder.AddEngine(new UpdateVelocityEngine());
|
||||||
|
|
||||||
|
WorldBuilder.AddEngine(new BallSpawner(BallTexture));
|
||||||
|
WorldBuilder.AddEngine(new BoundarySpawner());
|
||||||
|
|
||||||
|
WorldBuilder.AddOrderedRenderer(new Texture2DRenderer(SpriteBatch));
|
||||||
|
|
||||||
|
var paddle = WorldBuilder.CreateEntity();
|
||||||
|
WorldBuilder.SetComponent(paddle, new PlayerInputComponent(PongFE.Components.PlayerIndex.One));
|
||||||
|
WorldBuilder.SetComponent(paddle, new PaddleMoveSpeedComponent(400));
|
||||||
|
WorldBuilder.SetComponent(paddle, new PositionComponent(new MoonTools.Structs.Position2D(5, 5)));
|
||||||
|
WorldBuilder.SetComponent(paddle, new CollisionComponent(new MoonTools.Bonk.Rectangle(0, 0, 20, 80)));
|
||||||
|
WorldBuilder.SetComponent(paddle, new CanCauseBounceComponent());
|
||||||
|
WorldBuilder.SetComponent(paddle, new Texture2DComponent(PaddleTexture, 0));
|
||||||
|
|
||||||
|
WorldBuilder.SendMessage(
|
||||||
|
new BallSpawnMessage(
|
||||||
|
new MoonTools.Structs.Position2D(640, 360),
|
||||||
|
new System.Numerics.Vector2(-200, -50)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// top boundary
|
||||||
|
WorldBuilder.SendMessage(
|
||||||
|
new BoundarySpawnMessage(
|
||||||
|
new MoonTools.Structs.Position2D(0, -6),
|
||||||
|
1280,
|
||||||
|
6
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// right boundary
|
||||||
|
WorldBuilder.SendMessage(
|
||||||
|
new BoundarySpawnMessage(
|
||||||
|
new MoonTools.Structs.Position2D(1280, 0),
|
||||||
|
6,
|
||||||
|
720
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// bottom boundary
|
||||||
|
WorldBuilder.SendMessage(
|
||||||
|
new BoundarySpawnMessage(
|
||||||
|
new MoonTools.Structs.Position2D(0, 720),
|
||||||
|
1280,
|
||||||
|
6
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
World = WorldBuilder.Build();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
That paddle entity is looking out of place. Why don't we make a Spawner for it?
|
||||||
|
|
||||||
|
**PongFE/Messages/PaddleSpawnMessage.cs**:
|
||||||
|
|
||||||
|
```cs
|
||||||
|
using Encompass;
|
||||||
|
using MoonTools.Structs;
|
||||||
|
|
||||||
|
namespace PongFE.Messages
|
||||||
|
{
|
||||||
|
public struct PaddleSpawnMessage : IMessage
|
||||||
|
{
|
||||||
|
public Position2D Position { get; }
|
||||||
|
public PlayerIndex PlayerIndex { get; }
|
||||||
|
public int Width { get; }
|
||||||
|
public int Height { get; }
|
||||||
|
|
||||||
|
public PaddleSpawnMessage(Position2D position, PlayerIndex playerIndex, int width, int height)
|
||||||
|
{
|
||||||
|
Position = position;
|
||||||
|
PlayerIndex = playerIndex;
|
||||||
|
Width = width;
|
||||||
|
Height = height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Actually while we're here, let's consolidate our Enums.
|
||||||
|
|
||||||
|
Create a file, **Enums.cs**:
|
||||||
|
|
||||||
|
```cs
|
||||||
|
namespace PongFE.Enums
|
||||||
|
{
|
||||||
|
public enum PlayerIndex
|
||||||
|
{
|
||||||
|
One,
|
||||||
|
Two
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum HitOrientation
|
||||||
|
{
|
||||||
|
Horizontal,
|
||||||
|
Vertical
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum PaddleMoveDirection
|
||||||
|
{
|
||||||
|
Up,
|
||||||
|
Down
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Remove these references in the other files and make sure anything that uses these enums includes the line
|
||||||
|
|
||||||
|
```cs
|
||||||
|
using PongFE.Enums;
|
||||||
|
```
|
||||||
|
|
||||||
|
Now for the paddle spawner.
|
||||||
|
|
||||||
|
In **PongFE/Engines/Spawners/PaddleSpawner.cs**:
|
||||||
|
|
||||||
|
```cs
|
||||||
|
using Encompass;
|
||||||
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
|
using PongFE.Components;
|
||||||
|
using PongFE.Messages;
|
||||||
|
|
||||||
|
namespace PongFE.Spawners
|
||||||
|
{
|
||||||
|
public class PaddleSpawner : Spawner<PaddleSpawnMessage>
|
||||||
|
{
|
||||||
|
private Texture2D PaddleTexture { get; }
|
||||||
|
|
||||||
|
public PaddleSpawner(Texture2D paddleTexture)
|
||||||
|
{
|
||||||
|
PaddleTexture = paddleTexture;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Spawn(PaddleSpawnMessage message)
|
||||||
|
{
|
||||||
|
var paddle = CreateEntity();
|
||||||
|
AddComponent(paddle, new Texture2DComponent(PaddleTexture, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Actually, I'm starting to get a bad feeling here again. What if we wanted to change the size of the paddle? We have an implicit dependency between the paddle texture and the collision size, so why not make it explicit instead? All we'd have to do is scale a white pixel to our desired size to draw a filled rectangle.
|
||||||
|
|
||||||
|
In **PongFE/Components/Texture2DComponent.cs**:
|
||||||
|
|
||||||
|
```cs
|
||||||
|
using System.Numerics;
|
||||||
|
using Encompass;
|
||||||
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
|
|
||||||
|
namespace PongFE.Components
|
||||||
|
{
|
||||||
|
public struct Texture2DComponent : IComponent, IDrawableComponent
|
||||||
|
{
|
||||||
|
public Texture2D Texture { get; }
|
||||||
|
public Vector2 Scale { get; }
|
||||||
|
public int Layer { get; }
|
||||||
|
|
||||||
|
public Texture2DComponent(Texture2D texture, int layer, Vector2 scale)
|
||||||
|
{
|
||||||
|
Texture = texture;
|
||||||
|
Scale = scale;
|
||||||
|
Layer = layer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
And in **PongFE/Renderers/Texture2DRenderer.cs**:
|
||||||
|
|
||||||
|
```cs
|
||||||
|
public override void Render(Entity entity, in Texture2DComponent textureComponent)
|
||||||
|
{
|
||||||
|
ref readonly var positionComponent = ref GetComponent<PositionComponent>(entity);
|
||||||
|
|
||||||
|
_spriteBatch.Draw(
|
||||||
|
textureComponent.Texture,
|
||||||
|
positionComponent.Position.ToXNAVector(),
|
||||||
|
null,
|
||||||
|
Color.White,
|
||||||
|
0,
|
||||||
|
Vector2.Zero,
|
||||||
|
textureComponent.Scale.ToXNAVector(),
|
||||||
|
SpriteEffects.None,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Now go back to the paddle spawner...
|
||||||
|
|
||||||
|
```cs
|
||||||
|
using Encompass;
|
||||||
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
|
using PongFE.Components;
|
||||||
|
using PongFE.Messages;
|
||||||
|
|
||||||
|
namespace PongFE.Spawners
|
||||||
|
{
|
||||||
|
public class PaddleSpawner : Spawner<PaddleSpawnMessage>
|
||||||
|
{
|
||||||
|
private Texture2D WhitePixel { get; }
|
||||||
|
|
||||||
|
public PaddleSpawner(Texture2D whitePixel)
|
||||||
|
{
|
||||||
|
WhitePixel = whitePixel;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Spawn(PaddleSpawnMessage message)
|
||||||
|
{
|
||||||
|
var paddle = CreateEntity();
|
||||||
|
AddComponent(paddle, new PlayerInputComponent(message.PlayerIndex));
|
||||||
|
AddComponent(paddle, new PaddleMoveSpeedComponent(400));
|
||||||
|
AddComponent(paddle, new PositionComponent(message.Position));
|
||||||
|
AddComponent(paddle, new CollisionComponent(new MoonTools.Bonk.Rectangle(0, 0, message.Width, message.Height)));
|
||||||
|
AddComponent(paddle, new CanCauseBounceComponent());
|
||||||
|
AddComponent(paddle, new Texture2DComponent(WhitePixel, 0, new System.Numerics.Vector2(message.Width, message.Height)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
And in **PongFEGame.cs**:
|
||||||
|
|
||||||
|
```cs
|
||||||
|
WorldBuilder.AddEngine(new PaddleSpawner(WhitePixel));
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
WorldBuilder.SendMessage(
|
||||||
|
new PaddleSpawnMessage(
|
||||||
|
new MoonTools.Structs.Position2D(5, 5),
|
||||||
|
Enums.PlayerIndex.One,
|
||||||
|
20,
|
||||||
|
80
|
||||||
|
)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
You can get rid of the PaddleTexture references too. Why don't we do this for the ball as well?
|
||||||
|
|
||||||
|
**PongFE/Messages/BallSpawnMessage.cs**:
|
||||||
|
|
||||||
|
```cs
|
||||||
|
using System.Numerics;
|
||||||
|
using Encompass;
|
||||||
|
using MoonTools.Structs;
|
||||||
|
|
||||||
|
namespace PongFE.Messages
|
||||||
|
{
|
||||||
|
public struct BallSpawnMessage : IMessage
|
||||||
|
{
|
||||||
|
public Position2D Position { get; }
|
||||||
|
public Vector2 Velocity { get; }
|
||||||
|
public int Width { get; }
|
||||||
|
public int Height { get; }
|
||||||
|
|
||||||
|
public BallSpawnMessage(Position2D position, Vector2 velocity, int width, int height)
|
||||||
|
{
|
||||||
|
Position = position;
|
||||||
|
Velocity = velocity;
|
||||||
|
Width = width;
|
||||||
|
Height = height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**PongFE/Engines/Spawners/BallSpawner.cs**:
|
||||||
|
|
||||||
|
```cs
|
||||||
|
using Encompass;
|
||||||
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
|
using PongFE.Components;
|
||||||
|
using PongFE.Messages;
|
||||||
|
|
||||||
|
namespace PongFE.Spawners
|
||||||
|
{
|
||||||
|
public class BallSpawner : Spawner<BallSpawnMessage>
|
||||||
|
{
|
||||||
|
private Texture2D WhitePixel { get; }
|
||||||
|
|
||||||
|
public BallSpawner(Texture2D whitePixel)
|
||||||
|
{
|
||||||
|
WhitePixel = whitePixel;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Spawn(BallSpawnMessage message)
|
||||||
|
{
|
||||||
|
var ball = CreateEntity();
|
||||||
|
AddComponent(ball, new PositionComponent(message.Position));
|
||||||
|
AddComponent(ball, new VelocityComponent(message.Velocity));
|
||||||
|
AddComponent(ball, new CollisionComponent(new MoonTools.Bonk.Rectangle(0, 0, 16, 16)));
|
||||||
|
AddComponent(ball, new Texture2DComponent(WhitePixel, 0, new System.Numerics.Vector2(message.Width, message.Height)));
|
||||||
|
AddComponent(ball, new CanBeBouncedComponent());
|
||||||
|
AddComponent(ball, new BounceResponseComponent());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**PongFEGame.cs**
|
||||||
|
|
||||||
|
```cs
|
||||||
|
WorldBuilder.AddEngine(new BallSpawner(WhitePixel));
|
||||||
|
```
|
||||||
|
|
||||||
|
You can get rid of the BallTexture now too. We reduced the number of textures we're using and ensured that the visual display of our game entities will match their collision detection. It's always a good idea to analyze your current structures and see if they can be improved!
|
||||||
|
|
||||||
|
*Insert video here*
|
||||||
|
|
||||||
|
Getting closer...
|
|
@ -8,94 +8,158 @@ Now that we have the ball moving around and bouncing, let's get the computer-con
|
||||||
|
|
||||||
The computer-controlled paddle is essentially the same as the player-controlled paddle. The only difference is what causes it to move. So here's what we need to do.
|
The computer-controlled paddle is essentially the same as the player-controlled paddle. The only difference is what causes it to move. So here's what we need to do.
|
||||||
|
|
||||||
- Designate in the PaddleSpawnMessage what is controlling the paddle
|
- Designate in the PaddleSpawnMessage what is controlling the paddle, a player or the computer
|
||||||
- Implement an engine that sends PaddleMoveMessages to the paddle
|
- Implement an engine that sends PaddleMoveMessages to the paddle for the computer-controlled paddle
|
||||||
|
|
||||||
Let's revise our PaddleSpawnMessage. An enum type for our paddle control seems appropriate here.
|
Let's revise our PaddleSpawnMessage. An enum type for our paddle control seems appropriate here.
|
||||||
|
|
||||||
```ts
|
In **Enums.cs**:
|
||||||
import { Message } from "encompass-ecs";
|
|
||||||
|
|
||||||
export enum PaddleControlType {
|
```cs
|
||||||
player_one,
|
public enum PaddleControl
|
||||||
computer,
|
{
|
||||||
}
|
Player,
|
||||||
|
Computer
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
export class PaddleSpawnMessage extends Message {
|
In **PaddleSpawnMessage.cs**:
|
||||||
public x: number;
|
|
||||||
public y: number;
|
```cs
|
||||||
public width: number;
|
using Encompass;
|
||||||
public height: number;
|
using MoonTools.Structs;
|
||||||
public move_speed: number;
|
using PongFE.Enums;
|
||||||
public control_type: PaddleControlType;
|
|
||||||
|
namespace PongFE.Messages
|
||||||
|
{
|
||||||
|
public struct PaddleSpawnMessage : IMessage
|
||||||
|
{
|
||||||
|
public Position2D Position { get; }
|
||||||
|
public PlayerIndex PlayerIndex { get; }
|
||||||
|
public PaddleControl PaddleControl { get; }
|
||||||
|
public int Width { get; }
|
||||||
|
public int Height { get; }
|
||||||
|
|
||||||
|
public PaddleSpawnMessage(
|
||||||
|
Position2D position,
|
||||||
|
PlayerIndex playerIndex,
|
||||||
|
PaddleControl paddleControl,
|
||||||
|
int width,
|
||||||
|
int height
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Position = position;
|
||||||
|
PlayerIndex = playerIndex;
|
||||||
|
PaddleControl = paddleControl;
|
||||||
|
Width = width;
|
||||||
|
Height = height;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Then in the PaddleSpawner, where we add the PlayerOneComponent, let's put this instead:
|
Let's create a new component to designate computer control of the paddle.
|
||||||
|
|
||||||
```ts
|
**PongFE/Components/ComputerControlComponent.cs**:
|
||||||
if (message.control_type === PaddleControlType.player_one) {
|
|
||||||
paddle_entity.add_component(PlayerOneComponent);
|
```cs
|
||||||
} else if (message.control_type === PaddleControlType.computer) {
|
using Encompass;
|
||||||
paddle_entity.add_component(PlayerComputerComponent);
|
using PongFE.Enums;
|
||||||
|
|
||||||
|
namespace PongFE.Components
|
||||||
|
{
|
||||||
|
public struct ComputerControlComponent : IComponent
|
||||||
|
{
|
||||||
|
public PlayerIndex PlayerIndex { get; }
|
||||||
|
|
||||||
|
public ComputerControlComponent(PlayerIndex playerIndex)
|
||||||
|
{
|
||||||
|
PlayerIndex = playerIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then in the PaddleSpawner, where we add the PlayerInputComponent, let's put this instead:
|
||||||
|
|
||||||
|
```cs
|
||||||
|
if (message.PaddleControl == PaddleControl.Player)
|
||||||
|
{
|
||||||
|
AddComponent(paddle, new PlayerInputComponent(message.PlayerIndex));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AddComponent(paddle, new ComputerControlComponent(message.PlayerIndex));
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Now we can move on to the actual computer control behavior. The behavior of the Pong computer is pretty simple - it just moves the paddle towards the _y_ position of the ball.
|
Now we can move on to the actual computer control behavior. The behavior of the Pong computer is pretty simple - it just moves the paddle towards the _y_ position of the ball.
|
||||||
|
|
||||||
We don't actually have a way to get the ball from the game state yet, so let's make another marker component.
|
Let's create a new component.
|
||||||
|
|
||||||
In **game/components/ball.ts**:
|
In **PongFE/Components/CanBeTrackedComponent.cs**:
|
||||||
|
|
||||||
```ts
|
```cs
|
||||||
import { Component } from "encompass-ecs";
|
using Encompass;
|
||||||
|
|
||||||
export class BallComponent extends Component {}
|
|
||||||
|
|
||||||
|
namespace PongFE.Components
|
||||||
|
{
|
||||||
|
public struct CanBeTrackedComponent : IComponent { }
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
and in our BallSpawner:
|
and in our BallSpawner:
|
||||||
|
|
||||||
```ts
|
```cs
|
||||||
ball_entity.add_component(BallComponent);
|
AddComponent(ball, new CanBeTrackedComponent());
|
||||||
```
|
```
|
||||||
|
|
||||||
Now let's make our ComputerControlEngine. It will read the state of the game and tell the paddle to move in the direction of the ball.
|
Now let's make our ComputerControlEngine. It will read the state of the game and tell the paddle to move in the direction of the ball.
|
||||||
|
|
||||||
In **game/engines/computer_control.ts**:
|
In **PongFE/Engines/ComputerControlEngine.ts**:
|
||||||
|
|
||||||
```ts
|
```cs
|
||||||
import { Emits, Engine } from "encompass-ecs";
|
using Encompass;
|
||||||
import { BallComponent } from "game/components/ball";
|
using PongFE.Components;
|
||||||
import { PlayerComputerComponent } from "game/components/player_computer";
|
using PongFE.Enums;
|
||||||
import { PositionComponent } from "game/components/position";
|
using PongFE.Messages;
|
||||||
import { PaddleMoveMessage } from "game/messages/component/paddle_move";
|
|
||||||
|
|
||||||
@Emits(PaddleMoveMessage)
|
namespace PongFE.Engines
|
||||||
export class ComputerControlEngine extends Engine {
|
{
|
||||||
public update() {
|
[Reads(
|
||||||
const computer_components = this.read_components(PlayerComputerComponent);
|
typeof(ComputerControlComponent),
|
||||||
|
typeof(PositionComponent),
|
||||||
|
typeof(CanBeTrackedComponent)
|
||||||
|
)]
|
||||||
|
[Sends(typeof(PaddleMoveMessage))]
|
||||||
|
public class ComputerControlEngine : Engine
|
||||||
|
{
|
||||||
|
public override void Update(double dt)
|
||||||
|
{
|
||||||
|
foreach (ref readonly var entity in ReadEntities<ComputerControlComponent>())
|
||||||
|
{
|
||||||
|
if (HasComponent<PositionComponent>(entity))
|
||||||
|
{
|
||||||
|
ref readonly var trackerPositionComponent = ref GetComponent<PositionComponent>(entity);
|
||||||
|
|
||||||
const ball_component = this.read_component(BallComponent);
|
if (SomeComponent<CanBeTrackedComponent>())
|
||||||
if (!ball_component) { return; }
|
{
|
||||||
|
ref readonly var trackedEntity = ref ReadEntity<CanBeTrackedComponent>();
|
||||||
|
|
||||||
const ball_entity = this.get_entity(ball_component.entity_id);
|
if (HasComponent<PositionComponent>(trackedEntity))
|
||||||
if (!ball_entity) { return; }
|
{
|
||||||
|
ref readonly var trackedPositionComponent = ref GetComponent<PositionComponent>(trackedEntity);
|
||||||
|
|
||||||
const ball_position = ball_entity.get_component(PositionComponent);
|
if (trackerPositionComponent.Position.Y - trackedPositionComponent.Position.Y > 40)
|
||||||
|
{
|
||||||
for (const computer_component of computer_components.values()) {
|
SendMessage(new PaddleMoveMessage(entity, PaddleMoveDirection.Up));
|
||||||
const computer_entity = this.get_entity(computer_component.entity_id);
|
}
|
||||||
|
else if (trackerPositionComponent.Position.Y - trackedPositionComponent.Position.Y < -40)
|
||||||
if (computer_entity) {
|
{
|
||||||
const computer_position = computer_entity.get_component(PositionComponent);
|
SendMessage(new PaddleMoveMessage(entity, PaddleMoveDirection.Down));
|
||||||
|
}
|
||||||
if (computer_position.y - ball_position.y > 40) {
|
}
|
||||||
const message = this.emit_component_message(PaddleMoveMessage, computer_component);
|
}
|
||||||
message.direction = -1;
|
|
||||||
} else if (computer_position.y - ball_position.y < -40) {
|
|
||||||
const message = this.emit_component_message(PaddleMoveMessage, computer_component);
|
|
||||||
message.direction = 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -103,12 +167,29 @@ export class ComputerControlEngine extends Engine {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Notice how we are being careful not to assume the ball actually exists in this code. Remember - it never hurts to check if an object actually exists! You might save yourself from a nasty crash.
|
We have two new method showing up here, **SomeComponent**. SomeComponent is a handy method that lets us know if a component of a certain type exists in the World. **ReadEntity** just gives us an arbitrary entity that contains a component of a certain type.
|
||||||
|
|
||||||
Don't forget to add the new Engine in **game.ts**!
|
Notice how we are being careful not to assume the tracked entity actually exists in this code. Remember - it never hurts to check if a component actually exists! You might save yourself from a nasty crash.
|
||||||
|
|
||||||
|
Don't forget to add the new Engine in **PongFEgame.cs**!
|
||||||
|
|
||||||
|
```cs
|
||||||
|
WorldBuilder.AddEngine(new ComputerControlEngine());
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally, let's send a message to spawn the computer-controlled paddle.
|
||||||
|
|
||||||
|
```cs
|
||||||
|
WorldBuilder.SendMessage(
|
||||||
|
new PaddleSpawnMessage(
|
||||||
|
new MoonTools.Structs.Position2D(1255, 5),
|
||||||
|
Enums.PlayerIndex.Two,
|
||||||
|
PaddleControl.Computer,
|
||||||
|
20,
|
||||||
|
80
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
```ts
|
|
||||||
world_builder.add_engine(ComputerControlEngine);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
<video width="75%" autoplay="autoplay" muted="muted" loop="loop" style="display: block; margin: 0 auto;">
|
<video width="75%" autoplay="autoplay" muted="muted" loop="loop" style="display: block; margin: 0 auto;">
|
||||||
|
@ -117,4 +198,4 @@ world_builder.add_engine(ComputerControlEngine);
|
||||||
|
|
||||||
If we were doing this in an object-oriented way, we would have had to inherit from the paddle or introduce another state to the paddle, thus forcing us to refactor or increase the complexity of the paddle object itself.
|
If we were doing this in an object-oriented way, we would have had to inherit from the paddle or introduce another state to the paddle, thus forcing us to refactor or increase the complexity of the paddle object itself.
|
||||||
|
|
||||||
Notice how in our case we didn't really have to change any of our existing logic - all we had to do was create a new component and write a new engine for producing behavior from that component, while getting to retain all the behavior we got from the other paddle components. See how clean and de-coupled this is? This is the power of _composition_ over _inheritance_.
|
Notice how in our case we didn't really have to change any of our existing logic - all we had to do was create new components and write a new engine for producing behavior from those components, while getting to retain all the behavior we got from the other paddle components. See how clean and de-coupled this is? This is the power of _composition_ over _inheritance_.
|
||||||
|
|
Loading…
Reference in New Issue