I created a simple game in pygame that is sort of a simple bullet-hell style game, with the goal of creating a deep reinforcement learning agent to learn the game. I got the game to work in pygame alone using keyboard controls, and I am now working on making it work with OpenAI gym.
Everything seems to be working out OK except for the main player sprite that I use to visualize it's location. In the step
method I update the Player object rect based on the action, and then higher level I redraw the geometry of the level and flip it. What is interesting is that the bullet components seem to redraw and update perfectly fine, however the player sprite does not. What's more interesting is that if I analyze the player's rect using debug lines I created, it does seem to actually be moving, just the sprite itself does not move from the starting position. It may be easier to understand with a picture:
https://i.stack.imgur.com/Bbdty.png
Here the blue lines converge to where the player's rect is. You can verify that is what is done with the code below. On the left you can see the centerx
and centery
properties to ensure that it is indeed updating on every step call. However, you can see the "ship" associated with this player is not anywhere near the convergence point. I'm very confused as to how the bullets in the screenshot can update and move perfectly fine, however the player object does not. It appears it gets repeatedly drawn in the same location.
My current working theory is it is some kind of mismatch between self.player
and the Sprite object added to self.player_list
, but I'm not sure how to rectify that, especially since I need self.player
in other functions.
Any help would be appreciated.
(There is extraneous gym code I omitted for brevity)
MAIN:
def main():
env = BulletHell()
done = False
env.reset()
while not done:
action = env.action_space.sample()
# apply the action
obs, reward, done, info = env.step(action)
env.render()
env.close()
BULLETHELL CLASS:
class BulletHell(gym.Env):
metadata = {
"render_modes": ["human", "rgb_array", "single_rgb_array"],
"render_fps": 30
}
def __init__(self):
pygame.init()
self.stepcnt = 0
self.screen = pygame.display.set_mode(constants.SIZE)
self.player = Player(constants.width, constants.height) # spawn player
self.player.rect.x = constants.width // 2 # go to x
self.player.rect.y = constants.height // 2 # go to y
self.player_list = pygame.sprite.GroupSingle()
self.player_list.add(self.player)
self.timer = pygame.time.Clock()
self.SPAWN_BULLET = pygame.USEREVENT + 1
pygame.time.set_timer(self.SPAWN_BULLET, constants.BULLET_SPAWN_RATE)
self.closest = None
# 5 discrete actions; do nothing, up, down, left, right
self.action_space = Discrete(5)
# Observation space is the closest bullet count (each with a distance) and 2 numbers representing the x and y
# of the player's current position
self.observation_space = Box(low=0,
high=constants.width,
shape=(constants.CLOSEST_BULLET_COUNT + 2,))
self.bullet_list = pygame.sprite.Group()
for _ in range(5):
bullet = Bullet(constants.width, constants.height)
self.bullet_list.add(bullet)
bullet.set_starting_loc_and_vel()
self.steps = 2
def observe(self):
...
def step(self, action) -> Tuple[ObsType, float, bool, dict]:
done = False
reward = 0.1
self.stepcnt += 1
# Do nothing
if action == 0:
self.player.control(0, 0)
# Go up
if action == 1:
self.player.control(0, self.steps)
# Go down
if action == 2:
self.player.control(0, -self.steps)
# Go left
if action == 3:
self.player.control(-self.steps, 0)
# Go right
if action == 4:
self.player.control(self.steps, 0)
for event in pygame.event.get():
if event.type == self.SPAWN_BULLET:
bullet = Bullet(constants.width, constants.height)
self.bullet_list.add(bullet)
bullet.set_starting_loc_and_vel()
self.player.update()
self.bullet_list.update()
self.timer.tick(self.metadata['render_fps'])
if self.stepcnt % 2 == 0:
self.player.zero()
blocks_hit_list = pygame.sprite.spritecollide(self.player, self.bullet_list, False)
if len(blocks_hit_list) != 0:
done = True
reward = -5
# Debug code, print coordinates of player rect
print(self.observe()[-2:])
return self.observe(), reward, done, {}
def redraw(self):
self.screen.fill(constants.BLACK)
self.bullet_list.draw(self.screen)
self.player_list.draw(self.screen)
for dis, bullet in self.closest:
pygame.draw.line(self.screen, 255, Vector2(self.player.rect.centerx, self.player.rect.centery),
Vector2(bullet.rect.centerx, bullet.rect.centery), width=1)
pygame.display.flip()
def reset(self, *, seed: Optional[int] = None, return_info: bool = False, options: Optional[dict] = None,):
...
def render(self, mode="human"):
self.redraw()
...
EDIT: upon request I added the Player
class code:
class Player(pygame.sprite.Sprite):
def __init__(self, bounds_x, bounds_y):
pygame.sprite.Sprite.__init__(self)
self.movex = 0
self.movey = 0
self.bounds_x = bounds_x
self.bounds_y = bounds_y
img = pygame.image.load('Ship_1.png').convert_alpha()
self.image = img
self.rect = self.image.get_rect()
def control(self, x, y):
self.movex += x
self.movey += y
def update(self):
self.rect.x = self.rect.x + self.movex
self.rect.y = self.rect.y + self.movey
if self.rect.x >= self.bounds_x - self.image.get_width():
self.rect.x = self.bounds_x - self.image.get_width()
elif self.rect.x <= 0:
self.rect.x = 0
if self.rect.y >= self.bounds_y - self.image.get_height():
self.rect.y = self.bounds_y - self.image.get_height()
elif self.rect.y <= 0:
self.rect.y = 0
def zero(self):
self.movex = 0
self.movey = 0
def get_closest_bullets(self, bullet_list, n=10):
...
def dis(self, other):
...