Creating a script that runs itself?

(function() {
    console.log("This function will always run.");
})();

This is quite a nifty trick. With my root game object becoming bloated with handler scripts (e.g. InputManager) I would love to convert these.

The only thing stopping me is that these scripts still need to update every frame. I was wondering: can I still couple code to this update call, delta time included?

Short answer is yes, you can. You can check the PlayCanvas engine-only examples to see how they are hooking into the application’s update event:
https://playcanvas.github.io/#animation/blend.html

However, I would not recommend this, as it sounds you have a question on scripts management and organization. IIFE functions are better suited for a one-time run-and-forget function calls, like when you want to patch the engine with your custom logic before the app starts, or when you want to initialize a third party library.

What I usually do is I create some entity, specifically for scripts and add all my game logic there. You can have children entities under it for better organization of those scripts. For example:

Root
|-
|- 
|- Logic
   |- Managers
   |- Debug
   |- UI
   |  |- ...
   |- Gun
   |- ...

Organizing the scripts in a scene hierarchy allows the PlayCanvas engine to handle their lifetimes and resources management, which you otherwise would have to do manually and can lead to issues that are hard to maintain.

1 Like

That’s good to keep in mind. However, if I use the managers only to fire inputs or hold read only data, will I be okay? I don’t use two way coupled Managers (never liked them), so all my code does is read from it, never write to it.

I also wrote a quick test out of curiosity, but it didn’t work. Is this right?

(function() {
    var canvas = document.getElementById("application-canvas");

    // Create the application and start the update loop
    var app = new pc.Application(canvas);
    
    app.start();
    app.on('update', updateTest);
})();

function updateTest(dt) {
    console.log('dt is ' + dt);
}

Edit:
It only runs if I set the loading type to asset (I previously had it on “After Engine”), but it breaks otherwise functional scripts.

Well, the way you organize the data in your code is essentially your choice of preference. I might have an opinion, but it would be my own preferring. You will be fine either way, if you handle the data properly. Whether it is done by managers or not is not really an issue and either way is valid.

I can share my experience. After developing a number of projects, I came up with a concept of Sequences, that worked well for me so far. The principle is in categorizing the functionality under own areas of responsibility. Each area is then handled by a Manager, where each task within that area is called a sequence.

For example, you are making a basketball game, where you are throwing a ball into a basket. We can define following managers that handle the areas of interest:

  1. Ball Manager - anything related to ball, like its flying trajectory, spawning, physics, changing model, textures, etc.
  2. Basket Manager - anything related to basket, like physics and tracking where the ball hits the basket, tracking when a ball passes some area that gives a point, spawning the basket, etc
  3. Input Manager - anything related to game input, like tracking input start, end, drag, mouse, keyboard, joystick, etc
  4. General Manager - tracks the game state, like score, loading procedures, connections to backends, etc
  5. UI Manager and so on…

All the communication between Managers and between a Manager and its Sequences are done via events. The General Manager would be the main one and know when the game is loaded. It also knows when other Managers have loaded and initiates the game. Once initiated all Managers do the stuff they are responsible for.

For example, on game init event from the General Manager

  • Ball Manager would start a ball spawn Sequence, which finds the ball in the scene hierarchy, clones it and positions it in front of camera. Once done, it reports back to Ball Manager. Ball Manager knowing that the ball has spawned, starts the ball aiming Sequence, which based on input from Input Manager adjusts ball position.
  • Input Manager would start screen input Sequence, which tracks screen touches for example

The key takeaway here is that each sequence has an explicit start and an end. This allows a Manager to run them sequentially (hence the name), one after another. Like with the ball spawning example, where the aiming sequence starts only after spawning sequence. Manager in this case doesn’t do anything, except managing the sequences and their order of execution. As such, in order for this framework to work properly, I defined 3 rules:

Rules:

  1. Each Manager can only listen to another Manager or its own Sequences. Never on Sequences that do not belong to them.
  2. Each Sequence can listen on its siblings or its Manager. Never on other Managers or their Sequences. The only exception can be the input events from Input Manager, which is acceptable to be listened by other Sequences.
  3. Each Sequence must have an explicit start and an explicit end.

Explanation:

  1. Without following these rules, it will quickly become hard to maintain and know which Sequence called this function. When you are strict about who can talk to whom, then it is easier to debug later. For example, if the ball is flying awkwardly, it must be the flying Sequence having some issue and since nobody can talk to it except Ball Manager, you already half-solved the problem by isolating it.
  2. This one complements rule 1.
  3. This is important. A Sequence must not have a start without an end - only Managers have such characteristic. Most of them are very short lived and focusing on doing one specific thing. Again, the input sequence is an exception in the most of the smaller games, where you never end it and it always tracks the inputs. This quality guarantees that sequences can be run sequentially. It also allows to bind/unbind events, based on whether a sequence is currently active. For example, if the ball has not spawned, there is no need to listen on input events in the aiming sequence, which is currently inactive until the ball is spawned. This rule helps with the performance optimization as a bonus, since only currently required Sequences are active and doing their stuff.

As for your comment about read-only data, here each Sequence is responsible for handling its own data. For example, the aiming sequence could create position vector at initialization, and when it activates, use it by updating it based on input from Input Manager.

On bigger games, with additional tools like rStats, you can actually see which Sequences are active at the moment, their life times, performance measurements, etc:

3 Likes

This must be the definition of a top comment.

Thanks for sharing all this wisdom with me, I love it! I think you should make this its own forum and / or blog post with an example, because it sounds great. And, well… I don’t want it to go to waste. To tell you the truth, our project will conclude in 2 months time so a complete restructure like this is not necessary for this game ('d be fun though) and after that I’m going back to Unity. We choose PlayCanvas for its web performance, iteration and VR compatibility. And, as I learned, its great community.

Perhaps I’ll come back to PlayCanvas one day. For that day, your comment is bookmarked.

1 Like