1

So i'm making a system that runs through a list of "Tutorial Steps" during a tutorial, in sequence. the tutorial steps could be anything, but to guarantee some shared structure i've made an 'abstract' class TutorialStep. each unique type of tutorial step uses this as its metatable and __index, like this PressWASDStep for example:

TutorialStep.lua

local TutorialStepAbstract = {Name = "Tutorial Step"}
--setmetatable(TutorialStepAbstract, TutorialStepAbstract);
TutorialStepAbstract.isComplete = false;
function TutorialStepAbstract:new()
    local o = {};
    setmetatable(o, self);
    self.__index = self;
    --setmetatable(self, TutorialStepAbstract);
    return o
end
function TutorialStepAbstract:IsComplete()
    return self.isComplete;
end
function TutorialStepAbstract:Begin()
    print("beginning step: "..self.Name);
end
return TutorialStepAbstract;

PressWASDStep.lua

local TutorialStep = require(game.ReplicatedStorage.PPatrol_ReplicatedStorage.Tutorial.TutorialStep);
local UserInputService = game:GetService("UserInputService");
local HelperTChat = require(game.ReplicatedStorage.PPatrol_ReplicatedStorage.Modules.HelperTChat);

local Step = {}
--Step.__index = Step;
function Step:new()
    local o = TutorialStep:new();
    --setmetatable(o, self);
    --self.__index = self;
    o.Name = "Press WASD Step";
    o.isComplete = false;
    o.WPressed = false;
    o.APressed = false;
    o.SPressed = false;
    o.DPressed = false;
    return o;
end
--function Step:IsComplete()
--  return self.isComplete;
--end
function Step:Begin()
    task.spawn(function ()
        local messagedismisscallback = HelperTChat.ShowNewMessage("'You are currently at the patient's artery! Let’s start by going over how to move. Use the WASD keys to move.'", "nouserdimiss", "large");
        while not self.isComplete do
            self.WPressed = self.WPressed or UserInputService:IsKeyDown(Enum.KeyCode.W);
            self.APressed = self.APressed or UserInputService:IsKeyDown(Enum.KeyCode.A);
            self.SPressed = self.SPressed or UserInputService:IsKeyDown(Enum.KeyCode.S);
            self.DPressed = self.DPressed or UserInputService:IsKeyDown(Enum.KeyCode.D);

            if self.WPressed and self.APressed and self.SPressed and self.DPressed then
                self.isComplete = true;
                print("completing step "..self.Name);
                messagedismisscallback();
            end
            task.wait();
        end
    end, self)
end
return Step

the issue is, when i call CurrentStep:IsComplete(), it looks for the function in PressWASDStep, doesn't find it, and invokes the IsComplete() in TutorialStep, as it should. But the underlying property, self.isComplete, is always false here, because TutorialStep's self.isComplete and and PressWASDStep's self.isComplete are different variables.

I could get around this by referring to isComplete directly, but i'd rather figure out how to do this seemingly basic thing with lua, and i wouldn't be able to do anything else in :IsComplete() if i chose to later. I could also reimplement :IsComplete() in PressWASDStep, but then i'm losing the usefulness of a superclass/abstract class.

Is there a way to have the line self.isComplete = true; in PressWASDStep modify the property of its metatable instead of defining/modifying a property of itself? i'm not sure if that's the right way to phrase it, but essentially i want to have one shared property isComplete that is accessible to both TutorialStep and PressWASDStep.

Thanks for any help!

Nifim
  • 4,758
  • 2
  • 12
  • 31
Swanijam
  • 77
  • 1
  • 8

2 Answers2

1

What you want is proper inheritance. Right now, when creating a Step, you actually create a TutorialStep instead. Sure, you then fill it with values but it's metatable and therefore it's "type" is a TutorialStep.

Instead you want it to be a Step (the commented-out code), and pass missing method calls to it's super.

This adapted (and not very beautiful) example "extends" TutorialStepAbstract.

local Step = setmetatable({ }, {__index = TutorialStep})

function Step:new()
    local o = TutorialStep:new()
    setmetatable(o, {__index = Step})
    ...
    return o
end

When you now call IsComplete it looks inside Step, doesn't find the method and continues in its __index. Notice the possibility of longer inheritance chains here. For every call, self stays the same, thus "sharing the property". You should extend on that to have explicit access to super so you can extend a method instead of just overwriting.

But please, just use a premade library and don't reinvent the wheel here :) E.g.: https://github.com/Yonaba/30log

Luke100000
  • 1,395
  • 2
  • 6
  • 18
  • i'm pretty confused about how setmetatable and __index work then. could you elaborate on how that first line works? what's the difference between this and the code i have commented out? – Swanijam Feb 23 '23 at 16:17
  • The main difference is that you never made any kind of chaining. You assign a single metatable to an instance, so it can be either Step or TutorialStep. But for object oriented programming, it should be both. Therefore I give the instance a metatable pointing to Step, and Step has a metatable pointing to TutorialStep. – Luke100000 Feb 23 '23 at 16:29
  • that makes sense to me conceptually! could you explain how it works on the syntax level? – Swanijam Feb 23 '23 at 18:32
  • Sorry, I'm not sure what syntax is unclear, can you specify please? The first line assigns a metatable, with index pointing to the super class, to a new, empty table. The second setmetatable overwrites the metatable of o with a new metatable (uncached and unoptimized) pointing to the class. – Luke100000 Feb 23 '23 at 21:39
1

The way to think about the __index metamethod is that when you ask a table for a key that doesn't exist, it will then check for that key in the table assigned to the __index field.

That is why when you create an object with the new function, you create an essentially empty table and set the __index field. Since every field will be missing, the __index table effectively allows the empty table to behave like the original object. But, this isn't a true OOP constructor, it's duck typing.

It sounds like you want to use the metatable as the equivalent to the super class, but that's not how it works. If you want a value to be accessible to the self object, it needs to be defined on the object itself, not its metatable.

So to have a unified set of properties from base class to child class, rather than try to set the __index method twice, try explicitly copying all the fields from the base class into a new table, then use that table as the starting point for your child class. This is composition, not inheritance, and is generally an easier pattern to read in your code.

local TutorialStepAbstract = {}

function TutorialStepAbstract:extend(additionalProps)
    local o = {
        Name = "Tutorial Step",
        isComplete = false,
    }
    -- rather than use the __index metatable, explicitly copy all of the fields onto the new object
    for k, v in pairs(self) do
        o[k] = v
    end

    -- add in any supplied fields
    for k, v in pairs(additionalProps) do
        o[k] = v
    end

    return o
end
local TutorialStep = require(game.ReplicatedStorage.PPatrol_ReplicatedStorage.Tutorial.TutorialStep)

local Step = {}
Step.__index = Step

function Step.new()
    -- initialize the Step by extending the Abstract, this will initialize the object with all the properties and methods of the base class
    local o = TutorialStep:extend({
        Name = "Press WASD Step",
        WPressed = false,
        APressed = false,
        SPressed = false,
        DPressed = false,
    })

    -- use traditional metatables for this class's members for simplicity sake
    setmetatable(o, Step)
    return o
end
Kylaaa
  • 6,349
  • 2
  • 16
  • 27