LÖVE und PANZER - I Broke Everything... Again

I have some good news, and some bad news. Which would you like to hear first? The bad news? Okay. The bad news is... we broke it. However, the good news is we added several new features and fixes, so let me explain those first!

First off, we felt it was important to add bounding boxes to all of our objects. Bounding boxes are very useful for things like collision, be it two players ramming into each other, or one player shooting another. We decided right away that it was important for each mesh to have its own bounding box, that way shooting the air between the cannon and the hull wouldn't cause a hit. The way we created the bounding box was quite straight forward: While loading the IQE model, we looped through each vertex and checked if that vertex had a larger or smaller x, y, or z position than the previous record holder. By the end of the loop, we knew the closest and farthest points of the mesh.

local bounds = { min = {}, max = {} }  
for i=1, #mesh.vp do  
    bounds.min.x = bounds.min.x and math.min(bounds.min.x, vp[1]) or vp[1]
    bounds.max.x = bounds.max.x and math.max(bounds.max.x, vp[1]) or vp[1]
    bounds.min.y = bounds.min.y and math.min(bounds.min.y, vp[2]) or vp[2]
    bounds.max.y = bounds.max.y and math.max(bounds.max.y, vp[2]) or vp[2]
    bounds.min.z = bounds.min.z and math.min(bounds.min.z, vp[3]) or vp[3]
    bounds.max.z = bounds.max.z and math.max(bounds.max.z, vp[3]) or vp[3]
end  

With the min and max points, we could extrapolate the remaining six corners of the bounding box rectangle. With all eight points, cutting the box into triangles for collisions was quite simple.

local vertices = {  
    cpml.vec3(max.x, max.y, min.z),
    cpml.vec3(max.x, min.y, min.z),
    cpml.vec3(max.x, min.y, max.z),
    cpml.vec3(min.x, min.y, max.z),
    cpml.vec3(min),
    cpml.vec3(max),
    cpml.vec3(min.x, max.y, min.z),
    cpml.vec3(min.x, max.y, max.z),
}

local triangles = {  
    vertices[1], vertices[2], vertices[3],
    vertices[2], vertices[4], vertices[3],
    vertices[1], vertices[5], vertices[2],
    vertices[2], vertices[5], vertices[4],
    vertices[6], vertices[3], vertices[4],
    vertices[1], vertices[3], vertices[6],
    vertices[1], vertices[7], vertices[5],
    vertices[6], vertices[7], vertices[1],
    vertices[6], vertices[4], vertices[8],
    vertices[6], vertices[8], vertices[7],
    vertices[5], vertices[8], vertices[4],
    vertices[5], vertices[7], vertices[8],
}

Bounding Boxes

With bounding boxes finally in place, introducing shooting and collision would be the next steps. Unfortunately, our networking model remains primitive and a bit fragile (more on this later) so we opted to add in very rough shooting and collision while we worked the other issues out. For shooting, we simply sent a packet to the server saying "I shot my cannon!" and the server would take your turret's current position and add reply to everyone online with a "shot point" at the muzzel break. The clients would then use this position and the turret's direction to draw a simple tragectory in 3D space. No tanks are blowing up yet, but the basics are in place.

Shooting

As for collision, again since our networking code is primitive we opted to ignore the bounding boxes for now and instead use a very basic circle-circle collision. Circle-circle collision is very straight forward: Get the distance between two points (in this case, the centre of the circles) and check to see if that distance is less than or equal to the combined radii of the circles.

function intersect.circle_circle(c1, c2)  
    return c1.point:dist(c2.point) <= c1.radius + c2.radius
end  

If this returns true, we have a collision. Initially we ran into a problem where if we collided our tanks would become stuck. To solve that, we checked the collision on the new tank positions before they were applied. If there was a collision, the tanks would repel each other. This allowed for players to back up or turn and drive in a different direction.

One quick fix we snuck into this build was how player nametags were displayed. In the video posted on Part 3, there are some spots where a player's name seems to come out of Timbuktu and float oddly on screen. This was a silly math error on our part and has since been fixed.

table.insert(nametags, {  
    text = object.name or "<unnamed>",
    position = position,
    viewable = camera.direction:dot(object.position - camera.position) > 0
})

Simply put, if a player is behind you, their nametag becomes hidden instead of floating in the middle of your screen.

Driving around and chasing people can be amusing for a few minutes, but without any real interactions with other players, the game feels a bit lonely. The obvious solution to this was to turn the game into an IRC client. Oh, it wasn't? Well too bad!

A project Colby and myself worked on a while ago required us to build a pure Lua IRC library. We haven't officially released it yet, but it works pretty well. It was pretty simple to hook that up with our tank game and now when you log on our game server, you also join the official #love IRC channel. We'll be setting up a dedicated channel for this game down the line but for now we're just going to keep bug slime. ;)

IRC

A better UI element will be created for the IRC chat in the future. Right now we're just printing the IRC traffic to the console.

Finally, we come to the amusing bit. To reiterate: we broke it. How, you may be asking? Simple: We added gamepad support! Our fragile network code used an input replay system to send data back and forth. What this means is when a player presses a button, it would send a command to the server saying "I pressed this button!" and both the client and the server would perform the appropriate action. If you pushed up or W, the tank would move forward and the server would send your forward motion to all other clients. Okay, cool, but how did adding gamepad support break this? Well, we're still not 100% certain. The main culprit seems to be that by using analog sticks to vary our velocity, it is causing some weird things to happen, which causes a crash. Replacing all of our movement flags with raw numbers and then using those numbers within our movement code just isn't working as simply as we had expected.

player.position = player.position + cpml.vec3(0, player.move * self.move_speed * dt, 0):rotated(player.orientation.z, cpml.vec3(0, 0, 1))  
player.orientation.z = player.orientation.z + math.rad(player.turn * self.turn_speed * dt)  
player.turret_orientation.z = player.turret_orientation.z + math.rad(player.turret * self.turret_speed * dt)  
player.direction = player.orientation:orientation_to_direction()  

The issue we keep getting is that player.orientation is not a vector object. Instead of tracking this down, we decided it was the right time to completely rewrite our networking system and build a more robust, reliable, and lighter system that will allow us to scale in the future.

So that's it! We had hoped to implement proper collision and shooting for this post but in the end we found that it was best to wait until we had a really nice networking setup before persuing detailed gameplay mechanics. Part 5 will focus on building a new client-server model from scratch, so stay tuned!

I would also like to take this opportunity to apologize to Blizzard Entertainment. Several years ago I recall reading an article that mentioned some changes Blizzard attempted to make to World of Warcraft. Apparently when Blizzard toyed with the idea of increasing the size of your permanent bag (16 slots), many aspects of the game completely fell apart. I was completely baffled by the notion that absolutely anything relied on the bag size being exactly 16, and chalked it up to poor design. I have a new respect for developers who build online games after completely breaking my own game by adding gamepad support. So Blizzard, if you are reading this, I am sorry for doubting your capabilities. <3