Monday, February 4, 2013

Think of the Children: Avoid this

‹prev | My Chain | next›

I remain a bit stuck on how to introduce object oriented programming in 3D Game Programming for Kids. From a concept standpoint, it seems best to stick to JavaScript's prototype nature—it may not be natural for most existing coders, but it feels less burdened for the newcomer. This leaves me with a conundrum on how to inherit methods.

For instance, in the simple ramp game with which I am working, I would like the ramps to inherit the ability to support drag and drop capabilities:


I tried a function to decorate those ramps last night, with less than ideal results. Part of the problem may have been that I was too excited over the Ravens' Super Bowl win, but probably not—I tried sussing that out before the game.

Anyhow, the problem with my approach was twofold: (a) the API felt a bit awkward and (b) it didn't work. Actually, it did work, but not as I hoped. The problem was the mouse-movement handler, which called the onMouseMove handler of the registered objects:
    document.addEventListener("mousemove", function(e){
      var click_pos = rendererXY(e);
      mouse_objects.forEach(function(o){
        if (!o.onMouseMove) return;
        if (!o.mesh) return;
        o.onMouseMove({x:click_pos.x, y:click_pos.y});
      });
    });
What does not work here is that the onMouseMove handler of every registered object is called multiple times—once for each object in the mouse_objects list. I end up duplicating the code from the click handler, which ensures that the mouse movement events are sent only to Three.js objects underneath the mouse pointer:
    document.addEventListener("mousemove", function(e){
      var click_pos = rendererXY(e),
          position = new THREE.Vector3(click_pos.x, click_pos.y, 500),
          vector = new THREE.Vector3(0, 0, -1),
          ray = new THREE.Ray(position, vector);

      mouse_objects.forEach(function(o){
        if (!o.onMouseMove) return;
        if (!o.mesh) return;
        if (ray.intersectObject(o.mesh).length === 0) return;
        o.onMouseMove({x:click_pos.x, y:click_pos.y});
      });
    });
After a bit more work, I am able to drag the ramps about the game area with the following ramp prototype:
  var ramp = {
    startAt: function(location) {
      this.mesh = new Physijs.ConvexMesh( /* ... */ );
      this.setRotation(location.rotation);
      this.setPosition(location.position[0], location.position[1]);
      addMouseHandlers(this);
    },
    onDrag: function(event) {
      this.mesh.__dirtyPosition = true;
      this.mesh.position.x = this.mesh.position.x + event.x_diff;
      this.mesh.position.y = this.mesh.position.y + event.y_diff;      
    },
    // ...
  };
The onDrag() event is quite nice. It works as desired and, best of all, it does nothing other than change the mesh's position by the drag amount.

The problem that remains is that addMouseHandlers() is a bit odd-looking. Elsewhere in the book, I do my level best to teach kids about JavaScript events for keyboard interaction. I cannot expect readers to keep two different event handling styles straight: the document.addEventListener('keyboard', /* ... */) for keyboard events and addMouseHandlers() + onDrag() for mouse events.

My Recipes with Backbone co-author (and solo author of the very excellent Mobile Web Patterns with Backbone.js) Nick Gauthier suggested that I try a library that mimics document.addEventListener(). Instead of addMouseHandler(), I would expect that it would look something like:
  var ramp = {
    startAt: function(location) {
      this.mesh = new Physijs.ConvexMesh( /* ... */ );
      this.setRotation(location.rotation);
      this.setPosition(location.position[0], location.position[1]);
      Mouse.addEventHandler('drag', this.mesh, this.drag);
    },
    // ...
  };
This is slightly different from the document.addEventListener() method. I would be asking programmers to supply a mesh object in addition the usual event name and method called when the event occurs. I could probably explain that, but, of course, there is no way this code would work. Why not? The answer, of course, is this.

The drag() method would need the proper context in order to move around the Physijs / Three.js mesh. That means that Mouse.addEventHandler() would end up looking like:
    Mouse.addEventHandler('drag', this.mesh, this.drag, this)
I would dearly love to gloss over the this variable in the book, mainly because I hope for kids to actually like JavaScript.

I could gloss over the convention that Mouse-capable objects should include both a mesh and a onDrag property. Then the event handler might be added as:
    Mouse.addEventHandler('drag', this)
It seems silly, though, to require an onDrag method when adding a 'drag' event handler.

Still stymied, I call it a night at this point. I will ruminate on this a bit more and attack it fresher tomorrow.

(the code so far)


Day #651

No comments:

Post a Comment