Going somewhere?


Tower Defense

The "path"

as each enemy spawns, it has to have somewhere to go. I came up with the idea to have a hidden "path" that would outline where an enemy should go and what it should do. This path would, eventually, be hidden underneath the actual map, so its a more technical piece that guides enemies along, and is easy to interpret. Each path would have a "path manager" script that takes care of all the processing related to a path.

Where to start?

the first thing a path manager does is find the starting tile, and it does this by cycling through every tile until it finds tile #5 (start in the tilsheet). 

lua
self.start = nil     
self.xcor,self.ycor,self.w,self.h = tilemap.get_bounds("#Path")     
for x = self.xcor, self.w+self.xcor-1, 1 do         
    for y = self.ycor,self.h+self.ycor-1,1 do             
        local tile = tilemap.get_tile("#Path", "layer1", x, y)             
        if tile == 5 then self.start = vmath.vector3(x,y,0) end         
    end     
end

Once found it stores its x and y in a variable that can be returned to an enemy once the "get_start" message is sent to it. 

Wait, what is a 'tilesheet'?

for the path, I decided to use a tilemap, a simple way to make a path in 2d space. Each tile has a number(1-8) that associates it with a certain function. Each tile also has an x,y position. 

Path_tilesheet.png

Each tile has a number and they are arranged as (1,2,3,4   5,6,7,8). I can add each tile to the tilemap using the editor, creating a comprehensive map.

How does the enemy know where to go?

After getting the starting position and moving there, the enemy will ask the path manager for the next tile.

lua
go.set_position(message.pos)
self.last = message.pos
self.lastSpeed = self.speed        
move_char(self, message.next, 5)         
msg.post("/map#path_manager", "get_next",{pos = go.get_position(),last = vmath.vector3(0,0,0),layer = self.layer})

self.last is a variable used to make sure that the enemy doesn't end up just going backwards. The path manager will take as inputs, the position of the enemy, the last position of the enemy (or 0,0 if it just started) and the layer the enemy is on. I'll touch on that one later, it becomes important. 

The path manager then takes all these inputs and finds the tile position where the enemy should go. This was my first step.

for i,v in ipairs(positions) do         
    local npos = vmath.vector3(pos.x+v.x, pos.y+v.y,0)                  
    if npos ~= get_last(last) and not get_out_bounds(npos) and tilemap.get_tile("#Path", "layer"..layer, npos.x, npos.y) ~= 0 then             
        return npos,tilemap.get_tile("#Path", "layer"..layer, npos.x, npos.y)         
    end     
end     
return vmath.vector3(0,0,0)

positions is a table that simply stores positions (1,0), (0,1), (-1,0), (0,-1). The path manager will check each position. If there is a tile there, it isn't out of bounds (i.e. outside if the map), then it will give the coordinates back to the enemy as well as the tile number. So the enemy would get a response saying "There is tile #1 at (3,2)." The enemy will then move there using a simple animation, and might want to perform a special action based on the tile number.

The enemies animation is a built in function of the game engine. It will animate a property, and will call a function upon finishing. The function it calls will either ask the path manager for the next tile again, or delete the enemy if it has reached the end (tile#6).

Need for speed

Let's say the track has tile #7, a 'speed-up' tile or #8, a 'slow-down' tile. When the path manager returns the position, it also returns the tile number. Before animation, the enemy has a number of things it does, and one of them is speed. When the speed is changed, the self.speed variable is passed to the animation. The animation will take less time is the speed is higher (1/x).

lua
self.last = go.get_position()     
local speed = self.speed     
if tile == 7 then speed = speed * 1.5     
elseif tile == 8 then speed = speed * 0.5 end     
go.set("#sprite", "playback_rate", self.lastSpeed)

I went back and fourth a few times about who should control the tile management, the path manager or the enemy controller. If the path manager did, it would store a table of all the fast blocks and slow blocks, then pass that to the enemy controller. After doing this, I realized that it would be a messy and complicated system, way larger than necessary. That's when I tried just passing the tile number to the enemy controller and letting it do it's thing.

Going invisible

At first, I didn't really have a good idea for what to do with these blocks. I had initially had an idea to use them as "teleporters" which would have worked better with the first way of setting tile actions. I had some difficulty with layers and the way the API worked, so these blocks were part of my decision to change tile action from the path manager to the enemy controller.  Now, when an enemy comes into contact with the invisible tile (#4), it will toggle its visibility, either making it visible or not visible, so if you hit one and become invisible, you have to have a second one to become visible again. 

Layers

Tiles 2 and 3 have little arrows on them, and I intended to use these as layer blocks. This was by far the hardest part of making the path work. Having different layers allowed me to be able to have some overlap in the track, making it possible to have bridges and/or tunnels (using the invisible block on the bottom layer).  Once I had the initial idea, the process was fairly straight-forward. Each enemy stores a self.layer variable that is passes to the path manager when asking for the next tile. The path manager would then only look on that layer when looking at tiles (you can see this in the earlier code snippet: "layer"..layer). When an enemy finds tile 2 or 3, it simply just changes the self.layer value, that changes for every new tile.  I was actually surprised when I hit run and it just worked the first time, which is a first time.

Leave a comment

Log in with itch.io to leave a comment.