Tuesday, January 8, 2019

From Lasers to Lightning - By Josh Hollis

Prerequisite knowledge - Fundamental understanding of Canvas, some HTML5 and Javascript familiarity.
Disclaimer - Do not expect this code to magically work in your application.  The code in this post is meant to explain the concepts behind how beams and bolts are rendered in Inventaverse, not as a copy paste resource.


1.21 Gigawatts!

There comes a time in one's life when it becomes necessary to obliterate the enemy with more than just beams.  If we want to ensure that every atom of matter has been thoroughly and properly destroyed, we want to use 1.21 Gigawatts of power to do it!

In this article I'll go over the concepts behind creating a simple laser beam in Javascript.  Then I'll show you how we can spice up our laser beam by segmenting it and turning it into an awesome lightning bolt!


Lasers... an elegant weapon for a more civilized age

That's right, lasers!  Or, in our case, a couple of lines that alternate colors rapidly so as to give the impression of being a laser.  Here's how we do it:

With the grace of the great drawing artist Escher, we'll begin with a single line.

Our renderLaser function takes starting point, slope and distance.  We'll describe our line using it's slope as a parameter instead of supplying the start x,y and end x,y because we're going to do something with that later.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function renderLaser(start, slope, distance) {
  // first, we'll calculate the endpoint.
  let end = {
    X : start.X * slope * distance,
    Y : start.Y * slope * distance
  }

  context.beginPath();
  context.moveTo(start.X, start.Y);
  context.lineTo(end.X, end.Y);
  context.stroke();
}

Nothing to it, if you've wired up your canvas correctly, and you've correctly called this function then you should have a nice solid line that looks something like this line from Inventaverse:




Now, add one megajoule of pulsating laser power.  We'll alter our renderLaser function slightly by alternating it's color every frame between two colors supplied as a parameter, by using the modulus operator % (secretly the most useful operator for sprite animation, in the universe).


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
function renderLaser(start, slope, distance, colors) {
  // first, we'll calculate the endpoint.
  let end = {
    X : start.X * slope * distance,
    Y : start.Y * slope * distance
  }

  context.beginPath();
  context.strokeStyle = window.frameCounter % 2 === 0 ? colors[0] : colors[1];
  context.moveTo(start.X, start.Y);
  context.lineTo(end.X, end.Y);
  context.stroke();
}

You may have noticed an additional variable hanging off the window object here (which is generally a bad practice, but for the sake of this exercise, it's fine), frameCounter.  This is just a count of the total number of frame drawn since the script was loaded.  Simply increment this every time requestAnimationFrame calls your drawing function and your laser will alternate between two supplied colors quite nicely.

Let's animate one more property of our line, the width.  


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
function renderLaser(start, slope, distance, colors, widths) {
  // first, we'll calculate the endpoint.
  let end = {
    X : start.X * slope * distance,
    Y : start.Y * slope * distance
  }

  context.beginPath();
  context.strokeStyle = window.frameCounter % 2 === 0 ? colors[0] : colors[1];
  context.lineWidth = window.frameCounter %2 === 0 ? widths[0] : widths[1];
  context.moveTo(start.X, start.Y);
  context.lineTo(end.X, end.Y);
  context.stroke();
}

Now that we're all on the same page...

At this point, you're right about where I was at when I decided I wanted some of the weapons in Inventaverse to do more than just fire a beam.  I wanted a little more sizzle, and some variety.  

So, how do we turn lasers into lightning bolts?

Well, there's more than one way to discharge a massive bolt of electricity, and the way that made the most sense to me was this:

  • break our beam up into lots of little line segments.
  • at each of the points where the line segments touch, jitter the x and y coordinates such that the line is no longer straight.
  • season to taste.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function renderLightning(start, slope, distance, colors, widths) {
  const nodes = 500;
  const cracle = 16;
  let bn = [];
  let scalar = 0;

  // pick a bunch of points on our line and jitter them up!
  for(let n = 0; n < nodes; n++) {
    scalar += (Math.random() * ((distance-scalar)/nodes));
    let r = {
      X : (Math.random() * crackle) - crackle/2, // jitter the x a little
      Y : (Math.random() * crackle) - crackle/2  // jitter the y a little
    };
    bn.push({ X : slope * scalar + r.X, Y : slope * scalar + r.Y });
  }

  context.beginPath();
  context.strokeStyle = window.frameCounter % 2 === 0 ? colors[0] : colors[1];
  context.lineWidth = window.frameCounter %2 === 0 ? widths[0] : widths[1];
  context.moveTo(start.X, start.Y);

  bn.forEach((v, i) => {
    context.lineTo(v.X, v.Y);
  });

  context.stroke();
}

Here's what it ends up looking like!  Not too shabby eh?




Lightning Speed

Alright, we have lightning, but it's... slow lightning...  Let's see what we can do to speed this up.

Obviously the slowest part is the iteration.  Javascript is perfectly capable of executing 500 iterations in far less than a millisecond, so expecting it to do it in under 16.67 ms is perfectly reasonable.  However, there is certainly room for improvement.  Generating a random number is slow. 

Back in the day, when we wanted to avoid doing expensive computations at run time, we would generate a pool of results ahead of time, and then perform a quick lookup.  This is faster than generating individual random numbers inline by a degree of magnitude. 

Also, a basic for loop executes about 10 times faster than a forEach.  Sorry hipsters!  Save your fancy ES6 array functions for one-time situations, not real time graphics scenarios.

Make these minor modifications to your lightning bolt and you'll shave the render time down to practically nothing!

One final note: you could make your renderLaser function a little easier to call into by wrapping all it's parameters into one laser configuration json object, and adding a default parameter object.  Sometimes when you end up with 4+ parameters, things start looking a little crazy.

Using the techniques I've covered in this article, we can create some pretty amazing lightning effects, see for yourself:







At this point, the only question remaining is: How do you create a creature that is so awesome that it would necessitate using our badass new lightning bolt to destroy it?  

But that my friends, is a story for anther day.

Until next time...

Tuesday, October 16, 2018

Generating Procedural Worlds in Inventaverse (Part 1) - By Josh Hollis


It's a problem every programmer is faced with, early in their career.  You know... "Removing elements as you traverse an array changes the total number of array elements thus opening a gateway to the nether-world."  Although this is closely related to the problem that is the focus of this article, this is not the problem today.  Allow me to set the stage.

So there I was, writing algorithms for the purpose of generating procedural worlds in my indie game project, Inventaverse (a retro styled 2d massive multiplayer maker game that runs on the web).  The randomly generated worlds in question, need to have a good mix of open space, floating islands and tunnels.  Why?  Because Inventaverse features 14 playable characters, each with a unique play style.  Some have grappling hooks, some fly, some can dig through the ground, so building a random world generator that will consistently pose a challenge is not a trivial problem.  

Not impossible, but not trivial.

I know what you're thinking: "Why not use cellular automaton to generate random topographies".  I was thinking the same thing.  I pulled up some code I had written centuries ago that does just that.


Here's how it works.
1. Populate a matrix(2D array) with random values between 0 and 1.
2. Snap those values to 0 or 1 by checking if they're less than, or greater than a supplied parameter, we'll call it Q.
3. Evaluate the neighboring values of each index in the matrix, how many of them are 1's?
4. If the number of neighbors with a value of 1 is greater than some supplied parameter(C) then we set the value of the array at the index in question to 1.  Otherwise, we set it to 0.

The results were great.  They were random, lots of space, floating islands, tunnels, awesome!  However, there was a very noticeable artifact.  The topographies generated by this method were all somewhat... slanted.  After some brief investigation it occurred to me that the mere act of traversing the array top to bottom, left to right, was causing the anomaly.

See the upper-left to lower-right tendency?


When using a cellular automaton in this way, you're evaluating the neighbors around the current index, then subsequently changing the current value to 1 a portion of the time.  The slanting I was seeing was a side affect of altering the values in the matrix in a specific order.  And sometimes, it looked really cool!  I considered the slanting effect a moment and decided to keep it around, for variety!  

But clearly we couldn't have a slanted world every time, so I set out to traverse the matrix in such a way that the map generated wouldn't be skewed in any particular direction.  My options were as follows:

1. Consider the source matrix as immutable, write the results of my transformations to a new matrix and return that.
2. Traverse the matrix non-linearly by scanning the matrix in a back and forth fashion.

 
I opted for the latter.

So, now instead of processing the 2D array from top to bottom, left to right, I processed it left to right, then right to left, then left to right etc.


Uniformly distributed randomness, perfecto!

Doing this solved the problem straight away.  So I pulled the "slanted" world generation logic into a separate function which I called "Linear", and put the scanning style world generation logic into another method called "Scan".  Then I came up with the idea of processing the matrix by using spiral traversal, and the results were awesome.  

But that, my friends, is a story for another day.

Until next time...