Saturday, February 2, 2013

Prototypical Inheritance for Kids

‹prev | My Chain | next›

I think it a good idea to a least introduce kids to object oriented programming in 3D Game Programming for Kids. I have yet to figure out the best approach. Mostly this is a function of JavaScript's support for objects. As Crockford puts it, JavaScript is conflicted over its prototypical nature.

Most of the object-oriented JavaScript that kids will encounter in the wild will be more along the lines of the classical approach that I took last night with the ramps in the game that I am preparing:
  var Ramp = function(x, y, rotation) {
    // Build 3D Mesh
    // Attach event listeners
  };
  
  Ramp.prototype.setRotation = function(rotation) {
    // ...
  };
  
  Ramp.prototype.setPosition = function(x, y) {
    // ...
  };
This approach works just fine. I was able to build multiple interactive ramps by creating new instances of this ramp:


Upon further reflection, I think it best to avoid any of these constructs in the book. There are just too many concepts involved here. There is the function-is-an-object concept. There is the prototype property and concept. There are anonymous functions being assigned to properties. It is just too much.

Instead, I wonder if it might make more sense to introduce only JavaScript's prototypical nature:
  var ramp = {
    mesh: new Physijs.ConvexMesh(
      // ...
    ),
    setRotation: function(rotation) { /* ... */ },
    setPosition: function(x, y) { /* ... */ },
    // ...
  };
I can then use Object.create() to create new instances based on this prototype:
  var ramp1 = Object.create(ramp);
  ramp1.setPosition(0.4*width/2, -height/2 + 25);
  ramp1.setRotation(-Math.PI/4);
  ramp1.attachListeners();
  scene.add(ramp1.mesh);
  
  var ramp2 = Object.create(ramp);
  ramp2.setPosition(-0.3*width/2, 0);
  ramp2.setRotation(Math.PI/3);
  ramp2.attachListeners();
  scene.add(ramp2.mesh);
Creating new objects in this manner is not quite as clean as it was with the more classical approach. An actual constructor has the benefit of being able to invoke setPosition(), setRotation(), and attachListeners() when the new instance is created. Still, nothing prevents me from defining a setup() method on the ramp prototype.

In the book, I could start with object literals, which is what ramp is in this case. "Prototype" is a concept that kids could grasp—the ramp is a "quintessential" ramp, so let's make copies. That might work. Except...

Except that the code that I have written so far does not work. Instead of two ramps, I am only seeing the second:


My mistake was defining the Three.js / Physijs mesh on the ramp prototype:
  var ramp = {
    mesh: new Physijs.ConvexMesh(
      // ...
    ),
    // ...
  };
After creating two new ramps with Object.create(), I tried to set the position and rotation of this mesh. I had intended for that positioning and rotation to affect the mesh on that new ramp instances, but this is not classical inheritance with which I am dealing. This is prototype inheritance. The mesh is a property of the ramp prototype, so positioning and rotation of any instance is positioning and rotating the prototype's property, not the individual instances.

The solution is easy enough—I have to create a new mesh property for each instance. In other words, I need a setup() method. I may be pushing my luck, but since I would have already introduced object literals at this point, I think I can pass an object literal method to my setup method. And, since this setup method will be positioning ramps, I call it startAt():
  var ramp = {
    startAt: function(location) {
      this.mesh = new Physijs.ConvexMesh( /* ... */);
      this.setRotation(location.rotation);
      this.setPosition(location.position[0], location.position[1]);
      this.attachListeners();
    },
    setRotation: function(rotation) { /* ... */ },
    setPosition: function(x, y) { /* ... */ },
    attachListeners: function() { /* ... */ },
    // ...
  };
If I call startAt() for each instance, this will define the this.mesh property for the instance, not the prototype:
  var ramp1 = Object.create(ramp);
  ramp1.startAt({
    position: [0.4*width/2, -height/2 + 25],
    rotation: -Math.PI/4
  });
  scene.add(ramp1.mesh);
With that, I again have multiple ramps at game start up:


And I can independently place these ramps thanks to the event handlers:


My gut feeling is to use an approach similar to this for kids. They will not have the baggage of classical object-oriented programming so something like this may even make more sense for first time programmers. I still need an easy way to build mouse and keyboard listeners for these ramps so I am not entirely out of the woods. But I am encouraged.

(the code so far)

Day #649

No comments:

Post a Comment