Typescript - Object EventHandlers and Subclasses - How to get the Subclass from the Instance Events

0 favourites
  • 9 posts
From the Asset Store
This is a single chapter from the "Construct Starter Kit Collection". It is the Student Workbook for its Workshop.
  • Howdy Folks,

    First of all, VERY happy to have typescript running instead of vanilla JavaScript!

    I spent today converting my existing project and I was able to convert most of it, with one exception!

    The Main Question

    In typescript, how can you add an EventListener binding and utilise the registered SubClass of your type?

    The Setup

    In my version, I am combining event sheets and scripts. Might as well let the power of construct just do its thing!

    So I have a timer on the Sheet, and that spawns the enemies, nice and simple.

    My Level Controller class is setup and destroyed in and out of the level layouts, and in that we bind and unbind the instancecreate event, shown below. The EnemyControl script is the InstanceClass for all items under the Enemy family, and thus happily resolves the method call in the event callback (or explodes if i forgot to register the type, but oh well).

    This is of course all managed through the sheer dark magic of javascript typeless making the objects function

    Level Control Class

    {
     SetupLevel = () => {
     this.runtime.objects.Enemy.addEventListener("instancecreate", this.SetupEnemy);
     }
    
     Teardown = () => {
     this.runtime.objects.Enemy.removeEventListener("instancecreate", this.SetupEnemy);
     };
    
     SetupEnemy = (e) => e.Setup(this);
    }
    

    The Enemy Subclass

    export default class EnemyControl extends ISpriteInstance
    {
     constructor(){...}
     
     setup(levelControl){
     this.OnArrive = levelControl.OnEnemyArrive;
     this.OnKilled = levelControl.OnEnemyKilled;
     }
    }
    

    The Problem

    And now to Typescript, and the same event handler code looks something like

    	 SetupEnemy = (e: ObjectClassInstanceCreateEvent<InstanceType.Enemy>) => e.instance.setup(this);
    

    However, the PROBLEM is that of course the Enemy Instance Type does NOT have the extended functionality of the EnemyControl subclass, and so fails to compile

    Changing the event to be the <EnemyControl> type of course makes the callback binding fail because it doesn't match the signature required

    SO I am somewhat stuck

    Brainstorming Solutions

    I have only come up with about two options

    1. Move my Tower Controller class into a Global access scope and then just utilise it in the constructor of EnemyControl
    2. change the <T> on the event to be any - SetupEnemy = (e: ObjectClassInstanceCreateEvent<any>) => e.instance.setup(this); This at least does not appear to give me errors on compile!

    I am fairly certain that the <any> will work, as really that falls back to older javascript processing, I just cant say I like it. But I hate it less than the globaling.....

    Any ideas or things I missed please let me know!

  • It's difficult to help without seeing all your code, but one mistake I can spot right away is where you have class EnemyControl extends ISpriteInstance, it's incorrect to extend from ISpriteInstance - you should extend from its InstanceType class, e.g. InstanceType.Enemy.

  • It's difficult to help without seeing all your code, but one mistake I can spot right away is where you have class EnemyControl extends ISpriteInstance, it's incorrect to extend from ISpriteInstance - you should extend from its InstanceType class, e.g. InstanceType.Enemy.

    Sorry yes, something that I now realise wasn't clear was that those code snips are the older JS code, which was meant to show what I was converting from

    So updating that particular code snip to the Angular version

    Enemy Control

    export default class EnemyControl extends InstanceType.Enemy {
     OnArrive: EnemyControlEvent | undefined;
     OnKilled: EnemyControlEvent | undefined;
     constructor() {
     super();
     this.behaviors.MoveTo.addEventListener("arrived", this.onMoveToArrive);
     }
    
     onMoveToArrive = (e: BehaviorInstanceEvent<this, IMoveToBehaviorInstance<this>>) => {
     
     if (e.behaviorInstance.getWaypointCount() <= 0) {
     this.OnArrive!(this);
     this.destroy();
     }
     };
    
     setup(levelControl: TowerLevelControl) {
     this.OnArrive = levelControl.OnEnemyArrive;
     this.OnKilled = levelControl.OnEnemyKilled;
     }
    ...
    }
    

    Level Control

     SetupLevel = () => {
     this.runtime.objects.Enemy.addEventListener("instancecreate", this.SetupEnemy);
    ...
    }
    
    Teardown = () => {
     this.runtime.objects.Enemy.removeEventListener("instancecreate", this.SetupEnemy);
    ...
     };
    
     SetupEnemy = (e: ObjectClassInstanceCreateEvent<EnemyControl>) => e.instance.setup(this);
    

    with the output error of -

    Argument of type '(e: ObjectClassInstanceCreateEvent<EnemyControl>) => void' is not assignable to parameter of type '(ev: ObjectClassInstanceCreateEvent<Enemy>) => any'.

    Types of parameters 'e' and 'ev' are incompatible.

    Type 'ObjectClassInstanceCreateEvent<Enemy>' is not assignable to type 'ObjectClassInstanceCreateEvent<EnemyControl>'.ts(2345)

    Full code is available here - https://github.com/FictionalAroma/CyberTower/tree/typescript-convert

  • The GitHub link doesn't work (perhaps it's a private repository). The quickest way to help would be to share some minimal code that demonstrates the issue in the form of an actual project that can be opened up in VS Code and experimented with.

  • Damnit I thought it was already!

    It is public now, and I'll throw up a better example when I'm able

    Sorry for being a pain!

  • Try Construct 3

    Develop games in your browser. Powerful, performant & highly capable.

    Try Now Construct 3 users don't see these ads
  • I downloaded the project, did 'Set up TypeScript', opened it in VS Code, and started watch mode. It reported 0 errors. So I don't see any problem.

  • Heya, apologies for the delay!

    So I have taken the Ghost Shooter Typescript demo and added in samples to show the example of what I was finding

    Ghost Demo Updated

    The summary of the changes are -

    Monster.TS => Added a OnCreated method to use (it just sets a property, what it does isn't relevant here)

    Main.ts => added the 3 flavours of functions

    runOnStartup => Added bindings for "instancecreate" on the Monster object type

    The reason everything went fine in the github main project was because I used <any> on the typing of the callback event.

    However this overall just felt hacky and was trying to find the correct method

    Thanks again for the help!

  • OK thanks - I see the problem now.

    Ultimately this comes down to the fact runtime.objects.Monster has a static type of IObjectType<InstanceType.Monster>, which refers to the base class rather than the instance subclass. That base class is ultimately propagated to all its event handlers.

    It's difficult to static type this correctly as from the perspective of TypeScript, setInstanceClass is a runtime method and so can't be used to affect static typing.

    I think this is just one of those cases you have to override TypeScript's types as you know more than TypeScript does. There's a couple of ways around it. Probably the most straightforward is you can just use as to cast it to the right type, e.g.:

    runtime.objects.Monster.addEventListener("instancecreate", e => DoMonsterSpawnedFunction4(e.instance as MonsterInstance));
    
    // ...
    
    const DoMonsterSpawnedFunction4 = (inst: MonsterInstance) => {
     inst.OnCreated();
    };
    

    I'd recommend the above approach, but another possible approach is to make your own reference to the object type with the correct type IObjectType<MonsterInstance> - and then using addEventListener will propagate the type MonsterInstance down to the event handler, e.g.:

    const MonsterInstanceType = runtime.objects.Monster as IObjectType<MonsterInstance>;
    MonsterInstanceType.addEventListener("instancecreate", DoMonsterSpawnedFunction2);
    

    So yeah, basically just override the type for this case.

  • Well today I learned TS can just dynamic cast down the inheritance tree!

    I'm mainly dotnet and that really doesn't like you doing that so didn't occur!

    Just Cast It works!

    Thankyou very much!

Jump to:
Active Users
There are 1 visitors browsing this topic (0 users and 1 guests)