-
Notifications
You must be signed in to change notification settings - Fork 17
Arx 03 Classes
Currently we have only a single brick in our game, but we'll need more of them to construct a level.
Different bricks share most properties, such as update
and draw
functions, but differ in position.
We can create several bricks by making and maintaining an array of copies of the current brick
table.
However, the same problem -- maintaining several objects that share a lot of common properties but still have some differences -- can be accomplished much cleaner using classes.
For standardization and unification purposes, it is also convenient to define classes for the platform and the ball.
From a technical point, a class in Lua is a table, containing class functions (a.k.a. methods) and class variables.
Each instance (a.k.a. object) of the class is also a table, configured such that any missing field is looked at the class-table. This allows objects to have customized properties stored in their own table, while sharing common properties using the class table. In Lua, such arrangement is achieved by configuring so-called __index
metamethod in each instance of the class. This __index
metametod is either a function or another table that is used when an inquired field is missing in the quered table.
Here is an example:
> class_A = {}
> class_A.name = 'The Class'
> class_A.say_hello_method = function( z ) print( "hello from " .. z.name ) end
> class_A.name
The Class --(*1)
> class_A.say_hello_method( class_A )
hello from The class --(*2)
> object_of_class_A = {}
> object_of_class_A.name
nil --(*3)
> object_of_class_A.__index = class_A
> setmetatable( object_of_class_A, object_of_class_A )
> object_of_class_A.name
The Class --(*4)
> object_of_class_A.say_hello_method( object_of_class_A )
hello from The Class
> object_of_class_A.name = "An object"
> object_of_class_A.say_hello_method( object_of_class_A )
hello from An Object --(*5)
(*1): Field name
in the class_A
table is set to "The Class".
(*2): We call a function, stored in the field say_hello_method
of the class_A
table.
It expects a single argument z
. We pass class_A
itself as such argument.
(*3): We define a table object_of_class_A
. An attempt to index it's name
field returns nil
.
(*4): After __index
and setmetatable
magic, an attempt to index object_of_class_A.name
field doesn't return nil
any longer. However, at this point the field name
still isn't set at the object_of_class_A
, so the attempt to index it results in an invocation of the __index
method. In this case, it points to the class_A
, where it redirects the query for the name
. The value of this field is 'The Class', which is printed in the output.
(*5): We set field name
in the object_of_class_A
. Now it can be successfully indexed without any redirections. Still, the field say_hello_method
does not present in the object_of_class_A
and it's value is searched for in the class_A
(where __index
points to).
There is a syntactic trick in Lua, which simplifies writing and calling methods — the colon syntax. When a method is prefixed by the colon instead of the dot, a table before ':' is substituted as the first formal parameter of the method:
some_table:function_inside_table()
-- is equivalent to
some_table.function_inside_table( some_table )
That way it is possible to write:
> class_A:say_hello_method()
hello from The Class
> object_of_class_A:say_hello_method()
hello from An Object
It is also possible to define methods using colon syntax.
In that case, a first formal parameter with name self
is implied.
function some_table:function_inside_table( arg1, arg2, etc )
.....
end
-- is equivalent to
function some_table.function_inside_table( self, arg1, arg2, etc )
.....
end
In our case:
> function class_A:self_say_hello_method( hello_ending )
>> print( "self-hello from " .. self.name .. hello_ending )
>> end
> class_A:self_say_hello_method( "!!!" )
self-hello from The Class!!!
> object_of_class_A:self_say_hello_method( "???" )
self-hello from An Object???
The dot and the colon forms can be mixed freely:
> object_of_class_A.self_say_hello_method( object_of_class_A, "!!!" )
self-hello from An Object!!!
A common Lua idiom for class constructor looks like this:
function Class:new( o )
o = o or {}
setmetatable(o, self)
self.__index = self
o.name = o.name or "Class"
o.property_1 = o.property_1 or 10
o.property_2 = o.property_2 or 20
.....
return o
end
setmetatable(o, self)
sets Class as the metatable for an each new Class
object.
self.__index = self
sets __index
metamethod in the Class
to point to Class
itself.
To implement inheritance, it is necessary to configure __index
metamethod of the child class to point to the table representing the parent class. In this case it can be done simply by SubClass = Class:new()
. Then it is possible to proceed defining SubClass
methods. I won't need this, since I'm not going to use inheritance in this project.
One last thing: in Lua syntax some_function{}
is identical to some_function( {} )
.
Such shorthand is commonly found in calls to class constructors, so don't be surprised.
A common convention is to start class names from the capital letter. Let's convert the ball module to the Ball class using class constructor definition idiom:
local vector = require "vector" --(*1)
local love = love
local setmetatable = setmetatable
.....
function Ball:new( o )
o = o or {}
setmetatable(o, self)
self.__index = self
o.name = o.name or "ball"
o.radius = o.radius or 10 -- (*2)
o.position = o.position or vector( 300, 300 ) --(*3)
o.speed = o.speed or vector( 300, 300 )
return o
end
(*1): I'm loading HUMP vector module, which contains a class definition for a 2d-vector.
It is used at (*3) to set speed and position of the ball.
(*2): All the necessary ball properties are placed in the constructor.
(*3): Position and speed are now vector objects.
The update
and draw
functions now become methods, defined with the colon syntax:
function Ball:update( dt )
self.position = self.position + self.speed * dt
end
function Ball:draw()
local segments_in_circle = 16
love.graphics.circle( 'line',
self.position.x,
self.position.y,
self.radius,
segments_in_circle )
end
No other changes are needed for the Ball. A procedure to define the Brick and the Platform classes is mostly similar.
After the classes are defined, we need to call constructors of the classes in love.load()
to create game objects, that we are going to update and draw:
function love.load()
ball = Ball:new{}
platform = Platform:new{}
brick = Brick:new{}
end
In love.update
and love.draw
, the only change is basically the replacement of '.' by ':' .
function love.update( dt )
ball:update( dt )
platform:update( dt )
brick:update( dt )
end
function love.draw()
ball:draw()
platform:draw()
brick:draw()
end
While syntactically simple, it marks a shift in point of view from tables and functions in them to classes and methods.
Possible todo: Use hump.classes instead.
Feedback is crucial to improve the tutorial!
Let me know if you have any questions, critique, suggestions or just any other ideas.
Chapter 1: Prototype
- The Ball, The Brick, The Platform
- Game Objects as Lua Tables
- Bricks and Walls
- Detecting Collisions
- Resolving Collisions
- Levels
Appendix A: Storing Levels as Strings
Appendix B: Optimized Collision Detection (draft)
Chapter 2: General Code Structure
- Splitting Code into Several Files
- Loading Levels from Files
- Straightforward Gamestates
- Advanced Gamestates
- Basic Tiles
- Different Brick Types
- Basic Sound
- Game Over
Appendix C: Stricter Modules (draft)
Appendix D-1: Intro to Classes (draft)
Appendix D-2: Chapter 2 Using Classes.
Chapter 3 (deprecated): Details
- Improved Ball Rebounds
- Ball Launch From Platform (Two Objects Moving Together)
- Mouse Controls
- Spawning Bonuses
- Bonus Effects
- Glue Bonus
- Add New Ball Bonus
- Life and Next Level Bonuses
- Random Bonuses
- Menu Buttons
- Wall Tiles
- Side Panel
- Score
- Fonts
- More Sounds
- Final Screen
- Packaging
Appendix D: GUI Layouts
Appendix E: Love-release and Love.js
Beyond Programming: