Post-Jam Feedback: Scripting

#1

I’m still compiling my feedback from the jam (I have a lot :smile: ) but I felt that the scripting workflow in particular could use some good discussion, so I’m posting my thoughts here. I’m really excited about how far Wick has come and its potential as a very robust animation and game development tool, and resolving the issues below would be an important step in realizing that. :slight_smile:

Note that I’m not a JavaScript expert, so please correct me if some of these are points are inaccurate or have alternate solutions.

In summary, the way Wick handles scope and context can be difficult to deal with.

  • Functions defined like below in one code tab or one frame of a timeline should be accessible in others. They would attach to the parent clip, or root if there is none.

    function foo () {
        console.log("hi!");
    }
    //in the next frame...
    foo(); //error
    
    • Currently, I have to do this:

      this.foo = function () {
          console.log("hi!");
      }
      this.foo();
      
  • Functions sometimes refer to the wrong scope. In the example file ParentAndChild, there is a Clip named Clip and a child clip inside it.

    • //ATTACHED TO ROOT TIMELINE
      //(OR ATTACHED TO THE PARENT CLIP ITSELF, SAME RESULT)
      Clip.foo = function () {
      	Clip.x += 100;
      }
      
    • //INSIDE CHILD CLIP's TIMELINE
      this.parentClip.foo();
      //returns an error (Clip is not defined) because there is no
      //Clip called "Clip" around to increase the x value of.
      
    • When I call this, I want it to run from Clip's scope, not its child’s scope. (Naming the parent clip something else doesn’t change the result.)

      • Note that replacing the first snippet with this.foo = function () { this.x += 100; } and moving it to the Clip itself works as intended; the parent object shifts its position.
  • Undeclared variables can cause trouble. (These are variables that are never declared with var or as a property of an object. They automatically attach to Window and behave differently than other variables.)

    • If I, in a root timeline script, define an object like so:

      Foo = {};
      Foo.bar = function () {
          rootTimelineClip.play();
          console.log(otherRootTimelineClip.identifier);
      }
      
    • then call it within rootTimelineClip like this: Foo.bar();, weird errors can happen because it is running from the Clip’s scope rather than the root timeline’s scope.

      • Case in point: in the example file “ProjectStops2”, I have some functions defined like this in Frame 1 of layer “Actions”:
    Game = {
         //stuff
     };
     Game.caption = function(text) {
         alert("Setting caption to \""+text+"\"");
         Caption.text.setText(text);
         alert("all done");
     }
     
     Game.guessLetter = function(letter) {
         alert("Game.guessLetter("+letter+") is running");
         Game.caption(letter);
     }
    
    • In frame 3 of the Clip “Menu”, I have this code, which runs both of the functions above. However, they silently fail without throwing errors, probably because they are accidentally being executed from a lower scope.

    • if (Utils.isStringLetter(key)) {
          alert("About to run Game.guessLetter(key). key = "+key);
          Game.guessLetter(key);
          alert("THIS alert() NEVER GETS RUN FOR SOME REASON");
      }
      
    • Here’s what happens when I run this project:

      • The alert box says Setting caption to some unrelated introductory text, since I call Game.caption when the frame is first entered. The Caption object’s text is set successfully.
      • Alert box: all done
      • I click the pink button (Menu) and type the letter “a”. (Any letter of the alphabet will do.)
      • Alert box: About to run Game.guessLetter(key). key = a
      • Alert box: Game.guessLetter(a) is running
      • Alert box: Setting caption to "a"
      • Nothing changes on-screen. The caption is never set.
    • Solutions

      • What if Wick automatically attached undeclared variables (like Foo above) to the Clip that is calling the function? So if a Clip called myClip calls myVariable = 3, Wick will check if there is currently a myClip.myVariable defined previously. If not, it will treat the statement as though you typed this.myVariable = 3. (This applies whether the script is attached to the Clip or on its internal timeline.) For the root timeline, it attaches to root or project.
      • You could also ban undeclared variables – meaning, Wick throws an error when it finds one. I’ve read that they are bad coding practice anyway; they can cause hard-to-find ambiguities in code, and since they attach to Window they can easily overwrite (or be overwritten by) other code. However, this means that users must make sure to declare every variable they use. (Like this: var foo; and I think this.foo; counts too.)
    • I used a ton of undeclared variables in my jam game before I found out how dangerous they were. Almost every game-state variable is attached to one of 3 big global objects: Game, Match, and Utils, which meant I had to type stuff like Match.blahblahblah every single time I wanted to access something. I would like to avoid this problem next time and be able to write less verbose code. (See below.)

  • When I was scripting inside Clips’ timelines, I felt like I had to use the this keyword constantly.

    •   //Most of this is real code, some has been adjusted for clarity
        this.clipLength = 30;
        //Number of times this clip has passed on a potential puzzle
        this.numPasses = 0;
        this.visited = [];
        this.qna = {};
        this.qnas = [];
        
        this.loadPuzzle = function(variant) {
            this.stop();
            this.numPasses = 0;
            //mark that you've been here
            this.visited[this.currentFrameNumber] = true;
            if (variant !== undefined) {
             this.qna = variant;
           } else {
               this.qna = this.qnas[random.integer(0, this.qnas.length-1)];
           }
       }
      
    • Could Wick’s script execution be adjusted so that instead I can put:

      •   var clipLength = 30;
          //Number of times this clip has passed on a potential puzzle
          var numPasses = 0;
          var visited = [];
          var qna = {};
          var qnas = [];
          
          function loadPuzzle(variant) {
              stop();
              numPasses = 0;
              //mark that you've been here
              visited[currentFrameNumber] = true;
              if (variant !== undefined) {
                  qna = variant;
              } else {
                  qna = qnas[random.integer(0, qnas.length-1)];
              }
          }
        
      • …And still have other Clips be able to access clipLength and the other variables?

  • I’d also like the ability to use my browser’s dev console to print the root timeline and its children. Currently, I can’t find where it stores the hierarchy of Clips that you access through scripting. Perhaps there could be an object attached to Window called WickRoot or something, which contains all the child Clips, named according to their identifier. Then users could easily read their values and debug them whenever they want - no need to implement a custom debugger tool! (For example, type WickRoot.object1.x) to get object1’s x value.)

  • I also want to be able to run functions like setTimeout from the scope of a Clip. Currently, it throws reference errors, probably due to incorrect scope (from Window rather than from the WickObjects). Wick could have a setTimeout wrapper function to work around this.

    • This script, attached to the root timeline, logs an error to the browser console: "ReferenceError: gotoAndStop is not defined"
       setTimeout(() => {
           gotoAndStop(2);
       }, 1000);
      

