Howdy, Stranger!

It looks like you're new here. If you want to get involved, click one of these buttons!

Example of 3D tile based engine (to use for zelda like games)

edited February 2013 in Code Sharing Posts: 196

Hello,

. @Brandn3wbian and the few that were expecting this a couple of days ago, sorry for the delay, i've been busy with work, but here it is.
Tile based games are very easy to work with (simple ai, collision detection, etc...) so I figured I would share some code to show how it works.
You can make quite a few types of games that fit the constraints of this engine style, such as zelda, bomberman, pacman, diablo, etc...

Note that the art used here is just cubes and builtin codea textures, so it's ugly as hell ;)

With nice assets though, it can very easily look gorgeous (think zelda in 3D) like Oceanhorn.

Note that I updated my Blender to Codea exporter, and you can easily create better assets with it:
http://twolivesleft.com/Codea/Talk/discussion/962/blender-scene-exporter-for-codea

Also, the code here is made as simple as possible and can be easily optimised (1D array, etc..)
Anyways, here is the source (pastebin is here)

--# Main


-- Use this function to perform your initial setup
displayMode(FULLSCREEN)
function setup()
    stick = Stick()

    pylon = Wall("Cargo Bot:Crate Red 2")
    wall = Wall("Cargo Bot:Crate Yellow 2")
    floor = Floor("Cargo Bot:Crate Green 2")

    world = World()
    hero = Hero(3,3)

    TO_DEG = 180/math.pi
end

-- This function gets called once every frame
function draw()
    background(0)
    local TO_DEG = TO_DEG
    local hero = hero

    perspective(60)
    camera(hero.x, 3, 1 + hero.z, hero.x, 0, hero.z, 0, 1, 0)

-- Draw world
    pushMatrix()
    world:draw()
    popMatrix()

-- Draw hero
    translate(hero.x, hero.y, hero.z)
    rotate(stick.direction*TO_DEG, 0, 1, 0)

    -- roll animation
    if stick.active then
        rotate(-ElapsedTime*10*TO_DEG, 0, 0, 1)
    end

    scale(.25, .25, .25)
    hero:draw()

-- Restore orthographic projection
    ortho()
    viewMatrix(matrix())
    resetMatrix()

    -- fade out overlay
    sprite("Cargo Bot:Background Fade", WIDTH/2, HEIGHT/2, WIDTH, HEIGHT)

    if stick.active then
        local ceil = math.ceil
        stick:draw()

        -- move hero based on stick direction
        local mvtx = math.cos(stick.direction)/50*stick.dist
        local mvtz = -math.sin(stick.direction)/50*stick.dist
        hero.x = hero.x + mvtx
        hero.z = hero.z + mvtz

        -- convert to table coordinates
        hero.px = ceil(hero.x - .5)
        hero.py = ceil(hero.z - .5)

        -- lazy collision check
        if world.data[hero.py][hero.px] ~= 0 then
            hero.x = hero.x - mvtx
            hero.z = hero.z - mvtz
            hero.px = ceil(hero.x - .5)
            hero.py = ceil(hero.z - .5)
        end
    end

end

function touched(touch)
    stick:touched(touch)
end

--# World
World = class()

function World:init()

    -- define the world
    self.data =
    {
        {1, 1, 1, 1, 1, 1, 1, 1},
        {1, 2, 0, 0, 0, 0, 2, 1},
        {1, 0, 0, 0, 0, 0, 0, 1},
        {1, 0, 0, 1, 2, 0, 0, 1},
        {1, 0, 0, 2, 1, 0, 0, 1},
        {1, 0, 0, 0, 0, 0, 0, 1},
        {1, 2, 0, 0, 0, 0, 2, 1},
        {1, 1, 1, 1, 1, 1, 1, 1}
    }
end

