Search code examples
inheritanceluarobloxmetatableluau

Lua given table A with metatable B, have functions in B defer to properties in A *before* identically-named properties in B


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!


Solution

  • 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