diff --git a/package.json b/package.json
index c0a54ff8c9e7eb86461cc06d29e01c9b4f635c9a..410167cc3bc3645b21b4e5199ede0071fe97e34d 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,7 @@
     "prettier"
   ],
   "dependencies": {
+    "aframe": "0.7.0",
     "aframe-input-mapping-component": "https://github.com/fernandojsg/aframe-input-mapping-component",
     "aframe-teleport-controls": "https://github.com/netpro2k/aframe-teleport-controls#feature/teleport-origin",
     "naf-janus-adapter": "^0.1.4",
diff --git a/public/index.html b/public/index.html
index fdb873fde0f88b9085cb6c3dc90bc2c1b8f32dbe..195f3133d9f306b73de88f3c39129b6bb8dcb0e3 100644
--- a/public/index.html
+++ b/public/index.html
@@ -2,7 +2,6 @@
 
 <head>
     <title>Mozilla Mixed Reality Social Client</title>
-    <!-- <script src="https://aframe.io/releases/0.7.0/aframe.js"></script> -->
     <script src="./app.bundle.js"></script>
     <style>
         .a-enter-vr {
@@ -15,12 +14,13 @@
 <body>
     <a-scene
         networked-scene="adapter: janus;
-                            room: 2;
-                            serverURL: wss://quander.me:8989;
-                            audio: true;
-                            debug: true;
-                            connectOnLoad: false;"
-        mute-mic="eventSrc: a-scene; toggleEvents: action_mute">
+                         room: 2;
+                         serverURL: wss://quander.me:8989;
+                         audio: true;
+                         debug: true;
+                         connectOnLoad: false;"
+        mute-mic="eventSrc: a-scene; toggleEvents: action_mute"
+        virtual-gamepad-controls>
 
         <a-assets>
             <img id="grid" src="assets/grid.png" crossorigin="anonymous" />
@@ -59,15 +59,11 @@
                     scale="6 6 6"></a-entity>
             </script>
         </a-assets>
+        <a-entity id="player-rig" networked character-controller="pivot: #head">
+            <a-sphere scale="0.1 0.1 0.1"></a-sphere>
+            <a-entity id="head" camera="userHeight: 1.6" personal-space-bubble networked="template:#head-template;showLocalTemplate:false;"<script src="<a-entity id="nametag" networked="template:#nametag-template;showLocalTemplate:false;"></a-entity>
 
-        <a-entity id="player-rig" networked wasd-controls snap-rotation="pivotSrc: #head">
-            <a-sphere scale="0.1 0.1 0.1" segments-height="6" segments-width="8"></a-sphere>
-            <a-entity id="head" camera="userHeight: 1.6" personal-space-bubble look-controls networked="template:#head-template;showLocalTemplate:false;"></a-entity>
-
-            <a-entity id="nametag" networked="template:#nametag-template;showLocalTemplate:false;"></a-entity>
-
-            <a-entity id="left-hand" hand-controls="left" hand-controls-visibility axis-dpad="centerZone: 1" teleport-controls="cameraRig: #player-rig; teleportOrigin: #head; button: action_teleport_"
-                networked="template:#hand-template;showLocalTemplate:false;">
+            <a-entity id="left-hand" split-axis-events hand-controls="left" hand-controls-visibility axis-dpad="centerZone: 1" networked="template:#hand-template;showLocalTemplate:false;">
                 <a-entity id="watch" gltf-model="assets/hud/watch.gltf" position="0 0.0015 0.147" rotation="3.5 0 0">
                     <a-circle mute-state-indicator scale-audio-feedback="analyserSrc: #head; minScale: 0.035; maxScale: 0.08;" position="0 0.023 0"
                         rotation="-90 0 0" scale="0.04 0.04 0.04" material="color:#d8eece;shader:flat">
diff --git a/src/components/character-controller.js b/src/components/character-controller.js
new file mode 100644
index 0000000000000000000000000000000000000000..c1d74e8faa10654f7ffb6c9ccf5818b7ac180a50
--- /dev/null
+++ b/src/components/character-controller.js
@@ -0,0 +1,315 @@
+import AFRAME from "aframe";
+var CLAMP_VELOCITY = 0.01;
+var MAX_DELTA = 0.2;
+var TAU = Math.PI * 2;
+
+// Does not have any type of collisions yet.
+AFRAME.registerComponent("character-controller", {
+  schema: {
+    groundAcc: { default: 10 },
+    verticalAcc: { default: 80 },
+    easing: { default: 8 },
+    pivot: { type: "selector" },
+    snapRotationRadian: { default: TAU / 8 }
+  },
+
+  init: function() {
+    this.startTranslating = this.startTranslating.bind(this);
+    this.stopTranslating = this.stopTranslating.bind(this);
+    this.velocity = new THREE.Vector3(0, 0, 0);
+    this.accelerationInput = new THREE.Vector3(0, 0, 0);
+    this.onTranslateX = this.onTranslateX.bind(this);
+    this.onTranslateY = this.onTranslateY.bind(this);
+    this.onTranslateZ = this.onTranslateZ.bind(this);
+    this.onMoveForward = this.onMoveForward.bind(this);
+    this.onDontMoveForward = this.onDontMoveForward.bind(this);
+    this.onMoveBackward = this.onMoveBackward.bind(this);
+    this.onDontMoveBackward = this.onDontMoveBackward.bind(this);
+    this.onMoveLeft = this.onMoveLeft.bind(this);
+    this.onDontMoveLeft = this.onDontMoveLeft.bind(this);
+    this.onMoveRight = this.onMoveRight.bind(this);
+    this.onDontMoveRight = this.onDontMoveRight.bind(this);
+    this.boost = 1.0;
+    this.onBoost = this.onBoost.bind(this);
+
+    this.startRotating = this.startRotating.bind(this);
+    this.stopRotating = this.stopRotating.bind(this);
+    this.pendingSnapRotationMatrix = new THREE.Matrix4();
+    this.onSnapRotateLeft = this.onSnapRotateLeft.bind(this);
+    this.onSnapRotateRight = this.onSnapRotateRight.bind(this);
+    this.angularVelocity = 0; // Scalar value because we only allow rotation around Y
+    this.onRotateY = this.onRotateY.bind(this);
+  },
+
+  update: function() {
+    this.leftRotationMatrix = new THREE.Matrix4().makeRotationY(
+      this.data.snapRotationRadian
+    );
+    this.rightRotationMatrix = new THREE.Matrix4().makeRotationY(
+      -this.data.snapRotationRadian
+    );
+  },
+
+  play: function() {
+    var eventSrc = this.el.sceneEl;
+    this.el.sceneEl.addEventListener(
+      "start_translating",
+      this.startTranslating
+    );
+    eventSrc.addEventListener("stop_translating", this.stopTranslating);
+    eventSrc.addEventListener("translateX", this.onTranslateX);
+    eventSrc.addEventListener("translateY", this.onTranslateY);
+    eventSrc.addEventListener("translateZ", this.onTranslateZ);
+    eventSrc.addEventListener("action_move_forward", this.onMoveForward);
+    eventSrc.addEventListener(
+      "action_dont_move_forward",
+      this.onDontMoveForward
+    );
+    eventSrc.addEventListener("action_move_backward", this.onMoveBackward);
+    eventSrc.addEventListener(
+      "action_dont_move_backward",
+      this.onDontMoveBackward
+    );
+    eventSrc.addEventListener("action_move_left", this.onMoveLeft);
+    eventSrc.addEventListener("action_dont_move_left", this.onDontMoveLeft);
+    eventSrc.addEventListener("action_move_right", this.onMoveRight);
+    eventSrc.addEventListener("action_dont_move_right", this.onDontMoveRight);
+
+    eventSrc.addEventListener("rotateY", this.onRotateY);
+    eventSrc.addEventListener("action_snap_rotate_left", this.onSnapRotateLeft);
+    eventSrc.addEventListener(
+      "action_snap_rotate_right",
+      this.onSnapRotateRight
+    );
+    eventSrc.addEventListener("boost", this.onBoost);
+  },
+
+  pause: function() {
+    this.el.removeEventListener("start_translating", this.startTranslating);
+    this.el.removeEventListener("stop_translating", this.stopTranslating);
+    this.el.removeEventListener("translateX", this.onTranslateX);
+    this.el.removeEventListener("translateY", this.onTranslateY);
+    this.el.removeEventListener("translateZ", this.onTranslateZ);
+    this.el.removeEventListener("action_move_forward", this.onMoveForward);
+    this.el.removeEventListener(
+      "action_dont_move_forward",
+      this.onDontMoveForward
+    );
+    this.el.removeEventListener("action_move_backward", this.onMoveBackward);
+    this.el.removeEventListener(
+      "action_dont_move_backward",
+      this.onDontMoveBackward
+    );
+    this.el.removeEventListener("action_move_left", this.onMoveLeft);
+    this.el.removeEventListener("action_dont_move_left", this.onDontMoveLeft);
+    this.el.removeEventListener("action_move_right", this.onMoveRight);
+    this.el.removeEventListener("action_dont_move_right", this.onDontMoveRight);
+    this.el.removeEventListener("rotateY", this.onRotateY);
+    this.el.removeEventListener(
+      "action_snap_rotate_left",
+      this.onSnapRotateLeft
+    );
+    this.el.removeEventListener(
+      "action_snap_rotate_right",
+      this.onSnapRotateRight
+    );
+  },
+
+  startTranslating: function() {},
+
+  stopTranslating: function() {
+    this.accelerationInput.set(0, 0, 0);
+  },
+
+  startRotating: function() {},
+
+  stopRotating: function() {},
+
+  onTranslateX: function(event) {
+    // bug : the last trackpadaxismovex event.detail is the html el instead of a number.
+    // I don't know why this is...
+    // Is touch up considered an axismove to (0,0)?
+    if (typeof event.detail === "number") {
+      this.accelerationInput.setX(event.detail);
+    } else {
+      this.accelerationInput.setX(0);
+    }
+  },
+  onTranslateY: function(event) {
+    if (typeof event.detail === "number") {
+      this.accelerationInput.setY(event.detail);
+    } else {
+      this.accelerationInput.setY(0);
+    }
+  },
+  onTranslateZ: function(event) {
+    if (typeof event.detail === "number") {
+      this.accelerationInput.setZ(event.detail);
+    } else {
+      this.accelerationInput.setZ(0);
+    }
+  },
+
+  onMoveForward: function(event) {
+    this.accelerationInput.z = 0.8;
+  },
+
+  onDontMoveForward: function(event) {
+    this.accelerationInput.z = 0;
+  },
+
+  onMoveBackward: function(event) {
+    this.accelerationInput.z = -0.8;
+  },
+
+  onDontMoveBackward: function(event) {
+    this.accelerationInput.z = 0;
+  },
+
+  onMoveLeft: function(event) {
+    this.accelerationInput.x = -0.8;
+  },
+
+  onDontMoveLeft: function(event) {
+    this.accelerationInput.x = 0;
+  },
+
+  onMoveRight: function(event) {
+    this.accelerationInput.x = 0.8;
+  },
+
+  onDontMoveRight: function(event) {
+    this.accelerationInput.x = 0;
+  },
+
+  onRotateY: function(event) {
+    if (typeof event.detail === "number") {
+      this.angularVelocity = event.detail;
+    } else {
+      this.angularVelocity = 0;
+    }
+  },
+
+  onSnapRotateLeft: function(event) {
+    this.pendingSnapRotationMatrix.copy(this.leftRotationMatrix);
+  },
+
+  onSnapRotateRight: function(event) {
+    this.pendingSnapRotationMatrix.copy(this.rightRotationMatrix);
+  },
+
+  onBoost: function(event) {
+    this.boost = event.detail;
+  },
+
+  tick: (function() {
+    var move = new THREE.Matrix4();
+    var trans = new THREE.Matrix4();
+    var transInv = new THREE.Matrix4();
+    var pivotPos = new THREE.Vector3();
+    var rotationAxis = new THREE.Vector3(0, 1, 0);
+    var yawMatrix = new THREE.Matrix4();
+    var rotationMatrix = new THREE.Matrix4();
+    var rotationInvMatrix = new THREE.Matrix4();
+    var pivotRotationMatrix = new THREE.Matrix4();
+    var pivotRotationInvMatrix = new THREE.Matrix4();
+    var position = new THREE.Vector3();
+    var currentPosition = new THREE.Vector3();
+    var movementVector = new THREE.Vector3();
+
+    return function(t, dt) {
+      const deltaSeconds = dt / 1000;
+      const rotationSpeed = -3;
+      const root = this.el.object3D;
+      const pivot = this.data.pivot.object3D;
+      const distance = this.data.groundAcc * deltaSeconds;
+
+      pivotPos.copy(pivot.position);
+      pivotPos.applyMatrix4(root.matrix);
+      trans.setPosition(pivotPos);
+      transInv.makeTranslation(-pivotPos.x, -pivotPos.y, -pivotPos.z);
+      rotationMatrix.makeRotationAxis(rotationAxis, root.rotation.y);
+      rotationInvMatrix.makeRotationAxis(rotationAxis, -root.rotation.y);
+      pivotRotationMatrix.makeRotationAxis(rotationAxis, pivot.rotation.y);
+      pivotRotationInvMatrix.makeRotationAxis(rotationAxis, -pivot.rotation.y);
+      this.updateVelocity(deltaSeconds);
+      move.makeTranslation(
+        this.velocity.x * distance,
+        this.velocity.y * distance,
+        this.velocity.z * distance
+      );
+
+      yawMatrix.makeRotationAxis(
+        rotationAxis,
+        rotationSpeed * this.angularVelocity * deltaSeconds
+      );
+
+      // Translate to middle of playspace (player rig)
+      root.applyMatrix(transInv);
+      // Zero playspace (player rig) rotation
+      root.applyMatrix(rotationInvMatrix);
+      // Zero pivot (camera/head) rotation
+      root.applyMatrix(pivotRotationInvMatrix);
+      // Apply joystick translation
+      root.applyMatrix(move);
+      // Apply joystick yaw rotation
+      root.applyMatrix(yawMatrix);
+      // Apply snap rotation if necessary
+      root.applyMatrix(this.pendingSnapRotationMatrix);
+      // Reapply pivot (camera/head) rotation
+      root.applyMatrix(pivotRotationMatrix);
+      // Reapply playspace (player rig) rotation
+      root.applyMatrix(rotationMatrix);
+      // Reapply playspace (player rig) translation
+      root.applyMatrix(trans);
+
+      // @TODO this is really ugly, can't just set the position/rotation directly or they wont network
+      this.el.setAttribute("rotation", {
+        x: root.rotation.x * THREE.Math.RAD2DEG,
+        y: root.rotation.y * THREE.Math.RAD2DEG,
+        z: root.rotation.z * THREE.Math.RAD2DEG
+      });
+      this.el.setAttribute("position", root.position);
+
+      this.pendingSnapRotationMatrix.identity(); // Revert to identity
+    };
+  })(),
+
+  updateVelocity: function(dt) {
+    var data = this.data;
+    var velocity = this.velocity;
+
+    // If FPS too low, reset velocity.
+    if (dt > MAX_DELTA) {
+      velocity.x = 0;
+      velocity.z = 0;
+      return;
+    }
+
+    // Decay velocity.
+    if (velocity.x !== 0) {
+      velocity.x -= velocity.x * data.easing * dt;
+    }
+    if (velocity.z !== 0) {
+      velocity.z -= velocity.z * data.easing * dt;
+    }
+    if (velocity.y !== 0) {
+      velocity.y -= velocity.y * data.easing * dt;
+    }
+
+    // Clamp velocity easing.
+    if (Math.abs(velocity.x) < CLAMP_VELOCITY) {
+      velocity.x = 0;
+    }
+    if (Math.abs(velocity.y) < CLAMP_VELOCITY) {
+      velocity.y = 0;
+    }
+    if (Math.abs(velocity.z) < CLAMP_VELOCITY) {
+      velocity.z = 0;
+    }
+    var dvx = data.groundAcc * dt * this.accelerationInput.x * this.boost;
+    var dvz = data.groundAcc * dt * -this.accelerationInput.z * this.boost;
+    velocity.x += dvx;
+    velocity.z += dvz;
+  }
+});
diff --git a/src/components/snap-rotation.js b/src/components/snap-rotation.js
deleted file mode 100644
index 808acb7b675656a1f5b32a355da95f22d683754b..0000000000000000000000000000000000000000
--- a/src/components/snap-rotation.js
+++ /dev/null
@@ -1,93 +0,0 @@
-/**
- * @fileOverview
- * Rotate an entity in fixed increments based on events
- * @name snap-rotation.js
- * @TODO allow specifying multiple events and sources
- */
-
-AFRAME.registerComponent("snap-rotation", {
-  schema: {
-    rotationAxis: { type: "vec3", default: { x: 0, y: 1, z: 0 } },
-    rotationDegres: { default: 45 },
-
-    leftEvent: { default: "action_snap_rotate_left" },
-    leftEventSrc: { type: "selector", default: "a-scene" },
-
-    rightEvent: { default: "action_snap_rotate_right" },
-    rightEventSrc: { type: "selector", default: "a-scene" },
-
-    pivotSrc: { type: "selector" }
-  },
-
-  init: function() {
-    this.onButtonPressed = this.onButtonPressed.bind(this);
-  },
-
-  update: function() {
-    const { rotationAxis, rotationDegres } = this.data;
-
-    const angle = rotationDegres * THREE.Math.DEG2RAD;
-    this.lRotMat = new THREE.Matrix4().makeRotationAxis(rotationAxis, angle);
-    this.rRotMat = new THREE.Matrix4().makeRotationAxis(rotationAxis, -angle);
-  },
-
-  play: function() {
-    const { leftEventSrc, leftEvent, rightEventSrc, rightEvent } = this.data;
-    rightEventSrc &&
-      rightEventSrc.addEventListener(rightEvent, this.onButtonPressed);
-    leftEventSrc &&
-      leftEventSrc.addEventListener(leftEvent, this.onButtonPressed);
-  },
-
-  pause: function() {
-    const { leftEventSrc, leftEvent, rightEventSrc, rightEvent } = this.data;
-    rightEventSrc &&
-      rightEventSrc.removeEventListener(rightEvent, this.onButtonPressed);
-    leftEventSrc &&
-      leftEventSrc.removeEventListener(leftEvent, this.onButtonPressed);
-  },
-
-  onButtonPressed: (function() {
-    const trans = new THREE.Matrix4();
-    const transInv = new THREE.Matrix4();
-    const pivotPos = new THREE.Vector3();
-
-    return function(e) {
-      const {
-        rotationAxis,
-        rotationDegres,
-        leftEvent,
-        rightEvent,
-        pivotSrc
-      } = this.data;
-
-      var rot;
-      if (e.type === leftEvent) {
-        rot = this.lRotMat;
-      } else if (e.type === rightEvent) {
-        rot = this.rRotMat;
-      } else {
-        return;
-      }
-
-      const obj = this.el.object3D;
-      const pivot = pivotSrc.object3D;
-
-      pivotPos.copy(pivot.position);
-      pivotPos.applyMatrix4(obj.matrix);
-      trans.setPosition(pivotPos);
-      transInv.makeTranslation(-pivotPos.x, -pivotPos.y, -pivotPos.z);
-      obj.applyMatrix(transInv);
-      obj.applyMatrix(rot);
-      obj.applyMatrix(trans);
-
-      // @TODO this is really ugly, can't just set the position/rotation directly or they wont network
-      this.el.setAttribute("rotation", {
-        x: obj.rotation.x * THREE.Math.RAD2DEG,
-        y: obj.rotation.y * THREE.Math.RAD2DEG,
-        z: obj.rotation.z * THREE.Math.RAD2DEG
-      });
-      this.el.setAttribute("position", obj.position);
-    };
-  })()
-});
diff --git a/src/components/split-axis-events.js b/src/components/split-axis-events.js
new file mode 100644
index 0000000000000000000000000000000000000000..8ae9bb44ebc11ff6754e308171682793332f606b
--- /dev/null
+++ b/src/components/split-axis-events.js
@@ -0,0 +1,27 @@
+AFRAME.registerComponent("split-axis-events", {
+  init: function() {
+    this.pressed = false;
+    this.onAxisMove = this.onAxisMove.bind(this);
+    this.onButtonChanged = this.onButtonChanged.bind(this);
+  },
+
+  play: function() {
+    this.el.addEventListener("axismove", this.onAxisMove);
+    this.el.addEventListener("buttonchanged", this.onButtonChanged);
+  },
+
+  pause: function() {
+    this.el.removeEventListener("axismove", this.onAxisMove);
+    this.el.removeEventListener("buttonchanged", this.onButtonChanged);
+  },
+
+  onAxisMove: function(event) {
+    var name = "touchpad" + (this.pressed ? "pressed" : "") + "axismove";
+    this.el.emit(name + "x", event.detail.axis[0]);
+    this.el.emit(name + "y", event.detail.axis[1]);
+  },
+
+  onButtonChanged: function(event) {
+    this.pressed = event.detail.state.pressed;
+  }
+});
diff --git a/src/components/virtual-gamepad-controls.js b/src/components/virtual-gamepad-controls.js
index 5db0dc10b821e2bf5eb5ef445d9766dce57dca44..0553e7a8e597f951e42b2459451f37ed9a018676 100644
--- a/src/components/virtual-gamepad-controls.js
+++ b/src/components/virtual-gamepad-controls.js
@@ -6,10 +6,7 @@ const DEGREES = Math.PI / 180;
 const HALF_PI = Math.PI / 2;
 
 AFRAME.registerComponent("virtual-gamepad-controls", {
-  schema: {
-    movementSpeed: { default: 2 },
-    lookSpeed: { default: 60 }
-  },
+  schema: {},
 
   init() {
     // Setup gamepad elements
@@ -45,112 +42,41 @@ AFRAME.registerComponent("virtual-gamepad-controls", {
     this.leftStick = leftStick;
     this.rightStick = rightStick;
 
-    // Define initial state
-    this.velocity = new THREE.Vector3();
     this.yaw = 0;
 
-    // Allocate matrices and vectors
-    this.move = new THREE.Matrix4();
-    this.trans = new THREE.Matrix4();
-    this.transInv = new THREE.Matrix4();
-    this.pivotPos = new THREE.Vector3();
-    this.rotationAxis = new THREE.Vector3(0, 1, 0);
-    this.yawMatrix = new THREE.Matrix4();
-    this.rotationMatrix = new THREE.Matrix4();
-    this.rotationInvMatrix = new THREE.Matrix4();
-    this.camRotationMatrix = new THREE.Matrix4();
-    this.camRotationInvMatrix = new THREE.Matrix4();
-
-    this.cameraEl = document.querySelector("[camera]");
-
     this.onEnterVr = this.onEnterVr.bind(this);
     this.onExitVr = this.onExitVr.bind(this);
     this.el.sceneEl.addEventListener("enter-vr", this.onEnterVr);
     this.el.sceneEl.addEventListener("exit-vr", this.onExitVr);
   },
 
-  onJoystickChanged(event, joystick) {
+  onJoystickChanged(event, data) {
     if (event.target.id === this.leftStick.id) {
       if (event.type === "move") {
-        // Set velocity vector on left stick move
-        const angle = joystick.angle.radian;
-        const force = joystick.force < 1 ? joystick.force : 1;
-        const x = Math.cos(angle) * force;
-        const z = Math.sin(angle) * -force;
-        this.velocity.set(x, 0, z);
+        var angle = data.angle.radian;
+        var force = data.force < 1 ? data.force : 1;
+        var x = Math.cos(angle) * force;
+        var z = Math.sin(angle) * force;
+        this.el.sceneEl.emit("translateX", x);
+        this.el.sceneEl.emit("translateZ", z);
       } else {
-        this.velocity.set(0, 0, 0);
+        this.el.sceneEl.emit("translateX", 0);
+        this.el.sceneEl.emit("translateZ", 0);
       }
     } else {
       if (event.type === "move") {
-        // Set yaw angle on right stick move
-        const angle = joystick.angle.radian;
-        const force = joystick.force < 1 ? joystick.force : 1;
-        this.yaw = Math.cos(angle) * -force;
+        // Set pitch and yaw angles on right stick move
+        var angle = data.angle.radian;
+        var force = data.force < 1 ? data.force : 1;
+        this.yaw = Math.cos(angle) * force;
+        this.el.sceneEl.emit("rotateY", this.yaw);
       } else {
         this.yaw = 0;
+        this.el.sceneEl.emit("rotateY", this.yaw);
       }
     }
   },
 
-  tick(t, dt) {
-    const deltaSeconds = dt / 1000;
-    const lookSpeed = THREE.Math.DEG2RAD * this.data.lookSpeed * deltaSeconds;
-    const obj = this.el.object3D;
-    const pivot = this.cameraEl.object3D;
-    const distance = this.data.movementSpeed * deltaSeconds;
-
-    this.pivotPos.copy(pivot.position);
-    this.pivotPos.applyMatrix4(obj.matrix);
-    this.trans.setPosition(this.pivotPos);
-    this.transInv.makeTranslation(
-      -this.pivotPos.x,
-      -this.pivotPos.y,
-      -this.pivotPos.z
-    );
-    this.rotationMatrix.makeRotationAxis(this.rotationAxis, obj.rotation.y);
-    this.rotationInvMatrix.makeRotationAxis(this.rotationAxis, -obj.rotation.y);
-    this.camRotationMatrix.makeRotationAxis(
-      this.rotationAxis,
-      pivot.rotation.y
-    );
-    this.camRotationInvMatrix.makeRotationAxis(
-      this.rotationAxis,
-      -pivot.rotation.y
-    );
-    this.move.makeTranslation(
-      this.velocity.x * distance,
-      this.velocity.y * distance,
-      this.velocity.z * distance
-    );
-
-    this.yawMatrix.makeRotationAxis(this.rotationAxis, lookSpeed * this.yaw);
-
-    // Translate to middle of playspace (player rig)
-    obj.applyMatrix(this.transInv);
-    // Zero playspace (player rig) rotation
-    obj.applyMatrix(this.rotationInvMatrix);
-    // Zero camera (head) rotation
-    obj.applyMatrix(this.camRotationInvMatrix);
-    // Apply joystick translation
-    obj.applyMatrix(this.move);
-    // Apply joystick yaw rotation
-    obj.applyMatrix(this.yawMatrix);
-    // Reapply camera (head) rotation
-    obj.applyMatrix(this.camRotationMatrix);
-    // Reapply playspace (player rig) rotation
-    obj.applyMatrix(this.rotationMatrix);
-    // Reapply playspace (player rig) translation
-    obj.applyMatrix(this.trans);
-
-    this.el.setAttribute("rotation", {
-      x: obj.rotation.x * THREE.Math.RAD2DEG,
-      y: obj.rotation.y * THREE.Math.RAD2DEG,
-      z: obj.rotation.z * THREE.Math.RAD2DEG
-    });
-    this.el.setAttribute("position", obj.position);
-  },
-
   onEnterVr() {
     // Hide the joystick controls
     this.leftTouchZone.style.display = "none";
diff --git a/src/index.js b/src/index.js
index 9dec4f2c54f2b33524c58e01170f9ab0a07103aa..527a32cab42613b67f14ef87751120fd4cffb227 100644
--- a/src/index.js
+++ b/src/index.js
@@ -7,15 +7,15 @@ import "aframe-teleport-controls";
 import "aframe-input-mapping-component";
 
 import "./components/axis-dpad";
-import "./components/snap-rotation";
 import "./components/mute-mic";
 import "./components/audio-feedback";
 import "./components/nametag-transform";
 import "./components/avatar-customization";
 import "./components/mute-state-indicator";
 import "./components/hand-controls-visibility";
+import "./components/character-controller";
+import "./components/split-axis-events";
 import "./components/virtual-gamepad-controls";
-
 import "./systems/personal-space-bubble";
 
 import registerNetworkScheams from "./network-schemas";
diff --git a/src/input-mappings.js b/src/input-mappings.js
index c6112d69742f775a7b3d335f8388765e6f0e52af..05b431aedb005ddaf4d792e012f402b71cb37695 100644
--- a/src/input-mappings.js
+++ b/src/input-mappings.js
@@ -6,7 +6,11 @@ export default function registerInputMappings() {
         dpadleftdown: "action_snap_rotate_left",
         dpadrightdown: "action_snap_rotate_right",
         dpadcenterdown: "action_teleport_down", // @TODO once once #30 lands in aframe-teleport controls this just maps to "action_teleport_aim"
-        dpadcenterup: "action_teleport_up" // @TODO once once #30 lands in aframe-teleport controls this just maps to "action_teleport_teleport"
+        dpadcenterup: "action_teleport_up", // @TODO once once #30 lands in aframe-teleport controls this just maps to "action_teleport_teleport"
+        trackpadtouchstart: "start_translating",
+        trackpadtouchend: "stop_translating",
+        touchpadpressedaxismovex: "translateX",
+        touchpadpressedaxismovey: "translateZ"
       },
       "vive-controls": {
         menudown: "action_mute"
@@ -20,7 +24,15 @@ export default function registerInputMappings() {
       keyboard: {
         m_press: "action_mute",
         q_press: "action_snap_rotate_left",
-        e_press: "action_snap_rotate_right"
+        e_press: "action_snap_rotate_right",
+        w_down: "action_move_forward",
+        w_up: "action_dont_move_forward",
+        a_down: "action_move_left",
+        a_up: "action_dont_move_left",
+        s_down: "action_move_backward",
+        s_up: "action_dont_move_backward",
+        d_down: "action_move_right",
+        d_up: "action_dont_move_right"
       }
     }
   });