function World:draw()
    local floor, wall, pylon = floor, wall, pylon
    local offSet = 3
    local px, py = hero.px, hero.py

    -- look around the hero to draw whatever is around him
    translate(px - offSet, 0, py - offSet)
    for y = py - offSet, py + offSet do
        for x = px - offSet, px + offSet do
            if self.data[y] then
                local val = self.data[y][x]
                if val == 0 then
                    floor:draw()
                elseif val == 1 then
                    wall:draw()
                elseif val == 2 then
                    pylon:draw()
                end
            end
            translate(1,0,0)
        end
        translate(-(1 + 2 * offSet), 0, 1)
    end
end


--# Hero
Hero = class()

function Hero:init(x, z)
    self.x, self.y, self.z = x,0,z
    self.px, self.py = math.ceil(.5+x), math.ceil(.5+z)

    self.mdl = Wall("Cargo Bot:Crate Blue 2")
end

function Hero:draw()
    self.mdl:draw()
end


--# Wall
Wall = class()


function Wall:init(tex)
    -- all the unique vertices that make up a cube
    local vertices =
    {
        vec3(-0.5, -0.5,  0.5), -- Left  bottom front
        vec3( 0.5, -0.5,  0.5), -- Right bottom front
        vec3( 0.5,  0.5,  0.5), -- Right top    front
        vec3(-0.5,  0.5,  0.5), -- Left  top    front
        vec3(-0.5, -0.5, -0.5), -- Left  bottom back
        vec3( 0.5, -0.5, -0.5), -- Right bottom back
        vec3( 0.5,  0.5, -0.5), -- Right top    back
        vec3(-0.5,  0.5, -0.5), -- Left  top    back
    }

    -- now construct a cube out of the vertices above
    local verts =
    {
        -- Front
        vertices[1], vertices[2], vertices[3],
        vertices[1], vertices[3], vertices[4],
        -- Right
        vertices[2], vertices[6], vertices[7],
        vertices[2], vertices[7], vertices[3],
        -- Back
        vertices[6], vertices[5], vertices[8],
        vertices[6], vertices[8], vertices[7],
        -- Left
        vertices[5], vertices[1], vertices[4],
        vertices[5], vertices[4], vertices[8],
        -- Top
        vertices[4], vertices[3], vertices[7],
        vertices[4], vertices[7], vertices[8],
       -- Bottom
        vertices[5], vertices[6], vertices[2],
        vertices[5], vertices[2], vertices[1],
    }

    -- all the unique texture positions needed
    local texvertices =
    {
        vec2(0,0),
        vec2(1,0),
        vec2(0,1),
        vec2(1,1)
    }

    -- apply the texture coordinates to each triangle
    local texCoords =
    {
        -- Front
        texvertices[1], texvertices[2], texvertices[4],
        texvertices[1], texvertices[4], texvertices[3],
        -- Right
        texvertices[1], texvertices[2], texvertices[4],
        texvertices[1], texvertices[4], texvertices[3],
        -- Back
        texvertices[1], texvertices[2], texvertices[4],
        texvertices[1], texvertices[4], texvertices[3],
        -- Left
        texvertices[1], texvertices[2], texvertices[4],
        texvertices[1], texvertices[4], texvertices[3],
        -- Top
        texvertices[1], texvertices[2], texvertices[4],
        texvertices[1], texvertices[4], texvertices[3],
        -- Bottom
        texvertices[1], texvertices[2], texvertices[4],
        texvertices[1], texvertices[4], texvertices[3],
    }

    self.model = mesh()
    self.model.vertices = verts
    self.model.texture = tex
    self.model.texCoords = texCoords
    self.model:setColors(255,255,255,255)
end

function Wall:draw()
    self.model:draw()
end
--# Floor
Floor = class()

