Log in
Please log in or register.
Pages: [1]
  Print  
Author Topic: Sample code: adding ninja rope to Mode  (Read 2536 times)
Broco
Newbie
*
Posts: 4


View Profile
« on: Mon, Dec 7, 2009 »

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:

Code:
           //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:

Code:
// 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:

Code:
          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.
« Last Edit: Mon, Dec 7, 2009 by Broco » Logged
lithander
Full Member
***
Posts: 191



View Profile WWW
« Reply #1 on: Mon, Dec 7, 2009 »

That was really cool! Reminded me of Worms!

 I agree though, that the most awesome solution would be to have both versions combined! Smiley
Logged

Simplicity is the ultimate sophistication ~Leonardo Da'Vinci
xmorpher
Newbie
*
Posts: 39


View Profile
« Reply #2 on: Mon, Dec 7, 2009 »

 Shocked wow... it looks great... 
right now its difficult to test because the map is too loaded... it should have more open areas to test it better
Logged
Broco
Newbie
*
Posts: 4


View Profile
« Reply #3 on: Mon, Dec 7, 2009 »

Thanks Smiley.

Yes, this cluttered environment was okay to find glitches in the rope, but I'll start designing my own levels to figure out what works best from a gameplay point of view.
Logged
luc
Jr. Member
**
Posts: 87


no ablo anything except french


View Profile WWW
« Reply #4 on: Tue, Dec 8, 2009 »

 It's great !
I was wondering how to do this actually.(Spiderman you know Wink)
Are you planing  to release the full mode modified code ?
Would be useful in order to try to understand the logic behind.
Logged
Broco
Newbie
*
Posts: 4


View Profile
« Reply #5 on: Tue, Dec 8, 2009 »

Yes, if people are interested, I'll release it alongside my final game, after I've cleaned it up a bit.
Logged
xmorpher
Newbie
*
Posts: 39


View Profile
« Reply #6 on: Wed, Dec 9, 2009 »

good luck with your game, and yes! of course people will be interested (inclunding me Wink ) it's a interesting gameplay element
« Last Edit: Thu, Dec 10, 2009 by xmorpher » Logged
darthlupi
Full Member
***
Posts: 209


All Smiles


View Profile WWW
« Reply #7 on: Wed, Dec 9, 2009 »

Hell yeah I want an example.  I love rope games, and I have never been able to get collision and moving pivot points to work as smooth as you have!

You da man!  DA MAN!
Logged

To take care of that not so fresh feeling: #flixel on irc.freenode.net.

Use your favorite IRC client or  http://webchat.freenode.net/
Bonemonkey
Newbie
*
Posts: 4


View Profile
« Reply #8 on: Thu, Dec 10, 2009 »

Great work! I would like to see more samples as you complete your game. Just wish I had some good suggestions for you concerning the curvey rope. It probably goes without saying that once contact is made somewhere along the rope, you could "pull" the upper section straight and use the straight rope math to simplify things so you'd only be doing curvey calculations on at the end section still able to move freely. Of course, as you swing back off of a block/tile, you'd be jumping back to a longer curvey piece. Seems tricky.
Logged
Hzle
Newbie
*
Posts: 1


View Profile
« Reply #9 on: Fri, Dec 11, 2009 »

Yes very interested if you're happy to release the code. It's an excellent addition to the engine. Smiley

You'll have thought of this, but I imagine a game where you have to find certain items/weapons, and they give you the power to shoot/throw/use a rope etc. Actually I find the rope to be more fun than many weapons I've seen

I saw a flash game with a "gravity gun" (a la Half Life 2)* - that would be another interesting item to find..(John Citizen - it's on a java gaming site but is flash)
Logged
caspervanest
Newbie
*
Posts: 15


View Profile
« Reply #10 on: Fri, Dec 11, 2009 »

Hey that looks good Broco!
I'm definitely interested in your source code, as I'm currently implementing a rope/grappling hook as well. Your curved rope looks good already, although I'm not a fan of the controls for either ropes, but that's just from a gameplay point of view, and as you said it's just a tech demo.
Anyway, looking forward to your game, cheers!
Logged
st33d
Newbie
*
Posts: 2


View Profile
« Reply #11 on: Thu, Dec 17, 2009 »

The colliding rope is similar how I did the rope for Final Ninja.

I used a spring however to keep the player at the end of the rope, that I broke up as it hit corners.

I also used the dot product of the spring line instead of measuring the angle the rope came unstuck at. This is a bit faster than doing trig.

To account for the swingy behaviour I was looking for I eased up on the physics damping whilst swinging on the rope and after letting go of the rope till the player touched the keys again.

I think it's also important to kill the push to right when you're on the right hand side of the pivot point and the same goes for the left. Imagine you're on a swing - on one side of the pivot point you're only going to be able to give push in one direction.

I wasn't able to test, owing to the jump/fire-rope controls being counter-intuitive, but I've no idea wether you'll have the same problem I did firing the rope into the floor from above. I solved that one by turning off the spring physics whilst off the floor and above the pivot point.

It'd be nice to see another Flash rope game out there. Good work and good luck.
Logged
xmorpher
Newbie
*
Posts: 39


View Profile
« Reply #12 on: Thu, Dec 17, 2009 »

The colliding rope is similar how I did the rope for Final Ninja.
I like what you did on FrostBite, the rope&kook it's a nice gameplay element
Logged
Broco
Newbie
*
Posts: 4


View Profile
« Reply #13 on: Thu, Dec 17, 2009 »

Hi st33d, glad to hear from somebody who's been through the same thing :).  Congratulations on your finished game!

The colliding rope is similar how I did the rope for Final Ninja.

I used a spring however to keep the player at the end of the rope, that I broke up as it hit corners.

I also used the dot product of the spring line instead of measuring the angle the rope came unstuck at. This is a bit faster than doing trig.

Interesting, thanks.

I've also been toying with a spring approach lately.  A "hard" rope is easy to control, but somehow lacking in dynamism.

Quote
To account for the swingy behaviour I was looking for I eased up on the physics damping whilst swinging on the rope and after letting go of the rope till the player touched the keys again.

I think it's also important to kill the push to right when you're on the right hand side of the pivot point and the same goes for the left. Imagine you're on a swing - on one side of the pivot point you're only going to be able to give push in one direction.

I wasn't able to test, owing to the jump/fire-rope controls being counter-intuitive, but I've no idea wether you'll have the same problem I did firing the rope into the floor from above. I solved that one by turning off the spring physics whilst off the floor and above the pivot point.

It sounds like you took a very different approach than mine for the physics, so these don't resemble the kinds of problems that I've encountered so far.

I don't really have any physics damping as such: it's quite possible for the player to accelerate to ridiculous speeds in my engine (and I think this could potentially be fun).  And your second and third points are dealt with automatically, since I just have regular freeform air inertia/Mario physics within the less-than-ropelength-radius circle around the pivot point, and then just block further movement opposite the rope when the player moves to the edge of that circle.  Gravity takes care of creating a natural-looking swing.

A bigger problem for me is sort of the reverse of your second point: if the player is on the right and presses left, I just make him move straight left using Mario air control and fall via gravity instead of following the arc of the rope, which is a bit awkward.
Logged
caspervanest
Newbie
*
Posts: 15


View Profile
« Reply #14 on: Fri, Dec 18, 2009 »

Hey Broco,

I took a little bit of the code you posted and made my own grappling hook system with it.
Check it out:
http://casgames.progamestudios.com/
(X to jump, C to shoot rope, try to get as fast as possible from the leftmost white pixel to the rightmost white pixel, ESC to reset)
It's all still work in progress, but I just wanted to let you know I'm thankful for your rope code Wink
Cheers!
« Last Edit: Fri, Dec 18, 2009 by caspervanest » Logged
Pages: [1]
  Print  
 
Jump to: