diff --git a/content/pong/polish/title.md b/content/pong/polish/title.md index 918664e..8172dab 100644 --- a/content/pong/polish/title.md +++ b/content/pong/polish/title.md @@ -4,165 +4,257 @@ date: 2019-06-09T16:51:13-07:00 weight: 20 --- -It would be nice to have a title screen. Let's make that happen. +It would be nice to have a title screen instead of launching right into gameplay. Let's make that happen. -I would like us to have a concept of game state. The title menu is a pretty distinct thing from the game itself so it feels nicer to have it be self contained instead of managing extra state in the game world. +You might be tempted to create a separate World for the title screen or different game modes. While this is possible, I don't really recommend it, because it is difficult to share information between different Worlds by design. -Let's create a new class in **game/state.ts**: +We could introduce a concept of game state here. -```ts -export abstract class State { - public abstract load(): void; - public abstract update(dt: number): void; - public abstract draw(): void; +In **Enums.cs**: + +```cs +public enum GameState +{ + Title, + Game } ``` -Remember, _abstract_ means that the class cannot be used directly, but describes features that exist in inherited classes. So we know that anything we make that inherits from State must have a load(), update(dt), and draw() method. +In **PongFE/Components/GameStateComponent.cs**: -Let's create a new folder, **game/states**, and put **game.ts** in there. Let's also make it inherit from State: +```cs +using Encompass; +using PongFE.Enums; -```ts -export class Game extends State { -``` +namespace PongFE.Components +{ + public struct GameStateComponent : IComponent + { + public GameState GameState { get; } -Let's make a new State called Title. It doesn't need to do much - just display the game title and a prompt for the player to start the game. - -```ts -import { State } from "game/state"; - -export class Title extends State { - private title_font: Font; - private title_text: Text; - - private play_font: Font; - private play_text: Text; - - public load() { - this.title_font = love.graphics.newFont("game/assets/fonts/Squared Display.ttf", 128); - this.title_text = love.graphics.newText(this.title_font, "Encompass Pong"); - - this.play_font = love.graphics.newFont("game/assets/fonts/Squared Display.ttf", 32); - this.play_text = love.graphics.newText(this.play_font, "Press Space"); - } - - public update() {} - - public draw() { - love.graphics.draw( - this.title_text, - 640, - 240, - 0, - 1, - 1, - this.title_text.getWidth() * 0.5, - this.title_text.getHeight() * 0.5, - ); - - love.graphics.draw( - this.play_text, - 640, - 480, - 0, - 1, - 1, - this.play_text.getWidth() * 0.5, - this.play_text.getHeight() * 0.5, - ); - } -} -``` - -Now in **main.ts** we can put code to handle our states. - -```ts -let menu: Title; -let game: Game; -let current_state: State; - -love.load = () => { - ... - - menu = new Menu(); - menu.load(); - - game = new Game(); - game.load(); - - current_state = menu; -}; - -love.update = (dt) => { - current_state.update(dt); - if (current_state === menu) { - if (love.keyboard.isDown("space")) { - current_state = game; + public GameStateComponent(GameState gameState) + { + GameState = gameState; } } -}; - -love.draw = () => { - current_state.draw(); - - ... } ``` -The final result of **main.ts** should look like this. +This component will be something we call a _singleton component_. Just one of them exists in the world to conveniently track some kind of global state. -```ts -declare global {let PROF_CAPTURE: boolean; } -PROF_CAPTURE = false; // set this to true to enable profiling +In **PongFEGame.cs**: -import * as jprof from "encompass-jprof"; -import { State } from "game/state"; -import { Game } from "game/states/game"; -import { Title } from "game/states/title"; +```cs +var gameStateEntity = WorldBuilder.CreateEntity(); +WorldBuilder.SetComponent(gameStateEntity, new GameStateComponent(GameState.Title)); +``` -let menu: Title; -let game: Game; -let current_state: State; +Let's make a new Engine. -love.load = () => { - love.window.setMode(1280, 720, {vsync: false, msaa: 2}); - love.math.setRandomSeed(os.time()); - love.mouse.setVisible(false); +In **PongFE/Engines/GameStateEngine.cs**: - menu = new Title(); - menu.load(); +```cs +using Encompass; +using Microsoft.Xna.Framework.Input; +using PongFE.Components; +using PongFE.Enums; +using PongFE.Messages; - game = new Game(); - game.load(); +namespace PongFE.Engines +{ + public class GameStateEngine : Engine + { + public override void Update(double dt) + { + ref readonly var gameStateEntity = ref ReadEntity(); + ref readonly var gameStateComponent = ref GetComponent(gameStateEntity); - current_state = menu; -}; - -love.update = (dt) => { - current_state.update(dt); - if (current_state === menu) { - if (love.keyboard.isDown("space")) { - current_state = game; + if (gameStateComponent.GameState == GameState.Title) + { + if (Keyboard.GetState().IsKeyDown(Keys.Enter)) + { + StartGame(); + SetComponent(gameStateEntity, new GameStateComponent(GameState.Game)); + } + } } } -}; -love.draw = () => { - current_state.draw(); + private void StartGame() + { + ref readonly var playAreaComponent = ref ReadComponent(); + var playAreaWidth = playAreaComponent.Width; + var playAreaHeight = playAreaComponent.Height; - love.graphics.setBlendMode("alpha"); - love.graphics.setColor(1, 1, 1, 1); - love.graphics.print("Current FPS: " + tostring(love.timer.getFPS()), 10, 10); -}; + SendMessage( + new PaddleSpawnMessage( + new MoonTools.Structs.Position2D(20, playAreaHeight / 2 - 40), + Enums.PlayerIndex.One, + PaddleControl.Player, + 20, + 80 + ) + ); -love.quit = () => { - jprof.write("prof.mpack"); - return false; -}; + SendMessage( + new PaddleSpawnMessage( + new MoonTools.Structs.Position2D(playAreaWidth - 45, playAreaHeight / 2 - 40), + Enums.PlayerIndex.Two, + PaddleControl.Computer, + 20, + 80 + ) + ); + + SendMessage( + new BallSpawnMessage( + new MoonTools.Structs.Position2D(playAreaWidth / 2, playAreaHeight / 2), + 500, + 16, + 16 + ), + 0.5 + ); + + // top boundary + SendMessage( + new BoundarySpawnMessage( + new MoonTools.Structs.Position2D(0, -6), + playAreaWidth, + 6 + ) + ); + + // bottom boundary + SendMessage( + new BoundarySpawnMessage( + new MoonTools.Structs.Position2D(0, playAreaHeight), + playAreaWidth, + 6 + ) + ); + + // right boundary + SendMessage( + new GoalBoundarySpawnMessage( + Enums.PlayerIndex.One, + new MoonTools.Structs.Position2D(playAreaWidth, 0), + 6, + playAreaHeight + ) + ); + + // left boundary + SendMessage( + new GoalBoundarySpawnMessage( + Enums.PlayerIndex.Two, + new MoonTools.Structs.Position2D(-6, 0), + 6, + playAreaHeight + ) + ); + } +``` + +Notice how we took most of the *StartGame* stuff from the *PongFEGame.LoadContent* method. Make sure to take those message sends out of that method. + +Now we need to draw the title screen. There's many different approaches. Generic UI text rendering elements would probably be a good idea. But I'm lazy, so let's just set up a GeneralRenderer. + +In **PongFE/Renderers/TitleRenderer.cs**: + +```cs +using Encompass; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using PongFE.Components; +using PongFE.Enums; +using SpriteFontPlus; + +namespace PongFE.Renderers +{ + public class TitleRenderer : GeneralRenderer + { + private SpriteBatch SpriteBatch { get; } + private DynamicSpriteFont TitleFont { get; } + private DynamicSpriteFont InstructionFont { get; } + + public TitleRenderer(SpriteBatch spriteBatch, DynamicSpriteFont titleFont, DynamicSpriteFont instructionFont) + { + SpriteBatch = spriteBatch; + TitleFont = titleFont; + InstructionFont = instructionFont; + } + + public override void Render() + { + ref readonly var gameStateComponent = ref ReadComponent(); + ref readonly var playAreaComponent = ref ReadComponent(); + + if (gameStateComponent.GameState == GameState.Title) + { + var titleDimensions = TitleFont.MeasureString("PongFE"); + var titlePosition = new Vector2( + (playAreaComponent.Width - titleDimensions.X) / 2, + (playAreaComponent.Height - titleDimensions.Y) / 4 + ); + + var instructionDimensions = InstructionFont.MeasureString("Press Enter to begin"); + var instructionPosition = new Vector2( + (playAreaComponent.Width - instructionDimensions.X) / 2, + playAreaComponent.Height * 2 / 3 + ); + + SpriteBatch.DrawString(TitleFont, "PongFE", titlePosition, Color.White); + SpriteBatch.DrawString(InstructionFont, "Press Enter to begin", instructionPosition, Color.White); + } + } + } +} +``` + +And let's tweak the *CenterLineRenderer.Render* method a bit. + +```cs +public override void Render() +{ + ref readonly var gameStateComponent = ref ReadComponent(); + + if (gameStateComponent.GameState == GameState.Game) + { + ref readonly var playAreaComponent = ref ReadComponent(); + + DrawDottedLine(playAreaComponent.Width / 2, 0, playAreaComponent.Width / 2, playAreaComponent.Height, 20, 20); + } +} +``` + +Let's set everything up in **PongFEGame**. + +```cs + ... + + DynamicSpriteFont InstructionFont { get; set; } + + ... + + InstructionFont = DynamicSpriteFont.FromTtf( + File.ReadAllBytes(@"Content/Fonts/SquaredDisplay.ttf"), + 48 + ); + + ... + + WorldBuilder.AddEngine(new GameStateEngine()); + + ... + + WorldBuilder.AddGeneralRenderer(new TitleRenderer(SpriteBatch, ScoreFont, InstructionFont), 0); + + ... ``` Let's try it! -![pong title](/images/pong-title.png) +![pong title](/images/pong_title.png) Nice! diff --git a/static/images/pong-title.png b/static/images/pong-title.png deleted file mode 100644 index 08517e8..0000000 Binary files a/static/images/pong-title.png and /dev/null differ diff --git a/static/images/pong_title.png b/static/images/pong_title.png new file mode 100644 index 0000000..e16c4f6 Binary files /dev/null and b/static/images/pong_title.png differ