Planning base classes in AGS script

Started by Crimson Wizard, Fri 14/02/2025 22:25:52

Previous topic - Next topic

Crimson Wizard

I've started experimenting with necessary changes to the engine that would allow to have all script objects to have same parent class(es). There's a number of technical difficulties, but I won't discuss them here, they are related to historical "dirty" architecture of AGS engine and not interesting to anyone except engine developers. What I'm interested more is to discuss what these parent classes could look like in script.

The following is just my current thoughts, not a final plan.

I suppose that the problem of choosing parent classes is a problem of defining which qualities and roles do particular types have and share. If many types share same roles then we may create a base class for those roles. If there are types among them which share only a part of these roles, but others do not apply to them, then there may be a need to split the base class into two classes, one inheriting another, where first base class has role A and second roles A and B, and types that require only role A would use the first base class and those that require both would use the second. On another hand, sometimes it may seem that certain role is just too "small" and splitting another base class may not be convenient. In which case probably we could live with the fact that certain property is inherited by types that do not use it in any practical way.

In game engines there's another approach to this problem, called Entity-Component system, where functionalities are spread onto separate "components", which are then attached to a object or type of an object. Unfortunately, it's not clear whether this will be possible to do in AGS, but likely it will be more difficult to achieve than direct inheritance.

Keeping this approach in mind, which roles may we have for types in AGS script?

I'm not sure if all of the script structs should have a parent, although they could have, similar to "object" in C# or Java.
If so, we may consider that to be a role of "any builtin object", that is any object created by the engine, as opposed by types declared by user.
(Although, I suppose, one could invent a way to let managed user structs inherit some predefined base class too, maybe even automatically, but I won't touch this topic here.)
If I think about one, I can only see it having a property which returns its type name, useful for diagnostic purposes, and perhaps for casting too (although there could be builtin script syntax for testing that). Another thing that it could have is a check for validity, in case it may be disposed before all script references are gone: this is already a thing with DynamicSprite (can be Deleted), Overlay (can be Removed) and File (can be Closed).
Unfortunately, "Object" name is already taken by the room objects, so I'd use something like "Entity" for this:

Code: ags
builtin managed struct Entity {
    readonly attribute String TypeName;
    readonly attribute bool   Valid; // name taken from Overlay, but may be Disposed
};

Then, there are classes that are just utility ones, like File or DrawingSurface that exist only temporarily; and then there are classes that play a role, I'd say, of a "game element". These are parts of a game that take certain place in a game scene, whether a global one or a room, they have a prolonged lifetime, and may be saved in a game state. They have to be uniquely identified (have a script name), and may have custom properties attached to them, letting user to configure them for custom scripts. I think that these may have their own parent class, which I call "GameEntity":

Code: ags
builtin managed struct GameEntity extends Entity {
    readonly attribute String ScriptName;
    
    // Functions or properties for dealing with custom properties
    function int GetProperty(name);
    function void SetProperty(name, int);
    function String GetTextProperty(name);
    function void SetTextProperty(name, string);
};

Note that I don't use numeric ID here in this base class. The reason is that, unlike ScriptName, which is globally unique, numeric IDs are unique only among objects of same type. Therefore using ID from a base class makes no sense IMO, since you won't know which array to put it in, or which other IDs to compare with.

Then, there's an other distinction: objects that imply continuous "behavior" in the game (i was not sure how to put it in a better way).
The way I see this, there are types that do not really "act" on their own, but instead either represent "assets" which can be instanced by command, or are used with other types. The examples are: AudioClip (that actually creates a playback instance, but is not active itself), or Dialog (that represents a game substate, again creates a dialog instance when run), or View (that is a collection of animations applied to other objects), or InventoryItem (which is an abstract thing which characters hold in their bags).
And then there are types that actually do "act" and affect the game just as they enter the scene: being drawn, interacting with each other, etc: Characters, Objects, various areas and regions in the room, GUI and Overlays.
The common quality of this kind is that they may require a way of turning them on and off (having "enabled" state).
Arguably, they also look like all having a "position" in the scene, but I don't know if that may be a coincidence. Some of the types mentioned above sort of do have a location, but they do not allow to change that at runtime (masked regions).
In theory, I suppose, if we ever support Parent->Child node like relation between objects in game, then this base type may also host Parent/Child references.

I'd call this base type the "Actor", because it's "acting" in a scene:

Code: ags
builtin managed struct Actor extends GameEntity {
    attribute bool Enabled;
    // Fantasy attributes for parent->child relation:
    // attribute Actor* Parent;
    // attribute Actor* Children[];
};

Finally we're getting for things that have certain position in a scene space and may be moved around. Or rather they have full set of "transform" properties: position, scale and rotation. Note that these do not have to be visible, have a graphical representation. A theoretical example of an object that has a position but does not have a view can be a "marker" or "emitter" type of object: something that just marks a spot, but does not display a thing. Another example is an object that does not have a graphic, but displays something else, like Camera and Viewport.

I have no idea how to call this properly, so I named it silly as a "Thing" for now:
Code: ags
builtin managed struct Thing extends Actor {
    attribute int X;
    attribute int Y;
    attribute float ScaleX;
    attribute float ScaleY;
    attribute float Rotation;
};

And then finally we have 2 roles: a visual object which has all kinds of additional visual effects (such as transparency, blending etc) and object that may be interacted with by clicking. We do have visual objects that cannot be interacted with directly (like Overlays). But I don't know if it's worth or not separating these.

Code: ags
builtin managed struct GraphicObject extends Thing {
    attribute bool Visible;
    attribute int Transparency; // or Alpha
    attribute BlendMode BlendMode;
    // etc
    
    // For interactive objects
    attribute bool Clickable;
};

But like I mentioned earlier, having that amount of base types may not be convenient for AGS. Few of the above could be merged, while reserving an option to split them later. For example, Entity and GameEntity could be merged, if we don't want base class for everything (e.g. types like File or DateTime could go without a parent class).

Then there's a problem of adding Verb Interactions to this hierarchy. The objects that require these are Hotspots, Objects, Characters and InventoryItems, and first three may even be parents to each other, or share same "Interactable With Verbs" base class. But InventoryItem is a distinct case, because it does not have a position in the scene, it's only drawn within a gui, so it cannot share a base type that has position. One way to solve this is to move Verb Interactions to a higher parent class.

Crimson Wizard

On a side note, but relevant, we have practically completed a pointer type cast feature that allows to downcast from a parent type pointer to a child type pointer using script alone.
https://github.com/adventuregamestudio/ags/pull/2693
https://github.com/adventuregamestudio/ags/pull/2665


This means that you don't need to use properties like GUIControl.AsButton, but write "myControl as Button", and that will work for both builtin and user managed structs alike.

SMF spam blocked by CleanTalk