Saturday, January 22, 2011

Project Jumper Part 10:The Beginning and the (Bad) End

There's starting to be an actual game here, sort of kind of, so this seems like as good a time as any to actually give the game a little structure. That is, let's make an opening menu, a death screen, and let the game reset when you need to.

An opening menu is a good time to have a pretty splash screen, showcasing your artistic talent and getting the player excited about what he's about to play.

I'm going to use plain text on a plain background, and the finest crafted monochrome yellow triangle as a pointer. Ah, yeah.

Title Screen & Main Menu
A menu is basically an entirely different gamestate than the actual PlayState we've already made, so it makes a lot of sense to make a new FlxState. I'mma call it, oh, MenuState, because that's pretty descriptive. Also, it's what everyone else seems to do and I don't want to buck the trend. Standard stuff here, make a new class file named MenuState.as.

package com.chipacabra.Jumper
{
 import org.flixel.FlxG;
 import org.flixel.FlxSprite;
 import org.flixel.FlxState;
 import org.flixel.FlxText;
 import org.flixel.FlxU;
 
 /**
  * @author David Bell
  */
 public class MenuState extends FlxState
 {
  [Embed(source = '../../../../art/pointer.png')]public var imgPoint:Class;

  static public const OPTIONS:int=3; //How many menu options there are.
  
  private var _text1:FlxText;
  private var _text2:FlxText;
  private var _text3:FlxText;
  private var _text4:FlxText;
  private var _text5:FlxText;  // augh so many text objects. I should make arrays.
  private var _pointer:FlxSprite;
  private var _option:int;     // This will indicate what the pointer is pointing at
  
  override public function create():void
  {
   //Each word is its own object so we can position them independantly
   _text1 = new FlxText(FlxG.width/5, FlxG.height / 4, 320, "Project");
   _text1.size = 40;
   _text1.color = 0xFFFF00;
   _text1.antialiasing = true;
   add(_text1);
   
   //Base everything off of text1, so if we change color or size, only have to change one
   _text2 = new FlxText(FlxG.width / 2.5, FlxG.height / 2.5, 320, "Jumper");
   _text2.size = _text1.size;
   _text2.color = _text1.color;
   _text2.antialiasing = _text1.antialiasing;
   add(_text2);
   
   //Set up the menu options
   _text3 = new FlxText(FlxG.width * 2 / 3, FlxG.height * 2 / 3, 150, "Play");
   _text4 = new FlxText(FlxG.width * 2 / 3, FlxG.height * 2 / 3+30, 150, "Visit NIWID");
   _text5 = new FlxText(FlxG.width * 2 / 3, FlxG.height * 2 / 3 + 60, 150, "Visit flixel.org");
   _text3.color = _text4.color = _text5.color =0xAAFFFF00;
   _text3.size = _text4.size = _text5.size =16;
   _text3.antialiasing = _text4.antialiasing=_text5.antialiasing=true;
   add(_text3);
   add(_text4);
   add(_text5);
   
   _pointer = new FlxSprite();
   _pointer.loadGraphic(imgPoint);
   _pointer.x = _text3.x - _pointer.width - 10;
   add(_pointer);
   _option = 0;
   super.create();
  }

Nothing too unusual so far. Just creating a bunch of stuff, picking a spot for it, and adding it to the state so it will be displayed. Notice that I tried to refer to as many of the positions as possible in relative terms, stuff like FlxG.width*2/3. That way, if I change the window size or something, stuff will shift around to match and I won't have to fix as many things.

  override public function update():void
  {
   switch(_option)    // this is the goofus way to do it. An array would be way better
   {
    case 0:
    _pointer.y = _text3.y;
    break;
    case 1:
    _pointer.y = _text4.y;
    break;
    case 2:
    _pointer.y = _text5.y;
    break;
   }
   if (FlxG.keys.justPressed("UP"))
    _option = (_option +OPTIONS- 1) % OPTIONS;  // A goofy format, because % doesn't work on negative numbers
   if (FlxG.keys.justPressed("DOWN"))
    _option = (_option +OPTIONS + 1) % OPTIONS;
   if (FlxG.keys.justPressed("SPACE") || FlxG.keys.justPressed("ENTER"))
    switch (_option)
    {
     case 0:
     startGame(); 
     break;
     case 1:
     onURL();
     break;
     case 2:
     onFlixel()
     break;
    }
   
   super.update();
  }

Seriously, this is just goofy as heck. This is experimental code right here, not elegant or well-planned code. Instead of having a bunch of manually created elements, and switch-case statements to choose between them, it would be more elegant to use arrays. That would make it easier to add menu items, for example. Well, the important thing is that it works, even if it's awfully kludgy.

(_option +OPTIONS- 1) % OPTIONS  is a weird little expression. If you're not familiar, the % operator is the modulo operator. A % B means the remainder when you divide A by B. If B is 3, for example, the result will be 0,1, or 2. In this case, I'm using it so that when you move down on the bottom of the menu, it wraps around to the top. ((2 + 1) mod 3 equals 0.) However, in AS3 the % doesn't work properly on negative numbers. (0-1) mod 3 SHOULD equal 2, but not in Flash. So I just add OPTIONS again before taking the modulo, and that gets the correct answer.

  private function onFlixel():void
  {
   FlxU.openURL("http://flixel.org");
  }
  
  private function onURL():void
  {
   FlxU.openURL("http://chipacabra.blogspot.com");
  }
  
  private function startGame():void
  {
   FlxG.state = new PlayState;
  }
 }
}
These are the functions that get called when the player chooses a menu item. Pretty obvious how they work, I think. The important one is FlxG.state=new PlayState;. That's the expression that makes flixel stop paying attention to the menu and boot up the game proper.

Of course, if you run now the menu still won't show up. That's because we have to go all the way back to Jumper.as and tell it to start with the MenuState, not the PlayState.

super(640, 480, MenuState, 1); 

Now we've got a title screen/main menu, with a couple options to choose from. In theory, that's all I really need. In practice, I should make it fancier. For example, each FlxText is just a special case of FlxSprite, so anything we can do to a sprite, we can do the text. I could start the text boxes off screen, and set the _text1.velocity.x and _text2.velocity.x so they slide onscreen. ... In fact, I'm gonna do that right now.

// These changes are to create(); 
//_text1 = new FlxText(FlxG.width/5, FlxG.height / 4, 320, "Project");  // old version
_text1 = new FlxText(-220, FlxG.height / 4, 320, "Project");  // New, start offscreen
//_text2 = new FlxText(FlxG.width / 2.5, FlxG.height / 2.5, 320, "Jumper");
_text2 = new FlxText(FlxG.width-50, FlxG.height / 2.5, 320, "Jumper");

//Add this to update();
if (_text1.x < FlxG.width / 5)    _text1.velocity.x = TEXT_SPEED;
else _text1.velocity.x = 0;
if (_text2.x > FlxG.width / 2.5) _text2.velocity.x = -TEXT_SPEED;
else _text2.velocity.x = 0;
TEXT_SPEED is just a Number const I make that I can fiddle until I get a speed I like.

static public const TEXT_SPEED:Number=200;
Bam. Instant fancy.
Restarting the game
It kind of sucks when helmutguy gets splattered around, and you're left with nothing to do except watch Skel-Monsta do a little dance on the pieces. It would be nice if you could, you know, restart the game. Actually, this turns out to be only a few lines of code in PlayState.
First, a flag so we can tell if it's time to reset.

protected var _restart:Boolean;
And during create() we'll make sure it's off.

_restart=false;
Then we need to flip the switch. Head on down to update()

if (player.dead)
{
    if (FlxG.keys.justPressed("R")) _restart = true;
}

Pretty simple. This will only kick in if the player pushes R after dying, so he doesn't accidentally reset midgame.

Finally, we need to actually reset. Also super easy. Also in update()

if (_restart) FlxG.state = new PlayState;
And that's it. Again, it's pretty easy to fancy this up. I went ahead and added a bit of text letting the player know to hit R.

// Set up the game over text
_text1 = new FlxText(30, 30, 400, "Press R to Restart");
_text1.visible = false;
_text1.size = 40;
_text1.color = 0xFFFF0000;
_text1.antialiasing = true;
_text1.scrollFactor.x = _text1.scrollFactor.y = 0;

That scrollfactor = 0 bit there is kind of important. That's what keeps it on the same part of the screen once it's put down. Gonna need to keep that in mind when we start putting HUD elements in place.

Anyway, that's enough for today. Here's the source and the flash file. See you next time.

3 comments:

  1. Hey man, just wanted to say this is an excellent series and I'm really enjoying following along. My project and yours are slowly diverging, but it is SO useful to see what you're up to. One of, if not the, best tutorials for flixel out there (as long as your happy to get your hands dirty!).

    Thanks again.

    ReplyDelete
  2. FlxG.state now it's a only reader property.

    Now we put:

    FlxG.switchState(new PlayState);

    ReplyDelete