My second LÖVE project is close to being completed. This time I am working on a remake of Mortal Pongbat, but with one major new feature: multiplayer peer-to-peer networking.
I am happy to say the code quality of Pongbat is quite improved over Paratrooper, in part due to more programming experience with LÖVE and Lua. In part due to the addition of several high quality LÖVE libraries.
Development on Pongbat started on April 16 (~10 days ago). My time was not fully devoted on Pongbat, as I also spent time working for my Australian client. My daughter was also having a school holiday during this period, so I also spent plenty of time with my daughter.
I am very happy with the progress I made in this short period of time. LÖVE feels extremely productive. LÖVE becomes especially productive once you filter out quality libraries to use in your game and once you establish your own best development practices. I’d like to discuss both topics in this post.
Pongbat Dependencies
Now I will discuss in short what libraries I use in Pongbat and for which purposes:
-
sock.lua: Sock.lua is used to simplify the networking code as it wraps around enet.
-
bitser: Bitser is an optional dependency for sock.lua and is used for serializing & deserializing data in a compact format.
-
hump: Hump provides some utilities that most games will find useful. I make use of:
- Gamestate for switching screens, e.g. from Menu screen to Game screen.
- Class for object oriented programming using classes.
- Signal for game-wide notifications using the observer pattern.
- Vector for positioning and moving objects across the screen.
- Timer to execute some code after a small delay.
This is ~85% of everything that Hump provides!
-
lume Lume adds a variety of functions especially useful for games. I’ve mainly used:
lume.smooth()
to smoothen some animation curves using cubic interpolation, which can sometimes feel more natural than linear interpolation.lume.ripairs()
to loop arrays in reverse, which is especially useful when entities need to be removed from an array after being destroyed, as this prevents a crash that would occur when looping forwards.lume.format()
to format strings in a way that’s easier and more natural than using string concatenation.
-
anim8: Pongbat makes use of the anim8 to simplify animations using sprite sheets.
-
moonshine: Moonshine adds several pixel shaders. Pongbat makes use of the
vignette
andcolorgradesimple
shaders in the “game finished” state. -
push: Push is used for resolution handling. Basically it makes the game run in 640 x 480 pixel resolution regardless of window size.
-
suit: Suit is used for immediate mode UI controls like buttons, labels and such. Currently suit is used in the Menu, Connect and Disconnect screens to draw buttons and labels.
Best Practices: The Good, The Bad & The Ugly
I am still improving myself as a Lua / LÖVE developer. My code quality has definitly improved compared to my previous project. However there are still areas in which my code quality could improve further. I will not spend time improving the code quality of Pongbat, since I feel this time would be spent better working on a new project. Instead, I will just try to write better code in my next project, based on lessons learned from my work on Pongbat.
The Good
The libraries mentioned previously simplified my codebase in a big way. I will keep using these libraries in future project as each library is of high quality. I do make sure to fork every library on Github, which hopefully guarantees me that I can rebuild Pongbat even if these libraries would disappear in the future.
I’ve added these libraries to Pongbat as git submodules. I know git submodules receive much have, but perhaps in private projects one should encounter few issues.
I’ve added a few utility classes that should be mostly reusable in future projects, though some additional changes might be needed. I’ve added classes to simplify networking and to generate animations.
With regards to classes, I did figure out a way to add private methods. A class with public and private methods I implement as such:
local state = Class {} -- hump.class is used here
-- private method
local function doSomethingPrivate(state, arg1)
-- body
end
-- constructor
function state:init(arg1, arg2, ...)
-- body
end
-- public method
function state:doSomethingPublic(arg1, arg2, ...)
doSomethingPrivate(self, arg1)
end
return state
With regards to assets, I’ve also had a great experience using Blender to create 3D models and turning these into sprites using Aseprite. I might document my approach in a future post. Sprites designed using this approach are used for the balls in game.
The Bad
My project contains a constants file which is basically a huge list of globals. While this single list makes it easy to modify many aspects of the game, like paddle speed, ball speed, beam duration, etc… it does make the code more intertwined. Globals are generally linked to entities. For example a part of my constants file currently looks as such:
...
-- paddle configuration
PADDLE_WIDTH = 15
PADDLE_HEIGHT = 150
PADDLE_SPEED = 180
PADDLE_ATTACK_DELAY = 5.0
...
-- beam configuration
BEAM_SIZE_MIN = 1
BEAM_SIZE_MAX = 6
BEAM_HEIGHT = 6
BEAM_WIDTH = VIRTUAL_WIDTH - 30
BEAM_FIRE_DURATION = 0.5
...
-- ball configuration
BALL_SPEED_MIN = 200
BALL_SPEED_MAX = 500
BALL_ANGLE = 75
BALL_WIDTH = 20
...
In the future I will move constants to their respective entity classes and as a result my code will cope better with change.
The Ugly
I didn’t find a good solution for private fields, at least not while using a object oriented development approach. Figuring out accessiblity of fields can become confusing as class fields are always public, even if not meant to be.
For my next project I will prefix all fields that are meant to be private with an underscore. I will enforce on myself to never directly change private fields outside a class, instead I will add public method to modify privately marked fields as needed.
Conclusion
Making games with LÖVE has been a very productive and fun experience for me. I’ve tried several other game development frameworks in the past, but never did I really manage to finish a project. With other frameworks I had to put in more effort to get similar results. Also, just being able to develop just using a “simple” text editor as compared to a full-blown IDE has been a breath of fresh air to me.
One of the game development frameworks I’ve used in the past was SpriteKit. While SpriteKit was fun as well, its main disadvantage compared to LÖVE is that one can only write games for Apple platforms. LÖVE allows one to create games that support macOS, Windows, Linux, iOS & Android.