Kadoba / Advanced-Tiled-Loader

Imports Tiled maps into Lua for the LÖVE game engine. (NO LONGER IN DEVELOPMENT)
183 stars 30 forks source link

using drawAfterTile() #20

Closed eipporko closed 11 years ago

eipporko commented 11 years ago

Hi Kadoba,

I have an isometric map, and I want to draw my entities in such way that depending on their position in the map they are hidden by tiles or not (example: A Stone) . So, I use TileLayer:drawAfterTile(h, w, funct) method to get it.

I have one class named Level, who has one table named entities that contains objects like: player, mobs, bullets... And when I call Level:draw() method, I only have to make a for loop calling drawAfterTile() with all entities within the table.

But I have problems when:

Video example: https://vimeo.com/52391901

Source code related:

-- Level Class
(...)
Level = {}
Level.__index = Level

-- Some global stuff that the examples use.
global = {}
global.limitDrawing = false     -- If true then the drawing range example is shown
global.benchmark = false        -- If true the map is drawn 20 times instead of 1
global.useBatch = false         -- If true then the layers are rendered with sprite batches
global.drawObjects = false      -- If this is true then ObjectLayers will be drawn
global.scale = 1                -- Scale of the screen
(...)

function Level.create(tmxPath) 
    lvl = {}
    setmetatable(lvl,Level)

    loader = require("lib.AdvTiledLoader.Loader")
    loader.path = "res/levels/" --Change this to wherever your .tmx files are
    lvl.map = loader.load(tmxPath) --Change this to the name of your mapfile
    lvl.map.drawObjects = global.drawObjects -- Hide ObjectLayers
    lvl.map.useSpriteBatch = global.useBatch
    lvl.player = nil -- player entity shortcut
    lvl.entities = {} -- table of entities

       (...)

    return lvl
end

(...)

function Level:draw()
    (...)

    -- scale and translate the game screen for map drawing
    love.graphics.push()
    love.graphics.scale(global.scale)

    -- limit the draw range 
    self.map:autoDrawRange(0, 0, global.scale, padding)

    local ent = {}
    -- draw entities
    for i,entity in ipairs(self.entities) do 
        ent.worldX, ent.worldY = self.map:toIso(entity.x,entity.y)
        ent.tileX = math.floor(ent.worldX/self.map.tileHeight)
        ent.tileY = math.floor(ent.worldY/sef.map.tileHeight)
        self.map.layers["Grass"]:drawAfterTile(ent.tileX, ent.tileY, entity.draw)
    end

    -- draw map
    self.map:draw()

    love.graphics.pop()

       (...)

end

(...)
-- Player Class
(...)
Player = {}
Player.__index = Player

(...)

function Player.create(x,y,level)
    plyr = {}
    plyr.x, plyr.y = x, y

    (...)

    setmetatable(plyr,Player)
    return plyr
end

(...)

function Player:draw(x,y)
    playerAnimation[plyr.state..plyr.direction]:draw(playerSprites, plyr.x-16, plyr.y-32)
end

(...)
-- Bullet Class
(...)
Bullet = {}
Bullet.__index = Bullet

(...)

function Bullet.create(weapon, aimVector)
    bllt = {} 
    bllt.weapon = weapon
    bllt.x, bllt.y = weapon.owner.x , weapon.owner.y

        (...)

    setmetatable(bllt,Bullet)
    return bllt
end

(...)

function Bullet:draw(x,y)
    if bllt.alive == true then
        bulletAnimation["bullet"..bllt.direction]:draw(bulletSprites, bllt.x, bllt.y)
    end
end
eipporko commented 11 years ago

"When I call draw method from TileLayer:drawAfterTile() of more than one object of the same class, it only seems to draw the last one." : obj:methodname(args) is sugar for obj.methodname(obj,args), so If I want to get correct values for x and y coordinates of each object, I need to get it using self parameter within obj:draw() method, but I can't, because I would need to pass obj reference in TileLayer:drawAfterTile().

Now if I try to use self parameter inside obj:draw() method, I get "Error: *****: attempt to index local 'self' (a number value)" when it is called from TileLayer:drawAfterTile().

Kadoba commented 11 years ago

drawAfterTile is an old slow feature that I've neglected for a while. I really don't like it's current implementation so I'm working on a new one.

eipporko commented 11 years ago

It sounds great, so in the meanwhile i'll try to do another stuff . Thanks!

Kadoba commented 11 years ago

TileLayer:drawAfterTile() has been replaced with TileLayer:setAfterTileFunction(). Now, instead of setting a function for every tile only one function is set per TileLayer that gets called every time a tile is drawn.

See TileLayer:setAfterTileFunction() for more information. Let me know if you need any more help.

eipporko commented 11 years ago

Now works :),

You should edit the wiki documentation about this new method, because the function parsed should be of the form funct(tilePositionX, tilePositionY, tileDrawX, tileDrawY, ...), instead funct(layer, tilePositionX, tilePositionY, tileDrawX, tileDrawY, ...)

Finally I fix my problems using your new method and parsing it a function that check for a tile if there are any entity to be drawn

function Level:drawEntities(tilePositionX, tilePositionY, tileDrawX, tileDrawY, self)
    local ent = {}
    -- draw entities
    for i,entity in ipairs(self.entities) do 
        ent.worldX, ent.worldY = self.map:toIso(entity.x,entity.y)
        ent.tileX = math.floor(ent.worldX/self.map.tileHeight)
        ent.tileY = math.floor(ent.worldY/self.map.tileHeight)
        if tilePositionX==ent.tileX and tilePositionY==ent.tileY then
            entity:draw()
        end
    end
end

But with a very large table, it becomes inefficient with too many checks by tile.

Any suggestion?

Kadoba commented 11 years ago

You could use a spatial hash to divide up your entities into buckets. The grid class that comes with ATL is perfect for this.

local Grid = require("AdvTiledLoader.Grid")
local buckets = Grid:new()  -- The buckets to put entities into
local cellSize = 10         -- The number of tiles that a bucket represents
local floor = math.floor    -- Localize the flooring function

-- Gets a bucket at the location
local cellX, cellY
function getBucket(buckets, tileX, tileY)
   cellX, cellY = floor(tileX / cellSize), floor(tileY / cellSize)
   if not buckets(cellX, cellY) then buckets:set(cellX, cellY, {}) end
   return buckets(cellX, cellY)
end

-- Puts an entity into a bucket. If the entity has moved then provide the old tile location.
function putIntoBucket(buckets, entity, tileX, tileY, oldTileX, oldTileY)
   oldTileX, oldTileY = oldTileX or 0, oldTileY or 0
   getBucket(buckets, oldTileX, oldTileY)[ entity ] = nil
   getBucket(buckets, tileX, tileY)[ entity ] = true
end

-- Clears the buckets
function clearBuckets(buckets)
   buckets:clear()
end

-- Only check the buckets
function Level:drawEntities(tilePositionX, tilePositionY, tileDrawX, tileDrawY, self)
    for ent,_ in pairs( getBucket(buckets, tilePositionX, tilePositionY) ) do 
        ent.worldX, ent.worldY = self.map:toIso(entity.x,entity.y)
        ent.tileX = floor(ent.worldX/self.map.tileHeight)
        ent.tileY = floor(ent.worldY/self.map.tileHeight)
        if tilePositionX==ent.tileX and tilePositionY==ent.tileY then
            entity:draw()
        end
    end
end
eipporko commented 11 years ago

I didn't know about spacial hashing. I just tried this algorithm and it works really fine.

Yesterday, I was thinking about implementing a similar algorithm, using a 2-dimensional array but with an array cell for tile.

Thanks again.