Tutorial: Level reset [updated]

Hey guys and girls,
I just completed step 8 of the running frog tutorial and encountered an odd behavior. When mr. froggy runs into spikes of a platform on the ground, death animation plays, level gets resetted and the frog immediately dies again! After the second death everything is fine and it continues running.
For me it looks like the platform has not despawned when the hero is resetted, so it spawns in the spikes. I debugged the program with some print commands but the actions seem to be in the right order: message “reset” is sent to level.script in hero.script after the death animation. In level.script, the message with “reset” to hero.script is sent after all platforms have been deleted.
I think I stuck to the tutorial code, but to be safe, here is my code:

hero.script

-- gravity pulling the player down in pixel units/sˆ2
local gravity = -20
-- take-off speed when jumping in pixel units/s
local jump_takeoff_speed = 900

function init(self)
    -- this tells the engine to send input to on_input() in this script
    msg.post(".", "acquire_input_focus")

    -- save the starting position
    self.position = go.get_position()
    msg.post("#", "reset")

    -- keep track of movement vector and if there is ground contact
    self.velocity = vmath.vector3(0, 0, 0)
    self.ground_contact = false
end

function final(self)
    -- Return input focus when the object is deleted
    msg.post(".", "release_input_focus")
end

local function play_animation(self, anim)
    -- only play animations which are not already playing
    if self.anim ~= anim then
        -- tell the spine model to play the animation
        spine.play("#spinemodel", anim, go.PLAYBACK_LOOP_FORWARD, 0.15)
        -- remember which animation is playing
        self.anim = anim
    end
end

local function update_animation(self)
    -- make sure the right animation is playing
    if self.ground_contact then
        play_animation(self, hash("run_right"))
    else
        if self.velocity.y > 0 then
            play_animation(self, hash("jump_right"))
        else
            play_animation(self, hash("fall_right"))
        end
    end
end


function update(self, dt)
    local gravity = vmath.vector3(0, gravity, 0)

    if not self.ground_contact then
        -- Apply gravity if there's no ground contact
        self.velocity = self.velocity + gravity
    end

    -- apply velocity to the player character
    go.set_position(go.get_position() + self.velocity * dt)
    
    update_animation(self)

    -- reset volatile state
    self.correction = vmath.vector3()
    self.ground_contact = false
end

local function handle_geometry_contact(self, normal, distance)
    -- project the correction vector onto the contact normal
    -- (the correction vector is the 0-vector for the first contact point)
    local proj = vmath.dot(self.correction, normal)
    -- calculate the compensation we need to make for this contact point
    local comp = (distance - proj) * normal
    -- add it to the correction vector
    self.correction = self.correction + comp
    -- apply the compensation to the player character
    go.set_position(go.get_position() + comp)
    -- check if the normal points enough up to consider the player standing on the ground
    -- (0.7 is roughly equal to 45 degrees deviation from pure vertical direction)
    if normal.y > 0.7 then
        self.ground_contact = true
    end
    -- project the velocity onto the normal
    proj = vmath.dot(self.velocity, normal)
    -- if the projection is negative, it means that some of the velocity points towards the contact point
    if proj < 0 then
        -- remove that component in that case
        self.velocity = self.velocity - proj * normal
    end
end

function on_message(self, message_id, message, sender)
    if message_id == hash("reset") then
    	print(self.position.x)
    	print(self.position.y)
    	
        self.velocity = vmath.vector3(0, 0, 0)
        self.correction = vmath.vector3()
        self.ground_contact = false
        self.anim = nil
        go.set(".", "euler.z", 0)
        go.set_position(self.position)
        msg.post("#collisionobject", "enable")

    elseif message_id == hash("contact_point_response") then
        -- check if we received a contact point message
        
        if message.group == hash("danger") then
            -- Die and restart
            play_animation(self, hash("die_right"))
            msg.post("#collisionobject", "disable")            
            go.animate(".", "euler.z", go.PLAYBACK_ONCE_FORWARD, 160, go.EASING_LINEAR, 0.7)
            go.animate(".", "position.y", go.PLAYBACK_ONCE_FORWARD, go.get_position().y - 200, go.EASING_INSINE, 0.5, 0.2,
        		function()
         		   msg.post("controller#script", "reset")
        		end)
        		
        elseif message.group == hash("geometry") then
            handle_geometry_contact(self, message.normal, message.distance)
        end
    end
end

local function jump(self)
	print("jump")
    -- only allow jump from ground
    if self.ground_contact then
        -- set take-off speed
        self.velocity.y = jump_takeoff_speed
    end
