It's been a while. (Bought a house!) At long last, I'm finally digging back in, and I'd like to share a recent milestone: I made another level! That's not all that exciting in itself, but nonetheless it feels like a big step forward…
Lanes, Levels!
I just put a rudimentary system in place that allows me to both flexibly configure and then play through a series of lanes! Up to this point, I'd essentially just hard-coded a single lane right into my setup function and rolled with it. In fact, I built the ability to track the score over a full 10-frame game, but it essentially just re-ran setup
, restarting the game each frame in every way but score tracking. I've been playing the same lane again and again…and again.
This is a huge accelerator since at last I can begin designing and testing levels. In turn, this will give me a sense of what it will feel like to actually play the game, rather than just a sense of the core input mechanics and collision physics I've been focused on so far.
Pondering an Approach
I spent quite a while thinking about the best way to approach level configuration. I have no experience building games of any real complexity, so I came to this problem green.
The vision for the game includes a series of 10-lane alleys (levels), each with a unique theme and obstacles. Many of those lanes/levels will still operate in a manner generally consistent with traditional bowling, at least with respect to a goal of smashing the ball into a standard rack of 10 pins at the end. However, some I envision to have unique pin arrangements, or boundary conditions like a timer that could run out, or other more eccentric features.
My desire for flexibility made it difficult to know how much of the logic to build into each level itself, and how much to leave to the game engine. I briefly considered a fully declarative level configuration format, but quickly decided it wouldn't afford enough flexibility. At the opposite end of the spectrum, I considered deferring entirely to the level to control everything; however, that could lead to a lot of code duplication, seemed more error prone, and overall didn't feel like a solid framework for building levels quickly.
Ultimately, I decided to keep the core "bowling" logic (pin rack, pinfall callbacks, scoring, etc.) in the engine, but use some OOP techniques to bundle up common lane logic to make them easier to build. Each level manifests as a subclass of the Lane
class. This base class sets a bunch of defaults and does some basic setup required for any lanes using a standard 10-pin rack. At the same time, it allows pretty much anything to be overridden, including the position of the pins and terminating conditions for the lane.
The Lane Config
Here's what the lane pictured above looks like in code:
import "Lane"
-- a table of properties provides declarative configuration
class('CurveLeft', {
backgroundImage = "alleys/groovy-gutters/images/CurveLeft",
defaultBallPosition = { x = 15, y = 170 },
defaultAim = Facing.RIGHT,
rackPosition = { x = 320, y = 20 },
rackAngle = Facing.DOWN,
}).extends(Lane)
-- a bare bones init
function CurveLeft:init()
CurveLeft.super.init(self)
end
-- the engine provides the ball and pin rack to the level
function CurveLeft:load(ball, rack)
-- the superclass positions the ball and pin rack for us
CurveLeft.super.load(self, ball, rack)
-- custom gutters and obstacles
Gutter({
GutterSegment(35, 135, 200, 135, false),
GutterArcSegment(200, 50, 85, 180, 90, false, false),
GutterSegment(285, 50, 285, 0, false),
}, rack)
Gutter({
GutterSegment(35, 205, 200, 205, false),
GutterArcSegment(200, 50, 155, 180, 90, false, false),
GutterSegment(355, 50, 355, 0, false),
}, rack)
end
-- return a reference to the lane class itself
return CurveLeft
It's fairly compact. I love that the second argument in the class
definition accepts a table of key/value pairs to initialize class properties. (I initialize the defaults for these in the Lane
base class in the same manner, rather than in init
, so they can be overridden here.) This gives me the declarative feel I was after for the bits it makes sense for. There are a bunch of other properties defined in the superclass which may optionally be specified here as well, to override win conditions, set level timers, configure out-of-bounds tolerances, adjust aiming, etc.
init
itself is uninteresting; the load
function is where the custom level logic lives. Note that most lanes, including this example, also call load
on super
. I haven't used this technique in other Lua classes before (and the need to pass self
as the first arg tripped me up at first), but this approach lets all standard 10-pin levels repurpose the same logic to set up the angle and starting location for the ball and pin rack. The base class sets them up according to the properties declared for the class above.
Next, the lane can set up any sort of obstacles it wants — gutters, bumpers, walls, etc. The setup could be a static config as in this example; or it could be randomized; create timers and animators; prompt for input; anything! (Side note: I might switch those gutter init
functions to use table calling syntax so the arguments appear with named keys. It's easy to lose track of which number is which.)
There are also some lifecycle functions (not shown here) that the lane can implement to hook into events such as when the ball gets thrown, including an update
function to run logic each frame.
I'll call attention to one other unusual thing, right at the end of the file:
return CurveLeft
This is outside of function scope, and instead gets returned directly when this file is imported. I'll come back to why I do this shortly, which brings me to the next topic:
Sequencing Lanes
Players will play through collections of lanes in sequence. I wanted a way to easily order them, and also associate some other metadata with each alley — name, level imagery, etc. Here's what I came up with:
local GROOVY_GUTTERS = {
name = "Groovy Gutters",
coverImage = "alleys/groovy-gutters/images/cover",
bannerImage = "alleys/groovy-gutters/images/banner",
scorecardImage = "alleys/groovy-gutters/images/scorecard",
lanes = {
import "groovy-gutters/lanes/S_Curve",
import "groovy-gutters/lanes/CurveLeft",
import "groovy-gutters/lanes/U_Turn",
-- ...
}
}
local THE_BANKSHOT = {
name = "The Bankshot",
coverImage = "alleys/bankshot/images/cover",
bannerImage = "alleys/bankshot/images/banner",
scorecardImage = "alleys/bankshot/images/scorecard",
lanes = {
-- ...
}
}
-- ...
ALLEYS = {
GROOVY_GUTTERS,
THE_BANKSHOT,
-- ...
}
This gives me a nice declarative way to both provide level metadata and sequence lanes. I'm intentionally using descriptive names for each lane so they are easier to identify, and so that I can trivially shuffle their order as I build and test them. First I define each alley in its own block, and then those alleys themselves are sequenced at the bottom of the file to determine eventual level ordering.
A notable twist here is my use of the import
calls for each lane right within the declaration of each alley. This avoids the need to reference each lane twice: once at the top of the file for import, and again to specify the name of class within each of those files when declaring their sequence. Returning the class directly at the end of each lane's file enables this pattern. Note that I'm storing a reference to the lane class itself in these lists; not instances of them. They don't get instantiated until just before they get loaded, which I can do without even needing to know the name of the class for each lane, e.g.
currentLane = lanes[currentLaneIndex]() -- instantiate
currentLane:load(ball, rack) -- load
There's still some rough edges to iron out, but the critical ability to start building level content is a game changer, and I'm excited for what comes next!