Example Files
BugReport_ParentAndChild.wick (1.8 KB)
BugReport_ProjectStops2.wick (432.1 KB)
BugReport_FooBetweenFrames.wick (1.9 KB)
BugReport_SetTimeOut.wick (1.7 KB)

Wow, that was a lot. I’ll be posting more scripting-related feedback later - mainly about the UI and missing features like Find & Replace. I encourage everyone to leave your own feedback, issues, or potential solutions here!

2 Likes

#2

Script editor

Layout
  • It would be helpful if built-in keywords (such as .x, lerp, and project) were consistently highlighted so users don’t accidentally overwrite them.
  • Having the script editor in a floating window can be pretty unwieldy. But I have a potential solution. I haven’t used Godot Engine much, but I like the way it switches between the “stage” and the scripting view. You can easily access all your objects in either view. (This screenshot is an example project.)
  • I think this idea could work great for Wick, especially for mobile devices. Here’s a mockup of what that could look like:
Features
  • AutoComplete
    • This would be AMAZING for speeding up the coding workflow :heart_eyes_cat: It would allow you to see all the functions and variables of your objects.
    • Esprima already has this feature (https://esprima.org/demo/autocomplete.html)
  • Find and Replace
    • Search entire project
    • Ace already supports Find and Replace, but it doesn’t work. It gives errors when I press Ctrl-F:
      1563067845603
      • Newest github version of the editor gives these errors:
        image
  • List of scripts in project (see above)
  • Rename Symbol (like in VS Code - rename any variable or method everywhere)
    • Esprima already has this feature https://esprima.org/demo/rename.html
    • This is definitely a nice-to-have rather than a must-have, especially since there’s no find and replace yet!
  • Clean up code (format it correctly)
    • Esprima’s got you covered
    • Also not a super high-priority feature, but it could help beginners

Event handlers

  • Do we need both Default() and Load()? Default() on a frame is called every time a frame is entered, even though its description is “Once, before all scripts”. It seems to behave the same as Load().
    • Default() is automatically added to all Clips, Buttons, and frames. Maybe Load() should be added instead.
  • I think it would be helpful to have an onEvent handler that is called when the frame is entered for the first time. Users would be encouraged to set their variables here. Default() could be changed to do this, or a new handler could be added.
  • In summary, I would recommend this:
    • Default() is deprecated and removed from the UI’s list of scripts to add. However, it still works, for compatibility with old projects.
    • Load() is now the event automatically added to all objects, since it behaves the same as Default() (does it?)
    • To replace Default(), a new event called Preload() is run before Load(), but only once – the first time the frame is entered – unless the frame is reset.
    • EDIT: It would be helpful if Load() and Default() were only called AFTER all objects in every frame have finished their Preload() actions.

Misc

(I might have already posted these somewhere before, idk)

  • Playing a project and then opening the script editor should preserve its undo history
  • It would also be helpful if the script editor saved your place between opening and closing it so you don’t have to scroll. I’ve gotten a bit lost in my own code a few times, lol
3 Likes

#3

Hey this is all really great feedback!! I’m especially digging the the Hierarchy view you’ve mocked up here, that’s something the editor is really going to need and your design is a great place to work from.

For the next few weeks we’re mostly focusing on drawing and animation improvements, but I’m saving this thread for when we do a deep dive into coding in the near future!

3 Likes

#4

That sounds great, thanks! :smile:

I also investigated a couple more issues:

  • parent returns a Clip, while this.parent returns a frame. This is due to a bug in Tickable.js:
    • window.parent = this.parentClip;
    • This code only affects window.parent, so this.parent and Clip.parent will behave differently. They inherit their value from Wick.Base.
    • Solutions
      • Rename Wick.Base.parent everywhere to a less generic title, like parentBase, in order to avoid confusion. Then replace it with a value that returns the same as Wick.Base.parentClip.
      • Or, change window.parent here to window.parentClip and discourage the use of parent in Wick code.
  • Timeline functions have a confusing context bug.
    • If you run gotoAndPlay(); attached to a Clip, it will make its parent play.
    • If you run it on gotoAndPlay(); on a Clip’s internal timeline, it will make that Clip play.
    • That’s simple and intuitive, but here’s the bug:
      – Root timeline: Clip.foo();
      – Clip’s timeline: this.foo = function() { stop(); }
      Result: The parent will stop, but the CHILD should be the one stopping.
1 Like