Log In  

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?

Cart #tihuwubibu-0 | 2024-04-24 | Embed ▽ | License: CC4-BY-NC-SA
2

P#147234 2024-04-24 09:11

2

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
P#147240 2024-04-24 10:13 ( Edited 2024-04-24 10:17)

I could have set x = rnd(480) and y = rnd(270) within the enemy:new function.

P#147241 2024-04-24 10:18

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

P#147249 2024-04-24 12:44 ( Edited 2024-04-24 12:45)
1

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
P#147258 2024-04-24 15:23 ( Edited 2024-04-24 15:25)

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

P#147293 2024-04-25 12:09

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
P#147302 2024-04-25 14:50
2

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 :)

P#147310 2024-04-25 16:36 ( Edited 2024-04-26 16:26)
1

@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

P#147361 2024-04-26 09:15

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

P#147366 2024-04-26 09:43

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.

P#147369 2024-04-26 11:29

> "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)
P#147370 2024-04-26 11:59

Thank you, that was the missing link, I converted all my objects to this new system and everything is working great!

P#147372 2024-04-26 13:06

fantastic!

P#147374 2024-04-26 13:31

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
P#147781 2024-05-02 15:17

that seems ok. but you shouldn’t need to redefine new so many times!

P#147802 2024-05-03 01:56

Can you elaborate on that for me @merwork - I'm not sure what I'm doing wrong.

P#147823 2024-05-03 08:48

@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)
P#147859 2024-05-03 19:44

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?

P#147900 2024-05-04 11:44

@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
P#147913 2024-05-04 15:53

[Please log in to post a comment]