Log In  

Hi,

I'm using a metatable for particles in my game. For example, when the player dies the sprite explodes in a burst of pixels. I use rnd() to off-set the player position with a random number, and as I generate a lot of different particles (based on the colors the character is composed of) I want to move the for-loop into the constructor.

Unfortunately, for some reason the randomness doesn't work when I build the for-loop inside the particle constructor. Compare these two examples:

For loop in caller function

particles={}
particle = {
    x_offset=0,
    y_offset=0,
}

function particle:new(o)

    self.__index = self

    local pi = setmetatable(o or {}, self)

    pi.x=pi.x-rnd(pi.x_offset)
    pi.y=pi.y-rnd(pi.y_offset)

    add(particles,pi)

end

function game_over()

    for i=1,6 do
        particle:new({x=p.x,y=p.y,x_offset=12,y_offset=8})
    end

end

Result: I end up with 6 particles, each with a different x and y value due to the rnd() function being passed the x_offset and y_offset values.

For loop in constructor

particles={}
particle = {
    x_offset=0,
    y_offset=0,
}

function particle:new(count,o)

    self.__index = self

    for i=1,count do
        local pi = setmetatable(o or {}, self)

        pi.x=pi.x-rnd(pi.x_offset)
        pi.y=pi.y-rnd(pi.y_offset)

        add(particles,pi)
    end
end

function game_over()

    particle:new(6,{x=p.x,y=p.y,x_offset=12,y_offset=8})

end

Result: I end up with 6 particles, but they all have the same x and y values. As if the rnd() is called only once and then applied to the other 5 iterations of the for-loop.

Questions

  1. Why does the code behave this way?
  2. What's the right pattern to apply here?
P#62086 2019-02-19 20:41

Just an addendum, I get the exact same behavior (rnd() returning the same value for all iterations) when I call another function in between:

particles={}
particle = {
    x_offset=0,
    y_offset=0,
}

function multi_particle(count,o)

    for i=1,count do
        particle:new(o)
    end

end

function particle:new(count,o)

    self.__index = self

    local pi = setmetatable(o or {}, self)

    pi.x=pi.x-rnd(pi.x_offset)
    pi.y=pi.y-rnd(pi.y_offset)

    add(particles,pi)

end

function game_over()

    multi_particle(6,{x=p.x,y=p.y,x_offset=12,y_offset=8})

end
P#62087 2019-02-19 21:15 ( Edited 2019-02-19 21:16)

It looks like that code is reusing the same {x=p.x,y=p.y,x_offset=12,y_offset=8} table over and over. Basically you need to create a new table inside your "new" method and return that. You could just do a shallow copy of the table that gets passed in...but it looks like some of the properties are not even used outside of the constructor (if this example code is representative?) so it may be easier to just make the constructor take a few separate parameters.

Another thing: you only need to set the __index once.

Here's how I might do this (this is untested so I might have a typo or something, but it's based on real code I've used in carts):

function _init()
  particles = {}
end

-- start particle class
particle = {}
particle.__index = particle

function particle.new(x, y, xoffsetmax, yoffsetmax)
  local obj = {
    x = x,
    y = y
  }

  obj.x -= rnd(xoffsetmax)
  obj.y -= rnd(yoffsetmax)

  setmetatable(obj, particle)
  return obj
end
-- end particle class

function game_over()
  -- assumes p is player?
  multi_particle(6, p.x, p.y, 12, 8)
end

function multi_particle(count, x, y, xoffsetmax, yoffsetmax)
  for i = 1, count do
    add(particles, particle.new(x, y, xoffsetmax, yoffsetmax))
  end
end
P#62090 2019-02-20 00:35 ( Edited 2019-02-20 01:04)

Yeah, kittenm4ster is right. In your "constructor" you never return a new object, you just manipulate the current object (self) every time it's called.

Remember that Lua's OOP is prototype-based, so creating a new object means taking an existing object (prototype), and creating a new object based on it (and "basing" an object on another in Lua is done by setting the former's metatable to the latter). You're skipping the "creating a new object" part.

I recommend you read the parts of Programming in Lua that cover this, it's kind of hard to wrap your head around at first (especially if you're used to class-based OOP): https://www.lua.org/pil/16.html

P#62100 2019-02-20 09:42

Many thanks for the great help!

I implemented your suggestions and in the end narrowed my problem due to the re-use of the same object. I want to use a table as I have many properties that I set (width, height, color, dx, dy, gravity, friction, ...) and I want to avoid a long list of function arguments.

I solved the problem by doing a shallow copy, which currently looks like this:

function particle:new(o)

    local obj={}
    for key, value in pairs(o) do
        obj[key] = value
    end

    obj.x+=rnd(o.max_x_offset)
    obj.y-=rnd(o.max_y_offset)

    setmetatable(obj,particle)

    return obj

end

Is this the right way to do this? Or is there a native Lua/Pico-8 function I should use instead? (Couldn't find any in the docs)

P#62109 2019-02-20 20:17

yay glad you got it working :)

yep, that's the right way to do a shallow copy; there is no built-in function for it. btw I would just extract that out into a separate function since it is a general-purpose thing to do.

oh also if you aren't using "self" at all in the constructor, note that you can just use "particle.new" instead of the colon syntax...I usually don't use a colon for constructors because to me it's a bit confusing as to what the "self" is in that context (i.e. it's the class prototype, not an instance of the class, which is usually what "self" is) but that's just a matter of taste.

P#62114 2019-02-20 22:22 ( Edited 2019-02-20 22:23)

