Tuesday, March 1, 2011

Project Jumper Part 13: Push and Shove

Man, it's been a little while, hasn't it? I've been poking around with other projects lately, along with the oh-so-pleasant jobhunting process, so Jumper hasn't been getting my full attention. But I've reached a point in that other project where I've realized I have to tear out a whole big chunk and rewrite it. And I don't really want to right now. So here I am!

Actually, this is a weakness I've long recognized in my own creative process. Maybe you can sympathize. I start working on an idea, and then when I'm not 100% certain about which direction go, I stall because I'm afraid of taking a wrong path. For some reason, the fear of having to throw out any amount of work makes me more likely to do nothing at all. Obviously, this isn't very productive. Even partially unusable progress is still progress, and nothing educates like failure. Heck, I'm even using version control, I could always just rewind to before I went wrong, and no harm done.

Oh, well. The nice thing about Jumper is that I've got real motivation to press forward even if I don't know what I'm going to do. Even a trainwreck would make for a good blog post! So anyway, let's get back to work.

Pushable boxes
Lately on the Flixel forums, I've seen a lot of people having trouble with things like pushable crates and similar collision physics. Now, one solution is to just snag a library like Box2D to handle all the physics behind the scenes. But I don't want to implement a whole extra library of stuff just for one game mechanic, that would just be too easy. I'm going to see if I can get a satisfying result with plain old flixel. I'm warning you now, I have no idea if this is going to work. Let's find out!

A few seconds banging pixels together, and that crate doesn't look half bad, actually. As for adding it to the game, even though in one sense it's just a chunk of scenery, I think I'm going to try using FlxSprite instead of FlxTileblock. It shouldn't make a whole lot of difference, though.

OK, so I made the sprite, added it to the playstate next to where the player starts, and set it to collide with the map and with the player. The only physics I've got so far are the collision, and some y acceleration on the crate for gravity. Let's see what this does.
Attempt number one

Actually, this isn't too bad. The crate is pushable, but the actual collision behavior is a little off. It starts fast, and then slows down. I'd like it to start slowly, and speed up.

Digging around to try and figure out the default flixel behavior. The collide() function basically checks to see if the two objects are ramming into each other, and if they are then it calls hitLeft() or hitRight() or whatever on both objects. Here's the chunk of code from FlxU.solveXCollision() that determines what new velocities to send along:

else if(!f1 && !f2)
{
 overlap /= 2;
 if(Object1._group)
  Object1.reset(Object1.x - overlap,Object1.y);
 else
  Object1.x = Object1.x - overlap;
 if(Object2._group)
  Object2.reset(Object2.x + overlap,Object2.y);
 else
  Object2.x += overlap;
 sv1 *= 0.5;
 sv2 *= 0.5;
}

There's a lot of not-very-readable variables in there, but the basic gist is that if both objects are movable, then each of them will be set to half the velocity of the other. This leads to weird results, and here's why: When lizardhead bumps into the crate, he's going at some speed. I'm going to call it 100 just for easy math. The crate, obviously, is moving at 0. When they bump, the crate is set to move at half of lizardhead's velocity, or 50, while lizardhead is set to half of the crate's velocity, or 0. Since we're still holding the arrow key, lizardhead speeds up until he catches up, bumps the crate again, and eventually reaches a (slow) equilibrium.

As an experiment, I'm going to try changing the code to take the average of the two velocities, instead of just setting each to half the velocity of the other. Let's see what happens!

//sv1 *= 0.5;
//sv2 *= 0.5;
sv1 = (sv1 + sv2) / 2;
sv2 = sv1;

Attempt number two

As you can see, the crate pushes a lot more easily now. Too easily, in fact, and it never slows down. Well, let's just add some friction to it, which is just a matter of giving it a drag.x value. After a little experimenting, I settled on drag.x=600;

Attempt number three

This... works better than I expected, actually. The crate pushes smoothly, and moves a little slower than lizardhead's normal movement speed. There's ways to improve this. For instance, I could have the crate by default set to fixed=true, so that no amount of pushing will move it. Then set a timer, if the crate's been shoved against for a quarter second or so, then it sets fixed=false so it can slide around, then goes back to fixed=true when lizardhead stops pushing. In fact, that sounds like a good idea, let's see if I can do it.

Instead of having player.collide(_crates) in PlayState.update, I moved the collision handling into Crate.update():

override public function update():void
{
 super.update();
 var bumping:Boolean;
 bumping = collide(PlayState(FlxG.state).player); // Double duty, have Flx do the collision detection, and let us know the result
 if (!bumping)
 {
  if (velocity.x == 0)
  {
   fixed = true;
   bumptime = 0;
  }
 }
 else
 {
  if (bumptime > .25)
  {
   fixed = false;
  }
  bumptime += FlxG.elapsed;
 }
}

This is all pretty straightforward. Oh, there is a weird little thing there, let me digress.
PlayState(FlxG.state) is a way to refer to stuff that's normally out of the class's scope. If you remember, I mentioned earlier that FlxG.state is globally accessible. But Flash only knows that FlxG.state is a FlxState, which doesn't necessarily have a player attached to it. But by wrapping it up like this, PlayState(FlxG.state), we tell Flash that no, really, it's a PlayState. Trust us. This is casting, and it's pretty danged useful. Anyway. Back to boxes.

Attempt number 4

I'm pretty happy with the results. In a professional game, I'd include a little animation of lizardhead shoving the crate, but whatever. Also, I'd put the crate somewhere remotely useful, gameplay-wise. There's also still a few weirdnesses to iron out, such as collision in midair, but I think this is good enough for the basic idea.

EDIT: Actually, there's one last wrinkle to iron out, as was pointed out to me. If lizardhead is standing on top of the box, but kind of off to one edge, flixel still registers that as a horizontal collision and stuff goes flying. Probably the right thing to do is to dive into the flixel library and make it work properly. The way I'm going to do it is a quick and dirty hack. I went back into FlxU.solveXCollision(), and basically just added in one check to make it skip the whole process if one object is higher than the other.

static public function solveXCollision(Object1:FlxObject, Object2:FlxObject):Boolean
 {
  //Skip everything if no actual collision
  if ((Object1.y +Object1.height -1< Object2.y)||(Object2.y+Object2.height -1 < Object1.y))
   return false;



Now the crate will behave itself a little better when you walk around on top. Hard work is once again avoided a little longer.

Source code here, compiled game here.

3 comments:

  1. Yes! Very good news!

    and one question for future:
    Can you add ability to carry things and using them?
    (something like a classic quest-game)

    thanx =)

    ReplyDelete
  2. THANK YOU!!! THANK YOU!!! THANK YOU!!! THANK YOU!!! THANK YOU!!! THANK YOU!!! THANK YOU!!! THANK YOU!!! THANK YOU!!! THANK YOU!!! THANK YOU!!!

    A THOUSAND TIMES THANK YOU!!!

    I have been pulling my hair out because of this problem and your solutions work perfectly! Thanks one more time.

    ReplyDelete
  3. Quite a nice tutorial, helped me get into using flixel and as3(i have a C# and vb.net programming background)
    I would love to see this improved though with stuff like Evgeny suggested like being able to carry things and the likes maybe even a inventory screen etc.
    Also making it use the latest Flixel release since some code broke and i ended up switching to a older version to keep compatabilty with some features.
    Thanks for doing this!

    ReplyDelete