How to Extends / Alias / Overwrite existing class in MZ/ ES6 [Tutorial]

● ARCHIVED · READ-ONLY
Started by nio kasgami 113 posts Page 3 of 6 View original ↗
  1. Sorry if this has been brought up already, but perhaps instead of re-assigning the class, you could use a function to merge the prototypes together? This would retain the behavior of the "ES5/prototype" overwriting system, but allow for the syntax of ES6 classes.

    The follow example works even if the "child" class already exists, and modifying the "base" class will still affect the child. Outside of the MergeClass function itself (which you could just copy/paste), you never have to write "prototype". And finally, it automatically stores the old functions into a global namespace variable, so using IIFE doesn't prevent micro-managing things down the line (but it does prevent temporary ES6 classes from clogging up global space). Only downside is the calls to old functions are still somewhat long winded (NAMESPACE.CLASS.FUNC.call(this)), but at least it retains compatibility with "super" for existing child classes. Also, don't know if it works with "get" functions, but those may be impossible to properly overwrite anyway.

    I've tested this with both Node.JS (native ES6) and Babel (conversion to ES5). (PasteBin version with syntax colors)

    Code:
    // ====================================================
    // * Base Class
    // ====================================================
    
    class Base {
        test_return() {
            return 0;
        }
    
        test_super() {
            console.log("Base");
        }
    
        test_this() {
            if(!this._val) this._val = 0;
            return ++this._val;
        }
    }
    
    // ====================================================
    // * Child Class
    // ====================================================
    
    class Child extends Base {
        test_return() {
            return super.test_return() * 2;
        }
    
        test_super() {
            super.test_super();
            console.log("Child");
        }
    
        test_this() {
            super.test_this();
            return ++this._val;
        }
    }
    
    /*** ---------------------------------------------------------------------------- ***/
    
    // ====================================================
    // * Namespace (to store old functions)
    // ====================================================
    
    var Dev = Dev || {};
    Dev.PluginName = Dev.PluginName || {};
    
    (function($) { // $ = Dev.PluginName
    
    // ====================================================
    // * Create Merge Class
    // ====================================================
    
    function MergeClass(classObj, es6Class, namespace) {
        // Get Class Name as String (for ex: "Base")
        const className = classObj.prototype.constructor.name;
    
        // Add Class Name Property to Namespace
        namespace[className] = {};
    
        // Loop Through ES6 Prototype
        Object.getOwnPropertyNames(es6Class.prototype).forEach(function(name) {
    
            // Ignore Constructor
            if(name === "constructor") return;
    
            // If the Function Already Exists, Copy Into Namespace
            if(classObj.prototype[name]) {
                namespace[className][name] = classObj.prototype[name];
            }
    
            // Add/Overwrite Function Into Original Class
            classObj.prototype[name] = es6Class.prototype[name];
        });
    }
    
    // ====================================================
    // * Modify Base Class
    // ====================================================
    
    class ModifiedBase {
        test_return() {
            return 12;
        }
    
        test_super() {
            $.Base.test_super.call(this);
            console.log("Base Modified");
        }
    
        test_this() {
            $.Base.test_this.call(this);
            return ++this._val;
        }
    }
    
    // Add Content from ModifiedBase to Base
    MergeClass(Base, ModifiedBase, $);
    
    // ====================================================
    // * Test Child Class
    // ====================================================
    
    const c = new Child();
    
    // normally, this would be 0 since the Base.test_return is 0
    // but since Base now returns 12, this prints 24
    // base behavior modified while still retaining child behavior
    console.log(c.test_return()); // prints 24
    
    // this successfully prints the following (in order):
    // Base
    // Base Modified
    // Child
    c.test_super();
    
    // normally, this would return 2, since it is incremented once in both Base and Child
    // but now it increments by 3, since an increment is included in modified Base:
    console.log(c.test_this()) // prints 3
    console.log(c.test_this()) // prints 6
    
    })(Dev.PluginName);
  2. ?????? said:
    Interesting.. will have to run a few tests to check this out. from the looks of it, and considering es6 classes are just some sugar on the syntax, I wonder how difficult it would actually be to port a plugin from mv to mz.

    Until we can see some of the default code for mz though, we wont really know for sure how were going to have to implement things. :(
    Well, there's no need to move all code to the class syntax when porting plugins, so it might be quite easy especially for simpler plugins.

    Galenmereth said:
    And when you redefine pawn later on, then

    Code:
    Pawn = class extends Pawn {
        initParams() {
            super.initParams();
        }
    }
    
    // is functionally the same as doing
    
    var originalInitParams = Pawn.prototype.initParams;
    Pawn.prototype.initParams = function() {
        originalInitParams();
    }
    I think this is incorrect. My understanding of the syntax suggests that this extension method is actually equivalent to the following code:

    JavaScript:
    var oldPawn = Pawn; // except in the extends method the oldPawn variable doesn't exist
    Pawn = function() {} // I guess if you defined a constructor() function in the class its content would go here?
    
    Pawn.prototype = Object.create(oldPawn.prototype);
    Pawn.prototype.constructor = Pawn;
    
    Pawn.prototype.initParams = function() {
        oldPawn.prototype.initParams.call(this);
    };
  3. @Solar_Flare You’re right, that would be the properly equivalent full code.

    I mentioned and used a “functionally equivalent” example for brevity because in context of the discussion where the change to the method wouldn’t be picked up by code preceding it, it would be functionally the same. As would your example of course so for consistency I probably should’ve shown the whole thing like that :)
  4. Please don't replace existing classes just to modify a method on them. Create a new class with your code and then overwrite the old method with one from your class using prototype.
  5. I'm actually thinking about throwing away inheritance altogether in the default MZ codebase and replacing it with composition, while plugins will still use inheritance to add/edit/erase/extend existing class functionalities, for instance:
    RMMZ ES6 Extend Base Class.png
    The 1st part of this snippet has Parent and Child classes showing that inheritance doesn't work with extending adding/editing/erasing/extending bass class functionalities, and the 2nd part of this snippet has ParentDecorator and ChildDecorator classes showing that the composition approach works for such plugin development needs.
    Basically, the only change is replacing "extends BaseClass" in the subclass declararions with the extra code "this.super = new BaseClass()" in the subclass constructor :)

    P.S.: This will of course lead to increased memory usage and GC pressure as there are now more objects, so this approach does has its drawbacks :p
  6. @DoubleX Generally inheritance and composition have their uses.
    "is a" vs "has a" relationship.

    But wait...what's going on here

    Code:
    class ChildDecorator extends ParentDecorator {
       constructor() {
          this.super = new ParentDecorator();
       }
    }

    I haven't seen that kind of statement before lol
  7. Tsukihime said:
    @DoubleX Generally inheritance and composition have their uses.
    "is a" vs "has a" relationship.

    But wait...what's going on here

    Code:
    class ChildDecorator extends ParentDecorator {
       constructor() {
          this.super = new ParentDecorator();
       }
    }

    I haven't seen that kind of statement before lol
    It's my mistake, it should be this(I've already edited my post):
    Code:
    class ChildDecorator {
    
       constructor() {
          this.super = new ParentDecorator();
       }
    
    }
  8. I mean in the interest of discussion, I rarely use class based inheritance or extension anymore personally. In generally I think OOP isn't a good fit for game design anyway, and I keep drifting toward factory patterns and ECS (https://en.wikipedia.org/wiki/Entity_component_system)

    But when it comes to writing plugins for RM, this only works in isolation on your own game (which is where I use this) or in plugins not altering core engine behavior, but augmenting it. Once you need to alter core mechanisms as has been discussed a lot in this thread, you will need to use prototype and inheritance.
  9. @Galenmereth Reading about it, I guess Unity is basically designed around ECS, where you create gameObjects and then attach Components to them.

    Then it wouldn't be necessary to think of things in terms of inheritance: actors and enemies, while they are both battlers, would simply share the same components and therefore have the same interface, without necessarily being part of the same class hierarchy.

    Then you could define other custom battler objects like NPC's, environmental elements, etc, by simply attaching those components to them. Anything can become a battler.
  10. Yeah exactly. And when extending them in plugins, you wouldn't actually have to alter the existing components on the object, you could just attach another component with the added logic and lifecycle methods to adjust the properties you want. There's challenges with this approach too of course, and it requires a very different mindset from how RM has always worked under the hood.
  11. How about something like this?
    Spoiler
    JavaScript:
    // Default RMMZ codebase
    class DefaultMZStaticClass {
        static constructSuper(...args) {
            const className = Object.getPrototypeOf(this).constructor.name;
            this.super = new DefaultMZStaticClass[className](...args);
        }
        static inherit(Parent, Child) {
            const childProto = Child.prototype, childName = childProto.constructor.name;
            DefaultMZStaticClass[childName] = Parent;
            DefaultMZStaticClass._subclasses[childName] = Child;
            this._inherit(Parent.prototype, childProto);
        }
        static updateBaseClass(Base) {
            Object.entries(DefaultMZStaticClass._subclasses).forEach(([childName, Child]) => {
                const Parent = DefaultMZStaticClass[childName];
                if (Base.prototype.constructor.name !== Parent.prototype.constructor.name) return;
                this._inherit(Base.prototype, Child.prototype);
            });
        }
        static _inherit(parentProto, childProto) {
            Object.getOwnPropertyNames(parentProto).forEach(name => {
                const desc = Object.getOwnPropertyDescriptor(parentProto, name);
                if (!desc || typeof desc.value !== "function") return;
                childProto[name] = childProto[name] || parentProto[name];
            });
        }
    }
    DefaultMZStaticClass._subclasses = {};
    
    class DefaultMZBaseClass {
        constructor(val) { this._val = val; }
        reusedMethod() { return `Base reused method val: ${this._val}`; }
        overridenMethod() { return "Base overriden method"; }
        unusedMethod() { return "Base unused method"; }
    }
    
    class DefaultMZSubclass {
        constructor(val) { DefaultMZStaticClass.constructSuper.call(this, val); }
        reusedMethod() { return `${this.super.reusedMethod()} Sub reused method`; }
        overridenMethod() { return "Sub overriden method"; }
        newMethod() { return `Sub new method val: ${this.super._val}`; }
    }
    DefaultMZStaticClass.inherit(DefaultMZBaseClass, DefaultMZSubclass);
    //
    
    // Plugin A codebase
    const PluginADefaultMZBaseClass = DefaultMZBaseClass;
    DefaultMZBaseClass = class extends DefaultMZBaseClass {
        reusedMethod() { return `${super.reusedMethod()} Plugin A` };
        overridenMethod() { return "Plugin A Base overriden method"; }
        pluginANewBaseMethod() { return "Plugin A Base plugin a new base method"; };
    }
    DefaultMZStaticClass.updateBaseClass(DefaultMZBaseClass);
    
    const PluginADefaultMZSubclass = DefaultMZSubclass;
    DefaultMZSubclass = class extends DefaultMZSubclass {
        reusedMethod() { return `${super.reusedMethod()} Plugin A`; }
        overridenMethod() { return "Plugin A Sub overriden method"; }
        pluginANewSubMethod() { return "Plugin A Sub plugin a new sub method"; }
    }
    //
    
    // Plugin B codebase
    const PluginBDefaultMZBaseClass = DefaultMZBaseClass;
    DefaultMZBaseClass = class extends DefaultMZBaseClass {
        reusedMethod() { return `${super.reusedMethod()} Plugin B` };
        overridenMethod() { return "Plugin B Base overriden method"; }
        pluginBNewBaseMethod() { return "Plugin B Base plugin b new base method"; };
    }
    DefaultMZStaticClass.updateBaseClass(DefaultMZBaseClass);
    
    const PluginBDefaultMZSubclass = DefaultMZSubclass;
    DefaultMZSubclass = class extends DefaultMZSubclass {
        reusedMethod() { return `${super.reusedMethod()} Plugin B`; }
        overridenMethod() { return "Plugin B Sub overriden method"; }
        pluginBNewSubMethod() { return "Plugin B Sub plugin b new sub method"; }
    }
    //
    
    const base = new DefaultMZBaseClass("parent"), sub = new DefaultMZSubclass("child");
    // base.reusedMethod(): Base reused method val: parent Plugin A Plugin B
    console.info("base.reusedMethod():", base.reusedMethod());
    //
    // base.overridenMethod(): Plugin B Base overriden method
    console.info("base.overridenMethod():", base.overridenMethod());
    //
    // base.unusedMethod(): Base unused method
    console.info("base.unusedMethod():", base.unusedMethod());
    //
    // base.pluginANewBaseMethod(): Plugin A Base plugin a new base method
    console.info("base.pluginANewBaseMethod():", base.pluginANewBaseMethod());
    //
    // base.pluginBNewBaseMethod(): Plugin B Base plugin b new base method
    console.info("base.pluginBNewBaseMethod():", base.pluginBNewBaseMethod());
    //
    // sub.reusedMethod(): Base reused method val: child Sub reused method Plugin A Plugin B
    console.info("sub.reusedMethod():", sub.reusedMethod());
    //
    // sub.overridenMethod(): Plugin B Sub overriden method
    console.info("sub.overridenMethod():", sub.overridenMethod());
    //
    // sub.unusedMethod(): Base unused method
    console.info("sub.unusedMethod():", sub.unusedMethod());
    //
    // sub.newMethod(): Sub new method val: child
    console.info("sub.newMethod():", sub.newMethod());
    //
    // sub.pluginANewBaseMethod(): Plugin A Base plugin a new base method
    console.info("sub.pluginANewBaseMethod():", sub.pluginANewBaseMethod());
    //
    // sub.pluginBNewBaseMethod(): Plugin B Base plugin b new base method
    console.info("sub.pluginBNewBaseMethod():", sub.pluginBNewBaseMethod());
    //
    // sub.pluginANewSubMethod(): Plugin A Sub plugin a new sub method
    console.info("sub.pluginANewSubMethod():", sub.pluginANewSubMethod());
    //
    // sub.pluginBNewSubMethod(): Plugin B Sub plugin b new sub method
    console.info("sub.pluginBNewSubMethod():", sub.pluginBNewSubMethod());
    //
    Of course, I won't use it myself, as I'm ok with prototyping, those avoiding prototype like a plauge might want to tinker on this :)
  12. I think you guys are overthinking this. From the core code structure it doesnt look like mz even uses a modern js class structure, it still uses the same function ClassName(){} syntax as mv. If they didnt change how they define classes, why do we need to change how we alias them? :)
  13. ?????? said:
    I think you guys are overthinking this. From the core code structure it doesnt look like mz even uses a modern js class structure, it still uses the same function ClassName(){} syntax as mv. If they didnt change how they define classes, why do we need to change how we alias them? :)

    I don't want to type prototype so much lol

    There's also the possibility that they might switch to all classes, which leads to this aliasing problem that we have right now.
  14. There's no way to alias methods in JS without using prototype
  15. TBH I still kinda like this structure:
    Code:
    ((klass, parent) => {"use strict";
        klass.funkyname = function() {
            parent.funkyname.apply(this, arguments);
            // do junk
        };
    })(GameActor.prototype, GameBattler.prototype);

    Also neat enough for aliasing:
    Code:
    ((klass, parent) => {"use strict";
        const funkyname = klass.funkyname;
        klass.funkyname = function() {
            funkyname.apply(this, arguments);
            // do junk
        };
    })(GameActor.prototype, GameBattler.prototype);

    Compared to some of the other workarounds, that seems almost elegant (for js)
  16. TBH I still kinda like this structure:
    Code:
    ((klass, parent) => {"use strict";
        klass.funkyname = function() {
            parent.funkyname.apply(this, arguments);
            // do junk
        };
    })(GameActor.prototype, GameBattler.prototype);

    Also neat enough for aliasing:
    Code:
    ((klass, parent) => {"use strict";
        const funkyname = klass.funkyname;
        klass.funkyname = function() {
            funkyname.apply(this, arguments);
            // do junk
        };
    })(GameActor.prototype, GameBattler.prototype);

    Compared to some of the other workarounds, that seems almost elegant (for js)

    Following in the same line, what if we create a script for simplifying aliasing. Something kinda AOP-like with before and after pointcuts?

    It would simplify the "prototype fest" and maybe standardize the community's approach towards extensions.
    The drawback would be that every script would depend on it, but if it becomes a standard, I don't think it would be too much of a problem in the long run.
  17. CaRa_CrAzY said:
    Following in the same line, what if we create a script for simplifying aliasing. Something kinda AOP-like with before and after pointcuts?

    It would simplify the "prototype fest" and maybe standardize the community's approach towards extensions.
    The drawback would be that every script would depend on it, but if it becomes a standard, I don't think it would be too much of a problem in the long run.
    We tried that on MV. If it's not included in the engine nobody will use it.
  18. @Galenmereth Trying to implement a ECS system in MZ? Wow, that's going to be a lot of trouble, plus I doubt the editor will make it friendly to work in that environment. I mean, you could do it for your own project, but it seems like a lot of work simply to change something that is already functional.
  19. MushroomCake28 said:
    @Galenmereth Trying to implement a ECS system in MZ? Wow, that's going to be a lot of trouble, plus I doubt the editor will make it friendly to work in that environment. I mean, you could do it for your own project, but it seems like a lot of work simply to change something that is already functional.

    I attach battlers to arbitrary objects all the time just so they can "become" battlers without necessarily needing to be a battler themselves.

    In my grid battle code, I've been experimenting with attaching battler objects to tiles on the ground just so I can give them state animations, perform damage when you step on it, and some other stuff related to battler interaction. That way you could do all your work in the database, and apply it to "environment" effects.