title screen
continuous-integration/drone/push Build is passing Details

main
Evan Hemsley 2020-07-19 15:22:18 -07:00
parent c9f15ad85a
commit ac67f06793
3 changed files with 222 additions and 130 deletions

View File

@ -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; }
public GameStateComponent(GameState gameState)
{
GameState = gameState;
}
}
}
```
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.
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
import { State } from "game/state";
In **PongFEGame.cs**:
export class Title extends State {
private title_font: Font;
private title_text: Text;
```cs
var gameStateEntity = WorldBuilder.CreateEntity();
WorldBuilder.SetComponent(gameStateEntity, new GameStateComponent(GameState.Title));
```
private play_font: Font;
private play_text: Text;
Let's make a new Engine.
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");
In **PongFE/Engines/GameStateEngine.cs**:
this.play_font = love.graphics.newFont("game/assets/fonts/Squared Display.ttf", 32);
this.play_text = love.graphics.newText(this.play_font, "Press Space");
```cs
using Encompass;
using Microsoft.Xna.Framework.Input;
using PongFE.Components;
using PongFE.Enums;
using PongFE.Messages;
namespace PongFE.Engines
{
public class GameStateEngine : Engine
{
public override void Update(double dt)
{
ref readonly var gameStateEntity = ref ReadEntity<GameStateComponent>();
ref readonly var gameStateComponent = ref GetComponent<GameStateComponent>(gameStateEntity);
if (gameStateComponent.GameState == GameState.Title)
{
if (Keyboard.GetState().IsKeyDown(Keys.Enter))
{
StartGame();
SetComponent(gameStateEntity, new GameStateComponent(GameState.Game));
}
}
}
}
public update() {}
private void StartGame()
{
ref readonly var playAreaComponent = ref ReadComponent<PlayAreaComponent>();
var playAreaWidth = playAreaComponent.Width;
var playAreaHeight = playAreaComponent.Height;
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,
SendMessage(
new PaddleSpawnMessage(
new MoonTools.Structs.Position2D(20, playAreaHeight / 2 - 40),
Enums.PlayerIndex.One,
PaddleControl.Player,
20,
80
)
);
love.graphics.draw(
this.play_text,
640,
480,
0,
1,
1,
this.play_text.getWidth() * 0.5,
this.play_text.getHeight() * 0.5,
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<GameStateComponent>();
ref readonly var playAreaComponent = ref ReadComponent<PlayAreaComponent>();
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);
}
}
}
}
```
Now in **main.ts** we can put code to handle our states.
And let's tweak the *CenterLineRenderer.Render* method a bit.
```ts
let menu: Title;
let game: Game;
let current_state: State;
```cs
public override void Render()
{
ref readonly var gameStateComponent = ref ReadComponent<GameStateComponent>();
love.load = () => {
...
if (gameStateComponent.GameState == GameState.Game)
{
ref readonly var playAreaComponent = ref ReadComponent<PlayAreaComponent>();
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;
DrawDottedLine(playAreaComponent.Width / 2, 0, playAreaComponent.Width / 2, playAreaComponent.Height, 20, 20);
}
}
};
love.draw = () => {
current_state.draw();
...
}
```
The final result of **main.ts** should look like this.
Let's set everything up in **PongFEGame**.
```ts
declare global {let PROF_CAPTURE: boolean; }
PROF_CAPTURE = false; // set this to true to enable profiling
```cs
...
import * as jprof from "encompass-jprof";
import { State } from "game/state";
import { Game } from "game/states/game";
import { Title } from "game/states/title";
DynamicSpriteFont InstructionFont { get; set; }
let menu: Title;
let game: Game;
let current_state: State;
...
love.load = () => {
love.window.setMode(1280, 720, {vsync: false, msaa: 2});
love.math.setRandomSeed(os.time());
love.mouse.setVisible(false);
InstructionFont = DynamicSpriteFont.FromTtf(
File.ReadAllBytes(@"Content/Fonts/SquaredDisplay.ttf"),
48
);
menu = new Title();
menu.load();
...
game = new Game();
game.load();
WorldBuilder.AddEngine(new GameStateEngine());
current_state = menu;
};
...
love.update = (dt) => {
current_state.update(dt);
if (current_state === menu) {
if (love.keyboard.isDown("space")) {
current_state = game;
}
}
};
WorldBuilder.AddGeneralRenderer(new TitleRenderer(SpriteBatch, ScoreFont, InstructionFont), 0);
love.draw = () => {
current_state.draw();
love.graphics.setBlendMode("alpha");
love.graphics.setColor(1, 1, 1, 1);
love.graphics.print("Current FPS: " + tostring(love.timer.getFPS()), 10, 10);
};
love.quit = () => {
jprof.write("prof.mpack");
return false;
};
...
```
Let's try it!
![pong title](/images/pong-title.png)
![pong title](/images/pong_title.png)
Nice!

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 362 KiB