ct-js icon indicating copy to clipboard operation
ct-js copied to clipboard

Native class inheritance framework

Open CosmoMyzrailGorynych opened this issue 3 years ago • 7 comments

Describe the current state of the problem Writing external functions and copypasting is boring and wrong.

Describe the solution you'd like True inheritance with additional base classes beyond Copies: labels, buttons, progress bars, panels, static sprites to eat less memory, and other stuff.

Additional context

Use cases:

  • different enemies that share one behavior (the current workaround is to make a function in the Settings->Scripts panel)
  • constructing different buttons with a uniform style;
  • making abstract classes. e.g. for pickups, hero classes and other pawns, etc.

This issue is tightly coupled with the new event editor and new base classes (see #39).

CosmoMyzrailGorynych avatar Jul 30 '20 01:07 CosmoMyzrailGorynych

Curious about this: why go with inheritance?

What is so bad about composition?

JavaScript as a language is much more tuned towards composition. Being a prototypical instead of an object-oriented language all "class features" in JS are basically syntactical sugar to make it easier for Java (and similar) developers to adopt the language and bring their OOP principles. My brief dive into Game development has taught me that many engines rely on "Entity Component Systems". Basically, composition over inheritance, keeps code more organized and readable in the long run. Often times inheritance can neither implement structures nor adapt to their changes. While these are hard concepts in other languages, they come naturally to JavaScript, and OOP on the other hand is a hard context in itself. My personal experience teaching high schoolers and University freshman is that OOP confuses people while composition in the form of "they just use that function as well" come naturally.

different enemies that share one behavior (the current workaround is to make a function in the Settings->Scripts panel)

That is actually not a workaround but a beautiful orchestration! The thing tedious about is the management of custom scripts in ct.js IDE right now. But, should we improve that, this approach will prove to be more reliable in the long run.

Maybe custom scripts are also lacking ways to overwrite or extend event handler functions.


The above may be biased as I come from a strong JS background and agree with the sentiment that: "Classes confuse both humans and computers".

HoverBaum avatar Feb 09 '21 14:02 HoverBaum

What is so bad about composition?

Nothing. And inheritance doesn't exclude composition :D

Below goes my flow of thoughts, so better read it sequentially to the end to make sense of it.

So, @HoverBaum, a couple of things to consider:

  1. Game developers won't give a quack about what happens in ct.js under the hood.
  2. What matters is maintainability from ct.js developers and usability for game developers.

And under usability, I would expect:

  • Behavior inheritance (we don't need to copy-paste movement logic);
  • Ability to check whether a copy A is an instance of type B, even if it is not its direct constructor (is this copy a pickup? is this copy an enemy?);
  • Ability to get the list of all copies of a class B (get all hostiles regardless of their type and remove them from the map).
  • Proper type checks inside the code editor.
  • Understandable UI for all that.
  • Easy to use API.
  • And you have also reminded me of behaviors as components, a la GDevelop or something. A button behavior, a 2D platformer behavior, user-defined systems, et cetera.

Maintainability means, at the very least:

  • DRY code;
  • Which is easy to follow;
  • And easy to change/build upon, meaning that it is flexible enough to not produce technical debt.
  • We should foresee the usage of multiple base classes. Static, animated sprites, skeletons, meshes, 9-patch frames are those that will definitely be needed.

With JS class inheritance:

  • ✅ Behavior inheritance becomes easy as events are just methods on a class;
  • ✅ We can use instanceof;
  • ❓ We will have to code additional routines so that a copy ends up in multiple ct.types.list arrays, and gets cleaned out correctly;
  • ✅ Type checks come out correctly without any problems;
  • ✅ Behaviors as components, aka mixins, drop out of the purely class-based system, but oh well. Typings are not so complicated as in TypeScript, each class is also an interface that can be extended, and we can tell it to the code editors.
  • 🛑 Either we copy-paste all the code that currently makes an animated sprite a ct.js Copy (events, collision data), or we turn it into mixin. Of course, we will have to make it a mixin.
  • ❓ Class inheritance is relatively easy to implement in ct.js, and will be straightforward for ct.js developers, but users will have to use some monstrosity like this.parentType.OnStep.call(this) to call overwritten events, or something abstracted a bit.
  • ✅ If everything ct.js adds to pixi classes becomes mixins, then it is pretty easy to maintain. Base classes will be easy to implement, too.
  • ❓ Behaviors as components clearly fall out of class-based inheritance system, so we will have to use composition again.
  • ❓❓ And how do behaviors actually implement itself? Ideally they should be based on the OnCreate-OnStep-OnDraw-OnDestroy loop.
  • ❓❓❓ Wait, do we really inherit events as methods?
  • ❓❓❓❓ And how can we control when and what runs so we don't get one-frame delays because some behavior runs after another??
  • ❓❓❓❓❓ Oh no.

Ok, what does composition solve? Everything may be mimicked by factories and composition. A similar thing is already done in ct.inherit module. We have to use composition here and there anyways, but going fully compositional doesn't solve more problems.

So, the question is, how to make behavior sharing easier for both ct.js developers and game developers?


OOP confuses people

OOP just shouldn't be explained on animals and cars 😛

CosmoMyzrailGorynych avatar Feb 10 '21 06:02 CosmoMyzrailGorynych

Great viewpoints. Didn't really think about it from the ct.js maintainers' point of view. Thanks for the long reply! I might have been triggered to defend composition over inheritance here.

If this is more about some basic things for ct.js to provide for userspace it can be a very valid solution.

And I love the baseline points you laid out!

Maybe one more thing for usability:

  • Make it hard to do the wrong (bad to maintain and extend) and easy to do the right thing.

✅ Behavior inheritance becomes easy as events are just methods on a class; ✅ We can use instanceof; ❓❓ And how do behaviors actually implement itself? Ideally they should be based on the OnCreate-OnStep-OnDraw-OnDestroy loop.

What happens though if a user wants a new "base type"? Do we lock them into creating a new subclass? Or do classes have a property like type that also identifies them and really we do string-based comparisons, as they are more extendable? (And could be typed)

My insights are not that deep, but I would have pictured that a Type in it's onCreate could do something like:

// onCreate code

enemy(this)
moveDown(this)

// Code somewhere

const enemy = entity => {
  entity.types.push(entity) // One entity could have multiple types for collisions checking.
}

const moveDown = entity => {
  const originalOnStep = entity.onStep
  entity.onStep = () => {
    // Put you logic here
    originalOnStep()
  }
}

In my mind, this solves the problems of identifying what a Type is and remains more flexible. As a user of ct.js coming from the JS webdev world, this is how I would love to build up my Types. Maybe thinking a bit about React Hooks, which are a standardized way in React to add common functionality to multiple components.

Ability to check whether a copy A is an instance of type B, even if it is not its direct constructor (is this copy a pickup? is this copy an enemy?);

I think this could be perfectly solved by string comparison, you could type the string to be X, Y, or Z. (not sure how well this would work with user introduced types though, maybe need a place to be defined).


I think I yet lack the insights into both pixi and ct.js to follow all thoughts in your post and provide meaningful input 🙈 I do hope my points add some value though.

HoverBaum avatar Feb 13 '21 18:02 HoverBaum

I might have been triggered to defend composition over inheritance here.

Indeed, it sounded a bit fanatical :wink: :smile:

What happens though if a user wants a new "base type"? Do we lock them into creating a new subclass? Or do classes have a property like a type that also identifies them and really we do string-based comparisons, as they are more extendable? (And could be typed)

By a base type I mean any DisplayObject of PIXI extended with ct.js' Copy logic. See "Display objects implemented in PixiJS". I don't think you would need other base types :D And if you do, they can be made as modules, with their own type definitions. The only thing is that Copy's logic should be a mixin accessible to modders.

I think this could be perfectly solved by string comparison, you could type the string to be X, Y, or Z. (not sure how well this would work with user introduced types though, maybe need a place to be defined).

Doesn't work well with hierarchies as is, but can be improved with an array that shows an inheritance tree. And it is oftentimes needed: you have copies -> that are skeletons -> that are in-game playable characters -> that are manipulated by AI -> that are hostile ones to the player -> that shoot magic bolts. If we are thinking about tons of pawns with different functionality. This can be partially made with behaviors as components (combat logic, for example), but inheritance is a welcome tool, too.


But ok, let's return to the user's side. Inheritance and base classes are one thing, and shared behavior is, actually, another, though they do overlap. There is a thing that can be done right now and that will solve tons of problems: support for behaviors as components. I see it as a UI that allows ordering and adding built-in behaviors, as well as defining your own, and using the ones that are provided in modules. This requires a uniform declarative structure both for built-in behaviors, user-provided ones, and behaviors coming from modules.

Do you want to work on it? It solves this exact problem: "Writing external functions and copypasting is boring and wrong." Additional base classes can wait if done the correct way.

CosmoMyzrailGorynych avatar Feb 14 '21 23:02 CosmoMyzrailGorynych

Indeed, it sounded a bit fanatical 😉 😄

😅 Thanks for the feedback.

Great insights again! Thanks for taking the time even though I still lack insights into the project.


Do you want to work on it? It solves this exact problem: "Writing external functions and copypasting is boring and wrong." Additional base classes can wait if done the correct way.

I would generally be up to do some contributions. Unsure about how much time I can dedicate though, so best for it to be non-critical or non-urgent things.

I see it as a UI that allows ordering and adding built-in behaviors, as well as defining your own, and using the ones that are provided in modules. This requires a uniform declarative structure both for built-in behaviors, user-provided ones, and behaviors coming from modules.

So, there would be a new tab next to "Types" and "Textures" that lists created or otherwise imported "Mixins" (is that a good name?). Here you can: add, edit or remove those modules.

Inside a Type you can then call a module in the "onCreate"? Or should we rather have a UI where all Mixins are displayed and you checkmark the ones you want active for this Type?

"Mixins" are basically functions taking in "entities" (maybe types is the right word here?) and adding behavior to them.

  • Assuming this is a good approach I would need to learn, how to adapt behavior from a function.

I would need some guidance on how to build something like this the ct.js way. For example, I would build a system where users write functions that take in "entities" and add behavior to them. Functions like that are common in "normal JS projects for the testability. In ct.js however I could picture that it would be better to create an edit window where a "this" is present that can be operated on. Or even "onCreate", "onStep", etc... tabs, similar to Types.


That being said, I am thinking a better way into ct.js development could be developing a simpler module first 🤔

HoverBaum avatar Feb 15 '21 21:02 HoverBaum

That's the problem with modules: they have poor (almost non-existent) tools for user-provided code.

So I made a brought sketch... I'm thinking of a separate asset editor for behaviors that mostly copies room and type code editors' layout (four tabs) plus allows adding customizable fields. The latter work in the same way as modules' extends: for example, ct.place adds collision group fields, and the upcoming ct.light adds four fields to pick a light texture and its appearance. Basically an extensions-editor tag for extensions-editor tags :rofl:

Behaviors should be exportable and importable: simply a dump of a yaml/json format will be enough. This allows easier development of behaviors inside modules (ct.IDE can load them as is), as well as effortless sharing to other developers.

The behaviors can be added through a type editor. (Maybe even a room editor :eyes: I think user-defined behavior is a more common practice for rooms rather than copies.) A button on the left opens a selector modal where you can add/remove behaviors from types, or jump straight into creating a new one.

The list of all user-defined behaviors are also listed inside project's settings. I think tiny buttons to edit behaviors inside that modal will be handy to skip another trip to project's settings tab.

изображение

Through the lens of the exporter and ct.js as a library, I think that a new API set similar to ct.actions is needed. There may be an object with behavior templates, which are simply four optional functions (Create — OnStep — OnDraw — Destroy). Stitching this all together inside the exporter should be trivial.

CosmoMyzrailGorynych avatar Feb 16 '21 06:02 CosmoMyzrailGorynych

Hey there 👋 I marked this issue as stale as it hadn't brought much attention for quite a while. I do understand that stale issues are still issues, yet here stale issues receive the least attention from maintainers so they can focus on more relevant tasks. You can help with this issue by checking whether it affects latest versions of ct.js and writing about it if it does, providing an example project and steps to reproduce. Or maybe you can close it with a PR! Note that some platform-dependent issues can't be resolved by developers due to the absense of such devices :c We will need help from you for such tasks. If this issue won't get attention in three months, it will be closed.

stale[bot] avatar Jul 30 '22 06:07 stale[bot]

This issue is now closed! Please create a new one with a proper template if this issue still affects ct.js.

stale[bot] avatar Oct 28 '22 14:10 stale[bot]