My own action adventure engine

Showcase your libraries, tools and other projects that help your fellow love users.
Post Reply
User avatar
VideroBoy
Party member
Posts: 102
Joined: Wed Mar 31, 2010 6:12 pm
Location: Canada

My own action adventure engine

Post by VideroBoy »

Skip to latest version




A project I hope is going somewhere.
SpaceNinja.love
(2.57 MiB) Downloaded 561 times
Some issues though:
  • When the background music finishes, the game crashes.
  • I can't get collisions with wall corners working. My code for corners is based on Gamasutra's article on circle/sphere collisions (I'm treating the corner as a zero-radius circle) and my test looks like this:

Code: Select all

-- collision.lua

-- "line" represents movement vector
-- "px" and "py" is corner
local function pointCollision(line, px, py, radius)
    local result -- Time of collision, or nil if no collision will happen

    local dx = line.xf - line.xi
    local dy = line.yf - line.yi

    local moveLenSq = (dx * dx) + (dy * dy)

    -- Center of circle to point
    -- I'm just going to call this vector "v"
    local vx = px - line.xi
    local vy = py - line.yi

    local moveLen = math.sqrt(moveLenSq)

    local ndx = dx / moveLen
    local ndy = dy / moveLen

    -- Dot product between v and normal of movement vector.
    -- This is equal to the length of the projection vector of
    -- v onto the movement vector
    local dotprod = (vx * ndx) + (vy * ndy)
    assert(dotprod > 0) -- Circle should already be moving towards
                        -- corner, since it's moving towards the
                        -- corner's line (this function called by
                        -- line segment collision function)

    -- Closest distance circle will ever get to point
    local vLenSq = (vx * vx) + (vy * vy)
    local closestDistSq = vLenSq - (dotprod * dotprod)

    if closestDistSq < (radius * radius) then
        local moveOffsetSq = (radius * radius) - closestDistSq

        local realMoveLen = dotprod - math.sqrt(moveOffsetSq)

        if realMoveLen < moveLen then
            result = realMoveLen / moveLen
        end
    end

    return result
end
Last edited by VideroBoy on Fri Dec 03, 2010 6:02 pm, edited 3 times in total.
User avatar
Kingdaro
Party member
Posts: 395
Joined: Sun Jul 18, 2010 3:08 am

Re: Action adventure game map test

Post by Kingdaro »

Found a bug where in some instances you can walk through walls:
love 2010-09-02 15-23-32-11.png
love 2010-09-02 15-23-32-11.png (3.73 KiB) Viewed 9390 times
love 2010-09-02 15-23-40-97.png
love 2010-09-02 15-23-40-97.png (3.91 KiB) Viewed 9390 times
(Windows 7) After it seems that the music ends, love.exe has stopped working.
User avatar
Jasoco
Inner party member
Posts: 3725
Joined: Mon Jun 22, 2009 9:35 am
Location: Pennsylvania, USA
Contact:

Re: Action adventure game map test

Post by Jasoco »

You should take a look at my project (Adventure Game, GIT link in signature) for tips on how to do maps and collisions. Sorry for the lack of comments and mass of code, but it's a big project.
User avatar
VideroBoy
Party member
Posts: 102
Joined: Wed Mar 31, 2010 6:12 pm
Location: Canada

Re: Action adventure game map test

Post by VideroBoy »

Kingdaro wrote:Found a bug where in some instances you can walk through walls

(Windows 7) After it seems that the music ends, love.exe has stopped working.
I said there were these bugs in my first post. :huh:
Any suggestions on how to fix them?

Jasoco, what kind of collision detection does your game do? In my game the movable character uses a bounding circle. To test against the terrain I get any wall the character may intersect and generate lines for their sides to test against. I'm using a continuous collision detection method where I test when the character's bounding circle will make contact with a wall line or corner before moving the character.
User avatar
Jasoco
Inner party member
Posts: 3725
Joined: Mon Jun 22, 2009 9:35 am
Location: Pennsylvania, USA
Contact:

Re: Action adventure game map test

Post by Jasoco »

Is your game tile based? Or are you trying to check for collisions with arbitrarily located shapes that could be anywhere?

Mine is tile based which is easy as checking if the players X is overlapping a tile with a solid flag. (Hit map)
User avatar
VideroBoy
Party member
Posts: 102
Joined: Wed Mar 31, 2010 6:12 pm
Location: Canada

Re: Action adventure game map test

Post by VideroBoy »