end

local function abort_jump(self)
    -- cut the jump short if we are still going up
    if self.velocity.y > 0 then
        -- scale down the upwards speed
        self.velocity.y = self.velocity.y * 0.5
    end
end

function on_input(self, action_id, action)
    if action_id == hash("jump") or action_id == hash("touch") then
        if action.pressed then
            jump(self)
        elseif action.released then
            abort_jump(self)
        end
    end
end

level.script

go.property("speed", 6)

local grid = 460
local platform_heights = { 100} --, 200, 350 

function init(self)
    msg.post("ground/controller#script", "set_speed", { speed = self.speed })
    self.gridw = 0
    self.spawns = {}
end

function update(self, dt)
    self.gridw = self.gridw + self.speed

    if self.gridw >= grid then
        self.gridw = 0

        -- Maybe spawn a platform at random height
        if math.random() > 0.2 then
            local h = platform_heights[math.random(#platform_heights)]
            local f = "#platform_factory"
            if math.random() > 0.99 then
                f = "#platform_long_factory"
            end

            local p = factory.create(f, vmath.vector3(1600, h, 0), nil, {}, 0.6)
            msg.post(p, "set_speed", { speed = self.speed })
            table.insert(self.spawns, p)
        end
    end
end

function on_message(self, message_id, message, sender)
    if message_id == hash("reset") then 
        
        -- Delete all platforms
        for i,p in ipairs(self.spawns) do
            go.delete(p)
        end
        self.spawns = {}
        -- Tell the hero to reset.
        msg.post("hero#script", "reset")
        
    elseif message_id == hash("delete_spawn") then 
        for i,p in ipairs(self.spawns) do
            if p == message.id then
                table.remove(self.spawns, i)
                go.delete(p)
            end
        end
    end
end

Update

If I delete the spawns right before the death animations, it works. This is a workaround, but does not fix the actual problem.
If I don’t use death animation at all, it works as well.

1 Like

Some things to consider:

  1. A game object that is deleted (go.delete) will be flagged for deletion and deleted at the end of the frame. This means that until the frame has ended a deleted game object will still exist and it will be possible to interact with it and collide with it etc.
  2. The message system and the queue holding the messages is emptied in several passes during a frame. This means that messages can fly back and forth between game objects during a single frame.

You are probably seeing the effects of the above two things. The physics system detects a collision between the hero and the ground. This triggers an animation. When the animation ends some time later a reset message will be posted. This message will most likely be processed in the same frame and the spawns will be deleted (but exist for the rest of the frame). A message will also be posted back to the hero and the hero will be enabled again, and I’m guessing that the hero is colliding with the spawns again.

You need to wait with enabling the player again until the next frame or something like that.

3 Likes

Just add a variable respaw = 1 when player reset.
After he hit the ground, just set it to 0 and check the collision with this variable too :wink:

1 Like

Thanks for the explanation! I wasn’t aware of these points, especially the first one. It seemed logical to me, that when I say “delete” the game object will be deleted right in this moment.

In the end, I actually added a respawn timer as a fix for this problem. But thanks for the hint anyway :slight_smile:

1 Like

It is intuitive that deletion would happen instantly, but it can produce visual errors. In practice it could mean that parts of the game object are visible during the frame of deletion, while other parts are not. This appears as a 1-frame glitch and often produces a subtle perception of “messyness”, lower quality, or a bit broken game. That’s why we always delete at the end of the frame, to ensure that the game object has been processed and displayed in its entirety, regardless of when during the frame you chose to delete it.

3 Likes

Good to know, thank you. I’m a bit surprised that I was the first one struggling with this (at least the first one in this forum). I’d suggest adding this explanation as some kind of “background information” to the tutorial at some point.

5 Likes

Not sur Napolin but i think that we have all different code level and exp about coding game or using game engine.
I personatly have the problem when i try the tuto just for learn the api but i correct it naturally using the variable respaw because it was evident for me that deleting the object won’t be effective since the command was parse.
But your suggest is a good think for all the user and maybe we need (the community) to track a little more all those type of informations that need to be write in the tutorial section :wink:

2 Likes

Yeah, I’d appreciate that! For me, someone with no background in game developing, it is very important to learn such basics. And on the other hand, it can be very frustating if the tutorial code doesn’t work as expected

1 Like

I’m just getting started and had the same problem. Glad I found this thread. Going to work on a re-spawn timer now…

3 Likes