function Floor:init(tex)
    -- all the unique vertices that make up a cube
    local vertices =
    {
        vec3( 0.5,  -0.5,  0.5), -- Right top    front
        vec3(-0.5,  -0.5,  0.5), -- Left  top    front
        vec3( 0.5,  -0.5, -0.5), -- Right top    back
        vec3(-0.5,  -0.5, -0.5), -- Left  top    back
    }


    -- now construct a cube out of the vertices above
    local verts =
    {
        -- Bottom
        vertices[3], vertices[4], vertices[2],
        vertices[3], vertices[2], vertices[1],
    }

    -- all the unique texture positions needed
    local texvertices =
    {
        vec2(0,0),
        vec2(1,0),
        vec2(0,1),
        vec2(1,1)
    }

    -- apply the texture coordinates to each triangle
    local texCoords =
    {
        -- Bottom
        texvertices[1], texvertices[2], texvertices[4],
        texvertices[1], texvertices[4], texvertices[3],
    }

    self.model = mesh()
    self.model.vertices = verts
    self.model.texture = tex
    self.model.texCoords = texCoords
    self.model:setColors(255,255,255,255)
end

function Floor:draw()
    self.model:draw()
end

--# Stick
Stick = class()

function Stick:init()
    self.direction = 0
    self.dist = 0

    self.active = false
    self.origin = vec2(150, 150)
    self.center = self.origin
    self.pos = self.origin

    self.stick_bg = readImage("Space Art:Eclipse")
    self.stick = readImage("Space Art:UFO")

end

function Stick:draw()
    sprite(self.stick_bg, self.center.x, self.center.y)
    sprite(self.stick, self.pos.x, self.pos.y)
end

function Stick:touched(touch)
    if touch.state == BEGAN then
        self.center = vec2(touch.x, touch.y)
        self.active = true
    end

    self.pos = vec2(touch.x, touch.y)
    self.direction = math.atan2(self.pos.y - self.center.y, self.pos.x - self.center.x)

    self.dist = math.min(2, self.pos:dist(self.center)/32)

    if touch.state == ENDED then
        self.center = self.origin
        self.pos = self.center
        self.active = false
    end


end

Cheers

Tagged:

