-
Notifications
You must be signed in to change notification settings - Fork 0
Tutorial 2: Car
Important
This tutorial starts at the workshop-tutorial-1 branch and the complete solution can be found at workshop-tutorial-2 branch.
This tutorial focuses on the implementation of the Car class and also its subclass PlayerCar.
Note
This tutorial will implement/update the following modules:
There are some functions to be implemented to setup the car.
The first thing to do is initialize the fields of the Car class in the Car.__init__.
As we can see, the function already has a super().__init__(pos, rot) call to initialize the superclass Transformable. The fields should be easy to initialize, just initialize everything to zero except the ones provided by the parameters.
def __init__(
self,
pos: npt.NDArray[np.float32] = vec(0, 0),
rot: float = 0,
color: pygame.Color = pygame.Color(0, 0, 0),
out_of_track_color: pygame.Color = pygame.Color(255, 0, 0),
):
"""
Initialize the car.
:param pos: The position of the car
:param rot: The rotation of the car
:param color: The color of the car
:param out_of_track_color: The border color of the car when it is out
of track
"""
super().__init__(pos, rot)
#vvv NEW vvv#
self.color = color
self.out_of_track_color = out_of_track_color
self.speed = 0.0
self.acceleration = 0.0
self.angular_speed = 0.0
self.angular_acceleration = 0.0
self.out_of_track = False
self.progress = 0
self.total_progress = 0
#^^^ NEW ^^^#While the Car._get_input does not actually need to be implemented as it is expected to be implemented by subclasses, it is better to understand what it does.
It is to let it's subclasses to define how the car should move, we will call this method from Car.update and then handle the car movement from there.
This is nice because subclasses of Car does not need to care how is the movement achieved, they just need to know where should the car move next. It is like how drivers only need to know how to steer and accelerate the car without needing to know how the car engine actually works.
Take a look at the Car.Input class, we can see it contains 2 fields forward and turn. It is also annotated with a @dataclass, which helps us create a simple __init__ with all the fields as the parameter.
As we can see from the parameter of Car.reset_state, it is supplied the track. We are going to use this to initialize our starting position and rotation, which are fields inherited from Transformable.
Since this function is also called during each iteration, we would also need to reset speed, acceleration, etc.
One thing that is different from __init__ is the Car.total_progress field because we didn't have the track information in __init__. Now with the track information, we are going to set it to the total length of the track by using the number of line segments in track.polyline.
def reset_state(self, track: Track):
"""
Reset the state of the car.
:param track: The track
:return: None
"""
#vvv NEW vvv#
# Set the position to the first point of the track
#
# Set the rotation to the start direction of the track with
# `get_start_dir`
self.pos = track.curve.pts[0].pos.astype(np.float32)
self.rot = np.atan2(*track.get_start_dir()) - math.pi
# Reset everything else
self.speed = 0.0
self.acceleration = 0.0
self.angular_speed = 0.0
self.angular_acceleration = 0.0
self.out_of_track = False
self.progress = 0
self.total_progress = len(track.polyline)
#^^^ NEW ^^^#To implement the logics of a car, we are going to define how is the car updated and drawn on the screen each frame.
In the Car.update function, we are going to do several things - linear movement, angular movement, out of track handling, and progress updating.
def update(self, dt: float, track: Track):
"""
Update the car.
:param dt: The delta time
:param track: The track
:return: None
"""
#vvv NEW vvv#
# Get the input for the car from `_get_input`
#
# The `_get_input` method will be implemented in the subclasses
input_data = self._get_input()
# Linear acceleration
self.acceleration = input_data.forward * self.ACCELERATION * dt
if self.acceleration != 0:
# If there is acceleration, update the speed
self.speed += self.acceleration * dt
self.speed = clamp(self.speed, -self.MAX_SPEED, self.MAX_SPEED)
else:
# If there is no acceleration, apply deceleration
self.acceleration = (
self.speed / self.MAX_SPEED * self.DECELERATION * dt
)
dspeed = self.acceleration * dt
if abs(dspeed) > abs(self.speed):
self.speed = 0
else:
self.speed -= dspeed
# Angular movement
self.angular_acceleration = (
input_data.turn * self.ANGULAR_ACCELERATION * dt
)
if self.angular_acceleration != 0:
# If there is angular acceleration, update the angular speed
self.angular_speed += self.angular_acceleration * dt
self.angular_speed = clamp(
self.angular_speed,
-self.MAX_ANGULAR_SPEED,
self.MAX_ANGULAR_SPEED,
)
else:
# If there is no angular acceleration, apply angular deceleration
self.angular_acceleration = (
self.angular_speed
/ self.MAX_ANGULAR_SPEED
* self.ANGULAR_DECELERATION
* dt
)
dangular_speed = self.angular_acceleration * dt
if abs(dangular_speed) > abs(self.angular_speed):
self.angular_speed = 0
else:
self.angular_speed -= dangular_speed
# Out of track handling by checking with `shapely_polygon` of the track
self.out_of_track = not track.shapely_polygon.contains(
shapely.points(self.pos)
)
if self.out_of_track:
# If the car is out of track, limit the speed the penalty speed
self.speed = clamp(
self.speed,
-self.MAX_SPEED * self.OUT_OF_TRACK_PENALTY,
self.MAX_SPEED * self.OUT_OF_TRACK_PENALTY,
)
# Update the position and rotation using Transformable methods
#
# `translate_forward` updates the position based on the speed
#
# `rotate` updates the rotation based on the angular speed
self.translate_forward(self.speed * dt)
self.rotate(self.angular_speed * dt)
# Update the progress
#
# If the car is close to the next point in the track by using a simple
# distance check, update the progress
if self.progress + 1 < len(track.polyline):
check_point = track.polyline[self.progress + 1]
if (
np.linalg.norm(check_point - self.pos)
< track.width + self.HEIGHT
):
self.progress += 1
#^^^ NEW ^^^#