-
Notifications
You must be signed in to change notification settings - Fork 0
Tutorial 1: Game Flow
Important
This tutorial starts at the workshop-tutorial-0 branch and the complete solution can be found at workshop-tutorial-1 branch.
This tutorial focuses on the implementation of the game flow and the Camera class.
Note
This tutorial will implement/update the following modules:
Before everything, we need to set up the flow of the game. The game flow contatins the setup, the main loop, and the cleanup of the game. We are going to implement these in game.main and train.main.
Before doing anything, let's first import the pygame module.
import argparse
from pathlib import Path
from typing import List, Tuple
import pygame # NEWWe are first going to implement the main_scene function, which is called from the main function and given the arguments args, which we will use to setup the window. Take a look at the args.resolution and args.fullscreen arguments.
Solution Code
def main_scene(args: argparse.Namespace):
"""
Main scene for training the AI
:param args: The arguments
:return: None
"""
#vvv NEW vvv#
# Initialize pygame by calling `init`
pygame.init()
# Set the window title with `set_caption`
pygame.display.set_caption("Simple RL Driver - Game Play")
# Create a clock object to help control the frame rate
clock = pygame.time.Clock()
# Create a screen with `set_mode`
#
# `args.resolution` is a tuple[int, int] in the form of (width, height)
#
# `args.fullscreen` is a boolean indicating whether to run in fullscreen
# mode
screen = pygame.display.set_mode(
args.resolution,
(pygame.FULLSCREEN if args.fullscreen else 0) | pygame.RESIZABLE,
)
#^^^ NEW ^^^#Then, create the main loop, where we will check the pygame.event and update the game. We will quit the game if there is a pygame.QUIT event or if the user pressed Ctrl + Q.
# ...
screen = pygame.display.set_mode(
args.resolution,
(pygame.FULLSCREEN if args.fullscreen else 0) | pygame.RESIZABLE,
)
#vvv NEW vvv#
# Main loop forever while `running` is True
running = True
fixed_dt = 0.032
while running:
# Handle events from `pygame.event.get()`
for event in pygame.event.get():
if event.type == pygame.QUIT:
# If it is a quit event, we stop the loop
_____ = _____
elif event.type == pygame.KEYDOWN:
if (
pygame.key.get_mods() & pygame.KMOD_CTRL
and event.key == pygame.K_q
):
# If control + q is pressed, we quit the program
_____ = _____
# Clear the screen with white color by using `fill` method on the
# screen object
screen.fill(pygame.Color(255, 255, 255))
# Update the display with `update`
pygame.display.update()
# Tick the clock to control the frame rate
clock.tick(_____)
#^^^ NEW ^^^#Solution Code
# ...
screen = pygame.display.set_mode(
args.resolution,
(pygame.FULLSCREEN if args.fullscreen else 0) | pygame.RESIZABLE,
)
#vvv NEW vvv#
# Main loop forever while `running` is True
running = True
fixed_dt = 0.032
while running:
# Handle events from `pygame.event.get()`
for event in pygame.event.get():
if event.type == pygame.QUIT:
# If it is a quit event, we stop the loop
running = False
elif event.type == pygame.KEYDOWN:
if (
pygame.key.get_mods() & pygame.KMOD_CTRL
and event.key == pygame.K_q
):
# If control + q is pressed, we quit the program
running = False
# Clear the screen with white color by using `fill` method on the
# screen object
screen.fill(pygame.Color(255, 255, 255))
# Update the display with `update`
pygame.display.update()
# Tick the clock to control the frame rate
clock.tick(60)
#^^^ NEW ^^^#Lastly, after the loop finishes which indicates the game has quitted, we are going to do a cleanup. We can call the pygame.quit function to quit the game.
Solution Code
# ...
# Tick the clock to control the frame rate
clock.tick(60)
#vvv NEW vvv#
# Quit pygame
pygame.quit()
#^^^ NEW ^^^#Now if we run python main.py game -t demo0, we should see a white screen that can be closed by pressing the close button or Ctrl + Q.
Training mode is similar to gameplay mode, but we are going to add a few features, specifically the skip frames and the frame rate limit so that when training we can skip unnecessary frames to speed up the process.
Same as before, let's first import the pygame module.
import argparse
import math
from pathlib import Path
from typing import List, Optional, Tuple
import pygame # NEW
from engine.activations import activation_funcs
from engine.entity.ai_car import AICarWe are going to first setup the main_scene similar to that in the gameplay mode.
Solution Code
def main_scene(args: argparse.Namespace):
"""
Main scene for training the AI
:param args: The arguments
:return: None
"""
#vvv NEW vvv#
# Initialize pygame by calling `init`
pygame.init()
# Set the window title with `set_caption`
pygame.display.set_caption("Simple RL Driver - Training")
# Create a clock object to help control the frame rate
clock = pygame.time.Clock()
# Create a screen with `set_mode`
#
# `args.resolution` is a tuple[int, int] in the form of (width, height)
#
# `args.fullscreen` is a boolean indicating whether to run in fullscreen
# mode
screen = pygame.display.set_mode(
args.resolution,
(pygame.FULLSCREEN if args.fullscreen else 0) | pygame.RESIZABLE,
)
#^^^ NEW ^^^#Then, we are going to add the main loop, where it on top of the implementation of gameplay mode, we are also going to skip frames based on args.skip_frames and args.limit_fps arguments.
# ...
screen = pygame.display.set_mode(
args.resolution,
(pygame.FULLSCREEN if args.fullscreen else 0) | pygame.RESIZABLE,
)
#vvv NEW vvv#
# Main loop forever while `running` is True
running = True
fixed_dt = 0.032
skip_frame_counter = 0
while running:
# Handle events from `pygame.event.get()`
for event in pygame.event.get():
if event.type == pygame.QUIT:
# If it is a quit event, we stop the loop
_____ = _____
elif event.type == pygame.KEYDOWN:
if (
pygame.key.get_mods() & pygame.KMOD_CTRL
and event.key == pygame.K_q
):
# If control + q is pressed, we quit the program
_____ = _____
# Skip frames
#
# if counter is currently less than `args.skip_frames`, we increment
# the counter and just skip this loop, otherwise we reset the counter
_____ += 1
if _____ < args.skip_frames:
_____
_____ = 0
# Clear the screen with white color by using `fill` method on the
# screen object
screen.fill(pygame.Color(255, 255, 255))
# Update the display with `update`
pygame.display.update()
# Tick the clock to control the frame rate
if args.limit_fps and args.skip_frames == 0:
clock.tick(60)
#^^^ NEW ^^^#Solution Code
# ...
screen = pygame.display.set_mode(
args.resolution,
(pygame.FULLSCREEN if args.fullscreen else 0) | pygame.RESIZABLE,
)
#vvv NEW vvv#
# Main loop forever while `running` is True
running = True
fixed_dt = 0.032
skip_frame_counter = 0
while running:
# Handle events from `pygame.event.get()`
for event in pygame.event.get():
if event.type == pygame.QUIT:
# If it is a quit event, we stop the loop
running = False
elif event.type == pygame.KEYDOWN:
if (
pygame.key.get_mods() & pygame.KMOD_CTRL
and event.key == pygame.K_q
):
# If control + q is pressed, we quit the program
running = False
# Skip frames
#
# if counter is currently less than `args.skip_frames`, we increment
# the counter and just skip this loop, otherwise we reset the counter
skip_frame_counter += 1
if skip_frame_counter < args.skip_frames:
continue
skip_frame_counter = 0
# Clear the screen with white color by using `fill` method on the
# screen object
screen.fill(pygame.Color(255, 255, 255))
# Update the display with `update`
pygame.display.update()
# Tick the clock to control the frame rate
if args.limit_fps and args.skip_frames == 0:
clock.tick(60)
#^^^ NEW ^^^#Lastly, we are also going to cleanup the same way as the gamplay mode.
Solution Code
# ...
# Tick the clock to control the frame rate
if args.limit_fps and args.skip_frames == 0:
clock.tick(60)
#vvv NEW vvv#
# Quit pygame
pygame.quit()
#^^^ NEW ^^^#Now if we run python main.py train -t demo0 -n demo, we should see the same white screen as in gameplay mode that can be closed by pressing the close button or Ctrl + Q.
We are now going to implement the Transformable and Camera.
The transformable class will act as the base class for all entities that can be transformed, i.e. have a position and rotation. It will be used for the camera and the car. The implementation is very simple and straightforward.
We are first going to implement the __init__ where the Transformable.pos and the Transformable.rot fields will be initialized.
Solution Code
def __init__(
self,
pos: npt.NDArray[np.float32] = vec(0, 0),
rot: float = 0,
):
"""
Initialize the Transformable object.
:param pos: The position of the object
:param rot: The rotation of the object
"""
#vvv NEW vvv#
self.pos = pos
self.rot = rot
#^^^ NEW ^^^#Next, we are going to implement the Transformable.translate where the position will be moved by delta amount.
Solution Code
def translate(self, delta: npt.NDArray[np.float32]):
"""
Translate the object.
:param delta: The translation vector
:return: None
"""
#vvv NEW vvv#
self.pos += delta
#^^^ NEW ^^^#Note
We assume delta is always a 2D vector, i.e. npt.NDArray[np.float32] with shape (2,).
Then, let's implement the Transformable.rotate where the rotation will be rotated by rad amount. It is also good to wrap the rotation back into the
def rotate(self, rad: float):
"""
Rotate the object.
:param rad: The rotation in radians
:return: None
"""
self.rot = _____Solution Code
def rotate(self, rad: float):
"""
Rotate the object.
:param rad: The rotation in radians
:return: None
"""
self.rot = (self.rot + rad) % (2 * math.pi)Remember to import the math module so we can use the math.pi constant.
import math # NEW
import numpy as np
import numpy.typing as nptWhile the above 2 functions are sufficient for our use, it is often much more convenient to define a function that moves the position towards the position the object is facing. We are going to implement the Transformable.translate_forward function.
Solution Code
def translate_forward(self, dist: float):
"""
Translate the object forward.
:param dist: The distance to translate
:return: None
"""
#vvv NEW vvv#
self.translate(dir(self.rot) * dist)
#^^^ NEW ^^^#Also import the dir function.
import math
import numpy as np
import numpy.typing as npt
from engine.utils import dir, vec # UPDATEDNow that we have the transformable class, we can implement the Camera class.
We are first going to implement the __init__ where we will initialize the Camera.follow and the Camera.screen fields.
Solution Code
def __init__(
self, screen: pygame.Surface, follow: Optional[Transformable] = None
):
super().__init__()
#vvv NEW vvv#
self.follow = follow
self.screen = screen
#^^^ NEW ^^^#Note
We need to use super().__init__() to also call the Transformable.__init__ method for initializing the base class.
Next, we are going to implement the Camera.update where the camera will follow the Camera.follow transformable. Furthermore, we want to allow it to also follow a new transformable if it is passed as the argument, since later when we are training cars, we want to be able to keep following the AI car that is the fastest.
Solution Code
def update(self, dt: float, follow: Optional[Transformable] = None):
"""
Update the camera.
:param dt: The time delta
:param follow: The new object to follow
:return: None
"""
#vvv NEW vvv#
# If there is a new object to follow, update the follow field
if follow:
self.follow = follow
# Then update the position and rotation of the camera to match the
# object being followed
self.pos = self.follow.pos
self.rot = self.follow.rot
#^^^ NEW ^^^#Lastly, one of the most essential function of a camera is to let all other object to be displayed from the camera point of view, so we need to implement the Camera.get_coord.
The caller will pass in a pos argument which is a 2D vector, then the position should be transformed to the camera's point of view. We will first translate the object, then do a rotation, lastly we also need to center the object to the screen by translating it by the size of the camera divided by 2.
def get_coord(
self, pos: npt.NDArray[np.float32]
) -> npt.NDArray[np.float32]:
"""
Get the coordinate from the camera's perspective.
:param pos: The position to transform
:return: The transformed coordinate
"""
#vvv NEW vvv#
# Get the position from the camera's perspective
local_pos = _____ - _____
# Rotate the position about the camera position
rot_pos = np.dot(rot_mat(-_____), local_pos)
# Since the screen's origin is at the top left while we want the camera
# position to be at the center of the screen, we need to add the center
# of the screen to the rotated position
center = vec(*self.screen.get_rect().center)
return center + rot_pos
#^^^ NEW ^^^#Solution Code
def get_coord(
self, pos: npt.NDArray[np.float32]
) -> npt.NDArray[np.float32]:
"""
Get the coordinate from the camera's perspective.
:param pos: The position to transform
:return: The transformed coordinate
"""
#vvv NEW vvv#
# Get the position from the camera's perspective
local_pos = pos - self.pos
# Rotate the position about the camera position
rot_pos = np.dot(rot_mat(-self.rot), local_pos)
# Since the screen's origin is at the top left while we want the camera
# position to be at the center of the screen, we need to add the center
# of the screen to the rotated position
center = vec(*self.screen.get_rect().center)
return center + rot_pos
#^^^ NEW ^^^#Let's also import the rot_mat which was used to get a rotation matrix from the radians.
from typing import Optional
import numpy as np
import numpy.typing as npt
import pygame
from engine.entity.transformable import Transformable
from engine.utils import rot_mat, vec # UPDATEDNext, we are going to add some entities into the game.main We are going to add the Camera and the Track.
First, let's import everything. We are also going to import the PlayerCar, because we need something for the camera to follow, and player car which is not yet implemented but it is already able to be used.
import argparse
from pathlib import Path
from typing import List, Tuple
import pygame
#vvv NEW vvv#
from engine.entity.camera import Camera
from engine.entity.player_car import PlayerCar
from engine.entity.track import Track
#^^^ NEW ^^^Next let's create the track and camera. We are going to use the Track.load and the Camera.__init__ functions to create them. We are going to load the args.track argument into the track.
Solution Code
# ...
screen = pygame.display.set_mode(
args.resolution,
(pygame.FULLSCREEN if args.fullscreen else 0) | pygame.RESIZABLE,
)
#vvv NEW vvv#
# Setup the track by loading it
#
# `args.track` is a string representing the name of the track to play in
track = Track.load(args.track)
# Create a camera object
camera = Camera(screen, PlayerCar())
#^^^ NEW ^^^#
# Main loop forever while `running` is True
running = True
fixed_dt = 0.032
# ...Then we also need to call the Camera.update and call the Track.draw function.
# ...
# Main loop forever while `running` is True
running = True
fixed_dt = 0.032
while running:
# Handle events from `pygame.event.get()`
for event in pygame.event.get():
if event.type == pygame.QUIT:
# If it is a quit event, we stop the loop
running = False
elif event.type == pygame.KEYDOWN:
if (
pygame.key.get_mods() & pygame.KMOD_CTRL
and event.key == pygame.K_q
):
# If control + q is pressed, we quit the program
running = False
#vvv NEW vvv#
# Update the camera
camera.update(_____)
#^^^ NEW ^^^#
# Clear the screen with white color by using `fill` method on the
# screen object
screen.fill(pygame.Color(255, 255, 255))
#vvv NEW vvv#
# Draw the track and the cars on the screen
track.draw(_____, _____, 5)
#^^^ NEW ^^^#
# Update the display with `update`
pygame.display.update()Solution Code
# ...
# Main loop forever while `running` is True
running = True
fixed_dt = 0.032
while running:
# Handle events from `pygame.event.get()`
for event in pygame.event.get():
if event.type == pygame.QUIT:
# If it is a quit event, we stop the loop
running = False
elif event.type == pygame.KEYDOWN:
if (
pygame.key.get_mods() & pygame.KMOD_CTRL
and event.key == pygame.K_q
):
# If control + q is pressed, we quit the program
running = False
#vvv NEW vvv#
# Update the camera
camera.update(fixed_dt)
#^^^ NEW ^^^#
# Clear the screen with white color by using `fill` method on the
# screen object
screen.fill(pygame.Color(255, 255, 255))
#vvv NEW vvv#
# Draw the track and the cars on the screen
track.draw(screen, camera, 5)
#^^^ NEW ^^^#
# Update the display with `update`
pygame.display.update()Note
We passed in the camera object to the Track.draw function.
It is where the Camera.get_coord is used to correctly display the track.
Now if we run python main.py game -t demo0, we should see the track.
Tip
We can try to move the camera to see the track being updated in real time with respect to the camera's perspective!
Let's modify the Camera.update function so that it uses pygame.key.get_pressed to move the camera.
def update(self, dt: float, follow: Optional[Transformable] = None):
"""
Update the camera.
:param dt: The time delta
:param follow: The new object to follow
:return: None
"""
#vvv UPDATED vvv#
# # If there is a new object to follow, update the follow field
# if follow:
# self.follow = follow
# # Then update the position and rotation of the camera to match the
# # object being followed
# self.pos = self.follow.pos
# self.rot = self.follow.rot
if pygame.key.get_pressed()[pygame.K_w]:
self.translate_forward(100 * dt)
if pygame.key.get_pressed()[pygame.K_s]:
self.translate_forward(-100 * dt)
if pygame.key.get_pressed()[pygame.K_a]:
self.rotate(1.0 * dt)
if pygame.key.get_pressed()[pygame.K_d]:
self.rotate(-1.0 * dt)
#^^^ UPDATED ^^^#Then, let's try it with python main.py game -t demo0.
Lastly, let's also add the camera and the track to the training mode. Since in training mode, there can be multiple tracks in args.tracks argument, so we can use random.choice to randomly choose a track to train on.
Let's first import everything.
import argparse
import math
import random # NEW
from pathlib import Path
from typing import List, Optional, Tuple
import pygame
from engine.activations import activation_funcs
from engine.entity.ai_car import AICar
#vvv NEW vvv#
from engine.entity.camera import Camera
from engine.entity.player_car import PlayerCar
from engine.entity.track import Track
#^^^ NEW ^^^#Then, let's create the track and camera the same way we did for the gameplay mode.
# ...
screen = pygame.display.set_mode(
args.resolution,
(pygame.FULLSCREEN if args.fullscreen else 0) | pygame.RESIZABLE,
)
#vvv NEW vvv#
# Setup the tracks by loading them
#
# `args.tracks` is a list of strings representing the names of the tracks
tracks = [Track.load(t) for t in args.tracks]
track = _____._____(tracks)
# Create a camera object
camera = Camera(screen)
#^^^ NEW ^^^#
# Main loop forever while `running` is True
running = True
fixed_dt = 0.032
skip_frame_counter = 0
# ...Solution Code
# ...
screen = pygame.display.set_mode(
args.resolution,
(pygame.FULLSCREEN if args.fullscreen else 0) | pygame.RESIZABLE,
)
#vvv NEW vvv#
# Setup the tracks by loading them
#
# `args.tracks` is a list of strings representing the names of the tracks
tracks = [Track.load(t) for t in args.tracks]
track = random.choice(tracks)
# Create a camera object
camera = Camera(screen)
#^^^ NEW ^^^#
# Main loop forever while `running` is True
running = True
fixed_dt = 0.032
skip_frame_counter = 0
# ...Note
We don't need to pass in the player car to the camera because we are going to keep the followed car updated in the Camera.update function instead.
There is other way such that in gameplay mode we also don't need to pass in anything such that the camera always stay in place.
However, during my development, I didn't take that into consideration, thus this is the end result. You can try to fix it on your own if you want!
Next, let's update the camera and draw the track.
# ...
# Main loop forever while `running` is True
running = True
fixed_dt = 0.032
skip_frame_counter = 0
while running:
# Handle events from `pygame.event.get()`
for event in pygame.event.get():
if event.type == pygame.QUIT:
# If it is a quit event, we stop the loop
running = False
elif event.type == pygame.KEYDOWN:
if (
pygame.key.get_mods() & pygame.KMOD_CTRL
and event.key == pygame.K_q
):
# If control + q is pressed, we quit the program
running = False
#vvv NEW vvv#
# Update the camera
camera.update(_____, _____)
#^^^ NEW ^^^#
# Skip frames
#
# if counter is currently less than `args.skip_frames`, we increment
# the counter and just skip this loop, otherwise we reset the counter
skip_frame_counter += 1
if skip_frame_counter < args.skip_frames:
continue
skip_frame_counter = 0
# Clear the screen with white color by using `fill` method on the
# screen object
screen.fill(pygame.Color(255, 255, 255))
#vvv NEW vvv#
# Draw the track and the cars on the screen
track.draw(_____, _____, 5)
#^^^ NEW ^^^#
# Update the display with `update`
pygame.display.update()
# Tick the clock to control the frame rate
if args.limit_fps and args.skip_frames == 0:
clock.tick(60)
# ...Solution Code
# ...
# Main loop forever while `running` is True
running = True
fixed_dt = 0.032
skip_frame_counter = 0
while running:
# Handle events from `pygame.event.get()`
for event in pygame.event.get():
if event.type == pygame.QUIT:
# If it is a quit event, we stop the loop
running = False
elif event.type == pygame.KEYDOWN:
if (
pygame.key.get_mods() & pygame.KMOD_CTRL
and event.key == pygame.K_q
):
# If control + q is pressed, we quit the program
running = False
#vvv NEW vvv#
# Update the camera
camera.update(fixed_dt, PlayerCar())
#^^^ NEW ^^^#
# Skip frames
#
# if counter is currently less than `args.skip_frames`, we increment
# the counter and just skip this loop, otherwise we reset the counter
skip_frame_counter += 1
if skip_frame_counter < args.skip_frames:
continue
skip_frame_counter = 0
# Clear the screen with white color by using `fill` method on the
# screen object
screen.fill(pygame.Color(255, 255, 255))
#vvv NEW vvv#
# Draw the track and the cars on the screen
track.draw(screen, camera, 5)
#^^^ NEW ^^^#
# Update the display with `update`
pygame.display.update()
# Tick the clock to control the frame rate
if args.limit_fps and args.skip_frames == 0:
clock.tick(60)
# ...Now if we run python main.py train -t demo0 -n demo, we should see the track.
Tip
We can do the same thing that we did in gameplay mode to try to control the camera with keyboard and see if the track updates correctly with respect to the camera's perspective.