Step 6¶
Let us round up this tutorial by adding in the UI for the Game Over state and adding a Timer for extra challenge.
We will be using a UI.Button
to help us handle the user input for restarting the game.
Game Over¶
@define(game-over-ui {
UI.CentralPanel(
Contents: {
UI.Area(
Position: @f2(0.0 0.0)
Anchor: Anchor::Center
Contents: {
"GAME OVER" | UI.Label
UI.Horizontal(
Contents: {
"Final Score: " | UI.Label
total-score | ToString | UI.Label
"/" | UI.Label
@total-rounds | ToString | UI.Label
})
UI.Button(
Label: "Play Again"
Action: {
@reset-game-variables
}
)
}
)
}
)
})
UI.Button
is a UI element that appears as a clickable button with the text passed into itsLabel
parameter displayed on it.
When the button is pressed, we reset the game's variables, including game-over
which will be used to decide what is drawn in our game's window.
Navigate to ui-loop
and add in game-over
conditionals.
UI.CentralPanel(
Contents: {
UI.Horizontal(
Contents: {
UI.Area(
Position: @f2(-250.0 0.0)
Anchor: Anchor::Center
Contents: {
images | Take(left-image-index) | UI.Image
})
UI.Area(
Position: @f2(250.0 0.0)
Anchor: Anchor::Center
Contents: {
images | Take(right-image-index) | UI.Image
})
})
})
game-over
If(Predicate: Is(false) Then: {
UI.CentralPanel(
Contents: {
UI.Horizontal(
Contents: {
UI.Area(
Position: @f2(-250.0 0.0)
Anchor: Anchor::Center
Contents: {
images | Take(left-image-index) | UI.Image
})
UI.Area(
Position: @f2(250.0 0.0)
Anchor: Anchor::Center
Contents: {
images | Take(right-image-index) | UI.Image
})
})
})
} Else: {
@game-over-ui
})
- Draw the Game Over UI if game-over is true.
- Draw the Main Game UI if game-over is false.
The game will now display the main game area or a game over screen depending on whether game-over
is true.
Implementing a Timer¶
To add additional challenge to the game (for additional fun!), we can impose a time limit on each round.
Let us write @define
that decreases the time-remaining
every time it is called.
@define(time-tick {
game-over
When(Predicate: Is(false) Action: {
time-remaining | Math.Subtract(1) > time-remaining
When(Predicate: IsLessEqual(0) Action: {
Step(end-round)
})
})
})
- When the game is still running...
- ... decrease the time remaining by 1.
- The round is forced to end if the time remaining falls below 0.
We can have timer-tick
called every second consistently by using the Once
shard.
Place it within the logic-loop
where we are checking if a new round should be started.
@wire(logic-loop {
Once({
@time-tick
} Every: 1.0)
game-over
When(Predicate: {
Is(false) ;; (4)
And
new-round | Is(true) ;; (1)
} Action: {
@initialize-round ;; (3)
})
} Looped: true)
- Runs the timer-tick shard every 1 second.
Once
will run the code in itsAction
parameter every time the duration specified in itsEvery
parameter has passed.
@define(time-tick {
game-over
When(Predicate: Is(false) Action: {
time-remaining | Math.Subtract(1) > time-remaining
When(Predicate: IsLessEqual(0) Action: {
Step(end-round)
})
})
})
@define(game-over-ui {
UI.CentralPanel(
Contents: {
UI.Area(
Position: @f2(0.0 0.0)
Anchor: Anchor::Center
Contents: {
"GAME OVER" | UI.Label
UI.Horizontal(
Contents: {
"Final Score: " | UI.Label
total-score | ToString | UI.Label
"/" | UI.Label
@total-rounds | ToString | UI.Label
})
UI.Button(
Label: "Play Again"
Action: {
@reset-game-variables
}
)
}
)
}
)
})
@define(reset-game-variables {
@reset-round-variables
0 > total-score
1 > current-round
false > game-over
true > new-round
})
@wire(end-round {
Once({
0 >= new-round-number ;; (1)
})
current-round | Math.Add(1) > new-round-number ;; (2)
new-round-number
If(Predicate: IsMore(@total-rounds) Then: { ;; (3)
true > game-over ;; (4)
} Else: {
new-round-number > current-round ;; (5)
true > new-round ;; (6)
})
} Looped: true)
@define(reset-round-variables {
false > new-round
@max-timer > time-remaining
})
@define( initialize-round {
RandomInt(Max: total-images) > left-image-index
RandomInt(Max: total-images) > right-image-index
left-image-index
If(Predicate: { ;; (1)
Is(right-image-index) ;; (2)
} Then: {
true > same-image
} Else: {
false > same-image
})
@reset-round-variables
})
@define(total-rounds 10)
@define(max-timer 5)
@define(load-resources {
LoadImage("data/cats/cat01.png") | Push(Name: images) ;; (1)(2)
LoadImage("data/cats/cat02.png") | Push(Name: images)
LoadImage("data/cats/cat03.png") | Push(Name: images)
})
@define( initialize-variables {
;; Variables to reset each round
true >= new-round
@max-timer >= time-remaining
;; Variables to reset each game
0 >= total-score
1 >= current-round
false >= game-over
;; Other Shared Variables
0 >= left-image-index
0 >= right-image-index
Count(images) >= total-images
true >= same-image
})
@define( main-game-ui {
UI.BottomPanel(
Contents: { "Are they the same image? Press the UP arrow if YES, and the DOWN arrow if NO." | UI.Label
})
UI.TopPanel(
Contents: {
UI.Horizontal(
Contents: {
"Score: " | UI.Label
total-score | ToString | UI.Label
UI.Separator
"Round: " | UI.Label
current-round | ToString | UI.Label
UI.Separator
"Time Left: " | UI.Label
time-remaining | ToString | UI.Label
})
})
game-over
If(Predicate: Is(false) Then: {
UI.CentralPanel(
Contents: {
UI.Horizontal(
Contents: {
UI.Area(
Position: @f2(-250.0 0.0)
Anchor: Anchor::Center
Contents: {
images | Take(left-image-index) | UI.Image
})
UI.Area(
Position: @f2(250.0 0.0)
Anchor: Anchor::Center
Contents: {
images | Take(right-image-index) | UI.Image
})
})
})
} Else: {
@game-over-ui
})
})
@wire( ui-loop {
GFX.MainWindow(
Title: "Yes-No Game"
Width: 1280 Height: 768
Contents: {
Once({
GFX.DrawQueue >= ui-draw-queue
GFX.UIPass(ui-draw-queue) >> render-steps
})
ui-draw-queue | GFX.ClearQueue
Inputs.KeyDown(
Key: "up"
Action: {
same-image
When(Predicate: {
Is(true)
And
game-over | Is(false)
} Action: {
total-score | Math.Add(1) > total-score
Step(end-round)
})
}
)
Inputs.KeyDown(
Key: "down"
Action: {
same-image
When(Predicate: {
Is(false)
And
game-over | Is(false)
} Action: {
total-score | Math.Add(1) > total-score
Step(end-round)
})
}
)
UI(Contents: {
@main-game-ui
}) | UI.Render(ui-draw-queue)
GFX.Render(Steps: render-steps)
})
} Looped: true)
@wire(logic-loop {
Once({
@time-tick
} Every: 1.0)
game-over
When(Predicate: {
Is(false) ;; (4)
And
new-round | Is(true) ;; (1)
} Action: {
@initialize-round ;; (3)
})
} Looped: true)
@wire( game-loop {
Once({
@load-resources
@initialize-variables
})
Branch(Wires: [ui-loop logic-loop])
} Looped: true)
@mesh(main)
@schedule(main game-loop)
@run(main FPS: 60)
Note
You can adjust the difficulty by changing the value of the max-timer
constant we defined at the start.
Outcome¶
Congratulations on making it to the end!
Your game now has 10 rounds, calculates the total score, shows a Game Over screen and lets you play it again at the end. It even has a timer to add a time constraint to each round.
Now that the tutorial is over, why not challenge yourself further by implementing a high-score system? You could also try adding support for a second player!
The possibilities are endless with the power of Shards in your hands.
The full game script can be found here.