My game is tile based. It's just the controllable character that can be placed on any arbitrary (pixel) location.

I check for collisions before they happen, then move the character by the right amount. For the wall sides, I find out if collisions will occur by getting the lines segments bordering the walls the character might hit. Wall side collision is working, but not wall corner collision.

If I were to post code, how much would people be willing to look at?
User avatar
Jasoco
Inner party member
Posts: 3725
Joined: Mon Jun 22, 2009 9:35 am
Location: Pennsylvania, USA
Contact:

Re: Action adventure game map test

Post by Jasoco »

You need to check:

tile[floor(playerX / tileWidth)][floor(playerY / tileHeight)] == not solid

If the above array check is not a solid object, then you can walk there. Else you can't. But you need to check for all four corners of the player. (I know you want to use a circle, so it will be a different method) If you use a square, it would be playerX, playerY : playerX + playerW, playerY : playerX + playerW, playerY + playerH : playerX, playerY + playerH. For a circle, well that would be more of a distance thing, but I can't help you there. It's easier to use a square. Will your player really be a circle?
User avatar
VideroBoy
Party member
Posts: 102
Joined: Wed Mar 31, 2010 6:12 pm
Location: Canada

Re: Action adventure game map test

Post by VideroBoy »

Jasoco wrote:You need to check:

tile[floor(playerX / tileWidth)][floor(playerY / tileHeight)] == not solid
I'm already doing that in a way.

I think we're talking past each other, so I'm just going to post my collision code in full.

First of all, keep in mind that I'm using a fixed timestep, using a method described here. So, I don't multiply anything by dt.

My update function looks like this:

Code: Select all

local dx, dy = actor.getVelocity(player, currentButtons)
local time = collision.actorVSLevel(currentLevel, player, dx, dy)
actor.move(player, dx, dy, time)
First I get the player's velocity. Then I get a time value where I check the player against the map. This value is between zero and one; one if the player is not going to collide with anything, zero if the player is already up against something. So, time represents the moment in the current frame that the player collides with something. Note that the player has not actually moved yet. Movement only happens in the last function, which looks like this:

Code: Select all

function actor.move(act, dx, dy, time)
    local time = time or 1.0

    act.x = act.x + math.floor(dx * time)
    act.y = act.y + math.floor(dy * time)
end
This is what checking the player against the map looks like. The idea is I find the time, a value between zero and one, that the earliest collision occurs. I return 1 if I don't find any collision.

Code: Select all

function collision.actorVSLevel(level, act, dx, dy, time)
    local resultTime = time or 1.0

    local radius = act.type.radius

    local moveRect = collision.circleMoveRect(radius,
      act.x, act.y, dx, dy)

    local possibleTiles = levels.tilesInRect(level, moveRect)
    local moveLine = {xi = act.x, yi = act.y,
      xf = act.x + dx, yf = act.y + dy}

    for _, tile in ipairs(possibleTiles) do
        if levels.isWall(level, tile.x, tile.y) then
            if dx < 0 then
                -- Going west
                local line = levels.lineSegmentEast(level,
                  tile.x, tile.y)
                local normal = {x = 1, y = 0}

                local firstCorner = levels.hasNorthEastCorner(level,
                  tile.x, tile.y)
                local secondCorner = levels.hasSouthEastCorner(level,
                  tile.x, tile.y)

                iTime = lineCollision(moveLine, line, radius,
                  normal, firstCorner, secondCorner)

                if iTime and (iTime < resultTime) then
                    resultTime = iTime
                end
            elseif dx > 0 then
                -- Going east
                local line = levels.lineSegmentWest(level,
                  tile.x, tile.y)
                local normal = {x = -1, y = 0}

                local firstCorner = levels.hasNorthWestCorner(level,
                  tile.x, tile.y)
                local secondCorner = levels.hasSouthWestCorner(level,
                  tile.x, tile.y)

                iTime = lineCollision(moveLine, line, radius,
                  normal, firstCorner, secondCorner)

                if iTime and (iTime < resultTime) then
                    resultTime = iTime
                end
            end

            if dy < 0 then
                -- Going north
                local line = levels.lineSegmentSouth(level,
                  tile.x, tile.y)
                local normal = {x = 0, y = 1}

                local firstCorner = levels.hasSouthWestCorner(level,
                  tile.x, tile.y)
                local secondCorner = levels.hasSouthEastCorner(level,
                  tile.x, tile.y)

                iTime = lineCollision(moveLine, line, radius,
                  normal, firstCorner, secondCorner)

                if iTime and (iTime < resultTime) then
                    resultTime = iTime
                end
            elseif dy > 0 then
                -- Going south
                local line = levels.lineSegmentNorth(level,
                  tile.x, tile.y)
                local normal = {x = 0, y = -1}

                local firstCorner = levels.hasNorthWestCorner(level,
                  tile.x, tile.y)
                local secondCorner = levels.hasNorthEastCorner(level,
                  tile.x, tile.y)

                iTime = lineCollision(moveLine, line, radius,
                  normal, firstCorner, secondCorner)

                if iTime and (iTime < resultTime) then
                    resultTime = iTime
                end
            end
        end
    end

    return resultTime
