Quick Links: Download Gideros Studio | Gideros Documentation | Gideros community chat | DONATE
Dig Dug tunnel logic. — Gideros Forum

Dig Dug tunnel logic.

AstirianAstirian Member
edited June 2017 in Game & application design
Hi guys!

So, I'm trying to get my head around doing a Dig Dug type game. I decided to try with drawing rectangles. I've got something that looks promising but doesn't behave very well. :)
    --local tunnelHalfHeight = PLAYER.tunnel:getHeight()/2
    local tunnelY = PLAYER.tunnel:getY()
 
    --local playerHalfHeight = PLAYER:getHeight()/2
    local playerY = PLAYER:getY()
 
    local distance = playerY - tunnelY
    print("Tunnel distance is: "..distance)
 
    if distance > 1 then
        --PLAYER.tunnel:setY(PLAYER:getY())
        PLAYER.tunnel:setSize(PLAYER.tunnel:getWidth(), distance)
    end
The rectangle expands but the Y stays static. If I uncomment setY(), it never resizes as it moves with the player. (I'm using http://giderosmobile.com/forum/discussion/100/any-chance-of-a-setwidth-and-setheight-for-bitmaps/p1)

I thought of just creating a new rectangle with every +1 playerY but that sounds like it could turn into a performance nightmare.

Perhaps this is entirely the wrong approach though and I would be better off drawing straight onto canvas somehow. Assuming I can then limit monster movement to stay inside what is drawn, i.e. the tunnels (I was originally going to do rectangle collision checks)...

I'm guessing the way they used to do it was to calculate every line with every frame (or something?).
«1

Comments

  • hgy29hgy29 Maintainer
    Accepted Answer
    I would have done it with a RenderTarget acting as a mask: paint dug zones on that render target and use it both to display tunnel shape and to check for collisions by using getPixel()

    Likes: Astirian

    +1 -1 (+1 / -0 )Share on Facebook
  • This sounds like exactly what I need, thanks! :D
  • AstirianAstirian Member
    edited June 2017
    OK, so I had a look at this, looks way beyond my pay-grade haha!

    But seriously, I saw the example here: http://docs.giderosmobile.com/reference/gideros/RenderTarget

    Which is great, looks like we can basically manipulate render targets directly (had to Google render targets ;)))

    I guess I would add a transparent texture over diggable ground as a mask (this would be my target), then apply color transforms to certain areas of this mask depending on player direction, let's say make them black. Then I suppose I use getPixel like: if the color data is black I know it's a tunnel?

  • hgy29hgy29 Maintainer
    Yes, thats exactly what I was suggesting!

    Likes: Astirian

    +1 -1 (+1 / -0 )Share on Facebook
  • antixantix Member
    Any reason you aren't using a tilemap? It seems the logical choice for this kind of game since it's based on a grid right?
  • I haven't gotten to that part of my career yet :). Actually, I want to do a platformer next. I thought about a grid for this game, but I couldn't think of doing it without the grids sort of "popping out". I'm after that kind of continous "drawing" feel as the tunnel is dug.

    But I'm struggling a little bit with renderTarget (I'm still a baby programmer :))).

    I can't get a hold of the individual pixels in my buffer and I hope I don't have to write a shader because that's still way too advanced for me.

    Basically, I'm trying something like this on my RT (background):
     
    buffer = self.rt:getPixels(PLAYER:getX(), PLAYER:getY(), PLAYER:getWidth(), PLAYER:getHeight())
    --buffer:setAlpha(0)	
     
    for i=1, #buffer do
     
        local pixel = buffer[i]
        print(pixel:getAlpha()) -- returns nil, I don't know how to grab the pixel in the array...
        -- pixel:setAlpha(0)
    end
    I feel like I'm sort of close. If this works, then I could probably do collision checks for enemies against 0 alphas as hgy29 suggested. :)

    But maybe there's an easier way to paint on a render target..
  • antixantix Member
    After watching a youtube video on Dig Dug gameplay maybe you need a finer grained grid.

    I haven;t played with RenderTarget and its methods of getting pixels. I think that needs better documentation (ie; there is no documentation to my knowledge).

    In theory you could use a Rendertarget and at the start of the level fill it with a color (0xffffff for this example). Whenever the player moves further in the map just clear the immediate area of the RenderTarget (clear(x, y, w, h) and then for collision you can check if the pixels in the direction of travel are not 0xffffff. If they are not then you should be able to move in that direction.

    I think I need to find a Dig Dug emulator and play it to see how the controls actually work.
  • antixantix Member
    So Dig Dug is one strange game. Even though you can move in fine pixel increments, you can only change direction (l, r, u, d) on a grid alignment. Even stranger is that between each grid column and row there is an extra one for collision purposes with regards to the monsters movements.
  • hgy29hgy29 Maintainer
    @Astirian,

    To modify the rendertarget's pixels, just draw some sprite on it. There is no setPixel() method. For getPixels(), you were nearly there: the object returned is a string containing the R,G,B and A components of all pixels of the selected region (left to right, top to bottom), each char of the string being a component. I hope this make sense...
  • antixantix Member
    @Astirian I'm not sure how you intend to make this work but I'd be eager to find out. Im my mind it would be far easier to use a traditional grid of ones and zeros for collision.

    You could still maintain the "digging" feel with a grid, you would just use more grids, so one grid for the actual collision data and another for storing the digging information.

    Does that make sense?
  • AstirianAstirian Member
    edited June 2017
    @hgy29 @antix I got the first part working, basically doing:
    self.rt:draw(myShape)
    on onEnterFrame().

    So I guess I'm drawing the shape onto the render target each frame, seems to work, dunno if I'll have to add a line to remove the shape for performance. Actually, I know! I'll just create the shape outside onEnterFrame and just update the coordinates on onEnterFrame.

    As for the grid system I'm sure that would work as well, it never occurred to me to just use pixel sized grids! I had 32x32 stuck in my head for some reason.

    Collisions are up next.

    8-X
  • antixantix Member
    edited June 2017
    You probably don't need to use a pixel sized grid for collision, I think that would be a very large table :D

    One of the cool things about Dig Dug is that there are thin walls between rows and columns which not many other games have. Usually those other games have walls that are one tile wide and high.

    I got to thinking that you could just use a grid of cells and each cell would store connectivity data to specify if any direction (Left, Right, Up, Down ) was blocked or open. So a cell in the grid would be like so..
    {t = 0, l = true, r = false, u = true, d = false}
    In this example the cell in question has a graphical tile (t) of 0, and any game agent can travel left and up from it. Using this method, a wall can be any size you like, but in Dig Dug's case it would be quite thin.

    Just a side note that this type of grid is commonly used to generate mazes :)

    So I made a little example to show how it all works. Sorry it's a bit messy and there are a bunch of hardcoded things in there but you should be able to decipher it without too much problem.

    You will see in the example that the walls of the tunnels are very thin (like Dig Dug) and the simple collision checking implemented makes the agent only able to move in the tunnels.
    DDCollision.png
    512 x 512 - 74K
    zip
    zip
    DDCollision.zip
    128K
  • @antix Thanks that's awesome! Haven't touched my game in a while but will download this tonight after work and have a play around.

    On a side note, would adding enemies and doing collisions against them be object based? At the moment I'd probably do something like add each enemy on creation to an enemies array then iterate through the array in onEnterFrame to check for collisions using bump.lua, dunno if that's silly or not.

    I'm gonna have a fun time with AI and pathing too, I can tell. :P Great fun though. Especially when it all comes together on the screen! :D

    Likes: antix

    +1 -1 (+1 / -0 )Share on Facebook
  • antixantix Member
    @Astirian, good call on your objects. I would recommend that when using bump.lua you use bump:update() to move your enemies (without detection) and then bump:move() to move and detect collisions on your player.

    With a cell based system like the one in my example I would probably also run a 2d grid array alongside it. This grid would be used for path finding using one of the many a* libraries out there. Dig Dug does also have the whole "ghosty drifting" thing but that shouldn't be too hard to get working.
  • SinisterSoftSinisterSoft Maintainer
    This type of map can get complicated though if you have doors and 8 way movement.

    Sometimes it's easier to look in the direction of the move for a map cell that is empty or not - then set it as being occupied and move in that direction (over x frames), at the same time as setting the current cell to empty.
    Coder, video game industry veteran (since the '80s, ❤'s assembler), arrested - never convicted hacker (in the '90s), dad of five, he/him (if that even matters!).
    https://deluxepixel.com
  • antixantix Member
    @SinisterSoft true, but Dig Dug only has 4-way movement :)
  • Hi guys,

    Bump.lua is working well so far, I've added rocks and it all seems to work (well, anchor points seem to need some tweaking, hopefully that doesn't bite me in the bum later!)

    So I'm working on procedural generation of elements when the level loads up. The idea is that I'll move the rock if it collides against an enemy, only problem is, I'm stuck in an infinite loop (unless I remove that last IF on the collisionsResolved boolean):
    		-- If the rock is on top of an enemy, move it till it... isn't.
    		i = 1
     
    		if world:check(rock) then
    			print("Ouch!")
     
    			actualX, actualY, cols, len = world:check(rock, randomX, randomY)	
    			print("Collision Results: "..actualX.." | "..actualY.." | "..len)
    		end
     
    		if len == 1 then
    			collisionsResolved = false
    		end
     
    		--while collisionsResolved == false do
    		repeat
    			print("Attempt "..i.." to resolve collision.")
     
    			if (self.quadrantOneStatus ~= "full") then
    			print("Noo PLACEZ! 1")
    				randomX = math.random(self.quadrantOneOrigin.x, self.quadrantOneEndpoint.x)
    				randomY = math.random(self.quadrantOneOrigin.y, self.quadrantOneEndpoint.y)
    			end
     
    			if (self.quadrantOneStatus == "full") then
    			print("Noo PLACEZ! 2")
    				randomX = math.random(self.quadrantTwoOrigin.x, self.quadrantTwoEndpoint.x)
    				randomY = math.random(self.quadrantTwoOrigin.y, self.quadrantTwoEndpoint.y)
    			end
     
    			if (self.quadrantTwoStatus == "full") then
    			print("Noo PLACEZ! 3")
    				randomX = math.random(self.quadrantThreeOrigin.x, self.quadrantThreeEndpoint.x)
    				randomY = math.random(self.quadrantThreeOrigin.y, self.quadrantThreeEndpoint.y)
    			end
     
    			if (self.quadrantThreeStatus == "full") then
    			print("Noo PLACEZ! 4")
    				randomX = math.random(self.quadrantFourOrigin.x, self.quadrantFourEndpoint.x)
    				randomY = math.random(self.quadrantFourOrigin.y, self.quadrantFourEndpoint.y)
    			end
     
    			if (self.quadrantFourStatus == "full") then
    			print("Noo PLACEZ! 5")
    				randomX = math.random(self.quadrantFourOrigin.x, self.quadrantFourEndpoint.x)
    				randomY = math.random(self.quadrantFourOrigin.y, self.quadrantFourEndpoint.y)
    			end
     
    			rock:setPosition(randomX, randomY)	
     
    			actualX, actualY, cols, len = world:check(rock, randomX, randomY)
     
    			print("SIGH: "..len)
     
    			if len == 0 then
    				collisionsResolved = true
    				--break
    			end
    			i = i+1
    		until collisionsResolved == true
    		--end
  • antixantix Member
    edited June 2017
    @Astirian, something like this should work...
        local random = math.random
     
        local done = false
     
        repeat
          local x, y, c, l = world:check(rock, randomX, randomY)
     
          if l == 0 then
            done = true -- no collision detected so exit loop
          else
     
            if (self.quadrantOneStatus == "full") then
            print("Noo PLACEZ! Quad 1")
              randomX = random(self.quadrantTwoOrigin.x, self.quadrantTwoEndpoint.x)
              randomY = random(self.quadrantTwoOrigin.y, self.quadrantTwoEndpoint.y)
            end
     
            if (self.quadrantTwoStatus == "full") then
            print("Noo PLACEZ! Quad 2")
              randomX = random(self.quadrantThreeOrigin.x, self.quadrantThreeEndpoint.x)
              randomY = random(self.quadrantThreeOrigin.y, self.quadrantThreeEndpoint.y)
            end
     
            if (self.quadrantThreeStatus == "full") then
            print("Noo PLACEZ! Quad 3")
              randomX = random(self.quadrantFourOrigin.x, self.quadrantFourEndpoint.x)
              randomY = random(self.quadrantFourOrigin.y, self.quadrantFourEndpoint.y)
            end
     
            if (self.quadrantFourStatus == "full") then
            print("Noo PLACEZ! Quad 4")
              randomX = random(self.quadrantFourOrigin.x, self.quadrantFourEndpoint.x)
              randomY = random(self.quadrantFourOrigin.y, self.quadrantFourEndpoint.y)
            end
     
          end
        until done
     
        rock:setPosition(randomX, randomY)
    Of course whenever you use one of these kinds of loops there is always the possibility that the program could get caught in an infinite loop, however tiny the chance may be.

    Note that also even though it might not get caught forever.. it might get caught up for many seconds.

    Likes: Astirian

    +1 -1 (+1 / -0 )Share on Facebook
  • antixantix Member
    edited June 2017
    Thinking about this more, I am firmly of the opinion that everything should initially be placed into a game world based on a 2d grid.

    So I made this little example which populates the game world with game objects using a grid system. There is more code and maybe it is a bit messier but it will always work and never get caught in any strange loops.

    Looking at the screen dump you can see the world is divided up into 4 quadrants and each quadrant is inhabited by 4 enemies and 2 rocks.

    This example can be used for any game where you have quadrants that need objects inserted into them without those objects overlapping any other objects in the quadrant.

    Just ask if you have any questions about it :)
    digdugproc.png
    336 x 540 - 12K
    zip
    zip
    DigDugProc.zip
    15K

    Likes: Astirian

    +1 -1 (+1 / -0 )Share on Facebook
  • antixantix Member
    edited June 2017 Accepted Answer
    While the example above works, it is not very generic. I have made another example which now has a Plane class, which contains 4 quadrants...

    1 Upper left quadrant
    2 Upper right quadrant
    3 Lower left quadrant
    4 Lower right quadrant

    Create a plane with each quadrant having these dimensions.
    myPlane = Plane.new(width, height)
    Resize the plane with these new quadrant dimensions.
    myPlane:resize(width, height)
    Clear all quadrants in the plane.
    myPlane:reset()
    Get the position of a random empty cell in quad. data is optional and will be used to fill the empty cell, otherwise the cell will be filled with 1.
    local x, y = myPlane:getPosition(quad, data)
    Get the specified quad.
    local quad = myPlane:getQuad(quad)
    Get a plane which is a grid comprised of all 4 quadrants.
    local plane = myPlane:getPlane()
    An example use (minus drawing code).
    QUAD_WIDTH = 5
    QUAD_HEIGHT = 5
     
    ENEMIES_PER_QUAD = 5
    ROCKS_PER_QUAD = 2
     
    local myPlane = Plane.new(QUAD_WIDTH, QUAD_HEIGHT) -- our plane
     
    -- generate enemies
    for q = 1, 4 do
      for i = 1, ENEMIES_PER_QUAD do
    	local x, y = myPlane:getPosition(q, 1)
        print("enemy created at " .. x .. ", " .. y)
      end
    end
     
    -- generate rocks
    for q = 1, 4 do
      for i = 1, ROCKS_PER_QUAD do
    	local x, y = myPlane:getPosition(q, 2)
        print("rock created at " .. x .. ", " .. y)
      end
    end
    The attached example has drawing code as well and is a lot simpler because it uses getPlane() and uses that when drawing everything.
    digdugproc.png
    336 x 540 - 13K
    zip
    zip
    DigDugProc2.zip
    18K

    Likes: pie, Astirian

    +1 -1 (+2 / -0 )Share on Facebook
  • Oh wow! Thanks @antix, this is well and truly above and beyond the call of duty! I was expecting a one liner along the lines of "your boolean declaration's out of scope" or something.

    You are a gentleman and a scholar! Thank you very much. :)

    Likes: antix

    +1 -1 (+1 / -0 )Share on Facebook
  • antixantix Member
    @Astirian, you are welcome :)

    Another idea I had for a bump based solution would be to just move objects slowly away from each other until they didn't collide. This would work pretty well and the only case where you would need to choose a new random location would be when one of the moving objects overlapped with two or more other objects.

    I do feel however that the grid based system is far superior. I have been thinking about how to make the class better too so you could have sectors (name change of quad) and you could have a 3x3 matrix, or whatever sized matrix you liked, as opposed to the 2x2 matrix it currently has. This would be beneficial in games with big maps I think.

    Likes: Astirian

    +1 -1 (+1 / -0 )Share on Facebook
  • AstirianAstirian Member
    edited June 2017
    @antix Yeah, I think you're right. I guess there's a reason grid based symptoms are so popular. I was going to try and figure something out for a proper platformer but I think you've solved that for me too! :D

    I'm sure it'd be great for stuff like; "this sector is water" etc... Then you could just move the world/stage around when the player got close to the sides of the screen to mimic a camera moving with the player I guess.

    Of course I wouldn't be surprised if there was already a Lua library for this as platformers are so ubiquitous. I had read briefly about the Tiled editor a while back and was going to circle around back to it.
  • antixantix Member
    edited June 2017
    @Astirian well Gideros has built in support for tile maps that are created in Tiled. Add to that Bump and you can pretty much have a platform game up and running very quickly. Somewhere on the forums here I posted some code a while ago that can create bump collision rects from a tiled map.

    For zones I found it was quite easy to just use the characters midpoint for that. in your tilemap you can decide which blocks are which types of zone for example blocks 1 might be water and 2 might be lava, etc. I used an extra layer in my tiled map to edit them.

    Check which tile in the map the player is on. If it's in the water zone and the player is not set to be in water then set them in water, and do enterNewZone effects. If it's no zone (0 - 109) and they are not in no zone then set them in no zone and do exitLastZone effects.

    I chose this method because in a platformer you are always checking the players midpoint so it was natural to use it for zone entry/exit as well.

    Likes: Astirian

    +1 -1 (+1 / -0 )Share on Facebook
  • @antix Hiya! So I threw out everything and started with the grid based approach, works great but collision broke; I'm having issues adding "bump bodies" to the brushes. I think it treats the last type in the quad as the only brush or something...

    I tried replacing drawing brushes onto the RT with self:addChild and they're always just the last two in the last two quads for some reason.

    I'm probably missing something obvious though?
    --world:add(brush, x, y, 40, 46) -- x,y,w,h //Duplicate error.
     
    --if (c ~= 3 and c ~= 4 and r ~= 5 and r ~= 6) then
    	--if brush ~= nil then
    		brush:setPosition(c * TILESIZE, r * TILESIZE)
    		--canvas:draw(brush)
    		self:addChild(brush) -- Only adds last of each?
    	--end
    --end
  • antixantix Member
    edited July 2017 Accepted Answer
    @Astirian, okay I can see what is happening here now (took me a while)

    The brush is a SINGLE object but bump expects every object you add to its world to be UNIQUE (so you are getting the error because you are trying to add the same object repeatedly).

    The solution is to create a new collision rectangle for each tile that is solid and then add that to the bump world..
    local rect = {isWall = true}
    world:add(rect, c * TILESIZE, y * TILESIZE, 40, 46)
    Between levels I would suggest removing all items from the bump world..
    local items, len = world:getItems()
    if len > 0 then
      for i = 1, len do
        world:remove(items[i])
      end
    end
    Then at the start of each level add the new ones back in.

    Likes: Astirian

    +1 -1 (+1 / -0 )Share on Facebook
  • Hi guys! @antix @hgy29!

    So I've been away for a while (getting married and honeymooning hurrah!)...

    I've come back to this and it's looking pretty good. Although I've done a weird mix of both your approaches:

    The player may move at any point in any of the 4 directions, so he/she is not limited to a grid. The placement of the enemies and rocks is grid based, and my enemies check for a path to the player every 2 seconds or so (using a smaller grid)...

    The player draws a black rectangle onto the rt canvas on every frame to draw the tunnel. This works pretty well. Only...

    I've kind of designed myself into a corner. Basically because of the players freedom of movement, there's a chance that some slivers of dirt are left, and because the enemies check a grid for pathing (the smaller grid, not the one I use for the procgen placement), there's a chance that they'll 'glide' through the dirt (because I mark the grid as an obstacle using getPixel(), which gets the center pixel's info for that grid square).

    I've thought of three solutions to this but they're very expensive. The easiest one I've implemented is that I give the enemies black rectangles that they draw onto the rt canvas when they move on every frame (so they can essentially dig through small portions of dirt), but when you have 12 enemies, the game slows down too much because of the performance hit.

    Second thing I've thought about is a finer pathchecking grid but the path check for every enemy every 2 seconds is already quite expensive (due to getPixel()) so that's going to be a performance hit too.

    Third solution is even more use of getPixel(), which isn't an option.

    Man, and I thought a Dig Dug game was going to be easy! Hahaha!

    Likes: Atavismus, MoKaLux

    +1 -1 (+2 / -0 )Share on Facebook
  • antixantix Member
    edited October 2017
    @Astirian, firstly congratulations on your marriage :)

    Secondly, commiserations on your Dig Dug issues :(

    Does having collision enabled vs having collision disabled make a difference with many monsters? I'm not 100% sure but I would say that the issue would be the GetPixel() calls. Do you make only one GetPixel() call per monster or many?

    You might still be able to use Bump collision for this as well, I don't see the need for any GetPixel calls at all. I'll think up an example.

    Bump is now also included as a plugin in Gideros and is vastly faster than the lua based one (which should still be fine for your case).

    Anyway I'll try to think of an example using Bump.

    Would you be able to paste any code here that roughly shows how your current getPixel() collision works, or is that sharing too much secret sauce? ;)
  • Hi @antix!

    I don't know if bump is really having that much of an impact. Everything runs relatively smoothly, even the A* pathing calls that use getPixel (more or less, as getPixel seems fairly pricey anyway). It really looks like it's the enemies' drawing of the black rectangles onto the render target on every frame that's causing the slowness.

    Basically the slowdown isn't there when 12 enemies are moving without their black "clearing" rectangles but when they have them; the more enemies that move, the more everything slows down. 1-2 are fine then it gets a bit choppy with 3-4, by the time all 12 are moving, it definitely gets very er... slideshowy.

    P.S: My workaround with getPixel is to only have the enemies check for a path every 2 seconds as opposed to every frame. :)

    I'm happy to post up some code but there's a lot here haha! I can always send you the full monty if you like. Or write up an abridged version maybe? :D

  • antixantix Member
    edited October 2017 Accepted Answer
    Okay that explanation makes things a little clearer. You could stagger your black draws.

    Whenever a monster is created they are assigned a unique number (1 - 12 for 12 monsters).
    Each frame we update and reset a global frame counter (1- 12 for 12 monsters).
    When updating each monster only draw a black thing if the global frame counter matches their unique number.

    Using this method you will only have one monster drawing a black thing each frame. Here is some quick code to demonstrate roughly how it would go..
    Monster = Core.class(Sprite)
     
    function Monster:init(frame)
      self.frame = frame -- which frame this monster will draw a black thing
      self.active = true
      self.game = GAME
    end
     
    function Monster:update(dt)
      if self.active then
        local game = self.game
        if game.frame == self.frame then -- only draw black thing on matching frame
          self:drawBlackThing()
        end
     
        -- do other monstrous stuff
     
      end
     
    end
     
    function Monster:drawBlackThing()
      -- draw black shape to RenderTarget
    end
    Game = Core.class(Sprite)
     
    function Game:init()
      GAME = self -- make Game class avilable to all classes
     
      self.frame = 0 -- our frame counter
     
      local monsters = {} -- make some monsters
      for i = 1, 12 do
        local m = Monster.new(i)
        monsters[#monsters + 1] = m
      end
      self.monsters = monsters
    end
     
    function Game:enterFrame(e) -- call every frame
      local dt = e.deltaTime
     
      local frame = self.frame -- increment and reset frame counter
      frame = frame + 1
      if frame > 12 then
        frame = 0
      end
      self.frame = frame
     
      local monsters = self.monsters -- update all monsters
      for i = 1, 12 do
        local m = monsters[i]
        if m.active then -- only update active monsters
          m:update(dt)
        end
      end
     
    end
    I hope you get all that, just let us know if you don't ;)

    Likes: Astirian

    +1 -1 (+1 / -0 )Share on Facebook
Sign In or Register to comment.