diff --git a/content/pong/ball/bouncing/putting_it_together.md b/content/pong/ball/bouncing/putting_it_together.md deleted file mode 100644 index 2cd178e..0000000 --- a/content/pong/ball/bouncing/putting_it_together.md +++ /dev/null @@ -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 -``` - - - -All our hard work paid off. Look at that! *chef kiss* diff --git a/content/pong/ball/revisiting_spawners.md b/content/pong/ball/revisiting_spawners.md new file mode 100644 index 0000000..a541f52 --- /dev/null +++ b/content/pong/ball/revisiting_spawners.md @@ -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 + { + 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(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 + { + 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 + { + 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... diff --git a/content/pong/opponent/_index.md b/content/pong/opponent/_index.md index 049e4ff..f8278ab 100644 --- a/content/pong/opponent/_index.md +++ b/content/pong/opponent/_index.md @@ -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. -- Designate in the PaddleSpawnMessage what is controlling the paddle -- Implement an engine that sends PaddleMoveMessages to 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 for the computer-controlled paddle Let's revise our PaddleSpawnMessage. An enum type for our paddle control seems appropriate here. -```ts -import { Message } from "encompass-ecs"; +In **Enums.cs**: -export enum PaddleControlType { - player_one, - computer, -} +```cs + public enum PaddleControl + { + Player, + Computer + } +``` -export class PaddleSpawnMessage extends Message { - public x: number; - public y: number; - public width: number; - public height: number; - public move_speed: number; - public control_type: PaddleControlType; +In **PaddleSpawnMessage.cs**: + +```cs +using Encompass; +using MoonTools.Structs; +using PongFE.Enums; + +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 - if (message.control_type === PaddleControlType.player_one) { - paddle_entity.add_component(PlayerOneComponent); - } else if (message.control_type === PaddleControlType.computer) { - paddle_entity.add_component(PlayerComputerComponent); +**PongFE/Components/ComputerControlComponent.cs**: + +```cs +using Encompass; +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. -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 -import { Component } from "encompass-ecs"; - -export class BallComponent extends Component {} +```cs +using Encompass; +namespace PongFE.Components +{ + public struct CanBeTrackedComponent : IComponent { } +} ``` and in our BallSpawner: -```ts - ball_entity.add_component(BallComponent); +```cs +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. -In **game/engines/computer_control.ts**: +In **PongFE/Engines/ComputerControlEngine.ts**: -```ts -import { Emits, Engine } from "encompass-ecs"; -import { BallComponent } from "game/components/ball"; -import { PlayerComputerComponent } from "game/components/player_computer"; -import { PositionComponent } from "game/components/position"; -import { PaddleMoveMessage } from "game/messages/component/paddle_move"; +```cs +using Encompass; +using PongFE.Components; +using PongFE.Enums; +using PongFE.Messages; -@Emits(PaddleMoveMessage) -export class ComputerControlEngine extends Engine { - public update() { - const computer_components = this.read_components(PlayerComputerComponent); +namespace PongFE.Engines +{ + [Reads( + 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()) + { + if (HasComponent(entity)) + { + ref readonly var trackerPositionComponent = ref GetComponent(entity); - const ball_component = this.read_component(BallComponent); - if (!ball_component) { return; } + if (SomeComponent()) + { + ref readonly var trackedEntity = ref ReadEntity(); - const ball_entity = this.get_entity(ball_component.entity_id); - if (!ball_entity) { return; } + if (HasComponent(trackedEntity)) + { + ref readonly var trackedPositionComponent = ref GetComponent(trackedEntity); - const ball_position = ball_entity.get_component(PositionComponent); - - for (const computer_component of computer_components.values()) { - const computer_entity = this.get_entity(computer_component.entity_id); - - if (computer_entity) { - const computer_position = computer_entity.get_component(PositionComponent); - - 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; + if (trackerPositionComponent.Position.Y - trackedPositionComponent.Position.Y > 40) + { + SendMessage(new PaddleMoveMessage(entity, PaddleMoveDirection.Up)); + } + else if (trackerPositionComponent.Position.Y - trackedPositionComponent.Position.Y < -40) + { + SendMessage(new PaddleMoveMessage(entity, PaddleMoveDirection.Down)); + } + } + } } } } @@ -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); ```