Skip to content

Commit ce1db19

Browse files
committed
Boost pad tracker, improved sequence, relying on framework to begin and end rendering.
1 parent 986dbca commit ce1db19

File tree

4 files changed

+99
-19
lines changed

4 files changed

+99
-19
lines changed

src/bot.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from rlbot.utils.structures.game_data_struct import GameTickPacket
44

55
from util.ball_prediction_analysis import find_slice_at_time
6+
from util.boost_pad_tracker import BoostPadTracker
67
from util.drive import steer_toward_target
78
from util.sequence import Sequence, ControlStep
89
from util.vec import Vec3
@@ -13,38 +14,47 @@ class MyBot(BaseAgent):
1314
def __init__(self, name, team, index):
1415
super().__init__(name, team, index)
1516
self.active_sequence: Sequence = None
17+
self.boost_pad_tracker = BoostPadTracker()
18+
19+
def initialize_agent(self):
20+
# Set up information about the boost pads now that the game is active and the info is available
21+
self.boost_pad_tracker.initialize_boosts(self.get_field_info())
1622

1723
def get_output(self, packet: GameTickPacket) -> SimpleControllerState:
1824
"""
1925
This function will be called by the framework many times per second. This is where you can
2026
see the motion of the ball, etc. and return controls to drive your car.
2127
"""
2228

23-
# Start the renderer. Make sure you call end_rendering at the end of this function.
24-
self.renderer.begin_rendering()
29+
# Keep our boost pad info updated with which pads are currently active
30+
self.boost_pad_tracker.update_boost_status(packet)
2531

2632
# This is good to keep at the beginning of get_output. It will allow you to continue
2733
# any sequences that you may have started during a previous call to get_output.
2834
if self.active_sequence and not self.active_sequence.done:
29-
return self.active_sequence.tick(packet)
35+
controls = self.active_sequence.tick(packet)
36+
if controls is not None:
37+
return controls
3038

3139
# Gather some information about our car and the ball
3240
my_car = packet.game_cars[self.index]
3341
car_location = Vec3(my_car.physics.location)
3442
car_velocity = Vec3(my_car.physics.velocity)
3543
ball_location = Vec3(packet.game_ball.physics.location)
3644

37-
if car_location.dist(ball_location) > 1000:
45+
if car_location.dist(ball_location) > 1500:
3846
# We're far away from the ball, let's try to lead it a little bit
3947
ball_prediction = self.get_ball_prediction_struct() # This can predict bounces, etc
4048
ball_in_future = find_slice_at_time(ball_prediction, packet.game_info.seconds_elapsed + 2)
4149
target_location = Vec3(ball_in_future.physics.location)
50+
self.renderer.draw_line_3d(ball_location, target_location, self.renderer.cyan())
4251
else:
4352
target_location = ball_location
4453

4554
# Draw some things to help understand what the bot is thinking
4655
self.renderer.draw_line_3d(car_location, target_location, self.renderer.white())
4756
self.renderer.draw_string_3d(car_location, 1, 1, f'Speed: {car_velocity.length():.1f}', self.renderer.white())
57+
self.renderer.draw_rect_3d(target_location, 8, 8, True, self.renderer.cyan(), centered=True)
4858

4959
if 750 < car_velocity.length() < 800:
5060
# We'll do a front flip if the car is moving at a certain speed.
@@ -55,8 +65,6 @@ def get_output(self, packet: GameTickPacket) -> SimpleControllerState:
5565
controls.throttle = 1.0
5666
# You can set more controls if you want, like controls.boost.
5767

58-
# Send any drawing we may have done
59-
self.renderer.end_rendering()
6068
return controls
6169

6270
def begin_front_flip(self, packet):

src/util/ball_prediction_analysis.py

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,14 @@
88
# We will jump this number of frames when looking for a moment where the ball is inside the goal.
99
# Big number for efficiency, but not so big that the ball could go in and then back out during that
1010
# time span. Unit is the number of frames in the ball prediction, and the prediction is at 60 frames per second.
11-
COARSE_SEARCH_INCREMENT = 20
11+
GOAL_SEARCH_INCREMENT = 20
1212

1313

1414
def find_slice_at_time(ball_prediction: BallPrediction, game_time: float):
15+
"""
16+
This will find the future position of the ball at the specified time. The returned
17+
Slice object will also include the ball's velocity, etc.
18+
"""
1519
start_time = ball_prediction.slices[0].game_seconds
1620
approx_index = int((game_time - start_time) * 60) # We know that there are 60 slices per second.
1721
if 0 <= approx_index < ball_prediction.num_slices:
@@ -20,13 +24,24 @@ def find_slice_at_time(ball_prediction: BallPrediction, game_time: float):
2024

2125

