megamarc / Tilengine

Free 2D graphics engine with raster effects for retro/classic style game development
https://www.tilengine.org
Mozilla Public License 2.0
847 stars 95 forks source link

Persistent collision detection for unused sprite #74

Closed danielgholmes closed 4 years ago

danielgholmes commented 4 years ago

Hi,

I came across this issue after upgrading to Tilengine v2.8.4. I'm using the Python bindings, but it seems the issue is with Tilengine itself, I thought it would be best to post the issue here.

What I am finding is that collision detections seems to persist even for sprites that have been disabled. For example, in the game I am making, a projectiles sprites registers a collision with an enemy sprite, and the projectile sprite is subsequently disabled. Then when creating a new projectile using TLN_GetAvailableSprite, it uses the previously disabled one for a new projectile. This is expected. But what I have found is that the collision detection on the original sprite persists, so that the new sprite immediately registers a collision and gets disabled.

The Python code below reproduces the issue:

from tilengine import *

HORIZONTAL_RES = 480
VERTICAL_RES = 270
NUMBER_OF_SPRITES = 5

class Enemy(object):
    def __init__(self, x, y):
        self.sprite = engine.sprites[0]
        spriteset = Spriteset.fromfile("enemy")
        self.sprite.setup(spriteset)
        self.sprite.enable_collision(True)
        self.sprite.set_position(x, y)

class Projectile(object):
    def __init__(self, sprite_index):
        self.sprite = engine.sprites[engine.get_available_sprite()]  # reuses an available sprite
        # self.sprite = engine.sprites[sprite_index]  # always uses a new sprite
        spriteset = Spriteset.fromfile("projectile")
        self.sprite.setup(spriteset)
        self.sprite.enable_collision(True)
        print(self.sprite, self.sprite.check_collision())
        self.x = 100
        self.sprite.set_position(self.x, 100)

def setup_layer(layer, name):
    tilemap = Tilemap.fromfile(name)
    layer.setup(tilemap)
    engine.set_background_color(tilemap)

engine = Engine.create(HORIZONTAL_RES, VERTICAL_RES, 0, NUMBER_OF_SPRITES, 0)
engine.set_load_path("assets")
window = Window.create()
window.disable_crt_effect()
window.define_input_key(PLAYER1, Input.BUTTON4, 100)  # move right

x = 0
projectiles = []
fire_delay = 50
sprite_index = 1
enemy = Enemy(300, 100)

while window.process():
    if window.get_input(Input.RIGHT):  # press and hold to fire
        fire_delay -= 1
        if fire_delay == 0:
            projectiles.append(Projectile(sprite_index))
            fire_delay = 50
            sprite_index += 1

    for projectile in list(projectiles):
        if projectile.sprite.check_collision():
            print('collision!')
            projectile.sprite.disable()
            projectiles.remove(projectile)
            del projectile
        else:
            projectile.x += 1
            projectile.sprite.set_position(projectile.x, 100)

If I use a separate sprite index variable so that a new index is used each time, there is no problem. Obviously this is not a suitable solution because sprite indices need to be reused. But I included it here to help illustrate the issue.

I have been able to reproduce the issue on macOS 10.15.6 and Ubuntu 20.04. I'm using the latest version of the Python bindings.

Please advise if I am missing something or if this is a genuine bug.

megamarc commented 4 years ago

Hi! Thanks for the detailed report. It's a genuine bug, introduced after implementing active sprites as a linked list. Only active sprites get collision flag cleared at the beginning of each frame. So if you disable a sprite and reassign it in the same frame, it doesn't get its collision flag cleared. That's easy to fix.

danielgholmes commented 4 years ago

Great! Thanks for your assistance!