1

I am trying to make a simple galaga type game where the ship moves back and forth along the bottom of the screen and shoots automatically. It runs as expected at first but slows down drastically pretty quickly. I thought it was maybe slowing as it had more and more bullets to keep track of, but limiting the number of bullets on-screen didn't seem to help at all.

The main loop:

while game_is_on:

    screen.update()
    ship.slide()
    bullet_manager.shoot(ship.xcor())
    bullet_manager.move_bullets()

from bullet_manager:

def shoot(self, xcor):
    self.chance +=1
    if self.chance %6 == 0:
        new_bullet = Turtle("square")
        new_bullet.color("red")
        new_bullet.shapesize(stretch_wid=.1)
        new_bullet.pu()
        new_bullet.seth(90)
        new_bullet.goto(xcor, -200)
        self.all_bullets.append(new_bullet)


def move_bullets(self):
    for bullet in self.all_bullets:
        bullet.forward(10)
        self.all_bullets = self.all_bullets[-10:]
ggorlen
  • 44,755
  • 7
  • 76
  • 106
  • Yes, I imagine that `new_bullet = Turtle("square")` and then `self.all_bullets.append(new_bullet)` will keep accumulating bullets. Did you manage to limit the number of bullets? Can you include that code? – quamrana Aug 06 '22 at 17:30
  • @quamrana Looks like that's what the last line is for – DeepSpace Aug 06 '22 at 17:33
  • @quamrana Thanks for the reply! yeah, i thought that last line would limit the number of bullets that the loop has to keep track of to 10, but the way the program slows incrementally makes me think i must have done something wrong. Other than the incremental slowing, it works exactly as i'd expect. – LittleBuddy Aug 06 '22 at 18:24

1 Answers1

2

self.all_bullets = self.all_bullets[-10:] prunes turtles from your local list, but not from the application's memory. There are still turtles being managed by the turtle module even if they're not in your list. You can take a peek at turtle.turtles() to see how many turtles are being tracked internally.

Here's a minimal reproduction:

import turtle
from random import randint
from time import sleep


turtle.tracer(0)
turtles = []

while True:
    t = turtle.Turtle()
    turtles.append(t)

    if len(turtles) > 10:
        turtles = turtles[-10:]

    for t in turtles:
        if randint(0, 9) < 1:
            t.left(randint(0, 360))

        t.forward(randint(2, 10))

    print(len(turtles), len(turtle.turtles()))
    turtle.update()
    sleep(0.1)

You'll see the screen flood with turtles, and while your list stays at length 10, the turtle module's length keeps going up.

The first thought might be to chop off the turtle's internal list, but I prefer not to mess with library-managed resources without being given permission to do so. A quick attempt using memory stat code from this answer leaks:

import os, psutil
import turtle
from random import randint
from time import sleep


turtle.tracer(0)

while True:
    t = turtle.Turtle()

    if len(turtle.turtles()) > 10:
        turtle.turtles()[:] = turtle.turtles()[:10]

    for t in turtle.turtles():
        if randint(0, 9) < 1:
            t.left(randint(0, 360))

        t.forward(randint(2, 10))

    print(len(turtle.turtles()))
    process = psutil.Process(os.getpid())
    print(process.memory_info().rss)  # in bytes
    turtle.update()
    sleep(0.1)

How to fully delete a turtle is the canonical resource for deleting turtles, but a probably better solution for this use case is to pre-allocate a turtle pool and recycle turtles from it. When a bullet (or any other entity) is created, borrow a turtle from the pool and store it as a property on your object. When the bullet leaves the screen (or meets some other termination condition), return the turtle that the bullet instance borrowed back to the pool.

Here's an example, possibly overengineered for your use case, but you can use the pool as a library or adapt the high-level concept.

import turtle
from random import randint


class TurtlePool:
    def __init__(self, initial_size=8, capacity=32):
        if initial_size > capacity:
            raise ArgumentError("initial_size cannot be greater than capacity")

        self.capacity = capacity
        self.allocated = 0
        self.dead = []

        for _ in range(initial_size):
            self._allocate()

    def available(self):
        return bool(self.dead) or self.allocated < self.capacity

    def acquire(self):
        if not self.dead:
            if self.allocated < self.capacity:
                self._allocate()

        return self.dead.pop()

    def give_back(self, t):
        self.dead.append(t)
        self._clean_turtle(t)

    def _allocate(self):
        t = turtle.Turtle()
        self.allocated += 1
        assert self.allocated == len(turtle.turtles())
        self.dead.append(t)
        self._clean_turtle(t)

    def _clean_turtle(self, t):
        t.reset()
        t.hideturtle()
        t.penup()


class Bullet:
    def __init__(self, x, y, speed, angle):
        self.speed = speed
        self.turtle = turtle_pool.acquire()
        self.turtle.goto(x, y)
        self.turtle.setheading(angle)
        self.turtle.showturtle()

    def forward(self):
        self.turtle.forward(self.speed)

    def destroy(self):
        turtle_pool.give_back(self.turtle)

    def in_bounds(self):
        x, y = self.turtle.pos()
        return (
            x >= w / -2 and x < w / 2 and
            y >= h / -2 and y < h / 2
        )


def tick():
    if randint(0, 1) == 0 and turtle_pool.available():
        bullets.append(Bullet(
            x=0,
            y=0,
            speed=8,
            angle=randint(0, 360)
        ))

    next_gen = []

    for b in bullets:
        b.forward()

        if b.in_bounds():
            next_gen.append(b)
        else:
            b.destroy()

    bullets[:] = next_gen
    turtle.update()
    turtle.Screen().ontimer(tick, frame_delay_ms)


frame_delay_ms = 1000 // 30
turtle.tracer(0)
turtle_pool = TurtlePool()
w = turtle.window_width()
h = turtle.window_height()
bullets = []
tick()
turtle.exitonclick()

Keep in mind, this doesn't reuse Bullet objects and allocates a new list per frame, so there's room for further optimization.

ggorlen
  • 44,755
  • 7
  • 76
  • 106