Lagging Gameplay

Hey,

I’m using Twine 2.3.5, running Sugarcube 2.33.2. I have combined and included two P5.js sketches in my game’s JavaScript file. One is an animation and the other a graphic that maps onto image files stored on a server. I call them in various passages, of which I have around 50 so far - I need to include another 30, probably.

Everything works fine and they run beautifully when I test them. But when I play my story from the beginning, all graphics and text starts to lag around the seventh passage before getting progressively worse.

Here is my js code:

//////////sets up mifir land sketch and Uma people sketch/////////////

window.setup = window.setup || {};
window.setup.p5Loaded = false;
//window.setup.dotGUILoaded = false;

(function(){
  var p5 = document.createElement("script");
 // var dotGUI = document.createElement("script");

  p5.type ="text/javascript"; 
 // dotGUI.type = "text/javascript";

  p5.src = "https://cdn.jsdelivr.net/npm/p5@0.8.0/lib/p5.js";
 // dotGUI.src = "https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.6.5/dat.gui.min.js";

  p5.onload = function() {
    window.setup.p5Loaded = true;
  };

  /*dotGUI.onload = function() {
    window.setup.dotGUILoaded = true;
  };*/

  document.head.appendChild(p5);
 // document.head.appendChild(dotGUI);
}());

const UMA = function(imgPath){ 
  return function(p){
    //Variables
    p.img;

    p.segments = [];
    p.segmentLength;
    p.segmentSpacing;

    p.numSegments = 40;
    p.segmentResolution = 400;
    p.heightScale = 150;

    p.fadeIn = 0;
    p.speed = 0.51;

    p.mp = false;
    p.mouseIsDown = false;
    p.loaded = false;

    p.tt = 0;
    p.glitchFreq = 0.5;
    p.now = 0;
    p.lastTime = 0;
    p.id = 0;

    p.preload = function(){
      p.loadImage(imgPath, function(_img) {
        p.img = _img;
        p.img.loadPixels();
        p.loaded = true;
        p.segmentSpacing = (p.img.height / p.numSegments);
        p.segmentLength = p.img.width;
    
        for (let i = 0; i < p.numSegments; i++) {
          p.segments.push(new p.Segment({
            pos: { x: 0, y: i },
            len: p.segmentLength
          }));
        }
      });
    }

    p.setup = function(){
      p.createCanvas(p.windowWidth*2, p.windowHeight/1.5);
    }

    p.update = function(dt){
      p.segments.forEach(
        s => {
          s.update(dt)
        }
      );
    }

    p.mousePressed = function(){
      p.mouseIsDown = true;
      p.glitchFreq = 0.3;
    }

    p.mouseReleased = ()=>{
      p.mouseIsDown = false;
      p.glitchFreq = 0.5;
    }

    p.draw = function(){
      if(!p.loaded)
        return;
      
      let dt = (p.millis() - p.lastTime) / 1000;
      p.lastTime = p.millis();
      p.tt += dt;

      p.update(dt);

      p.fill(0, 250);
      p.noStroke();
      p.rect(0,0, p.windowWidth, p.windowHeight);

      let gridHeight = p.numSegments * p.segmentSpacing;	

      p.push();
      p.heightScale = 60 + (p.sin(p.millis()/1000) / p.TAU) * 50;
      p.translate(p.windowWidth / 2 - p.img.width, p.windowHeight / 2 - p.img.height / 2 - 100);
      p.scale(2, 1.25);
      
      if(p.mouseIsDown){
        p.tint(156, 190, 48);
        p.image(p.img, 0, 0);
      }
      else{
        p.tint(255);
      }

      let trans = false;

      let ss = 0;

      p.segments.forEach(s => {
        ss++;
        let n = 0;

        if (ss > 30) {
          n = p.noise(p.millis() / 200);
        }
        
        s.draw();
      });

      if (trans) {
        p.pop();
      }
    
      p.pop();
    }

    p.Segment = class Segment {
      constructor(cfg) {
        Object.assign(this, cfg);
    
        this.t = 0;
        this.dist = 0;
        this.id = p.id++;
        this.nextNoise = 0;
        // [x,y, x,y, ...]
        // add 2 for a point starting at the very start of the viewport and one of the end.
        this.vertices = new Float32Array(p.segmentResolution * 2 + 2);
    
        this.update(0);
      }
    
      update(dt) {
        this.t += dt * 1;
        p.fadeIn += dt * .008;
        p.fadeIn = p.constrain(p.fadeIn, 0, 1);
    
        let xSpacing = this.len / p.segmentResolution;
    
        this.pos.y -= dt * p.speed;
        this.dist += dt * p.speed;
        //if (this.pos.y > img.height / segmentSpacing) {
        //  this.pos.y -= img.height / segmentSpacing;
        //}
        if (this.pos.y < 0) {
          this.pos.y += p.img.height / p.segmentSpacing;
        }
    
        for (let i = 0; i < this.vertices.length; i += 2) {
    
          let x = (i / 2) * xSpacing;
          let y = this.pos.y * p.segmentSpacing;
    
          if (p.mp) {
            let d = dist(x, y, (p.mouseX - p.windowWidth / 2 + p.windowWidth / 4 + 50) / 2, (p.mouseY - p.windowHeight / 2 + p.windowHeight / 4 + 250) / 2);
            if (d < 40) {
              let a = (8 / d) * 8;
              y += (a > 50) ? 50 : a;
            }
          }
    
          let col = p.img.get(x, y);
          let intensity = col[0] / 255;
    
          this.vertices[i + 0] = x;
          this.vertices[i + 1] = y - (intensity) * p.heightScale;
        }
    
      }
    
      draw() {
        p.strokeWeight(1);
    
        let waves = (p.sin(this.pos.y / 2.0 + this.t ) / p.PI) + 0.5;
        let vignette = p.sin(this.pos.y * p.segmentSpacing / p.img.height * p.PI);
        p.stroke(255, 255 * p.fadeIn * waves * vignette + 50 * vignette);
        // fill(0);
        p.noFill();
    
        p.beginShape();
        for (let i = 0; i < this.vertices.length; i += 2) {
          if (i === 0) {
            p.vertex(-1000, this.vertices[i + 1]);
          } else if (i + 2 === this.vertices.length) {
            p.vertex(10000, this.vertices[i + 1]);
          } else {
            p.vertex(this.vertices[i + 0], this.vertices[i + 1]);
    
            if (this.id < p.numSegments - 1) {
              let s = p.segments[p.id + 1];
              p.vertex(this.vertices[i + 0], this.vertices[i + 1]);
            }
          }
        }
        p.endShape();
      }
    }
  }
}
////////////////////////////Mifir sketch////////////////////////////////