end
First thing I do is get the rectangle that encloses the player's movement, which looks like this:
move_rect.png
move_rect.png (18.01 KiB) Viewed 9333 times

Code: Select all

function collision.circleMoveRect(radius, x, y, dx, dy)
    local width = (radius * 2) + math.abs(dx)
    local height = (radius * 2) + math.abs(dy)

    local rx = x - radius
    if dx < 0 then
        rx = rx + dx
    end

    local ry = y - radius
    if dy < 0 then
        ry = ry + dy
    end

    -- return {x = x or 0, y = y or 0,
    --   width = width or 0, height = height or 0}
    return utility.rectangle(rx, ry, width, height)
end
Then I get the tiles covered by the movement rectangle:

Code: Select all

function levels.tilesInRect(level, rect)
    local minX = levels.tileX(level, rect.x)
    local minY = levels.tileY(level, rect.y)
    local maxX = levels.tileX(level, rect.x + rect.width)
    local maxY = levels.tileY(level, rect.y + rect.height)

    if minX < 0 then
        minX = 0
    end

    if minY < 0 then
        minY = 0
    end

    if maxX >= level.width then
        maxX = level.width - 1
    end

    if maxY >= level.height then
        maxY = level.height - 1
    end

    local result = {}

    for x = minX, maxX do
        for y = minY, maxY do
            table.insert(result, {x = x, y = y})
        end
    end

    return result
end
Now that I have all the tiles the player will cross, I loop through them and find any wall tiles. When i find a wall tile, I figure out which sides the play may hit. So, if the player is moving east, I get a line segment representing the wall's west side.

Code: Select all

function levels.lineSegmentNorth(level, x, y)
    -- Goes west to east
    local xi = x * TILE_SIZE
    local yi = y * TILE_SIZE
    local xf = xi + TILE_SIZE
    local yf = yi

    return {xi = xi, yi = yi, xf = xf, yf = yf}
end

function levels.lineSegmentSouth(level, x, y)
    -- Goes west to east
    local xi = x * TILE_SIZE
    local yi = (y * TILE_SIZE) + TILE_SIZE
    local xf = xi + TILE_SIZE
    local yf = yi

    return {xi = xi, yi = yi, xf = xf, yf = yf}
end

function levels.lineSegmentEast(level, x, y)
    -- Goes north to south
    local xi = (x * TILE_SIZE) + TILE_SIZE
    local yi = y * TILE_SIZE
    local xf = xi
    local yf = yi + TILE_SIZE

    return {xi = xi, yi = yi, xf = xf, yf = yf}
end

function levels.lineSegmentWest(level, x, y)
    -- Goes north to south
    local xi = x * TILE_SIZE
    local yi = y * TILE_SIZE
    local xf = xi
    local yf = yi + TILE_SIZE

    return {xi = xi, yi = yi, xf = xf, yf = yf}
end
I also check whether or not the wall tile has any corners facing the player. That is, I don't check the player against the wall tile's corners if the wall tile is part of a larger wall.

Code: Select all

function levels.hasNorthEastCorner(level, x, y)
    return not ( levels.isWall(level, x, y - 1)
      or levels.isWall(level, x + 1, y) )
end

function levels.hasSouthEastCorner(level, x, y)
    return not ( levels.isWall(level, x, y + 1)
      or levels.isWall(level, x + 1, y) )
end

function levels.hasNorthWestCorner(level, x, y)
    return not ( levels.isWall(level, x, y - 1)
      or levels.isWall(level, x - 1, y) )
end

function levels.hasSouthWestCorner(level, x, y)
    return not ( levels.isWall(level, x, y + 1)
      or levels.isWall(level, x - 1, y) )
end
I finally check the player against a line segment representing a wall side.

