Making a Beat-Em-Up Fighting Game in Flash: Part SEVEN In this section we'll be dealing with the concept of lives and taking damage. And we'll throw in a basic layout. Whoah, this is a big chapter. Yikes, so many changes I feel like saying "Just download the swf" and not bothering to explain how any of it works. But then this wouldn't be a tutorial and nobody would learn zip. There's quite a few more additions to the code, and a lot to grasp for the noobie Flash dude, but the good news is that you get the FLA at the end to peruse over to your hearts content. Anyway, if you've come this far grasshopper then you're not much longer for graduating from the Beat-em Up School of EvilKris. Yes, I'll be wrapping it up in the next two or three chapters. Indeed there is a lot more that goes into a game than is highlighted in this tutorial, but with the bare bones we've covered so far, I think it's safe to say a good coder with a bit of talent would know how to take the bits and pieces and turn it into a simple game. Mission accomplished. At the end of these tutorials I'll be discussing how certain types of games could be made with the functions we've discovered, detailing the theory behind making use of the splatterhouse demo to create something such as a Streetfighter or Dungeons & Dragons clone. As a favour from you to me, I will ask you that if you do find that I've missed out important steps in the tutorials or see some vast improvements that can be made to the current code, send me a pm and don't assume t-hat somebody else has already reported it because that's what everybody else thinks. Cheers. For the purpose of this lesson, and to show you a few new tricks, I'll be doing many of the effects through Actionscripting rather than manually editing the mc's directly through the art-based approach. Coding in Actionscript has one other benefits other than keeping things neat and tidy, it also allows us the create functions that can be used for more than one mc, and basically saves us time in the long run. I'm not a big believer of having tons of graphics on stage and sticking kaboodles of code on individual mc's. Not for games. Somewhere down the line you forget we're you've put some code and before you know it there's errors all over the place. Plus the depth and hit collision problems that occur don't help. Keep your code neatly on the first frame and most of your graphics in the library rather than on the stage. For clarity if nothing else. First and foremost grab the new images for this lesson. TAKING DAMAGE When Rick gets hit, he loses a life. Let's analyse every step of that event.
His mc gets knocked back a few pixels away from his the direction of the object instigating the attack. Simultaneously, his mc flashes on and off and glows a brighter hue than his current one. He is invincible for about 2 seconds after the attack. If he's carrying a weapon, it is dropped to the ground from whatever position he currently is in. If he has no remaining lives, he dies. An animation of him falling to the ground is triggered.
Continuing with splatter5.swf, before we get into the guts of the new code to recreate the above, some old stuff has to be altered. _root.attachMovie("cleaver", "weapon", 40000 , {_x:250, _y:300});
Change the depths of the above line to 40000, to keep the weapons from clashing with onscreen mcs.
Next, for every weapon we should give it a tag value that is the same as the linkage. So within the cleave movieclip on the first frame put the following; this.tag="cleaver";This allows us to identify the weapon that Rick picks up and to swap it with a new identical mc that is attached to rick_mc and not _root.
function CheckForWeapon()
{
if(rick_mc.hitTest(weapon) and rick_mc.crouch)
{
var temptag=weapon.tag;
removeMovieClip(weapon);
//delete cleaver;
rick_mc.attachMovie(temptag, "weapon", 40000);
rick_mc.weapon.gotoAndStop("pickup");
rick_mc.pickup=true;
rick_mc.weaponheld=true;
rick_mc.weaponframe=false;
}
By the way, should you have more than one weapon on the screen this code won't work properly. Because ever mc needs a unique depth and name, you will have to add an extra identifier to the new mc. Such as- rick_mc.attachMovie(temptag, "weapon"+weapontag, 40000+weapontag);You will also have to run the above code in a loop to account for any weapons that appear on the screen. The best way to do this is to say, have an array of mc's called WeaponsOnScreen(). (similar to our MonstersInScene() one). Push the mc's into the array every time they are instantiated and run a loop for every item in the array to detect a collision. Very similar to the RenderEnemy() function. Another time-saver we'll use it to set the direction of the mc's in English rather than binary. Because we now know that Rick reacts to the direction of the attacking object, we should define exactly what direction the enemy/weapon is coming from when it hits Rick and work from there. We will add the familiar stance variable to our enemy declarations at the start, except instead of confusing ourselves by setting stance=0 for left and stance=1 for right, we'll define 0 . At the top, but below MonsterInArray = new Array(), put; LEFT=0; RIGHT=1;Now, everywhere in the code where you see stance=0, replace it with stance=LEFT instead. stance=1 should also now be altered to stance=RIGHT. Much less confusing. We're going to need some new variables now, so in function Initialize() put the following (anywhere);
rick_mc.alive=true;
rick_mc.lives=3;
rick_mc.invincible=false;
rick_mc.damage=false;
Also adds these to our SpawnEnemy() function;
attachedObj.uvel=40; attachedObj.gravity=6; attachedObj.alive = true; attachedObj.timer = 20; Right we're nearly up to scratch now but next what do you say we get a layout up and running? We've eventually got to get around to having an area for displaying health, lives, score etc anyway, so now's as good a time as any. CTRL+F8(New Symbol/MovieClip) name it 'layout' and drag your layout1.png from the library into the new symbol. Align it to the bottom left, so that any co-ordinates we give it at a later stage will relate to that point. Give it the linkage 'layout'. Also, do the same for life.png. Create a new movieclip 'heart' with the same name for the linkage. Align to center (not bottom-left).
_root.attachMovie("cleaver", "weapon", 40000 , {_x:250, _y:300});
Put;
_root.attachMovie("layout", "layout", 50000 , {_x:0, _y:Stage.height});
Which ensures that 'layout' loads at the bottom of the screen. Note the high depth setting (50000). This is to ensure that pretty much everything else in the game is displayed behind the layout. Anything we put on the layout will be set at a higher depth.
Next we're going to dynamically attach some life icons to the bar. We will be doing this in a function so put this in your code near the top; function Lives()
{
for(i=0;i < rick_mc.lives+1;i++)
removeMovieClip(_root.layout["heart" + i]);
for(i=0;i < rick_mc.lives;i++)
_root.layout.attachMovie("heart", "heart"+i, 51000+i , {_x:130+(i*50), _y:-30});
}
OK. This is a little tricky to undertstand so bear with me. Firstly, the function kind of works backwards. It starts of by removing mc's that don't even exist yet. This is because later on, they will exist and when Rick loses or gains health we want to reuse this function again and again. The second for loop gets how many lives/enegy bars Rick has left and dynamically attaches heart icons at 50 pixels incrementations along the bar. Every mc is given a unique identifier, heart + a number. So now the first heart is named heart1, the second heart2. You could reference one by using _root.layout.heart1._x=500 for example. However, to reference them from a loop is a little more tricky. You need to use a completely different syntax from "heart"+i. Instead it goes; _root.layout["heart"+i]). Strange eh? Something like _root.layout.heart + i will not work. If you look carefully you'll notice that the heart icons are attached to the layout mc and not the _root level like most everything else. So, when we give the mc's co-ordinates those x,y values will be relative to the layout mc not the Stage global co-ords (where 0,0 is top left of the screen). Doesn't make much sense now I know, but it will later on. Just to save you some pain later on in your games programming career, know now that there is a command called LocalToGlobal() that converts the nested x,y points to global 'stage' x,y points. You will need it one day. So now that we have that function available, let's initiate it at the beginning when the game is run; Lives(); goes under- ////////////// MAIN PROGRAM CALLS SpawnEnemy(); Initialize();Before moving forward with the Actionscript, I think we should set Rick mc up with his new animations. So now let's create a new addition to rick_mc, the hurt animation. Create a new symbol, name it 'damage' and pull hurt1.png and hurt2.png sequentially onto the first 2 frames, centering each. Put a stop() in the actionscript for the second frame so that when this animation is called it doesn't advance beyond this frame. To achieve a 'glow' we will now need to add some filters to the second frame. Click on the second frame and tap F8 to convert it into a movie clip(or Graphic). It doesn't really matter what you name it. When it's a movie clip you can then select it on the stage and you should be able to edit the Color panel now on Properties. Select Brightness and set it up to about 60%. We now have our glow.
_root.wakeup=false;
Back to the main AS code then. What we want to do is check that when our enemy hits/touches Rick (and he's not attacking or in his flashing invincible stage) and then take off a life before calling our Lives() function. Remember AIZombie()? Time for a short revision; function AIZombie()
{
if(this.alive)
{
this.gotoAndStop("walk");
//YOUR FIRST AI
if(rick_mc._x<=this._x-20)
{
this._xscale = 100;
this._x-=this.speed;
}
else if(rick_mc._x>=this._x+20)
{
this._x+=this.speed;
this._xscale = -100;
}
if(this.hitTest(rick_mc) && !rick_mc.attack && !rick_mc.invincible)
{
rick_mc.lives--;
rick_mc.invincible=true;
Lives();
rick_mc.damage=true;
rick_mc.onEnterFrame = function()
{
Damage(rick_mc, this.stance);
}
}
}
else
{
if(rick_mc.weaponheld and rick_mc.weaponframe)
this.gotoAndStop("decap");
else
this.gotoAndStop("dead");
}
}
Some juicy new code in this part.Namely- rick_mc.onEnterFrame = function(). What this does is it takes the rick_mc, and then runs an EnterFrame event on the mc seperately from all the other code. So Rick will do whatever we tell him to do in that function until that event is deleted. In other words, even though we're already checking for all his movement on the main onEnterFrame event, we're going to stop that happening while he's being 'damaged' and run this onEnterFrame event instead. This is called dynamic event handling and it is -to put it mildly-, the best thing to happen to Flash since err, me. Essentially every time you want an mc to 'do it's own thing' without getting in the way of the other code, you might want to consider using it. It's very handy for things such as bullets, particle explosions, AI. There are other ways I could have done the same thing, using a lot of new state variables or functions but I've saved time and lines of code by doing it like this. (great tutorial here by the way). Next we'll stick in a function to deal with the damage animation. The function takes two parameters, a reference to the object being damaged (in this case rick_mc) and the direction the attacker is facing when the collision occurs. function Damage(ob,direction)
{
//OB NOW REFERS TO RICK_MC AND DIRECTION THIS.STANCE//
ob.gotoAndStop("damage");
ob._y-=ob.uvel/2;
ob.uvel-=ob.gravity*2;
if(direction==LEFT)
ob._x+=ob.step/2;
else if(direction==RIGHT)
ob._x-=ob.step/2;
if(ob._y+20 > ground_mc._y-115)
{
ob._y=ground_mc._y-115;
ob.uvel=40;
ob.damage=false;
delete ob.onEnterFrame;
}
}
Looks a lot like the Jump function doesn't it? It's almost the same except for the variables (gravity and uvel) which are halved value so as to make the 'jump' more of a 'bump'. As soon as rick (or the ob mc) touches the ground, the event deletes itself and keyboard control is give back to the main onEnterFrame event via the setting of ob.damage to false.
Wrap the entire main onEnterFrame event in an if loop conditional to damage; onEnterFrame = function()
{
RenderEnemy();
if(!rick_mc.damage)
{
if(!rick_mc.weaponheld)
CheckForWeapon();
if(!rick_mc.attack && !rick_mc.jump)
{
Movement();
}
if(rick_mc.jump)
Jump();
if(rick_mc.attack)
{
Attack();
}
if (Key.isDown (Key.CONTROL))
{
keydown=true;
}
else keydown=false;
if(rick_mc.stance==LEFT)
rick_mc._xscale = -100;
else
rick_mc._xscale = 100;
}
}
Put into English, if Rick is hit, he can't move or really do anything until he hits the ground (damage is false).
Going back to our damage animation, to make Rick 'flicker' for a set amount of time (whilst invincible basically) we have to create a function that swaps his opacity values every couple of seconds or so. function Flash(ob,t)
{
if((t % 2)==0)
ob._alpha=10;
else
ob._alpha=90;
if(t<=0)
{
ob.timer=30;
//ob._visible=true;
ob._alpha=100;
ob.invincible=false;
}
}
Which can be called by adding;
if(rick_mc.invincible) Flash(rick_mc,rick_mc.timer--); On top of our main onEnterFrame event. That % symbol is the mod operator. It takes two values, divides them and gives the remainder. So if the remainder is zero, then we have a match. So we call this function on every frame and vary the alpha depending on whether or not the current timer value (which decrements every call) divides exactly. So if you set the mod to 5 it would work every 5 frames and so on. We use the _alpha command to switch the opacity of the mc from 10 to 90. When the timer runs the function resets the object timer to 30. In fact, since our main Enterframe event is getting cluttered, let's create a new function called MainLoop() to put everything within. function MainLoop()
{
if(rick_mc.invincible)
Flash(rick_mc,rick_mc.timer--);
if(!rick_mc.damage)
{
if(!rick_mc.weaponheld)
CheckForWeapon();
if(!rick_mc.attack && !rick_mc.jump)
{
Movement();
}
if(rick_mc.jump)
Jump();
if(rick_mc.attack)
{
Attack();
}
if (Key.isDown (Key.CONTROL))
{
keydown=true;
}
else keydown=false;
if(rick_mc.stance==LEFT)
rick_mc._xscale = -100;
else
rick_mc._xscale = 100;
}
else if(rick_mc.weaponheld)
{
rick_mc.weapon.onEnterFrame=DropWeapon;
}
};
Did you notice the new line rick_mc.weapon.onEnterFrame=DropWeapon;?
That's right, we're using another dynamic event handler to drop the weapon every time Rick gets hit. We actually need two new functions for this.
function DropWeapon()
{
rick_mc.weaponheld=false;
var temptag=rick_mc.weapon.tag;
removeMovieClip(rick_mc.weapon);
_root.attachMovie(temptag, "weapon", 40000 , {_x:rick_mc._x, _y:rick_mc._y});
_root.weapon.onEnterFrame=Fall;
}
function Fall()
{
rick_mc.crouch=false;
_root.weapon.gotoAndStop(1);
_root.weapon._y+=15
if(_root.weapon._y>=300)
{
_root.weapon._y=300;
delete this.onEnterFrame;
}
/* THIS IS OKAY TOO
if(_root.ground_mc.hitTest(_root.weapon))
{
delete this.onEnterFrame;
}
*/
}
There's just two more additions left to do. So, when Rick loses all his
health, what happens? Well, we want him to fall over like he does in
SplatterHouse and then get up afterwards, as if you'd restarted the
level with a new life.
So, we can emulate this sequence with a new and improved main
onEnterFrame event.
onEnterFrame = function()
{
RenderEnemy();
if(rick_mc.lives>0)
{
if(!wakeup)
MainLoop();
else
WakeUp();
}
else
{
rick_mc.alive=false;
}
if(!rick_mc.alive)
GameOver();
button.onPress = function ()
{
rick_mc.alive=true;
rick_mc.lives=3;
wakeup=true;
Lives();
removeMovieClip(_root.button);
}
}
There's
not much to this. The main program loops around until rick runs out of
lives, then when the lives are gone it goes into the GameOver()
function which plays the 'die' frame and sticks a button on the screen.
When you hit the button, it boosts the lives back up to 3 and actives
the 'wake up' frame.
(You can see why we stuck the wake_up=false code on the final frame of the wake_up mc now eh?)
function GameOver()
{
rick_mc.weapon.onEnterFrame=DropWeapon;
_root.attachMovie("button", "button",9, {_x:Stage.width/3, _y:Stage.height/3});
button._alpha=50;
rick_mc.gotoAndStop("die");
}
function WakeUp()
{
rick_mc.gotoAndStop("wakeup");
}
In
a real game, you'd could replace the button with a function that fades
the screen and then sets all the variables after that's done.DONE BABY!! Wow, spent the whole day on that and I'll bet my socks I missed something out. Anyway I think for the next chapter we'll refine the collision detection by using bounding boxes, and get some sounds pumping. Boy I sure would love to see some Flash C64 remakes. Karateka? Fist II? IK+. Hawkeye?Anybody? Last Ninja?...
Questions/advice/tutorial errors: email to youngdude77@hotmail.com | |