> Is this the right way to do this?

Note that you're setting the metatable to particle, not to self (they're the same in your example; the colon in particle:new just means that particle is passed as the self argument). That means inheritance won't work, but you probably don't need it here.

You also don't set self.__index to anything. This special variable is used if a key is missing from the object; accessing it it will look it up in the metatable instead. This can be used instead of doing a shallow copy like you do.

So this might be more of a "right" way to do it (although yours is fine too, as long as it does what you want):

function particle:new(o)
  -- if no table is passed, just use an empty table so we inherit from particle
  local obj=o or {}

  -- set metatable and "inherit"
  setmetatable(obj,self)
  self.__index=self

  -- this now needs to be done down here, after we have set __index above
  obj.x+=rnd(obj.max_x_offset)
  obj.y-=rnd(obj.max_y_offset)

  return obj
end

I haven't tested this though, currently on mobile, but that's the basic gist of how objects are usually created in Lua.

> to me it's a bit confusing as to what the "self" is in that context (i.e. it's the class prototype, not an instance of the class, which is usually what "self" is)

Lua doesn't have classes – the prototype is an instance. Like I said above, self is simply the object that the new() method was called on. And the colon syntax is very simple actually, it's just syntactic sugar:

-- these are equivalent
particle:hello()
particle.hello(particle)

-- these are also equivalent
function particle:hello()
  print(self.name)
end

function particle.hello(self)
  print(self.name)
end

Again I highly recommend reading chapter 16 in PIL, which I linked before! It contains a full tutorial on using metatables to implement OOP and inheritance. The wiki also has some information: https://pico-8.fandom.com/wiki/Setmetatable

P#62125 2019-02-21 08:44 ( Edited 2019-02-21 08:44)

Thank you, it's a lot clearer now! Missed your previous reference to the PIL. Just read it, and a lot wiser from it. Some aspects of LUA are quite foreign to me, but slowly getting the hang of it.

P#62136 2019-02-21 19:59

@tobiasvl just one more thing: am I correct that the shallow copy was missing in your example? When I did:

local obj=o or {}

I got the exact same behavior as before (all particles having the same values). This is the current state of my code (including the shallow copy) which works as expected:

function copy(source)

    local target={}

    for key, value in pairs(source) do
        target[key] = value
    end

    return target

end

function particle:new(o)

    local obj=copy(o) or {}

    setmetatable(obj,self)
    self.__index=self

    obj.x+=rnd(obj.max_x_offset)
    obj.y-=rnd(obj.max_y_offset)

    return obj

end
P#62137 2019-02-21 20:08

It's not clear to me why you need to make a shallow copy. What is o exactly? Is it an existing non-particle object? Usually, the argument to the constructor is just an ad hoc table that overrides the defaults (ie. the values the new object inherits from its prototype). But you're perhaps passing in an existing table that you use for other things too, hence the need for a copy? What kind of table is that, and why don't you create the new oject based on that one instead?

P#62139 2019-02-21 20:33

o contains the configuration of the particle.

For example, when a monster dies I generate the following particles:

make_particles(8,{x=m.x,y=m.y,c=c.indigo})
make_particles(1,{x=m.x,y=m.y,c=c.darkblue})
make_particles(1,{x=m.x,y=m.y,c=c.darkpurple})

The make_particles function looks like this:

function make_particles(count,config)
    for i=1, count do
        add(particles,particle:new(config))
    end
end

And then finally, this is what the particle "constructor" looks like:

function particle:new(o)

    local obj=copy(o) or {}

    setmetatable(obj,self)
    self.__index=self

    obj.x+=rnd(obj.max_x_offset)
    obj.y-=rnd(obj.max_y_offset)

    return obj

end
P#62141 2019-02-21 20:44

I think there's a bit of misunderstanding going on between my approach and tobiasvl's approach. Such is the nature of Lua OOP since it is so free-form :)

The shallow copy used in vincent_io's code (and suggested by me) is simply a way to initialize properties of an "object" and is not related to OOP, so @tobiasvl I think you may have misinterpreted what that was being used for (?)

> You also don't set self.__index to anything. This special variable is used if a key is missing from the object; accessing it it will look it up in the metatable instead

@tobiasvl I think vincent_io was just posting an excerpt of the code, and setting of "__index" was there, but it was done in a separate place, on the prototype. Keep in mind that setting the "__index" property of each newly created object has the same effect as setting "__index" on the prototype once and then using the prototype as the metatable itself for each new object. I guess it's a bit of a hack to use the same table as both the prototype and the metatable itself (for the record, I didn't make that up; I saw some other people do it online), but again this is Lua so there are no definite "right" ways to do OOP, as long as it works for whatever the use-case is, as you also pointed out :)

Also I think it goes without saying, but there is no point to doing OOP unless you have other methods in this "class" besides the constructor, vincent_io, but I'm assuming you do have those and just didn't post that part of the code :)

P#62140 2019-02-21 20:50 ( Edited 2019-02-21 20:55)

Yeah, maybe!

> Keep in mind that setting the "index" property of each newly created object has the same effect as setting "index" on the prototype once and then using the prototype as the metatable itself for each new object.

Sure, in a class-like OOP system. But if you have an object A, which you use as the prototype for object B, and then you use B as the prototype for C, you would probably want to set __index in the constructor so you don't keep using A as the lookup for C, right?

P#62143 2019-02-21 21:08

[Please log in to post a comment]

Follow Lexaloffle:          
Generated 2024-03-28 19:43:20 | 0.019s | Q:24