2226
def predict_future_goal(ball_prediction: BallPrediction):
23-
return find_matching_slice(ball_prediction, 0, lambda s: abs(s.physics.location.y) >= GOAL_THRESHOLD)
24-
25-
26-
def find_matching_slice(ball_prediction: BallPrediction, start_index: int, predicate: Callable[[Slice], bool]):
27-
for coarse_index in range(start_index, ball_prediction.num_slices, COARSE_SEARCH_INCREMENT):
27+
"""
28+
Analyzes the ball prediction to see if the ball will enter one of the goals. Only works on standard arenas.
29+
Will return the first ball slice which appears to be inside the goal, or None if it does not enter a goal.
30+
"""
31+
return find_matching_slice(ball_prediction, 0, lambda s: abs(s.physics.location.y) >= GOAL_THRESHOLD,
32+
search_increment=20)
33+
34+
35+
def find_matching_slice(ball_prediction: BallPrediction, start_index: int, predicate: Callable[[Slice], bool],
36+
search_increment=1):
37+
"""
38+
Tries to find the first slice in the ball prediction which satisfies the given predicate. For example,
39+
you could find the first slice below a certain height. Will skip ahead through the packet by search_increment
40+
for better efficiency, then backtrack to find the exact first slice.
41+
"""
42+
for coarse_index in range(start_index, ball_prediction.num_slices, search_increment):
2843
if predicate(ball_prediction.slices[coarse_index]):
29-
for j in range(max(start_index, coarse_index - COARSE_SEARCH_INCREMENT), coarse_index):
44+
for j in range(max(start_index, coarse_index - search_increment), coarse_index):
3045
ball_slice = ball_prediction.slices[j]
3146
if predicate(ball_slice):
3247
return ball_slice

src/util/boost_pad_tracker.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from dataclasses import dataclass
2+
from typing import List
3+
4+
from rlbot.utils.structures.game_data_struct import GameTickPacket, FieldInfoPacket
5+
6+
from util.vec import Vec3
7+
8+
9+
@dataclass
10+
class BoostPad:
11+
location: Vec3
12+
is_full_boost: bool
13+
is_active: bool # Active means it's available to be picked up
14+
timer: float # Counts the number of seconds that the pad has been *inactive*
15+
16+
17+
class BoostPadTracker:
18+
"""
19+
This class merges together the boost pad location info with the is_active info so you can access it
20+
in one convenient list. For it to function correctly, you need to call initialize_boosts once when the
21+
game has started, and then update_boost_status every frame so that it knows which pads are active.
22+
"""
23+
24+
def __init__(self):
25+
self.boost_pads: List[BoostPad] = []
26+
self._full_boosts_only: List[BoostPad] = []
27+
28+
def initialize_boosts(self, game_info: FieldInfoPacket):
29+
raw_boosts = [game_info.boost_pads[i] for i in range(game_info.num_boosts)]
30+
self.boost_pads: List[BoostPad] = [BoostPad(Vec3(rb.location), rb.is_full_boost, False, 0) for rb in raw_boosts]
31+
# Cache the list of full boosts since they're commonly requested.
32+
# They reference the same objects in the boost_pads list.
33+
self._full_boosts_only: List[BoostPad] = [bp for bp in self.boost_pads if bp.is_full_boost]
34+
35+
def update_boost_status(self, packet: GameTickPacket):
36+
for i in range(packet.num_boost):
37+
our_pad = self.boost_pads[i]
38+
packet_pad = packet.game_boosts[i]
39+
our_pad.is_active = packet_pad.is_active
40+
our_pad.timer = packet_pad.timer
41+
42+
def get_full_boosts(self) -> List[BoostPad]:
43+
return self._full_boosts_only

src/util/sequence.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,20 @@ class StepResult:
1313

1414
class Step:
1515
def tick(self, packet: GameTickPacket) -> StepResult:
16+
"""
17+
Return appropriate controls for this step in the sequence. If the step is over, you should
18+
set done to True in the result, and we'll move on to the next step during the next frame.
19+
If you panic and can't return controls at all, you may return None and we will move on to
20+
the next step immediately.
21+
"""
1622
raise NotImplementedError
1723

1824

1925
class ControlStep(Step):
26+
"""
27+
This allows you to repeat the same controls every frame for some specified duration. It's useful for
28+
scheduling the button presses needed for kickoffs / dodges / etc.
29+
"""
2030
def __init__(self, duration: float, controls: SimpleControllerState):
2131
self.duration = duration
2232
self.controls = controls
@@ -36,14 +46,18 @@ def __init__(self, steps: List[Step]):
3646
self.done = False
3747

3848
def tick(self, packet: GameTickPacket):
39-
while True:
40-
if self.index >= len(self.steps):
41-
self.done = True
42-
return SimpleControllerState()
49+
while self.index < len(self.steps):
4350
step = self.steps[self.index]
4451
result = step.tick(packet)
45-
if result.done:
52+
if result is None or result.controls is None or result.done:
4653
self.index += 1
4754
if self.index >= len(self.steps):
55+
# The bot will know not to use this sequence next frame, even though we may be giving it controls.
4856
self.done = True
49-
return result.controls
57+
if result is not None and result.controls is not None:
58+
# If the step was able to give us controls, return them to the bot.
59+
return result.controls
60+
# Otherwise we will loop to the next step in the sequence.
61+
# If we reach here, we ran out of steps to attempt.
62+
self.done = True
63+
return None

0 commit comments

Comments
 (0)