From Game Editor
Programming languages are like modelling clay; They are extremely pliable and can be fired to become rock hard, or watered down to become soft.
But one must understand the basic logic behind it all before one can make serious games. If one does not use sensible logic, the script will expand too rapidly, becoming difficult to edit and nearly impossible to control.
The more you customize your script to automate calculations for you, the better your games will be. After all, the whole purpose of a computer is to do the work for us. Don't bang your head against a wall to break it down while refusing to pick up the sledgehammer that's right next to you. Breaking walls is exactly what sledgehammers are for!
The list of DO's include:
- using variables instead of concrete numbers.
- condensing and simplifying your scripts.
- using Algorithms to reduce the amount of scripting.
- using inheritance to distribute numbers and reduce calculations.
- writing functions to give yourself total control over the game.
Variables (NOT magic numbers)!
Using magic numbers is the first issue that most new users have; They want to describe things in the only way they know how: Basic math.
What happens then is that the user must keep track of all these numbers, and fill them in by hand. Why keep track of numbers in your head? That's what a computer is for!
Using variables is key in creating anything of any complexity. If you challenge this, and decide to use your brain only, the computer will beat you. Easily.
Condense your scripts
GE contains premade functions for many different actions, and some of them are intented only to help new users bypass learning certain things; not all of these functions are meant to be relied on in the long term.
The collisions in GE, for example, are pixel perfect and very fast, so they are intended for professional game use. KEYUP events, however, are simply there to help new users and are not intended for use in professional games.
Consider this way of scripting key events: KeyDown>LeftArrow>
xvelocity+=5; ChangeAnimation("Event Actor", "walk_left", FORWARD);
This is incorrect, and will prevent you from editing your game. There are three things wrong with this method:
- If we have to change the speed, or the animation, we will have to go through 8 separate event scripts to make the change; 4 KeyDowns and 4 KeyUps.
- It gives us absolutely no ability to control these actions from other scripts.
- When we do try to make changes in other scripts, these will get in our way. You can spend hours wondering why a script isn't working; only to find that a previous script (in a different event window) was overriding that script.
The proper way to script key events is a combination of Draw Actor, Global Scripts, and Variables. Here is an example:
KeyDown>RightArrow
dir=0; vdir=1;
Simple eh? Work counterclockwise from the far right, to match the degree system ge uses, where right is 0/360, up is 90, left is 180, and down is 270.
So our dirs are right=0, up=1, left=2, and down=3; vdir is another variable that gets set to 1 on all the keydown events.
Now what do we do with dir? Use it in draw actor>
switch(dir){ case 0: ChangeAnimation("Event Actor", "walk_right", FORWARD); break; case 1: ChangeAnimation("Event Actor", "walk_up", FORWARD); break; } angle=dir*90; directional_velocity=speed*vdir; if(vdir==0){ ChangeAnimationDirection("Event Actor", STOPPED); animpos=0; }
where dir and speed are actor variables.
Now we can change player speed anytime, not only for upgrades but also for walking through mud or being poisoned.
Notice too, that we only need to state the angle/velocity once, in one script. It doesn't matter what value dir is, the character will move in the proper direction.
With this method, you don't need to use the KeyUp script at all! At the bottom of the draw actor script, add the line:
vdir=0;
Now when the user lets go of the Keys, vdir resets to 0 and the player stops moving, but dir does not change, so the player remains facing the direction he was last walking in. And the vdir animation check will automatically stop the actor from walking in place; if there is no key pressed, the animation will stop and reset to frame 0 (which should be the standing still frame).
Since the inputs are run before the draw script, the constant keypress will keep resetting vdir to 1, so the reset to 0 in the draw will only affect gameplay if a key is not pressed next frame.
Now when i want to change the user speed, i just set the speed, ONCE, in one script. Imagine if you were testing out speeds, but you had to change it in four locations each time you tried a new speed?
That would be an insane and illogical way to code, and will not result in finished, quality software.
Now if we apply this logic to other events, such as player>collision>any side>enemy shot>
state=1; //initiate 'hurt' state typed=collide.type; //store bullet type (used for modifiers) damaged=collide.damage; //store bullet damage DestroyActor("Collide Actor"); //destroy bullet
and then in player>draw actor>
float deflect=1; //used to store defense if(typed==armortype){ //if we have proper armor for the bullet deflect=.5; //set the modifier } switch(state){ case 0: //normal operation break; case 1: //hurt operation health-=damaged*deflect; //take modified damage here state=0; //reset state to not hurt anymore tempd=0; //reset damage storage break; }
Consider what happens if player gets Mirror Armor, that deflects 50% of all laser damage; but doesn't deflect any bullet damage; Instead of dealing with all that script in the collisions, we can adjust the damage here in draw actor.
In this example, i stored the bullet type and damage, and compared it to my armor before taking damage. We initialized deflect at 1, so if we don't have armor, the deflect won't change the damage. This is much more efficient that using a case switch to assign damage based off bullettype.
Using this state method in draw actor gives you many other options too, for instance, if i want the player to flash when hit, I can change the animpos to hit in state 1, then reset it to 0 when we return to state 0.
I can also take the damage once, but count down to release the state, thus i can keep the actor in a hit flash as long as i want.
I can also, after that script, check my health, and if it's <=0, we can set the state to 3, and in 3, have the dying/explosion animation.
Now you don't need an explosion actor. The actor can store its own, and using state with draw actor, explode itself.
So the extra information stored on the bullet hit, such as type and damage, does not take up extra memory and cpu, because it results in many optimizations afterward that save us even more resources.
But most importantly, its simple to change this script. Once you've set it up this way, you won't have to edit player>collision>enemyshot EVER AGAIN.
Using Algorithms
Do not be afraid of this word. Though it seems like something only used by Google and Microsoft, you will find yourself creating them from the start.
An Algorithm is a method to solve mathematical problems of any sort, to create structures, and to modify data.
Algorithms will save you exponential resources and time. Here is one example:
A user decided to make a graphical keypad using the Canvas.
The user input every single coordinate for every single button, and then again for a button outline, resulting in not only a lot of work, not only a very long script, but its absolutely unchangeable. If the user wants to change the layout in any way, colors, buttonsize, location, adding new buttons - the user must go through and type all the numbers by hand.
The correct way is to write your own algorithm.
In this case, i used a loop to create the button locations:
int i; int j; int k; int startx; int starty; int bwidth=80; //our buttons will be 80x40 int bheight=40; for(i=0; i<9; i++){ //creates 9 buttons startx=j*100; //since the buttons are wider than they are tall, starty=k*50; //we'll spread them out in a similiar way. moveto(startx, starty); //draw the button lineto(startx+bwidth, starty); lineto(startx+bwidth, starty+bheight); lineto(startx, starty+bheight); lineto(startx, starty); j++; //move to the next button if(j==3){ //if we reached the end of the row j=0; //return to the start of the row k++; //move to the next row } }
We have now drawn all nine buttons with only one loop; we could draw 1000 buttons if we want, the script won't get any longer.
Likewise, we can define an offset to relocate the keypad, and only have to add that to startx and starty, instead of adding it to magic numbers in the line drawing functions.
Now, let's take it a step further. We can reuse this algorithm!!!!!
We can find where the user clicked using the same method.
So on mousebuttondown>
int i; int j; int k; int startx; int starty; int bwidth=80; int bheight=40; for(i=0; i<9; i++){ startx=j*100; starty=k*50; if(xmouse>startx && xmouse<(startx+bwidth) && ymouse>starty && ymouse<(starty+bheight)){ //if the mouse is in bounds of current button clicked=i; //store current button id } j++; if(j==3){ j=0; k++; } }
}
Noticed we used the same mathematics on both of our scripts; one to draw them and one to tell us which one is clicked on.
We can condense this even further by using functions.
Inheritance
Inherited behaviour is a staple for professional games companies; redundant calculations waste resources. Inheritance allows you more precise control over game variables; it also allows you to push algorithms farther up the chain of command, resulting in more variable referencing and less variable calculation.
Even more important, it saves YOU time and work!
An example is the speed and direction of shots;
Shot>createactor>
directional_velocity=5; angle=direction(x, y, player.x, player.y);
This script works, but it doesn't give us any control, nor does it allow us to condense actors and scripts.
Consider if an enemy has two weapons; a machine gun and a sniper rifle. Both shoot in very different patters, in fact the only thing they have in common is that they both hurt the player if they hit.
Instead of a complex calculation in the createactor of the bullet (in the above example, we used the direction(); function which is very cpu expensive; now the machine gun will spit out hundreds of bullets that will all make this calculation; that is a waste of resources), instead we will calculate these things in the enemy itself, and simply transfer them to the bullet.
enemy>shotcreationscript>
myangle=direction(x, y, player.x, player.y); shotspeed=5; CreateActor("bullet", "bulleta", "no parent", "no path", 0, 0, false);
bullet>createactor>
angle=creator.myangle; directional_velocity=creator.shotspeed;
Now when we wish the enemy to switch weapons, we don't need different bullets or anything. Just change myangle and shotspeed, and the shots will inherit the new events. This makes it simple for the enemy to use dual wield!
enemy>shotcreationscript>
//left hand holds sniper rifle myangle=direction(x, y, player.x, player.y); shotspeed=10; //sniper bullets are fast! shotdamage=10; //sniper bullets are deadly! CreateActor("bullet", "bulleta", "no parent", "no path", 0, 0, false); //right hand holds machine gun myangle=direction(x, y, player.x, player.y); shotspeed=5; //machine gun bullets are not as fast shotdamage=5; //machine gun bullets do less damage; myangle+=rand(10)-5; //machine gun bullets aren't as accurate either CreateActor("bullet", "bulleta", "no parent", "no path", 0, 0, false);
Remember that games run frame by frame, in a linear method; so one script must complete before any others can run. Therefore, these inherited variables, such as shotspeed, apply to everything that happens after shotspeed is set, until such time as its set to something else.
Using functions
This is not a tutorial on functions, but rather an explanation of why we use them.
If we create a spreadshot, as 2d shmups use, we don't have to create new bullet patterns for every bullet.
Instead, we can create a function to create these; and then any enemy or player can access the same function, simply inputting different variables.
What this means is that we can reduce a draw actor script to only a few functions; we don't have to rewrite functions; instead, we can focus on making our bullet spread patterns better and better!
Functions are an integral part of the final, and most important topic of this page:
Editability
Making your code easy to edit in a logical manner is the absolute key to making a successful game, because logical programming:
- makes the game easier to edit and customize to your liking
- uses fewer resources (that can then be applied toward graphic fx)
- make the game easier to port to other platforms
- allows other programmers to understand your code and help you with it.
There are no tricks or tips that will save your game from being a resource hog and crashing; what will save you is proper game structure.
The less control you have over enemy actions, enemy spawning, and level flow, the less you will be able to create the game the way you want it to be.
Assuming that you want your game to be fun, the easier it is to edit, the more fun you can make it. And the more fun your game is, the more effective it will be.
Consider setting difficulty levels; When the player chooses 'hard', do you go through and set all the variables in 1000 different scripts to something else based on a complicated case switch?
No, that would be silly.
But if you've created a system of inheritance, you can change one variable at the top of the pyramid, and it will change the appropriate variables on down the line.
A proper program structure, viewed from the side, appears as a pyramid, with each level controlling objects in the level(s) below it;
From the top, it appears as a stacked clock, made of gears; where when the center gear turns, it makes all the other gears turn in relation to each other.
And your loops and checks are simply cycles running around these gears;
For instance, when a player targets enemies, the player is more important, and is on a higher tier than the enemies; each action the player has is one tooth of the player's gear; Whichever tooth of the gear is at 12 0'clock is the one that is active.
So imagine the player's gear of the clock rotates so the player's targeting tooth is at 12, and spins the enemy's gear of the clock; and whatever tooth of the enemy gear is at 12 represents one enemy, and player will look at that tooth, that enemy, and decide if that is a good target or not, and either stop checking, or turn the gear to the next tooth. When the enemy gear has done a full rotation, and player has a target, player gear then turns so the 'shoot' tooth is at 12. And when the player has finished his gear rotation, the higher gear will change its position and cause something else to happen.
And like a fine watch, these gears must move in synchronization, free from clutter, and the closer you get to this concept, the more powerful your game will be.