Hi Flixelers,
I've been working on adding a ninja rope to Mode over the past few weeks, with the intent of eventually making a game based on it. So far I've got more of a tech demo than a game, but I thought people in these forums might be interested in what I've been doing.
I've implemented two kinds of ninja rope: one of them collides the environment and wraps around corners, and the other one goes through things but has a loose, natural appearance. (I've been trying to create a rope that combines the two, but so far I've been having trouble ironing out the glitches.)
You can download SWF files at
http://dl.dropbox.com/u/2435938/Mode_ninja_rope.zip. The controls are WASD+space to jump, and space again while in mid-air to launch the ninja rope, and left-click to shoot in the direction of the cursor. Screenshots:


The mod involves lots of changes all over Mode and the Flixel engine itself, so I can't explain completely what I did in this post, but here are some of the meatier parts:
//PLAYER PHYSICS WHEN AT END OF TAUT ROPE
if(_hook.state == _hook.LATCHED) {
// Calculate angle of rope relative to player.
var relX:Number = this.getCenterPoint().x - _hook.GetCornerPoint().x;
var relY:Number = this.getCenterPoint().y - _hook.GetCornerPoint().y;
var ropeAngle:Number = Math.atan2(relY, relX);
// Convert player velocity to polar coordinates.
var speedAngle:Number = Math.atan2(this.velocity.y, this.velocity.x);
var speedMagnitude:Number = Point.distance(new Point(), this.velocity);
// Convert to polar coordinates relative to the rope instead of the origin.
var relSpeedAngle:Number = speedAngle - ropeAngle;
// Convert to cartesian in rope space. Apply 90-degree rotation to think in terms of rope hanging downward.
var relSpeed:Point = Point.polar(speedMagnitude, relSpeedAngle - Math.PI/2);
// Actually perform the transformation here. The rope nullifies any speed directly pulling against it.
if (relSpeed.y < 0 && _hook.isRopeTaut()) {
relSpeed.y = 0;
}
_relSpeed = relSpeed;
// Convert back to polar in rope coordinate space.
relSpeedAngle = Math.atan2(relSpeed.y, relSpeed.x);
speedMagnitude = Point.distance(new Point(), relSpeed);
// polar in main coordinate space
speedAngle = relSpeedAngle + ropeAngle;
// Cartesian in main coordinate space.
this.velocity = Point.polar(speedMagnitude, speedAngle + Math.PI/2);
//**********************************************************
//ROPE CORNER COLLISIONS FOR PLAYER PHYSICS PURPOSES
if(_hook.COLLIDE_ROPE) {
// Eliminate old collision points
while(_hook.cornerPoints.length > 1) {
const USE_SPEED_SIDE:Boolean = false;
// Last corner, and next-to-last corner. The relation of these two corners determines the
// ray we need to compare the player's position with to determine if the rope is no longer colliding.
// Another way of looking at this is that we need to recreate the context of when the last corner
// did not exist to redetermine if it should exist.
var lastCorner:Point = _hook.cornerPoints[_hook.cornerPoints.length - 1];
var baseCorner:Point = _hook.cornerPoints[_hook.cornerPoints.length - 2];
// The angle of the last corner relative to the next-to-last.
var cornerAngle:Number = Math.atan2(lastCorner.y - baseCorner.y, lastCorner.x - baseCorner.x);
// The angle of the player relative to the next-to-last. It needs to be on the same scale as the previous one.
var playerAngle:Number = Math.atan2(this.getCenterPoint().y - baseCorner.y, this.getCenterPoint().x - baseCorner.x);
// Which side of the ray collides?
var collideSide:Boolean = _hook.cornerDirs[_hook.cornerDirs.length - 1].x > 0;
// Option to take speed into account in case position alone is flaky
if(USE_SPEED_SIDE) {
// The speed angle of the player relative to the next-to-last.
var relSpeedCornerAngle:Number = speedAngle - playerAngle;
// Convert to cartesian in next-to-last space.
var relSpeedCorner:Point = Point.polar(speedMagnitude, relSpeedCornerAngle - Math.PI/2);
var speedSide:Boolean = relSpeedCorner.x > 0;
if(collideSide == speedSide) break;
}
// Calculate angle of player relative to angle of ray representing the collision
var angleDiff:Number = (playerAngle - cornerAngle);
// Bring angle difference back into [-Pi,Pi] interval
if(angleDiff < -Math.PI) { angleDiff += Math.PI*2; } else if(angleDiff > Math.PI) { angleDiff -= Math.PI*2; }
// Which side of the ray is the player on?
var playerSide:Boolean = angleDiff > 0;
if(collideSide != playerSide) {
_hook.cornerPoints.pop();
_hook.cornerDirs.pop();
} else {
break;
}
}
// Create new collision points
var cornerPos:Point = _hook.GetCornerPoint();
var playerPos:Point = this.getCenterPoint();
var ropeCollidePt:Point = _hook._tilemap.collideRope(cornerPos, playerPos, relSpeed);
if(ropeCollidePt) {
_hook.cornerPoints.push(ropeCollidePt);
_hook.cornerDirs.push(relSpeed); // keep track of relSpeed at time of creation only for elimination
}
}
}
The first part of the above code constrains the player's movement based on the rope. It considers the player's position within the circle whose center is the hook (or last collided corner) and radius is the length of the rope. If the player is at the edge of the circle, then the component of his velocity aligned with the diameter is zeroed. (If he's within the circle, the rope has no effect.) So this is a "tight" rope without any springiness.
The second part collides the rope with the tilemap, and maintains a stack of "corner points" where the rope changes direction. To decide whether to pop a corner from the stack, I check whether the player is on the wrong side of the ray from the next-to-last corner to the last corner. To collide the rope, I use a pixel-by-pixel line drawing algorithm to find collisions, then deduce the corner that must've been hit based on the quadrant of the rope direction and the orientation of the player's velocity. (So the collision code only works with the Tilemap variant of Mode, because this method is better suited to colliding with a bitmap of tiles than a list of blocks.) This is done in the function collideRope below:
// Calls f(p, [more args...]) on each pixel p in a line between the points 'first' and 'last'.
// If f returns an object, then terminates immediately and return that; if f returns null, then continues.
public function doForLinePoints(first:Point, last:Point, f:Function, argArray:Array):* {
var dx:Number = last.x - first.x;
var dy:Number = last.y - first.y;
var dist:Number = Math.sqrt((dx * dx) + (dy * dy));
argArray.unshift(null);
for (var i:Number = 0; i <= 1; i += 1/dist) {
var xDiff:int = (dx < 0) ? Math.ceil(dx * i) : Math.floor(dx * i);
var yDiff:int = (dy < 0) ? Math.ceil(dy * i) : Math.floor(dy * i);
argArray[0] = new Point(first.x + xDiff - this.x, first.y + yDiff - this.y);
var r:* = f.apply(this, argArray);
if(r) { return r; }
}
return null;
}
// Collide straight rope with obstacles for purposes of player physics/straight rope drawing.
public function collideRope(first:Point, last:Point, relSpeed:Point) : Point {
function collideRopePoint(p:Point, relSpeed:Point, dx:Number, dy:Number) : Point {
var ix:uint = Math.floor(p.x/_tileSize);
var iy:uint = Math.floor(p.y/_tileSize);
if(isCollideTile(ix, iy)) {
var xpos:uint = ix*_tileSize;
var ypos:uint = iy*_tileSize;
function hasTopLeftCorner():Boolean { return !isCollideTile(ix-1,iy) && !isCollideTile(ix,iy-1); }
function hasTopRightCorner():Boolean { return !isCollideTile(ix+1,iy) && !isCollideTile(ix,iy-1); }
function hasBottomLeftCorner():Boolean { return !isCollideTile(ix-1,iy) && !isCollideTile(ix,iy+1); }
function hasBottomRightCorner():Boolean { return !isCollideTile(ix+1,iy) && !isCollideTile(ix,iy+1); }
// Use the orientation of the rope (NW,NE,SW or SE) and the direction of player velocity to logically
// deduce which corner of a rope must be hitting.
if (relSpeed && relSpeed.x < 0) {
if(dx < 0 && dy > 0 && hasTopLeftCorner()) { xpos--; ypos--; }
else if(dx < 0 && dy < 0 && hasTopRightCorner()) { xpos += _tileSize; ypos--; }
else if(dx > 0 && dy > 0 && hasBottomLeftCorner()) { xpos--; ypos += _tileSize; }
else if(dx > 0 && dy < 0 && hasBottomRightCorner()) { xpos += _tileSize; ypos += _tileSize; }
else return null;
} else {
if(dx > 0 && dy < 0 && hasTopLeftCorner()) { xpos--; ypos--; }
else if(dx > 0 && dy > 0 && hasTopRightCorner()) { xpos += _tileSize; ypos--; }
else if(dx < 0 && dy < 0 && hasBottomLeftCorner()) { xpos--; ypos += _tileSize; }
else if(dx < 0 && dy > 0 && hasBottomRightCorner()) { xpos += _tileSize; ypos += _tileSize; }
else return null;
}
return new Point(xpos, ypos);
}
return null;
}
var dx:Number = last.x - first.x;
var dy:Number = last.y - first.y;
return doForLinePoints(first, last, collideRopePoint, [relSpeed, dx, dy]);
}
To implement the curvy-looking rope in the second sample, I keep an array of 20 length-5 rope segments from the hook to the player, initialized in a straight line. Then, I constrain each point on the rope to within 5 units of the previous point, in a loop starting at the player. As this tends to pull the final point quite far away from the hook, I then redo the same loop starting at the hook instead. This gets pretty good results, but hey, computers are fast, so for good measure I rerun the whole thing 10 times to make it converge to a clean straight line when the rope is at max length:
var i:int;
var STIFFENING_FACTOR:Number = 1.0;
var maxlength:Number = (ropeLength/rope_seg_num) * STIFFENING_FACTOR;
var ratio:Number = 1.0;
// Drag p to be within 1/rope_seg_num distance of point puller
function drag(p:Point, puller:Point, collide:Boolean):Point {
var seglength:Number = Point.distance(p, puller);
ratio = 1.0;
if(seglength > maxlength) {
ratio *= maxlength / seglength;
}
var midpt:Point = Point.interpolate(p, puller, ratio);
if(COLLIDE_ROPE && collide) {
// TODO: Fix bugs with rope passing through stuff and looking jittery
return _tilemap.collideRopePoint(midpt, p, puller);
} else {
return midpt;
}
}
var j:uint;
// iterate 10 times to converge on solution
for(j = 0; j < 10; j++) {
// Player drags rope
rope[0] = player.getCenterPoint();
for (i = 1; i <= rope_seg_num; i++) {
rope[i] = drag(rope[i], rope[i-1], true);
}
// Hook pulls back on rope
rope[rope_seg_num] = getCenterPoint();
for (i = rope_seg_num-1; i >= 0; i--) {
rope[i] = drag(rope[i], rope[i+1], true);
}
}
That's the main stuff. Note that I also implemented Biomechanic's mouse aiming mod (thanks!) and tweaked the firing to have a shotgun-style bullet with very high recoil. This interacts interestingly with the ninja rope, since you can use it to accelerate your swing in mid-air.
Feel free to ask me questions if you're interested in adding a similar mechanic to your own game. Or, for the geometry wizards out there, I have a question of my own: any ideas for could I go about making the curvy rope segments collide against blocks in a stable manner? When I've tried to do this, if I simply constrain the segment endpoints individually to not enter blocks, the fixed-segment-length invariant becomes violated as the endpoints of one segment drift across different sides of the block and make a very long segment going clear through the block. I've tried adding a hack to make colliding segments go straight across the edge the block they collided with, but that has proven very jittery. I have the feeling I'd have to change approach entirely (find a way to constrain it in one iteration?) to do this.