const Mifir = function(p){
    p.scale = 20;
    p.cols;
    p.rows;
    p.w = 1400;
    p.h = 1000;

    p.flightPos = 0;
    // let flightSpeed = 0.08;
    // let noiseDelta = 0.16;
    p.terrain = [];
    // let terrainHeight = 112;

    p.Controls = function() {
        this.flightSpeed = 0.04;
        this.noiseDelta = 0.16;
        this.terrainHeight = 100;
    };

    p.controls = new p.Controls();

    p.setup =function() {
        // createCanvas(displayWidth, displayHeight, WEBGL);
        
			
			p.createCanvas(1400, 720,p.WEBGL);
			//p.createCanvas(1280, 720,p.WEBGL);

        /* let gui = new dat.GUI({width: 295});
       gui.close();
        gui.add(p.controls, 'flightSpeed', 0, 0.4).name("Flight speed").step(0.02);
        gui.add(p.controls, 'noiseDelta', 0.05, 0.4).name("Noise delta").step(0.01);
        gui.add(p.controls, 'terrainHeight', 0, 200).name("Terrain height").step(1); */

        p.cols = p.w / p.scale;
        p.rows = p.h / p.scale;
        
        for (let x = 0; x < p.cols; ++x) {
            p.terrain[x] = [];
        }
    }

    p.draw = function() {
        p.flightPos -= p.controls.flightSpeed;
        p.shiftNoiseSpace();

        p.background(0);
        p.stroke(255);
        p.noFill();

        p.rotateX(p.PI / 3);
        p.translate((-p.w / 2) + 1, (-p.h / 2) + 30);

        for (let y = 0; y < p.rows - 1; ++y) {
            p.beginShape(p.TRIANGLE_STRIP);
            for (let x = 0; x < p.cols; ++x) {
                p.vertex(x * p.scale, y * p.scale, p.terrain[x][y]);
                p.vertex(x * p.scale, (y + 1) * p.scale, p.terrain[x][y + 1]);
            }
            p.endShape();
        }
    }

    p.shiftNoiseSpace = function(){
        let yOffset = p.flightPos;
        for (let y = 0; y < p.rows; ++y) {
            let xOffset = 0;
            for (let x = 0; x < p.cols; ++x) {
                p.terrain[x][y] = p.map(p.noise(xOffset, yOffset), 0, 1, -p.controls.terrainHeight, p.controls.terrainHeight);
                xOffset += p.controls.noiseDelta;
            }
            yOffset += p.controls.noiseDelta;
        }
    }
}

function GenerateSketch(sketch,canvasID)
{
    let newSketch = new p5(sketch,canvasID);

    console.log("Sketch created with name: " + canvasID);
	
	return newSketch;
}