Code: Select all

local function lineCollision(a, b, radius, bNormal, bHasFirstPoint,
  bHasSecondPoint)
    local result = nil

    local aDX = a.xf - a.xi
    local aDY = a.yf - a.yi

    local bDX = b.xf - b.xi
    local bDY = b.yf - b.yi

    local denom = (bDY * aDX) - (bDX * aDY)

    -- Shift wall towards circle
    local rx = radius * bNormal.x
    local ry = radius * bNormal.y

    local crossX = a.xi - b.xi - rx
    local crossY = a.yi - b.yi - ry

    local intersectA = ((bDX * crossY) - (bDY * crossX)) / denom
    local intersectB = ((aDX * crossY) - (aDY * crossX)) / denom

    if (intersectA >= 0) then
        -- Circle will cross plane of wall

        if (intersectA <= 1)
          and (intersectB >= 0) and (intersectB <= 1) then
            -- Circle hitting wall itself
            result = intersectA
        else
            -- Test against wall's end points

            if bHasFirstPoint and (intersectB < 0) then
                -- Test against first point
                result = pointCollision(a, b.xi, b.yi, radius)
            elseif bHasSecondPoint and (intersectB > 1) then
                -- Test against second point
                result = pointCollision(a, b.xf, b.yf,
                  radius)
            end
        end
    end

    return result
end
The first part is a standard line intersection test, based on this geometry article. Since the wall line is always horizontal or vertical, I just have to shift it towards the player by the player's radius. intersectA corresponds to when the (infinitely extended) movement line will intersect the (infinitely extended) wall line. If it's between 0 and 1, the intersection will happen during this frame. If it's less than 0, the wall only intersects with the movement line extended backwards from the movement line segment, corresponding to sometime in the past. If intersectA is greater than 1, the two lines will intersect but only in the future. intersectA is treated like a time value.

When the two lines are not intersecting is when I try to test against the corners, and this is the part I'm struggling with. I'll post my function for testing against a corner point again.

Code: Select all

local function pointCollision(line, px, py, radius)
    local result

    local dx = line.xf - line.xi
    local dy = line.yf - line.yi

    local moveLenSq = (dx * dx) + (dy * dy)

    -- Center of circle to point
    -- I'm just going to call this vector "v"
    local vx = px - line.xi
    local vy = py - line.yi

    local moveLen = math.sqrt(moveLenSq)

    local ndx = dx / moveLen
    local ndy = dy / moveLen

    -- Dot product between v and normal of movement vector.
    -- This is equal to the length of the projection vector of
    -- v onto the movement vector
    local dotprod = (vx * ndx) + (vy * ndy)
    assert(dotprod > 0) -- Circle should already be moving towards
                        -- corner, since it's moving towards the
                        -- corner's line

    -- Closest distance circle will ever get to point
    local vLenSq = (vx * vx) + (vy * vy)
    local closestDistSq = vLenSq - (dotprod * dotprod)

    if closestDistSq < (radius * radius) then
        local moveOffsetSq = (radius * radius) - closestDistSq

        local realMoveLen = dotprod - math.sqrt(moveOffsetSq)

        if realMoveLen < moveLen then
            result = realMoveLen / moveLen
        end
    end

    return result
end
And the article I got my algorithm from:
http://www.gamasutra.com/view/feature/3 ... php?page=2

I am ambitious about my collision detection you can tell. :nyu:
User avatar
VideroBoy
Party member
Posts: 102
Joined: Wed Mar 31, 2010 6:12 pm
Location: Canada

Re: Action adventure game map test

Post by VideroBoy »

I decided to switch to using a bounding box for the player. It actually did simplify my code since I'm assuming horizontal and vertical lines only, though the implementation process was harder that I would have thought. :ehem:

I also figured out the music crash thing. I didn't set the music variable to looping nor bind it to a variable outside the load function's scope, so it got garbage collected when the song finished.

I've also updated the player sprite to better reflect the dimensions I intend my game characters to have and made the walls less "sticky" like they were in the first demo.
You can go into praise mode now.
Attachments
SpaceNinja.love
(2.86 MiB) Downloaded 315 times
User avatar
thelinx
The Strongest
Posts: 857
Joined: Fri Sep 26, 2008 3:56 pm
Location: Sweden

Re: Action adventure game map test

Post by thelinx »

Your player character suffers from PO2 syndrome.
Post Reply

Who is online

Users browsing this forum: Google [Bot] and 37 guests