A Simple OOP Game Tutorial #2
WELCOME TO THE 3RD INSTALLMENT OF ECS VS. OOP WEEKLY SERIES!
Before proceeding, if you have not read the previous post and previous part, please check those first, this post will wait for you. You will need the information and guide provided in the previous posts for this one.
Done? Good! You may now resume your quest!
UPDATES
Source code is now available here
DISCLAIMER
This post will not try to teach Lua coding or any programming logic. This is to focus primarily in OOP design and pattern.
If you want to learn basic programming and game development, let me know. I will make an in-depth guide about it for beginners if requested.
INTRODUCTION
So far, we implemented the basic classes, base and subclasses, that we need for the game. But, to properly see if all that we did is working, let us write a simple test to see if all the shapes are indeed working.
Write the following so that your main.lua
file will look like this:
local Shapes = {
Rectangle = require("classes.rectangle"),
Circle = require("classes.circle"),
Square = require("classes.square"),
}
local rectangle, square, circle
function love.load()
love.graphics.setBackgroundColor(1, 1, 1, 0.8)
rectangle = Shapes.Rectangle(128, 64)
square = Shapes.Square(128)
circle = Shapes.Circle(96)
end
function love.update(dt)
rectangle:update(dt)
square:update(dt)
circle:update(dt)
end
function love.draw()
rectangle:draw()
square:draw()
circle:draw()
end
function love.keypressed(key)
if key == "r" then
love.event.quit("restart")
end
end
With that code, you should see three shapes drawn on screen and they are moving. Press r
to restart the game and you should see that they spawn in random place with random color. Neat!
The above is just for testing the progress so far. You may now safely clean the file again as we are going to change a lot later on.
Implementing Clicking
It would not be a game if there is no interaction, right? So let us implement mouseclick
. In the previous post, we already did the logic for checking for mousepressed
event, now let us do the highlighting part.
Why not implement the method in the base class (shape)?
That is possible and without question an okay thing to do, but my reasoning is that, the circle
shape will not inherit that since the algorithm for checking point-to-box is a little bit different than point-to-circle. So instead of the circle
shape modifying the method of the base class, for me that is a waste, implement it directly in the circle
shape. The rectangle
and the square
classes would share the same method though.
The algorithm for the highlighting is:
- Check if the property
is_clicked
istrue
. - If
true
, draw another shape behind the original shape with the following property:- bigger size
- offset the position
- pure black color
This is a quick-and-dirty method to implement a somewhat outline look.
First of all, add this property to shape.lua
:
--inside the Shape:new() method
self.outline_color = {0, 0, 0, 1} --black color
self.offset = 4
For the rectangle.lua
:
function Rectangle:draw()
if self.is_clicked then
love.graphics.setColor(self.outline_color)
love.graphics.rectangle("fill",
self.x - self.offset,
self.y - self.offset,
self.width + self.offset * 2,
self.height + self.offset * 2)
end
love.graphics.setColor(self.color)
love.graphics.rectangle("fill", self.x, self.y, self.width, self.height)
end
For the circle.lua
:
function circle:draw()
if self.is_clicked then
love.graphics.setcolor(self.outline_color)
love.graphics.circle("fill",
self.x, self.y,
self.radius + self.offset,
self.radius)
end
love.graphics.setcolor(self.color)
love.graphics.circle("fill", self.x, self.y, self.radius, self.radius)
end
To test if it is working, your main.lua
(testing) should look like this:
local Shapes = {
Rectangle = require("classes.rectangle"),
Circle = require("classes.circle"),
Square = require("classes.square"),
}
local rectangle, square, circle
function love.load()
love.graphics.setBackgroundColor(1, 1, 1, 0.8)
rectangle = Shapes.Rectangle(128, 64)
square = Shapes.Square(128)
circle = Shapes.Circle(96)
end
function love.update(dt)
--stop moving so we can click on them
-- rectangle:update(dt)
-- square:update(dt)
-- circle:update(dt)
end
function love.draw()
rectangle:draw()
square:draw()
circle:draw()
end
function love.mousepressed(mx, my, mb)
rectangle:mousepressed(mx, my, mb)
square:mousepressed(mx, my, mb)
circle:mousepressed(mx, my, mb)
end
function love.keypressed(key)
if key == "r" then
love.event.quit("restart")
end
end
Click inside the shapes, they should be highlighted/outlined.
Awesome?!
Implementing Unclicking
So how do we un-outline the same when we click on them again (unclicking mechanics)?
Very simple! Go to the rectangle
and circle
classes and modify:
--mousepressed() method
if is_clicked then
self.is_clicked = not self.is_clicked --this
end
Basically it just flips the property when we clicked on it.
Implementing Spawner
Now, we will go to the interesting part. It’s time to create another class called spawner
.
But what does the spawner
class do? Basically, it just runs an internal timer, every time we hit the max timer, we reset it to zero, and then spawn a shape. That’s it!
So head over to the classes/
directory and create a file there named spawner.lua
and code the following:
local class = require("modules.classic")
local Spawner = class:extend()
function Spawner:new(spawn_time, spawn)
self.spawn_time = spawn_time
self.spawn = spawn
self.timer = 0
end
function Spawner:update(dt)
self.timer = self.timer + dt
if self.timer >= self.spawn_time then
self.timer = 0
self:spawn()
end
end
return Spawner
Basically it is just an interface where we can put callbacks (spawn method) for each of the shape classes.
Here’s how we are going to use it:
local Shapes = {
Rectangle = require("classes.rectangle"),
Square = require("classes.square"),
Circle = require("classes.circle"),
}
local Spawner = require("classes.spawner")
local rect = Shapes.Rectangle(64, 64)
local rectangle_spawner = Spawner(3, function()
rect = Shapes.Rectangle(64, 64)
end)
local square = Shapes.Square(48)
local square_spawner = Spawner(3, function()
square = Shapes.Square(48)
end)
local circle = Shapes.Circle(32)
local circle_spawner = Spawner(3, function()
circle = Shapes.Circle(32)
end)
function love.load()
love.graphics.setBackgroundColor(1, 1, 1, 0.8)
end
function love.update(dt)
rectangle_spawner:update(dt)
square_spawner:update(dt)
circle_spawner:update(dt)
end
function love.draw()
rect:draw()
square:draw()
circle:draw()
end
Now when you run this game, wait for every 3
seconds, and then the shape instances should move to another place randomly!
Implementing Containers
Ofcourse, we do not want to only have 3
instances, we also do not want to create a lot of named variables and manually add them to the needed methods. To make everything dynamic, we will implement containers for each shape using Lua tables.
Here’s the main.lua
file now:
local Shapes = {
Rectangle = require("classes.rectangle"),
Square = require("classes.square"),
Circle = require("classes.circle"),
}
local shapes = {}
local Spawner = require("classes.spawner")
local rectangle_spawner = Spawner(3, function()
local rectangle = Shapes.Rectangle(64, 64)
table.insert(shapes, rectangle)
end)
local square_spawner = Spawner(3, function()
local square = Shapes.Square(48)
table.insert(shapes, square)
end)
local circle_spawner = Spawner(3, function()
local circle = Shapes.Circle(32)
table.insert(shapes, circle)
end)
function love.load()
love.graphics.setBackgroundColor(1, 1, 1, 0.8)
end
function love.update(dt)
rectangle_spawner:update(dt)
square_spawner:update(dt)
circle_spawner:update(dt)
end
function love.draw()
for i, obj in ipairs(shapes) do
obj:draw()
end
end
As you can see, every 3 seconds, a rectangle, square, and circle shape instance will appear. But the problem is, there is no way right now to limit or have constraints to when should an instance be inserted to the container. So if you run the game for a very long time, it should eat a lot of memory of your machine and thus at a certain time will hang your machine.
Can we implement containers for spawner instances?
Sure! That will save a lot of lines and give us more flexibility.
Here’s how it should look now:
local Shapes = {
Rectangle = require("classes.rectangle"),
Square = require("classes.square"),
Circle = require("classes.circle"),
}
local Spawner = require("classes.spawner")
local spawners = {}
local shapes = {}
local rectangle_spawner = Spawner(3, function()
local rectangle = Shapes.Rectangle(64, 64)
table.insert(shapes, rectangle)
end)
local square_spawner = Spawner(3, function()
local square = Shapes.Square(48)
table.insert(shapes, square)
end)
local circle_spawner = Spawner(3, function()
local circle = Shapes.Circle(32)
table.insert(shapes, circle)
end)
table.insert(spawners, rectangle_spawner)
table.insert(spawners, square_spawner)
table.insert(spawners, circle_spawner)
function love.load()
love.graphics.setBackgroundColor(1, 1, 1, 0.8)
end
function love.update(dt)
for i, spawner in ipairs(spawners) do
spawner:update(dt)
end
end
function love.draw()
for i, obj in ipairs(shapes) do
obj:draw()
end
end
function love.mousepressed(mx, my, mb)
for i, obj in ipairs(shapes) do
obj:mousepressed(mx, my, mb)
end
end
function love.keypressed(key)
if key == "r" then
love.event.quit("restart")
end
end
For now it does not look clean, we will fix that soon when we have our gameplay logic implemented by using states.
Next Week’s Post:
For the next post, we will continue to:
- implement destruction of shapes (scoring system)
- implement the UI classes and subclasses
- implement state classes and gameplay logic
So for next week we will do that as well.
Stay tuned via RSS or follow me on Twitter