title screen
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
parent
c9f15ad85a
commit
ac67f06793
|
@ -4,165 +4,257 @@ date: 2019-06-09T16:51:13-07:00
|
||||||
weight: 20
|
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
|
In **Enums.cs**:
|
||||||
export abstract class State {
|
|
||||||
public abstract load(): void;
|
```cs
|
||||||
public abstract update(dt: number): void;
|
public enum GameState
|
||||||
public abstract draw(): void;
|
{
|
||||||
|
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
|
namespace PongFE.Components
|
||||||
export class Game extends State {
|
{
|
||||||
```
|
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.
|
public GameStateComponent(GameState gameState)
|
||||||
|
{
|
||||||
```ts
|
GameState = gameState;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
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
|
In **PongFEGame.cs**:
|
||||||
declare global {let PROF_CAPTURE: boolean; }
|
|
||||||
PROF_CAPTURE = false; // set this to true to enable profiling
|
|
||||||
|
|
||||||
import * as jprof from "encompass-jprof";
|
```cs
|
||||||
import { State } from "game/state";
|
var gameStateEntity = WorldBuilder.CreateEntity();
|
||||||
import { Game } from "game/states/game";
|
WorldBuilder.SetComponent(gameStateEntity, new GameStateComponent(GameState.Title));
|
||||||
import { Title } from "game/states/title";
|
```
|
||||||
|
|
||||||
let menu: Title;
|
Let's make a new Engine.
|
||||||
let game: Game;
|
|
||||||
let current_state: State;
|
|
||||||
|
|
||||||
love.load = () => {
|
In **PongFE/Engines/GameStateEngine.cs**:
|
||||||
love.window.setMode(1280, 720, {vsync: false, msaa: 2});
|
|
||||||
love.math.setRandomSeed(os.time());
|
|
||||||
love.mouse.setVisible(false);
|
|
||||||
|
|
||||||
menu = new Title();
|
```cs
|
||||||
menu.load();
|
using Encompass;
|
||||||
|
using Microsoft.Xna.Framework.Input;
|
||||||
|
using PongFE.Components;
|
||||||
|
using PongFE.Enums;
|
||||||
|
using PongFE.Messages;
|
||||||
|
|
||||||
game = new Game();
|
namespace PongFE.Engines
|
||||||
game.load();
|
{
|
||||||
|
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);
|
||||||
|
|
||||||
current_state = menu;
|
if (gameStateComponent.GameState == GameState.Title)
|
||||||
};
|
{
|
||||||
|
if (Keyboard.GetState().IsKeyDown(Keys.Enter))
|
||||||
love.update = (dt) => {
|
{
|
||||||
current_state.update(dt);
|
StartGame();
|
||||||
if (current_state === menu) {
|
SetComponent(gameStateEntity, new GameStateComponent(GameState.Game));
|
||||||
if (love.keyboard.isDown("space")) {
|
}
|
||||||
current_state = game;
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
love.draw = () => {
|
private void StartGame()
|
||||||
current_state.draw();
|
{
|
||||||
|
ref readonly var playAreaComponent = ref ReadComponent<PlayAreaComponent>();
|
||||||
|
var playAreaWidth = playAreaComponent.Width;
|
||||||
|
var playAreaHeight = playAreaComponent.Height;
|
||||||
|
|
||||||
love.graphics.setBlendMode("alpha");
|
SendMessage(
|
||||||
love.graphics.setColor(1, 1, 1, 1);
|
new PaddleSpawnMessage(
|
||||||
love.graphics.print("Current FPS: " + tostring(love.timer.getFPS()), 10, 10);
|
new MoonTools.Structs.Position2D(20, playAreaHeight / 2 - 40),
|
||||||
};
|
Enums.PlayerIndex.One,
|
||||||
|
PaddleControl.Player,
|
||||||
|
20,
|
||||||
|
80
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
love.quit = () => {
|
SendMessage(
|
||||||
jprof.write("prof.mpack");
|
new PaddleSpawnMessage(
|
||||||
return false;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
And let's tweak the *CenterLineRenderer.Render* method a bit.
|
||||||
|
|
||||||
|
```cs
|
||||||
|
public override void Render()
|
||||||
|
{
|
||||||
|
ref readonly var gameStateComponent = ref ReadComponent<GameStateComponent>();
|
||||||
|
|
||||||
|
if (gameStateComponent.GameState == GameState.Game)
|
||||||
|
{
|
||||||
|
ref readonly var playAreaComponent = ref ReadComponent<PlayAreaComponent>();
|
||||||
|
|
||||||
|
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!
|
Let's try it!
|
||||||
|
|
||||||
![pong title](/images/pong-title.png)
|
![pong title](/images/pong_title.png)
|
||||||
|
|
||||||
Nice!
|
Nice!
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 75 KiB |
Binary file not shown.
After Width: | Height: | Size: 362 KiB |
Loading…
Reference in New Issue