Trouble understanding classes

Questions about the LÖVE API, installing LÖVE and other support related questions go here.
Forum rules
Before you make a thread asking for help, read this.
User avatar
AlexCalv
Prole
Posts: 49
Joined: Fri Aug 08, 2014 7:12 pm
Contact:

Trouble understanding classes

Post by AlexCalv »

Can someone explain how to create and use classes to me as basic as they can? I'm having trouble understanding them and I'd much rather use classes to make menu buttons or at least some other method as opposed to the way I do now. I've been using tables to create buttons and I was looking at some other projects and it seemed like making them classes was much more manageable. I just don't know how to.
Snake174rus
Prole
Posts: 32
Joined: Thu Aug 28, 2014 8:38 am
Location: Russia
Contact:

Re: Trouble understanding classes

Post by Snake174rus »

I'm use HUMP classes
May see here
here
or here
And more and more ...
User avatar
kraftman
Party member
Posts: 277
Joined: Sat May 14, 2011 10:18 am

Re: Trouble understanding classes

Post by kraftman »

If you have access to pluralsight there's a nice intro to classes/inheritance and metatables called "Object Oriented Code Organization" here: http://www.pluralsight.com/courses/beginning-lua

Otherwise, it's worth reading up a bit on object oriented coding and then metatables: http://www.lua.org/pil/13.html then http://lua-users.org/wiki/SimpleLuaClasses

Classes in lua are just tables, but when they look up a method and can't find it, they look in another table instead using metatables.

Lets say I make a table called player with variables for x, y and draw a cube to represent a player:

Code: Select all

local player = {}

player.x = 200
player.y = 200

function player.GetX()
	return player.x
end

function player.GetY()
	return player.y
end

function love.draw()
	love.graphics.rectangle("fill",player.GetX(),player.GetY(),20,20 )
end
This works fine for one player, but what if I want to add another player? I'll need to add player2.GetX() and player2.GetY(), which does exactly the same thing but for a new player. As we add more players, and properties to those players, we end up adding even more code duplication, which adds more places that our code can go wrong, and makes things messy.

It would be much nicer if we could define GetX() and GetY once, and have anything that acts like a player use this new generic GetX() method.

We do this by telling the our player table that if it can't find something it's trying to access in its own table, then it should look in another table instead. Setmetatable tells it where to look, and __index is a metamethod that describes the action being performed on the table:

Code: Select all

local GenericPlayer = {}
GenericPlayer.__index = GenericPlayer

function GenericPlayer.GetX(self)
	return self.x
end

function GenericPlayer.GetY(self)
	return self.y
end

function GenericPlayer.New(x,y)
	local t = {}
	t.x = x
	t.y = y
	setmetatable(t,GenericPlayer)

	return t
end

function love.load()
	player1 = GenericPlayer.New(100, 130)
	player2 = GenericPlayer.New(200,130)
end

function love.draw()
	love.graphics.rectangle("fill",player1.GetX(player1),player1.GetY(player1),20,20 )
	love.graphics.rectangle("fill",player2.GetX(player2),player2.GetY(player2),20,20 )
end
So player1.GetX(player1) looks for a GetX() method in player1, which it doesn't find, so it looks in GenericPlayer instead and finds it there. We also pass in player1 so that the GetX() method knows which table to access the x variable of, note that we dont want GenericPlayer to looks for GenericPlayer in itself because GenericPlayer has no x variable.

Since passing the table itself into itself is done very often, they added a nice syntax to make it look cleaner. Instead of

Code: Select all

function GenericPlayer.GetY(self)
	return self.y
end
player1.GetX(player1)
We can do

Code: Select all

function GenericPlayer:GetY()
	return self.y
end

player1:GetX()
By using the colon during the function definition and call, the table itself is passed in as a hidden variable called self.
So now if we want to add a new method to all of our new players, we can do it just once, on the GenericPlayer table, and it will be there for any player table to access.

Below I've neatened up the functions to use the colon syntax, neatened up the setmetatable creation, and added SetX(x) and SetY(y) methods.

Code: Select all


local GenericPlayer = {}
GenericPlayer.__index = GenericPlayer

function GenericPlayer:GetX()
	return self.x
end

function GenericPlayer:GetY()
	return self.y
end

function GenericPlayer:SetX(x)
	self.x = x
end

function GenericPlayer:SetY(y)
	self.y = y
end

function GenericPlayer.New(x,y)
	local t = setmetatable({}, GenericPlayer)
	t:SetX(x)
	t:SetY(y)
	return t
end

function love.load()
	player1 = GenericPlayer.New(100, 130)
	player2 = GenericPlayer.New(200,130)
end

function love.draw()
	love.graphics.rectangle("fill",player1:GetX(),player1:GetY(),20,20 )
	love.graphics.rectangle("fill",player2:GetX(),player2:GetY(),20,20 )
end


User avatar
rmcode
Party member
Posts: 454
Joined: Tue Jul 15, 2014 12:04 pm
Location: Germany
Contact:

Re: Trouble understanding classes

Post by rmcode »

In my opinion closure based classes are the simplest ones you can create. I use them in all of my projects / games and never had a problem with them.

Example class:

Code: Select all

-- ------------------------------------------------
-- Module (CLASS)
-- ------------------------------------------------

local Foo = {};

-- ------------------------------------------------
-- Constructor (CREATES OBJECTS)
-- ------------------------------------------------

function Foo.new()
    local self = {};

    self.health = 100; -- public variable
    local dead = false; -- local variable

    local function secret()
        -- invisible on the outside
    end

    function self:update(dt)
        -- call local function.
        secret();
        -- do stuff
    end

    function self:draw()
    end

    return self;
end

-- ------------------------------------------------
-- Return Module
-- ------------------------------------------------

return Foo;
Usage:

Code: Select all

local Foo = require('Foo'); -- require Foo module

local npc = Foo.new() -- create new object from foo class

function love.update(dt)
    npc:update(dt);
end

function love.draw()
    npc:draw();
end
It's just a quick and dirty example because I have to leave in a few minutes, but if you have further questions just ask. The links kraftman gave above are also worth a read. In the end it just comes down to personal preference and what you find easier to work with :)
User avatar
AlexCalv
Prole
Posts: 49
Joined: Fri Aug 08, 2014 7:12 pm
Contact:

Re: Trouble understanding classes

Post by AlexCalv »

kraftman wrote:stuff
Using this method, how would I create independent movement for each player? This is throwing me an error

Code: Select all

function player:Update(dt)
	if love.keyboard.isDown("d") then
		player1:setX() = player1:setX() + 10
	end
end
I'm assuming it's because I'm calling the functions, but surely there's a way to call them individually?

Sorry for the late response, got super busy with life stuff.

Edit:
Wow I'm an idiot. I fixed it within 2 minutes of posting lol. My thinking skills need to improve before posting, rather than after lol. I just did this:

Code: Select all

player1.x = player1.x + 10
Also, to
Snake174rus wrote:I'm use HUMP classes
May see here
here
or here
And more and more ...
I prefer to see how things are created and used by scratch, rather than using libraries. After I know how it works I start to use the libraries. Reading the wiki only gets me so far as well, I need good examples before I start understanding things well.
User avatar
rmcode
Party member
Posts: 454
Joined: Tue Jul 15, 2014 12:04 pm
Location: Germany
Contact:

Re: Trouble understanding classes

Post by rmcode »

AlexCalv wrote:

Code: Select all

function player:Update(dt)
	if love.keyboard.isDown("d") then
		player1:setX() = player1:setX() + 10
	end
end
That's not really OO or a class - at least from what I can tell from that code snippet. Where does the player1 table come from?

What you (probably) want is this:

Code: Select all

function player:Update(dt)
        if love.keyboard.isDown("d") then
            self:setX(self:getX() + 10);
        end
end
A setter usually needs an argument. You can't do self:setFoo() = baz. Also the : is a syntactic sugar for player.update(player, dt). If you use : the table itself will be passed to the function as an argument which you then can access via "self".

Again, it is hard to tell what you are doing here without seeing the complete code :)
User avatar
AlexCalv
Prole
Posts: 49
Joined: Fri Aug 08, 2014 7:12 pm
Contact:

Re: Trouble understanding classes

Post by AlexCalv »

This is the complete code

Code: Select all



player = {}
player.__index = player

function player:getX()
	return self.x
end

function player:getY()
	return self.y
end

function player:setX(x)
	self.x = x
end

function player:setY(y)
	self.y = y
end

function player.New(x, y)
	local p = setmetatable({}, player)
	p:setX(x)
	p:setY(y)
	return p
end

function player:Load()
	player1 = player.New(0, 25)
	player2 = player.New(0, 100)
end

function player:Update(dt)
--Player 1
	if love.keyboard.isDown("d") then
		player1.x = player1.x + 100 * dt
	end
	if love.keyboard.isDown("a") then
		player1.x = player1.x - 100 * dt
	end

--Player 2
	if love.keyboard.isDown("right") then
		player2.x = player2.x + 100 * dt
	end
	if love.keyboard.isDown("left") then
		player2.x = player2.x - 100 * dt
	end
end

function player:Draw()
	love.graphics.setColor(255, 0, 0)
	love.graphics.rectangle("fill", player1:getX(), player1:getY(), 20, 20)
	love.graphics.setColor(0, 0, 255)
	love.graphics.rectangle("fill", player2:getX(), player2:getY(), 20, 20)
end
Also, how would I, I guess, delete or add these tables constantly? Like if I were to do bullets, or drop-in and drop-out with players. Would I just use tables without classes like using ipairs for that?
User avatar
ivan
Party member
Posts: 1911
Joined: Fri Mar 07, 2008 1:39 pm
Contact:

Re: Trouble understanding classes

Post by ivan »

When it comes to OO I prefer to use a slightly lower-level approach instead of libraries:

Code: Select all

Object = {}
ObjectMT = { __index = Object }

function Object:create(x, y, mt)
  local self = {}
  setmetatable(self, mt or ObjectMT)
  self.x = x
  self.y = y
  return self
end

function Object:destroy(self)
  self.x = nil
  self.y = nil
end
Note that the last and optional parameter "mt" in the constructor is a metatable.

This allows us to implement inheritance:

Code: Select all

Container = {}
ContainerMT = { __index = Container }

setmetatable(Container, { __index = Object })

function Container:create(image, x, y, mt)
  local self = Object:create(x, y, mt or ContainerMT)
  self.image = image
  return self
end

function Container:destroy()
  self.image = nil
  Object.destroy(self)
end
For this approach to work we pass the "container" metatable to the "object" constructor.
Also note that we call the "object" destructor explicitly from "container": "Object.destroy(self)"

Lastly, I would advise against code like:

Code: Select all

function player:update(dt)
  if love.keyboard:isDown("k") then
    ....
  end
end
I would just add "player:set_movement(direction)" and call it from outside.
The reason is that it messes up the encapsulation, by making your object dependent on arbitrary external functions.
For certain things this is fine.
For example, if your class is "CollidiableObject" then you may expect it to depend on an external collision library.
But in this case, I wouldn't expect a "player" class to be dependent or "coupled" to "keyboard".
In theory, you should be able to add joystick/touchscreen support without changing the "player" class.
The theory is that you should be able run a simulation of your game without any graphics, input or audio libraries at all. Unfortunately in practice this is rarely possible for most games...
Just my opinion, I'm sure some people would disagree. :)
User avatar
zorg
Party member
Posts: 3441
Joined: Thu Dec 13, 2012 2:55 pm
Location: Absurdistan, Hungary
Contact:

Re: Trouble understanding classes

Post by zorg »

ivan wrote:The theory is that you should be able run a simulation of your game without any graphics, input or audio libraries at all. Unfortunately in practice this is rarely possible for most games...
Just my opinion, I'm sure some people would disagree. :)
I would agree though :3

But encapsulation depends highly on the coding style (as in OOP or ECS or a mix of them, or something else) you choose to do.

Nevertheless, doing classes closure style would be my advice and choice as well... at least in the beginning.
Me and my stuff :3True Neutral Aspirant. Why, yes, i do indeed enjoy sarcastically correcting others when they make the most blatant of spelling mistakes. No bullying or trolling the innocent tho.
User avatar
AlexCalv
Prole
Posts: 49
Joined: Fri Aug 08, 2014 7:12 pm
Contact:

Re: Trouble understanding classes

Post by AlexCalv »

ivan wrote:When it comes to OO I prefer to use a slightly lower-level approach instead of libraries:

Code: Select all

Object = {}
ObjectMT = { __index = Object }

function Object:create(x, y, mt)
  local self = {}
  setmetatable(self, mt or ObjectMT)
  self.x = x
  self.y = y
  return self
end

function Object:destroy(self)
  self.x = nil
  self.y = nil
end
Note that the last and optional parameter "mt" in the constructor is a metatable.

This allows us to implement inheritance:

Code: Select all

Container = {}
ContainerMT = { __index = Container }

setmetatable(Container, { __index = Object })

function Container:create(image, x, y, mt)
  local self = Object:create(x, y, mt or ContainerMT)
  self.image = image
  return self
end

function Container:destroy()
  self.image = nil
  Object.destroy(self)
end
For this approach to work we pass the "container" metatable to the "object" constructor.
Also note that we call the "object" destructor explicitly from "container": "Object.destroy(self)"

Lastly, I would advise against code like:

Code: Select all

function player:update(dt)
  if love.keyboard:isDown("k") then
    ....
  end
end
I would just add "player:set_movement(direction)" and call it from outside.
The reason is that it messes up the encapsulation, by making your object dependent on arbitrary external functions.
For certain things this is fine.
For example, if your class is "CollidiableObject" then you may expect it to depend on an external collision library.
But in this case, I wouldn't expect a "player" class to be dependent or "coupled" to "keyboard".
In theory, you should be able to add joystick/touchscreen support without changing the "player" class.
The theory is that you should be able run a simulation of your game without any graphics, input or audio libraries at all. Unfortunately in practice this is rarely possible for most games...
Just my opinion, I'm sure some people would disagree. :)
I took a short break from this and what partial understanding I had, has faded away. So for the method you posted, do I have to type both code boxes you posted? And how do I actually start to interact with these classes. Like, how do I move them, or even draw them?
Post Reply

Who is online

Users browsing this forum: Google [Bot] and 34 guests