function GetSketch(ImgPath, SketchType)
{
    switch (SketchType) {
        case "UMA":
            return UMA(ImgPath);
            break;
        case "Mifir":
            return Mifir;
    }
}

function CheckDependeciesStatus()
{
    if(!window.setup.p5Loaded || typeof p5 !== 'function' /*|| !window.setup.dotGUILoaded || typeof dat.GUI !== 'function'*/)
        return false;
    else
        return true;
}

let ProcessDependecies = function(imgPath, canvasID, sketchType)
{
    console.log("Starting to create sketch!");
    
    if(!CheckDependeciesStatus())
    {
      console.log("Dependecies are not loaded, waiting for them to laod...");
      
      let waitForDependecies = setInterval(() => {
        if(CheckDependeciesStatus())
        {
          console.log("Dependecies are loaded, creating sketch");
          clearInterval(waitForDependecies);
            
          let sketch = GetSketch(imgPath,sketchType);
          return GenerateSketch(sketch,canvasID);
        }
      }, 100);
    }
    else{
        let sketch = GetSketch(imgPath,sketchType);
        return GenerateSketch(sketch,canvasID);
    }
}

window.setup.CreateSketch = {
    UMA: (imgPath, canvasID) => ProcessDependecies(imgPath,canvasID,"UMA"),
    Mifir: (canvasID) => ProcessDependecies(null,canvasID,"Mifir"),
}

Here is how I call the Uma sketch in a passage:

<div id="Homeland"></div><script>window.setup.CreateSketch.UMA("http://localhost:3000/public/landscape.jpg", "Homeland");</script>

Note: Homeland is the passage name

And here is how I call the Mifir sketch:

<div id="Pathway1"></div>
<script>
console.log(window.setup);
window.setup.CreateSketch.Mifir("Pathway1");
</script>

Again: Pathway1 is the passage name

After reading this forum post, I put the following into a passage with a script tag:

config.disableHistoryTracking = true;

But this made no difference.

Perhaps there is a more economic way of calling my sketches? Any help and advice is appreciated, thanks.

For large amounts of data, like images or animations, you should make sure that you only load them into JavaScript variables or temporary variables (the ones that start with “_”), and then reload them as necessary. You don’t want to load those into story variables (the ones that start with “$”), since that will definitely cause lag, due to bloating up the game history.
 

That’s because that’s SugarCube v1 code, and you’re using SugarCube v2. For SugarCube v2 you’d do this:

Config.history.maxStates = 1;

(See Config.history.maxStates for details.)

Also, in your code I noticed that you used “window.setup” a few times. You should just use “setup” instead, since that will use the SugarCube setup object.

Additionally, instead of calling functions within an HTML <script> element, you should either use the SugarCube <<script>> macro or put the code inside of a <<set>> or <<run>> macro. For example:

<div id="Pathway1"></div>
<<run console.log(setup)>>
<<run setup.CreateSketch.Mifir("Pathway1")>>

That said, I’m surprised that that works, since the “Pathway1<div> won’t exist in the document at the time that you call the CreateSketch.Mifir() function. (Does p5.js wait for it to exist?)

Normally you’d have to do that something like this:

<div id="Pathway1"></div>
<<script>>
	$(document).on(":passageend", function (event) {
		console.log(window.setup);
		setup.CreateSketch.Mifir("Pathway1");
	});
<</script>>

That causes the code to wait until the “Pathway1<div> exists in the document, before it calls the CreateSketch.Mifir() function. (See the :passageend event for details.)

Also, if you want to simplify and improve your initialization code (plus modify the rest to use the SugarCube setup object) then you could do this instead:

setup.p5Loaded = false;
var lockID = LoadScreen.lock();  // Lock loading screen
importScripts("https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.6.5/dat.gui.min.js")  // Your JavaScript files go here
	.then(function() {
		setup.p5Loaded = true;
		// Reload current passage since imported scripts can function now.
		Engine.play(passage(), true);
		LoadScreen.unlock(lockID);  // Unlock loading screen
	}).catch(function(error) {
		alert(error);
	}
);
...
function CheckDependeciesStatus()
{
    if(!setup.p5Loaded || typeof p5 !== 'function' /*|| !setup.dotGUILoaded || typeof dat.GUI !== 'function'*/)
        return false;
    else
        return true;
}
...
setup.CreateSketch = {
    UMA: (imgPath, canvasID) => ProcessDependecies(imgPath,canvasID,"UMA"),
    Mifir: (canvasID) => ProcessDependecies(null,canvasID,"Mifir")
};

If you want to set that up to work from a local JavaScript file (instead of an online one) you might want to take a look at my “Loading External Scripts” sample code. (Click “Jump to Start” in the UI bar to see other sample code there.)

Hope that helps! :grinning:

That’s really helpful, @HiEv - thanks :slight_smile: