Monday, January 17, 2011

Project Jumper Part 9: Agility Training

It's time to improve helmutguy's maneuverability. I've got a few things that I want to do here.
  • Mario style jumping
  • Double jumping
  • Ladders
  • Walljumping
 I'm not sure I'll actually get to all of them, though. Remember, I'm making this up as I go.

Slimming down
Before I get started, I'd like to tidy up the hit detection a little. You've probably noticed while jumping around that the game is pretty sloppy about when helmutguy hits his head on the ceiling, especially when he's in the crouched animation frame.

We can look at how flixel handles hitboxes pretty easily. Just include the line FlxG.showBounds = true; somewhere in your code. I actually made it a toggle by adding

if (FlxG.keys.justPressed("B"))
{
    FlxG.showBounds = !FlxG.showBounds;
}
to PlayState. Now tapping B during the game will show off color coded hitboxes for everything in the game.
It's full of squares
We can see that helmutguy's box extends a few pixels further to the sides and above him than we'd like. Let's trim him up a bit. The relevant variables for this are width, height, offset.x, and offset.y. I'm going to trim helmutguy's hit detection to a 16 x 16 box, by adding this to the Player constructor:

height = 16;
offset.y = 4;
width = 16;
offset.x = 2;

This actually trims the hitbox to slightly smaller than his walking sprite. I'm good with that; it lets helmutguy fit into gaps a single tile high, and makes the game a little more forgiving when moving around. Exactly how you tweak your hitbox will depend on what sort of feel you want for your game. For example, Adam Atomic shifted the hitbox backward a little in Canabalt, so that the player has a little more time before actually falling off a ledge. Don't feel constrained to match the hitbox perfectly to the sprite.


Controlling jump height


AKA Mario style jumping. Basically, what we want is for the height of the jump to be determined by how long you hold the jump button. Well, Mario physics also implies limited control over horizontal acceleration during a jump, but we can worry about that another time, if ever. Also, Mario jumps higher if you're running faster. I'll save that for another time, too, if I even bother.

The method described on flashgamedojo is pretty simple; when you push the button, it starts a counter and begins your jump. While you're holding the button, it keeps setting your velocity, which means gravity doesn't have a chance to pull you down. When you let go of the button or the timer reaches a certain time, then the jump velocity cuts out and gravity can kick in again. This means that holding the button longer makes you jump higher. Easy.

Here's an abridged version of what Player looks like with the new jump code:

public class Player extends FlxSprite 
    {
    
        private var _jump:Number;
        

        public override function update():void
        {
            if(FlxG.keys.justPressed("UP") && !velocity.y)  // Only play the jump sound once per jump
            {                                                // There are probably better ways to do this, but this is fast and good enough
                FlxG.play(sndJump, 1, false, 50);
            }
            
            if((_jump >= 0) && (FlxG.keys.UP)) //You can also use space or any other key you want
            {
                _jump += FlxG.elapsed;
                if(_jump > 0.25) _jump = -1; //You can't jump for more than 0.25 seconds
            }
            else _jump = -1;
 
            if (_jump > 0)
            {
                if(_jump < 0.035)   // this number is how long before a short slow jump shifts to a faster, high jump
                    velocity.y = -.6 * maxVelocity.y; //This is the minimum height of the jump
                    
                else 
                    velocity.y = -.8 * maxVelocity.y;
            }
            super.update();
        }

        override public function hitBottom(Contact:FlxObject, Velocity:Number):void  // This fires off whenever the player's bottom edge collides with something i.e. the tilemap.
        {
            if (!FlxG.keys.UP) // Don't let the player jump again until he lets go of the button
            _jump = 0;
            super.hitBottom(Contact, Velocity);
        }
    }

You'll notice I fiddled with the numbers. Also, I kept the sound effect out of the new code. It would tend to fire up repeatedly if I put it there. By leaving the sound effect attached to the old keys.justPressed business, it only fires off once when the button is pressed.


Double Jump
This is another super easy thing to do. The important thing is to decide exactly what under what conditions a double jump is allowed. One way to be if at least a certain amount of time has passed since the first jump. The way I'm going to do it just requires that the player be falling.


First, we need a variable to track whether it's ok to double jump. I'll just add

private var _canDJump:boolean;
to the Player class. Then we add another chunk of code to that jumping stuff:

