4
\$\begingroup\$

I'm creating a 2D game in JS, and I've made the generic class that all objects in the game will be (I've decided they are all circular for now).

An object has motion and angMotion array properties which contain the 0th, 1st and 2nd derivatives of the object's (angular) motion.

I've also introduced aliases for each of these derivatives (like acc for acceleration, etc).

Upon initialization, the default for all derivatives should be 0.

I'm satisfied with how the class works as it is now as long as it remains 2 dimensional and I don't need higher derivatives of (angular) motion.


However, I feel that my code is repetitive in some places which would require changes in multiple places supposing I want to change how it works slightly.

Eg. I've considered the possibility of wanting to include higher derivatives of motion (without introducing more aliases like jerk) or changing the dimension I'm working in (ignore angular motion in this case).


Note: I wanted the derivatives of (angular) motion to be stored in arrays so that I can loop over them to simulate Physics using Taylor's Expansion as shown below. The simulatePhysics function works regardless of the number of derivatives or dimension.

I'm also wondering from a design point of view - would it be a better idea to put simulatePhysics as a method of the game instead of the object?

class Game2DObject {
  constructor(options) {

    if (options) {

      this.motion = options.motion || []
      this.angMotion = options.angMotion || []

      this.dis = options.dis || [0, 0]
      this.vel = options.vel || [0, 0]
      this.acc = options.acc || [0, 0]

      this.angDis = options.angDis || 0
      this.angVel = options.angVel || 0
      this.angAcc = options.angAcc || 0

      this.radius = options.radius || 0
      this.mass = options.mass || 0

    } else {

      this.motion = [[0, 0], [0, 0], [0, 0]]
      this.angMotion = [0, 0, 0]
      this.radius = 0
      this.mass = 0
    }
  }
  //motion
  get dis() {
    return this.motion[0]
  }

  set dis(x) {
    this.motion[0] = x
  }

  get vel() {
    return this.motion[1]
  }

  set vel(x) {
    this.motion[1] = x
  }

  get acc() {
    return this.motion[2]
  }

  set acc(x) {
    this.motion[2] = x
  }
  //angMotion
  get angDis() {
    return this.angMotion[0]
  }

  set angDis(x) {
    this.angMotion[0] = x
  }

  get angVel() {
    return this.angMotion[1]
  }

  set angVel(x) {
    this.angMotion[1] = x
  }

  get angAcc() {
    return this.angMotion[2]
  }

  set angAcc(x) {
    this.angMotion[2] = x
  }

  simulatePhysics(dt) {
    function taylor(arr) {
      return arr.reduceRight((acc, cur, i) => acc * dt / (i + 1) + cur, 0)
    }

    for (var dim = 0; dim < this.motion[0].length; dim++) {
      for (var deriv = 0; deriv < this.motion.length; deriv++) {
        this.motion[deriv][dim] = taylor(this.motion.slice(deriv).map((cur) => cur[dim]))
      }
    }

    for (var deriv = 0; deriv < this.angMotion.length; deriv++) {
      this.angMotion[deriv] = taylor(this.angMotion.slice(deriv))
    }
  }
}

//*DEBUG

var a = new Game2DObject({acc: [-6, 0], angVel: 9})
console.log(a)
console.log(a.dis[0] = 1)
console.log(a.dis[1] = 5)
console.log(a.acc[1] = -2)
console.log(a)

a.simulatePhysics(1)
console.log(a)
a.simulatePhysics(1)
console.log(a)
a.simulatePhysics(1)
console.log(a)

Output:

enter image description here

\$\endgroup\$
8
  • 2
    \$\begingroup\$ How many derivatives do you think you would use? Should someone be able to get the 21st derivative? \$\endgroup\$
    – dfhwze
    Sep 5, 2019 at 16:39
  • \$\begingroup\$ The main purpose of this is for a basic game at the moment (in which case, the 1st 2 will do). But I'm also considering more complex games/physics simulation in future. \$\endgroup\$
    – Shuri2060
    Sep 5, 2019 at 16:42
  • \$\begingroup\$ I don't think you'll ever need more than jounce, I would just include upto this one. \$\endgroup\$
    – dfhwze
    Sep 5, 2019 at 16:43
  • \$\begingroup\$ ok :D But I think quite a few more than 3 dimensions is a possibility to consider? \$\endgroup\$
    – Shuri2060
    Sep 5, 2019 at 16:51
  • 1
    \$\begingroup\$ Reusable for future games/simulations. The game I'm making now can use the class as is with no problems. \$\endgroup\$
    – Shuri2060
    Sep 5, 2019 at 16:58

1 Answer 1

3
\$\begingroup\$

Poor performance

For games performance dominates all levels of design but becomes critical as you approach the low level building blocks of the game. Every increase in performance opens up your market, there are far more low end devices than high end with a 20% performance increase doubling the number of devices that can run the game at acceptable levels.

If you are not interested in marketing and its just for fun, then you can solve performance problems with hardware and ignore this answer.

Some points on performance

  • Avoid getters and setters as they are slower than direct property access.
  • Avoid array iteration for small arrays. Inline the operations.
  • Be memory efficient, pre-allocate, reuse rather than deference (delete)
  • Use small generic objects to cover a wide range of uses, rather than many complex targeted objects

Array.slice and Array.map require allocation and GC overhead. Array iterators require closure, counters, and logic overheads. For large arrays the overheads become insignificant, but for small arrays 2,3 items long the overhead becomes a significant part of the processing.

One of the biggest overheads of using array iteration is that the coder hides a large number of redundant operations because the iterators obscure the expanded calculations.

Needless CPU wastage

Lets analyze and simplify the function simulatePhysics as it is very inefficient with a lot of repeated calculations, or completely unneeded calculations. Every time it is called it create a lot of garbage for GC to clean up (15 new Array objects)

  simulatePhysics(dt) {
    function taylor(arr) {
      return arr.reduceRight((acc, cur, i) => acc * dt / (i + 1) + cur, 0)
    }

    for (var dim = 0; dim < this.motion[0].length; dim++) {
      for (var deriv = 0; deriv < this.motion.length; deriv++) {
        this.motion[deriv][dim] = taylor(this.motion.slice(deriv).map((cur) => cur[dim]))
      }
    }

    for (var deriv = 0; deriv < this.angMotion.length; deriv++) {
      this.angMotion[deriv] = taylor(this.angMotion.slice(deriv))
    }
  }

This function requires the creation of 15 new arrays, 27 call stack push and pops, 30 temp variable, and 108 math operations.

It can be significantly reduced to no new arrays, no call stack overhead, 6 temp variable, and 19 math operations.

Reducing simulatePhysics

The steps I used to simplify the function and remove unneeded overhead.

1 First to remove all the coding noise we can create some aliases for the names.

 const m = this.motion;
 const aM = this.angMotion;

2 Inline the 2 loops removing all the iterators slice and map overhead.

 m[0][0] = taylor([m[0][0], m[1][0], m[2][0]])
 m[1][0] = taylor([m[1][0], m[2][0]])
 m[2][0] = taylor([m[2][0]])
 m[0][1] = taylor([m[0][1], m[1][1], m[2][1]])
 m[1][1] = taylor([m[1][1], m[2][1]])
 m[2][1] = taylor([m[2][1]])   


 aM[0] = taylor([aM[0], aM[1], aM[2]])  
 aM[1] = taylor([aM[1], aM[2]])  
 aM[2] = taylor([aM[2]])  

3 Replace the taylor function with inlined calculations

  m[0][0] = ((0 * dt / 3 + m[2][0]) * dt / 2 + m[1][0]) * dt / 1 + m[0][0]
  m[1][0] = (0 * dt / 2 + m[2][0]) * dt / 1 + m[1][0]
  m[2][0] = 0 * dt / 1 + m[2][0]
  m[0][1] = ((0 * dt / 3 + m[2][1]) * dt / 2 + m[1][1]) * dt / 1 + m[0][1]
  m[1][1] = (0 * dt / 2 + m[2][1]) * dt / 1 + m[1][1]
  m[2][1] = 0 * dt / 1 + m[2][1]


  aM[0] = ((0 * dt / 3 + aM[2]) * dt / 2 + aM[1]) * dt / 1 + aM[0]
  aM[1] = (0 * dt / 2 + aM[2]) * dt / 1 + aM[1]
  aM[2] = 0 * dt / 1 + aM[2]

4 Remove the multiply by zero and divide by ones

  m[0][0] = (m[2][0] * dt / 2 + m[1][0]) * dt + m[0][0]
  m[1][0] = m[2][0] * dt + m[1][0]
  m[2][0] = m[2][0]
  m[0][1] = (m[2][1] * dt / 2 + m[1][1]) * dt + m[0][1]
  m[1][1] = m[2][1] * dt + m[1][1]
  m[2][1] = m[2][1]


  aM[0] = (aM[2] * dt / 2 + aM[1]) * dt + aM[0]
  aM[1] = aM[2] * dt + aM[1]
  aM[2] = aM[2]

5 Cache and substitute with constants, remove unneeded assignments.

 const dt2 = dt / 2;

 m[0][0] = (m[2][0] * dt2 + m[1][0]) * dt + m[0][0]
 m[1][0] = m[2][0] * dt + m[1][0]
 m[0][1] = (m[2][1] * dt2 + m[1][1]) * dt + m[0][1]
 m[1][1] = m[2][1] * dt + m[1][1]

 aM[0] = (aM[2] * dt2 + aM[1]) * dt + aM[0]
 aM[1] = aM[2] * dt + aM[1]

6 More aliases to reduce indexing overhead and rebuild the function

simulatePhysics(dt) {
    const m = this.motion, m0 = m[0], m1 = m[1], m2 = m[2];
    const aM = this.angMotion;
    const dt2 = dt / 2;

    m0[0] = (m2[0] * dt2 + m1[0]) * dt + m0[0];
    m1[0] =  m2[0] * dt  + m1[0];
    m0[1] = (m2[1] * dt2 + m1[1]) * dt + m0[1];
    m1[1] =  m2[1] * dt  + m1[1];
    aM[0] = (aM[2] * dt2 + aM[1]) * dt + aM[0];
    aM[1] =  aM[2] * dt  + aM[1];

}

Whats the gain

Arguably there is some loss in readability, personally it makes a lot more sense than a collection of arrays iterators and copying. But the gain is (tested on chrome) huge.

// tested on random set of 1000 objects
// µs is 1/1,000,000th second. OPS is operations per second. 
// An operation is a single call to the function being tested.
Optimized..: MeanTime 0.175µs OPS 5,710,020 Test Total   322ms 1,836,000 operations
Original...: MeanTime 4.198µs OPS   238,185 Test Total 6,566ms 1,564,000 operations

The optimized version is 25 times faster, able to do 5.7million operations in the same time as the original could do 0.3million

Is readability more important than performance? for games you must seriously consider what you lose via traditional coding styles.

\$\endgroup\$
4
  • \$\begingroup\$ Thanks for the great answer on optimizing the code. Being new to game development this is new for me. \$\endgroup\$
    – Shuri2060
    Sep 6, 2019 at 9:42
  • \$\begingroup\$ Would having 9 variables (sx, sy, vx, vy, ax, ay, as, av, aa) be more efficient than storing them in a 2D array? (I'm going to forget about aliasing now) \$\endgroup\$
    – Shuri2060
    Sep 6, 2019 at 10:00
  • \$\begingroup\$ If so, I'll go with this revamped version pastebin.com/v42ukRZS. Is it worth it to create an alias for 2 lookups as I've done in the simulatePhysics function here, or is it only worth it for arrays (you mentioned reducing indexing overhead)? \$\endgroup\$
    – Shuri2060
    Sep 6, 2019 at 10:35
  • \$\begingroup\$ @Shuri2060 Alias lookups is preferred for properties and array indexing when using Chrome, For FF aliases actually present a loss in performance. \$\endgroup\$
    – Blindman67
    Sep 8, 2019 at 20:23

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.