Comments

  • Jmv38Jmv38 Mod
    Posts: 3,295

    Awsome!

  • Very nice! I modified two readImage lines in Stick.lua to get it to run on 1.4.6 :

    self.stick_bg = readImage("Small World:Dialog Icon")
        self.stick = readImage("Small World:Bush")
    

    This should let it use some (arbitrarily chosen) sprites which were available.

    Cheers,
    Richard

  • Posts: 196

    Oh thanks @RichardMN, didn't even realize :D

  • Posts: 69

    Thank you so much for showing how it works @xavier <3

  • Posts: 69

    Are you using moddels made in blender in the video above?

  • Posts: 196

    . @Warox I don't know if the game used blender to create the models. It's the same studio that made Death Rally, so they probably went for a more professional (pricey) solution.
    The video above is just to show what 3D tile game engines can look like. The project is called Oceanhorn, more info can be found here: http://oceanhorn.blogspot.fr

    Note that Blender, while free, is a viable alternative to 3DSMax and the likes

    Cheers

  • Posts: 1,255

    This looks simply marvelous, @Xavier. I think it's the niftiest thing I've seen in Codea.

  • Posts: 196

    . @Mark the video shows Oceanhorn, an iOS project from the makers of Death Rally. I only included it to show that tile engines weren't outdated, and can look amazing with nice art. I apologize for the confusion!

    Cheers

  • Posts: 502

    Nice example! Thanks. I've imported OBJ file format from meshlab with this code

    https://gist.github.com/tnlogy/4690383

    saw that you made some character animations in your earlier video as well, have you done any kind of skeleton animations or similar?

  • edited February 2013 Posts: 196

    . @tnlogy Well, I have done keyFrame animation in Codea (the stuff you get in quake 1 and 2). I have the project saved somewhere, I should look for it again, I suppose I understand lua better now than a year ago and could make it faster, and this was before the mesh class and its fancy functions... I was using my software 3D renderer haha...

    Still, the obvious problem I see is speed when interpolating one keyFrame to the next, since in a proper model you loop through hundreds of vertices, and do a few table lookups, operations and function calls.

    It's fine when it's the only thing you have to do, but for speed and simplicity, the animation I use in my upcoming game is actually just rotations and translations of the meshes (like breathing for the body, and swing animation for the sword, for example).. It's fine with unrealistic models ;)

    Cheers

  • nice example @Xavier, but can you explain me this:

    -- Restore orthographic projection
    ortho()
    viewMatrix(matrix())
    resetMatrix()

    I know that it's about lights in game but I don't get it. Thanks

  • edited February 2013 Posts: 196

    Hi @Cabernet - This is basically to reset back to "2D" mode, so I can draw GUI elements (stick, buttons, etc...).

    You want to draw them last since they usually use transparency.

    The following code enables perspective projection with a Field of View of 60 degrees. The camera is at the eye, and points to the target. The last fields is the up vector (defines which way is up)

        perspective(60)
        camera(eye.x, eye.y, eye.z, target.x, target.y, target.z, 0, 1, 0)
    

    And this resets it back to the default Codea orthographic projection (meaning no vanishing point), so you can do your basic 2D drawings

        ortho()
        viewMatrix(matrix())
        resetMatrix()
    

    Note that Codea uses OpenGL, and that means it's all 3D. By default, it's just the illusion of 2D ^^

    Don't hesitate to ask if something isn't clear. That's the whole point of sharing code !

    Cheers

    edit:clarified

  • Jmv38Jmv38 Mod
    Posts: 3,295

    Hahaha! Nice one! ( it's just the illusion of 2D)

  • edited February 2013 Posts: 196

    ;)

  • Posts: 666

    I'd be interested in the rigging/skeletal stuff...I wanted to make a rigged animal like a dog, but the time investment to write rigging code is real high.

  • Posts: 69

    is there any better more compact way to create vec3 moddels as shown in the code ?

  • Posts: 196

    . @warox I'm not sure what you mean, but you can use http://twolivesleft.com/Codea/Talk/discussion/962/blender-scene-exporter-for-codea. It will export a blender model to an image format, which makes it easy to manage, if that's what you mean.

    . @aciolino The animation code is actually straightforward, go through vertices, and interpolate each vertex position to next frame.
    Character rigging however is a painful and long process, especially if you're like me and have little experience doing it :(

    I wish I had the time to learn more on the matter ... There should be some tutorials around that show best practice in order to make me faster/better at it ^^

  • Posts: 9

    Hi Xavier, I would be very interested in checking out how you handle 3d animations in codea. My problems are the opposite to yours, I work as a 3d animator and rigging/animating i can do, but my Lua/codea skills are very basic, I am getting there ;) So anything you can throw my way would be greatly appreciated!

  • Posts: 196

    . @nozzy well I need to find the lua code... It should be archived on my mac somewhere :/

    You should know that it's a bit obsolete now though. It was part of a 3D renderer I had build in Codea before we had access all the cool things we have now, I wanted to be able to store animations, but it wasn't smooth enough...
    I want to rewrite one now that were have shaders available, should be fine then :P

    In the meantime, Jeff Lamarche made a nice set of tutorials a few years ago, and one talks about keyframe animations. You can find it here: http://iphonedevelopment.blogspot.fr/2009/12/opengl-es-from-ground-up-part-9a.html
    This is where I found out more about keyframe animation and how it works.
    Note that It's obviously not ideal to use Lua to do the interpolation, but it should be a quick read and help you understand. Let me know if you have any questions.

    Here is a shader implementation example if you're interested: http://www.opengl.org/wiki/Keyframe_Animation

    You will obviously need to build buffers in Codea in order to use the vertex shader for keyframe animation.

    I will release the code once i make a new version, but I just started a new job last week, so I have little spare time :(

    Cheers

  • Posts: 9

    Thanks Xavier for the info, I'll start digging through it tomorrow. Looking forward to your updates.

  • Okay Xavier

  • Posts: 1,976

    @lruizlopez137 Please look at the date of when things were posted, and stop bumping old discussions.

  • BriarfoxBriarfox Mod
    Posts: 1,542

    If ya have nothing to add to the discussion please do not bump it!

  • Posts: 557

    My daughter and I made a maze out of this:

    https://gist.github.com/GlueBalloon/2f75849193aeedf0b508

    It's not easy! See if you can do it without peeking at the code.

    Thanks Xavier for leaving this awesome thing here for all of us.

  • How do you make enemies spawn on a certain place when the world is build up like this? Like if you want a enemy to spawn on the zero in the middle?

    self.data =
        {
            {1, 1, 1, 1, 1, 1, 1, 1},
            {1, 0, 0, 0, 0, 0, 0, 1},
            {1, 0, 0, 0, 0, 0, 0, 1},
            {1, 0, 0, 0, 0, 0, 0, 1},
            {1, 0, 0, 0, 0, 0, 0, 1},
            {1, 0, 0, 0, 0, 0, 0, 1},
            {1, 0, 0, 0, 0, 0, 0, 1},
            {1, 1, 1, 1, 1, 1, 1, 1}
        }
    end
    
  • IgnatzIgnatz Mod
    edited September 2015 Posts: 5,396

    Divide the world up into "tiles", eg 10 pixels by 10 pixels.

    Then if you want an enemy to spawn in the middle of tile [5][3], you do it at the screen position (5.5, 3.5) x 10 = (55, 35)

    You don't have to use just 1 and 0. You can use any character you like, eg "c" means a chest, "g" means gold, "e" means enemy spawn point, etc, to tell your program what to put in each tile on the map. I wrote a bunch of posts on how to make a side scrolling game, using a tile map, here.

  • edited September 2015 Posts: 34

    @Ignatz Is it possible to make one more of those an the other one draw on top of the other one? So you just need to put an E in the middle of the other one, do you understand what I am asking for?

  • @Ignatz if we use the code abow as an exemple how would you do to make it work with it? :)

  • edited September 2015 Posts: 557

    @llEmill, you really need Xavier for the best answer, but I've been messing with this code very recently so perhaps I can help.

    The code works by detecting the tile you are on, and then looking at the map to find out what all the tiles are around you. This order is important. First, where are you. Second, what's around you. This is how it manages to only draw the tiles that are onscreen, and not draw any of the tiles that are offscreen.

    The drawing code is in the World class, and if you look at it you can see the section where it decides what tile to draw based on whether it has encountered a 0, 1, or 2. This is purely about what gets drawn--there is no game logic here to start with, but you can add some if you want.

    The game logic is in the draw method of the Main tab, and as originally coded there are really only two logistics that get evaluated:

    • are you moving? If so, tell the World class to redraw the world based on your movement.
    • are you moving into a wall? If so, don't allow the player to go any further.

    There's a lot more that can be done, and that's why Xavier left it for us.

    So to do what you want:

    • in setup, define a new instance of Floor and name it spawner--because it should look just like the normal floor.
    • in World, pick a number to represent a spawning tile that's not an already-defined tile type--let's say 8. On the map, in the place where you want the monster to spawn, replace the 0 with an 8.
    • in World's draw method, find the place where it goes through the numbers to decide what to draw, and at the end of that list add code that draws a spawner whenever it detects an 8.

    ...so, that's the basic mechanics of adding a spawner tile. To actually make it spawn something, that's a different story, and I'm not as qualified to help there. I would suppose you would have to have World detect whenever it's drawing a spawner tile, and call some method that places a monster there. Then the monster would have to have its own logic for moving, etc, which would be in another class--or in the draw method, if you prefer.

    It may help you to look through the mod I made myself, to see how I did tile replacement. I ended up defining a couple new tiles myself.

  • Thank you very much @UberGoober it helped a lot!

Sign In or Register to comment.