if (FlxG.keys.justPressed("UP") && (velocity.y > 0) && _canDJump==true)  // If it's ok to doublejump
{
    FlxG.play(sndJump, 1, false, 50);
    _jump = 0; // start the jumping process
    _canDJump = false;
}
That should go right before the regular jumping code.

Finally, we need to reset the _canDJump flag. I'll make it reset when helmutguy is on solid ground, so the line

_canDJump=true;
goes into the hitBottom() function.

Ladders
OK, this is the part I wasn't looking forward to, hehe. It's not really that complicated, but there's lots of ways this could get buggy. First off, if we're going to be shimmying up and down ladders, we don't want the same button to be used for climbing as for jumping. I mean, we could, but I always find that control scheme awkward and imprecise. So all of the times when the code says UP, we want it to say Z instead. Now Z is the jump button. ... Actually, I don't like jump to be to the left of shoot. So C will be jump instead.

Now, let's add some ladders to the level. I went back into DAME, and drew some ladders onto a new layer. Now there are three map layers: the background, the tilemap, and the ladders. Straightforward stuff.

Before I start coding in the logic, I should probably decide exactly what I want that logic to be. For the sake of this game, let's make it so that if helmutguy presses up or down while he is on a ladder, he enters a 'climbing' state where he is no longer affected by gravity, and up and down move him in those directions. The climbing state will end if helmutguy is not on a ladder, or if he jumps. Right, sounds good.

A new public boolean called 'climbing'. Check. Tweak the player.update() function to turn off gravity when climbing.

if (climbing) acceleration.y = 0;  // Stop falling if you're climbing a ladder
else acceleration.y = GRAVITY;
Check. Code to move up and down when pushing the up and down buttons:

if (FlxG.keys.UP && climbing)
{
    velocity.y = -RUN_SPEED;
}
if (FlxG.keys.DOWN && climbing)
{
    velocity.y = RUN_SPEED;
}
      
Check. Take it for a quick test drive by setting climbing=true; in the Player creation method. ... Oops, slight problem. Unlike moving sideways, there's no restoring force to make helmutguy stop moving, so once you push the button he'll keep climbing forever. That's not what we want. Fortunately, we can just add a drag.y along with our drag.x to prevent that. It will affect how quickly you jump, but as a bonus I think I like the effect; it kind of smooths out sudden acceleration of jumping.

Now it's a matter of making helmutguy jump while climbing, and turn off climbing to do so. There's no real planning, here. I just kept throwing bits of code at it until it worked. Basically, I turned this:

if(FlxG.keys.justPressed("C") && !velocity.y)  // Only play the jump sound once per jump
{                                                // There are probably better ways to do this, but this is fast and good enough
    FlxG.play(sndJump, 1, false, 50);
}

into this:

if (FlxG.keys.justPressed("C"))
{
    if (climbing)
    {
        _jump = 0;
        climbing = false;
        FlxG.play(sndJump, 1, false, 50);
    }
    if (!velocity.y)
        FlxG.play(sndJump, 1, false, 50);
}
It seems to work ok. OK, finally, we need to be able to tell when helmutguy is on a ladder, so that pushing up or down will engage climbing mode.

This is trickier than it seems. See, we can't just use overlaps() to compare the player to the ladder map to see if they're overlapped. That's because overlaps() compares against the ENTIRE tilemap, not just the elements that are solid, so it would always register as true. So let's try a more brute force method instead:

_xgrid = int((x+width/2) / 16);   // Convert pixel positions to grid positions. int and floor are functionally the same, 
_ygrid = int((y+height-1) / 16);   // but I hear int is faster so let's go with that.
            
if (_parent.ladders.getTile(_xgrid,_ygrid)) {_onladder = true;}
else 
{
    _onladder = false;
    climbing = false;
}
_xgrid and _ygrid are protected ints, and _onladder is a protected boolean. But what's that _parent nonsense?

I actually tweaked Player.as a bit. Here's the important part:

protected var _parent:*;

public function Player(X:int,Y:int,Parent:*,Gibs:FlxEmitter,Bullets:FlxGroup):void
{
      // Other stuff here 
      _parent=Parent; 
      // The rest of the function down here
}
And back in PlayState we change our Player declaration AGAIN, this time to

add(player = new Player(1000, 640,this,_gibs,_bullets));

Basically, now PlayState is sending itself along with the information it sends to Player. That way, Player can access the stuff like tilemaps that are properties of the PlayState.

Anyway, we're almost done here, just a few more tweaks. Right now, helmutguy will go rocketing off the top of the ladder if you keep climbing, so let's just add a quick check that will make him stop climbing when there's no more ladder.

// Climbing
if (FlxG.keys.UP)
{
    if (_onladder) 
    {
        climbing = true;
        _canDJump = true;
    }
    if (climbing && (_parent.ladders.getTile(_xgrid, _ygrid-1))) velocity.y = -RUN_SPEED;
}

One last thing. If you notice, one of the ladders overlaps the terrain. It would be nice to still be able to use that ladder. Here's one way to do it: In Player.update(), add

if (climbing) solid = false;
else solid = true;

It could be better, but this post has already gone on long enough. I'll leave it to you to think of ways to tighten up the ladder behavior in your own game. The basic mechanics are here, it's all about the tweaking to get it just how you want it.

Lots of changes this time, so you should probably check out PlayState.as and Player.as in the source code. I'm not 100% sure I mentioned all of the changes in the post. Flash file is here.

6 comments:

  1. Thank you, it's great.

    another question:
    how to combine sprites(with different sizes)?
    how to make a horse through the union of 2 tiles(sprites)?
    First tile with a horse's head(high), second - the rear part of the horse(low)

    Can I do this in Flixel?

    ReplyDelete
  2. Off the top of my head, a quick kludgy way to do it is to make two sprites, a 'main' sprite and a 'secondary' sprite. In the secondary sprite's update() function, set its .x and .y values to the main sprite's values, with whatever offset you need.

    ReplyDelete
  3. FlxG.showBounds doesn't exist anymore. Now is FlxG.visualDebug. So, the new code is right this:

    FlxG.visualDebug = !FlxG.visualDebug

    ReplyDelete
  4. functions hitBottom/top/left/rigth doesn't exist anymore. Now, in the last release, we only need put on our update event (course, from the Player.as) the next function

    if (this.isTouching(DOWN)) {
    if (!FlxG.keys.UP) {
    this._jump = 0;
    }
    }

    Really "this" keyword is not neccesary, but I like to differentiate private attributes from variables from the event.

    Function isTouching (isTouching(DOWN), isTouching(up), isTouching(LEFT), isTouching(RIGHT)) are our replacement for events hitBottom, hitTop, hitLeft, hitRight.

    ReplyDelete
  5. Another problem implementing double jump.

    I don't know the mechanical of

    if (FlxG.keys.justPressed("UP") && (velocity.y > 0) && _canDJump==true)

    ...in version used in this tutorial, but in mine (AdamAtomic-flixel-2c1e5dc.zip), that doesn't work properly.

    I mean, double jump is working, but the sound for the first jump is gone, so, the best and short way that I could make it work again, was adding the same line that allow play the jump sound after this line

    this.velocity.y = -.6 * this.maxVelocity.y;

    i.e.

    if (this._jump > 0) {
    if (this._jump < .035) {
    this.velocity.y = -.6 * this.maxVelocity.y;
    FlxG.play(this.sndJump, 1, false);
    }else {
    this.velocity.y = -.8 * this.maxVelocity.y;
    }
    }

    instead of

    if (this._jump > 0) {
    if (this._jump < .035) {
    this.velocity.y = -.6 * this.maxVelocity.y;
    }else {
    this.velocity.y = -.8 * this.maxVelocity.y;
    }
    }

    A last little thing in this issue, I put

    if (FlxG.keys.justPressed("UP") && this.velocity.y && this._canDjump)

    instead of

    if (FlxG.keys.justPressed("UP") && (velocity.y > 0) && _canDJump==true)

    ReplyDelete
  6. Finally in this chapter, I make a few changes for a best climb on the ladders:
    This lines seems don't take effect

    if (this._climbing && ( this._parent.ladders.getTile(this._xgrid, this._ygrid - 1) ) ) {
    this.velocity.y = -RUN_SPEED;
    }
    so, I deleted.

    And, our character only move on the ladder if we move it with UP and DOWN keys respectively, adding this lines:

    if (FlxG.keys.justReleased("UP")) {
    this.velocity.y = 0;
    }

    if (FlxG.keys.justReleased("DOWN")) {
    this.velocity.y = 0;
    }

    "Solid" attribute seems don't take effect too, but I don't remove it... One never know :)

    ReplyDelete