I've been using this tutorial as my principal information for programming, since there's no picotron specific resource.
I'm experimenting with table-based classes, according to the guide I'm supposed to be able to create a base blueprint of an object and then instantiate it, but when I do so following the example, the object is not copied but instead it becomes a reference, because every change gets applied to the first object.
I made a sample project, first I try the guide's way, then I try it in a way I know works
enemies = {} enemies2 = {} enemy = { type = 0, sp = 1, x = 0, y = 0, dx = 0, dy = 0, update=function(self) self.x += self.dx self.y += self.dy end, draw=function(self) spr(self.sp, self.x, self.y) end } goblin = enemy --copy enemy class goblin.sp = 2 goblin.type = 3 goblin.x = 6 goblin.y = 10 ogre = enemy --copy enemy class ogre.sp = 3 ogre.type = 4 ogre.x = 40 ogre.y = 50 function _init() add(enemies, enemy) add(enemies, goblin) add(enemies, ogre) add_enemy(16,16,4) add_enemy(32,32,5) add_enemy(64,64,6) end function add_enemy(new_x,new_y,sprIndex) add(enemies2, { type = 0, sp = sprIndex, x = new_x, y = new_y, dx = 0, dy = 0, update=function(self) self.x += self.dx self.y += self.dy end, draw=function(self) spr(self.sp, self.x, self.y) end }) end function _draw() for e in all(enemies) do e:draw() end for en in all(enemies2) do en:draw() end end |
If I define the object in the add function then each object acts as independent object, is this how tables are supposed to function?
goblin = enemy isn't copying, it's just creating a reference
Have a look at metatables. With this approach goblin and devil will inherit base properties and functions from enemy, but you can override properties and functions (maybe devil has a special update function)
enemy = {} enemy.__index = enemy function enemy:new(name) local o = setmetatable({}, enemy) o.name = name -- other base properties can go here return o end function enemy:update() -- do your updates here self.x += 1 self.y += 1 end function enemy:draw() -- do your draw here print(self.c, self.x, self.y) end function _init() goblin = enemy:new("goblin") goblin.c = "G" goblin.x = rnd(480) goblin.y = rnd(270) devil = enemy:new("devil") devil.c = "D" devil.x = rnd(480) devil.y = rnd(270) end function _update() goblin:update() devil:update() end function _draw() cls(0) goblin:draw() devil:draw() end |
I could have set x = rnd(480) and y = rnd(270) within the enemy:new function.
Thank you, that was really helpful!
It's interesting that these objects know of "self" without having it as a parameter in the function.
It looks like that I might still require a factory if I want to create different enemies, but with this it's just a matter of instantiating the base enemy and changing the properties and it's good to know that = creates object references instead of copies, that's going to be useful.
Is there a way to have an inherited class with this system?
In case I want goblin itself be a blueprint with its own :new method I can use to quickly create one, but also take the defaults from enemy
The obvious way I see would be a create_goblin() function that instantiates enemy and sets the properties or the overridden functions, but I have the feeling there might be a more proper way to do it
I'm literally working on something like that right now.
I have a character class
I create a character than inherits from that class
I create a player that inherits from character
My thinking about this is to keep characters separate to players. Players are a character but only inherit some parts of a character, the rest is for the game.
Character = {} Character.__index = Character function Character:new(name) local instance = setmetatable({}, Character) instance.name = name instance.effects = {} instance.attributes = { x = 0, y = 0, sp_scale = 1, w = 16, h = 16, original_invincibility_frames = 10, invincibility_frames = 0, max_health = 100, health = 20, recovery = 0.0, armor = 0, move_speed = 1, might = 1, weapon_projectile_speed = 0, weapon_duration = 0, weapon_area = 0, weapon_cooldown = 0, weapon_projectiles_amount = 0, revival = 0, magnet = 0, luck = 0, growth = 0, greed = 0, curse = 0, reroll = 0, skip = 0, banish = 0 } return instance end -- Create characters antonio = Character:new("antonio") antonio.attributes.recovery = 0.5 antonio.effects = { { description="Gains 10% Might every 2 Levels", stat="might", value=1, level_increment=2, max_level_increment=10, mode="percent"}, { description="Gains 0.5 Recovery every 1 level", stat="recovery", value=0.5, level_increment=1, max_level_increment=5} } characters = {antonio = Antonio} -- Define the player class Player = setmetatable({}, {__index = Character}) function Player:new(character) local instance = setmetatable({ name = character.name, x = 0, y = 0, xp = 0, level = 1, inventory = {}, attributes = {}, -- Initializing attributes here to ensure it's not nil effects = {} }, {__index = Player}) -- Ensure that Player methods are correctly inherited -- Copy attributes for key, value in pairs(character.attributes) do instance.attributes[key] = value end for i, effect in pairs(character.effects) do instance.effects[i] = deepcopy(effect) -- Assuming a deep copy function is available end return instance end function Player:update_effects_target(target) for _, effect in pairs(self.effects) do effect.target = target end end function Player:apply_effects(level) local effects = self.effects for _, effect in pairs(effects) do if level % effect.level_increment == 0 and level <= effect.max_level_increment then local target = effect.target.attributes if effect.mode == "percent" then target[effect.stat] += (target[effect.stat] or 0) * effect.value else target[effect.stat] = (target[effect.stat] or 0) + effect.value end end end end function Player:update() if btn(0) then self.x -= 1 end if btn(1) then self.x += 1 end if btn(2) then self.y -= 1 end if btn(3) then self.y += 1 end end function Player:draw() print("X", self.x, self.y) end -- Initialize game with a player based on Antonio function _init() t = 0 run = {} run.player = {} local char = characters["antonio"] new_player = Player:new(char) new_player:update_effects_target(new_player) add(run.player, new_player) player = run.player[1] run.level = 1 end function _update() if t % 60 == 0 then player.attributes.health += player.attributes.recovery end player:update() if btnp(4) then run.level += 1 player:apply_effects(run.level) end t += 1 end function _draw() local y = 0 cls(0) print("C", player.x, player.y) for k, v in pairs(player.attributes) do if type(v) != "table" then print(k..":"..v, 0, y) y += 8 end end local y = 0 print("Level: "..run.level, 240, y) end function deepcopy(orig) local orig_type = type(orig) local copy if orig_type == 'table' then copy = {} for orig_key, orig_value in next, orig, nil do copy[deepcopy(orig_key)] = deepcopy(orig_value) end setmetatable(copy, getmetatable(orig)) else -- for numbers, strings, booleans, etc copy = orig end return copy end |
Thank you but this is a little confusing to me for some reason, I'm trying to make a base "actor" class and then have various subclasses like "barrel", "box", "player" each with their separate logic but I'm having a hard time figuring out how the whole metatable stuff works, your implementation works by creating an actor with a specific name and then basing the player on that, which is a little confusing to me
I'm still trying to figure this out myself. Does this help?
Actor = {} Actor.__index = Actor function Actor:new(name) local instance = setmetatable({}, Actor) instance.name = name instance.pet = "Cat" return instance end function Actor:hello() return "Hello World, I'm an actor" end Barrel = setmetatable({}, {__index = Actor}) function Barrel:new() local instance = Actor:new("barrel") setmetatable(instance, {__index = Barrel}) instance.pet = "Dog" -- overrides Actor instance.my_attribute = 15.4 return instance end function Barrel:barrel_hello() return "I'm a fat barrel" end function _init() my_barrel = Barrel:new("Fat Barrel") end function _update() end function _draw() cls(0) print("Actor test") print(my_barrel:hello()) -- uses function on Actor print(my_barrel:barrel_hello()) -- uses function on barrel print(my_barrel.pet) -- uses value from barrel print(my_barrel.my_attribute) -- uses value from barrel end |
the tutorial you’re following is good for the normal table sections, but has some wrong bits in the instances part (edit: nerdy teachers updated it!)
I just published a long explainer that I wrote originally on discord: https://www.lexaloffle.com/bbs/?tid=141946
it should help you complete your understanding and fix your issues here :)
@merwok thank you a lot, thanks to your tutorial I finally understand how the tables work for this purpose and I was able to simplify code a lot
@supercurses thank your for you example, although it seems that the initialization of the inherited objects can be simplified a lot with this new knowledge
Final result:
Actor = { life=100, spriteIndex=0, } --actor.__index = actor function Actor:new(new_x,new_y) local o = { x=new_x, y=new_y, } return setmetatable(o, {__index=self}) end function Actor:update() end function Actor:draw() spr(self.spriteIndex,self.x,self.y) end |
Barrel = Actor:new() Barrel.spriteIndex = 3 function Barrel:update() -- code for explosion will go here end |
Particle = Actor:new() Particle.life=4 Particle.spriteIndex=81 function Particle:update() self.life-=1 if self.life<=0 then del(actors,self) end end |
Finally, I now put all my actors in an actors table and then just
function _draw() for b in all(actors) do b:draw() end end function _update() for b in all(actors) do b:update() end end |
A problem I noticed is that I'm not really sure how to count the different kind of actors for debugging purposes, I guess I can still use separate lists for separate object types so that I can quickly count them in debug
oh @merwok one more question:
how do I create a subclass with it's own new function?
I see there's
archer=goblin:new()
but I'd like to have archer have a new() function with more parameters and code, I'm not sure how to go about it
I'm not @merwok (that was a great write up of prototype based OOP btw) but hopefully neither of you mind if I answer the question.
And the answer is: you just define a new
function on archer. Lua will first check the object itself (say arcgob1
) for the property (new
in this case.) If the object itself doesn't have the property then it checks that object's prototype. If the prototype doesn't have it then it checks the prototype's prototype and so on until either a) the property is found on some prototype somewhere in the chain, or b) the prototype chain ends.
So if you do:
archer = goblin:new() archer.sp = 52 function archer:new(x, y, extra1, extra2) -- let 'goblin' handle the basic stuff local obj = goblin:new(x, y) -- then do archer specific stuff obj.extra1 = extra1 obj.extra2 = extra2 -- 'obj' has 'goblin' as a prototype at the moment -- which isn't what we want so we just change it. return setmetatable(obj, {__index=self}) end -- Add some new functionality to archer. function archer:show_extra() print(self.extra1, 10, 50) print(self.extra2) end arcgob1 = archer:new(50, 20, 1, 2) function _draw() cls() arcgob1:draw() -- from goblin arcgob1:show_extra() -- from archer end |
Th nice thing here is that you can let goblin
initialize the general goblin stuff and then just add to it. obj
initially has goblin
as its prototype with x
and y
set with goblin:new()
. Then you do whatever extra stuff you want to do in. The last line of new
changes the prototype of obj
to self
(which is archer
in this case.) Since archer
itself has goblin
as a prototype you have access to any new stuff you've added to archer
but also still have access to everything from goblin
which is why you can still call draw
on arcgob1
.
> "A problem I noticed is that I'm not really sure how to count the different kind of actors for debugging purposes, I guess I can still use separate lists for separate object types so that I can quickly count them in debug"
And you can use your new new
to solve this problem also.
archer = goblin:new() archer.count = 0 function archer:new(x, y, etc) self.count += 1 local obj = goblin:new(x, y) return setmetatable(obj, {__index=self}) end arcgob1 = archer:new(1, 2) arcgob2 = archer:new(3, 4) print(archer.count) |
Thank you, that was the missing link, I converted all my objects to this new system and everything is working great!
I have a mechanic in my game where the base_values of a enemy are modified so any new enemy that is spawned uses those new values (and current enemies are unaffected) - essentially enemy scaling per level.
Is this the right way to go about it? It seems to work. Basically, I set class level base_variables on the sub classes and use those within the instance for new spawned enemies.
I'm simulating a level up with btnp(4) - which will work through any current enemies and update their classes.
Mob = {} function Mob:new(o) o = o or {} o.name = "Mob" o.type = "Mob" setmetatable(o, self) self.__index = self return o end function Mob:update() -- do something end function Mob:draw() -- do something end Gremlin = Mob:new() Gremlin.base_damage = 4 function Gremlin:new(o) o = o or {} o.name = "Gremlin" o.hp = rnd(40) o.damage = self.base_damage setmetatable(o, self) self.__index = self return o end Goblin = Mob:new() Goblin.base_damage = 40 function Goblin:new(o) o = o or {} o.name = "Goblin" o.hp = rnd(4) o.damage = self.base_damage setmetatable(o, self) self.__index = self return o end function _init() spawn_mobs() end function spawn_mobs() mobs = {} for i = 1, 10 do add(mobs, Gremlin:new()) add(mobs, Goblin:new()) end end function _update() if btnp(4) then local updatedClasses = {} for mob in all(mobs) do --permanetnly increase base damage of the class by 10 local class = getmetatable(mob) if not updatedClasses[class] then class.base_damage = class.base_damage + 10 updatedClasses[class] = true end end spawn_mobs() end end function _draw() cls() print(#mobs, 0,0,7) local y = 8 for mob in all(mobs) do print( " Name: "..mob.name.. " Damage:"..mob.damage.. " hp:"..mob.hp.. " Class Base Damage:"..mob.base_damage) end end |
that seems ok. but you shouldn’t need to redefine new
so many times!
Can you elaborate on that for me @merwork - I'm not sure what I'm doing wrong.
@supercurses, looks like you've got a typo in @merwok's username so they may not have seen your last comment.
You're not necessarily doing anything wrong. At the end of the day if it works, it works. But there is a bunch of repetition you can get rid of. As a reminder, Lua doesn't really have classes. If you haven't already you should definitely go read the post linked in merwok's first comment in this thread. It's a good explanation on how Lua handles inheritance.
Anyway. You really only need to define a different new
function on "sub-classes" (again, not really classes) if you want them to have different input parameters than the base type. In your example above none of the types, Mob
, Goblin
or Gremlin
actually take input parameters at all so the base Mob
type should be able to handle everything. (I don't know if you're using o
in other parts of your program but in what you've got here o
in the new
functions is always going to be nil
.)
Here's a simplified version of your types. Again, if you haven't, go read merwok's post on prototype inheritance. It explains how all of this stuff works.
Mob = { hp_max = 1, base_damage = 1, type = "Mob" } function Mob:new() local o = { hp = ceil(rnd(self.hp_max)), damage = self.base_damage } return setmetatable(o, {__index=self}) end -- No need to redefine 'new' each time just set the relevant values on -- the sub-types and 'new' will use those instead of the ones on 'Mob' Gremlin = Mob:new() Gremlin.base_damage = 4 Gremlin.hp_max = 40 Gremlin.type = "Gremlin" Goblin = Mob:new() Goblin.base_damage = 40 Goblin.hp_max = 4 Goblin.type = "Goblin" grem1 = Gremlin:new() Gremlin.base_damage = 5 grem2 = Gremlin:new() gob1 = Goblin:new() Goblin.base_damage = 50 gob2 = Goblin:new() print(grem1.type.." grem1") print("damage: "..grem1.damage) print("hp: "..grem1.hp) print(grem2.type.." grem2") print("damage: "..grem2.damage) print("hp: "..grem2.hp) print(gob1.type.." gob1") print("damage: "..gob1.damage) print("hp: "..gob1.hp) print(gob1.type.." gob2") print("damage: "..gob2.damage) print("hp: "..gob2.hp) |
Thanks for that @jasondelaat and apologies for the mis-tag @merwok.
I think I've got it now. But let's say I have 50 different mobs defined:
Goblin:new()
Gremlin:new()
Slime:new()
SlimeBoss:new()
...
What I'd like to be able to do is iterate through them and update their base values.
My current approach that works is to create one instance and load that into a list:
function _init() mob_dictionary = {} add(mob_dictionary, Goblin:new()) add(mob_dictionary, Gremlin:new()) end |
Then I do this in the update:
for mob in all(mob_dictionary) do --permanently increase base damage of the "class" by 0.50 local proto = getmetatable(mob) if not updatedProtos[proto] then proto.base_speed = proto.base_speed + 0.50 updatedProtos[proto] = true end end |
It works, but wondering if there is a way to do this without first creating an instance?
@supercurses
Might be worth starting your own thread if you still have questions but yes, you can do it without having to create an instance. You can actually see that I did that in my example: I created the Gremlin
object with Gremlin.base_damage = 4
and then used Gremlin:new()
to create grem1
. Then changed Gremlin.base_damage
to 5 and created grem2
. When you print them out you'll see that grem1
and grem2
have damage values 4 (grem1
) and 5 (grem2
).
In your loop you're using getmetatable
to get the prototype but Gremlin
(or Goblin
or Slime
or whatever) is the prototype so you can just modify it directly. Gremlins created before the change have the old value and gremlins created after the change get the new value. So you can just do this assuming all your mob types have been defined before _init
runs.
function _init() mob_dictionary = {Goblin, Gremlin, Slime, Slimeboss} end for mob in all(mob_dictionary) do --permanently increase base damage of the "class" by 0.50 if not updatedProtos[mob] then mob.base_speed = mob.base_speed + 0.50 updatedProtos[mob] = true end end |
[Please log in to post a comment]