2DShooter Part4

From Unify Community Wiki
Jump to: navigation, search

By Eric Haines (a.k.a. Eric5h5)

Scripting the enemy

First we want the enemy to appear in the game: start off by loading the Game level. Click on the Manager object in the Hierarchy, and open the arrow next to the Object Prefabs variable in the Load Level script in the Inspector. By now you can probably guess what you need to do: change the Size element to 4, and drag your Enemy prefab onto element 3. If you run the game now, your enemies will appear in the locations where you put them in the level editor. They won't actually do anything right now, since they don't have any scripts yet, but they do have colliders, so if you run into them, your ship will explode. If all you wanted was indestructible stationary mines, then you could stop here, and you'd be done.

That's a little boring, though, so let's add some game logic. We'll have the enemy be a kamikaze pilot that tries to crash into the player, but only if the player comes within a certain distance, and it will stop if the player gets far enough away. Also it will be able to crash into walls.

Click once on the Scripts folder in the Project pane, and select Create and then JavaScript. Rename the NewBehaviourScript file that was created to Enemy. Double-click on the Enemy script to open it in your text editor.

Let's start out with some variables.

var moveSpeed : float = 5.5;
var trigger : EnemyTrigger;
var explosion : GameObject;
private var myTransform : Transform;
private var myTrigger : EnemyTrigger;
private var exploding = false;
@HideInInspector
var ship : Transform;

By writing these first, we've defined them as global variables, that we can use inside any function. "moveSpeed" is a public variable that says how fast the enemy will go. This is just a guess right now, which is a good reason for it to be a public variable--that way, you can change it in the Inspector without having to recompile the script. A common mistake, even for people who've been using Unity for a long time, is to forget that the value of a public variable is always derived from the Inspector, which overrides the value in the script. It's easy to change moveSpeed in your script to something other than 5.5, recompile, and wonder why it had no effect...always remember to look in the Inspector. Private variables, by contrast, are always derived from the script.

"trigger" is the EnemyTrigger prefab that you made, which we'll assign in the Inspector later. We're defining it as an EnemyTrigger type (i.e., the name of the script) instead of a GameObject type, since this will make it a bit more convenient to refer to, since we won't have to use GetComponent. It's easiest at first to always define objects and prefabs as GameObjects, which is fine, but it's more convenient (and faster) to use the type you'll be using most in the script. For example, if you had an object where you defined it as "var someObject : GameObject", and you're always using "someObject.transform.position" and "someObject.transform.rotation" in the script, then it would be better to say "var someObject : Transform" and then use "someObject.position" and "someObject.rotation" instead. You can still get the GameObject component if you need to in this case by doing "someObject.gameObject".

"explosion" is the explosion prefab, which again we'll assign in the Inspector.

"myTransform" is a reference to the transform of the enemy object itself. (The reason will be discussed later.) It's a private variable because it's only used in the script itself, so there's no point cluttering the Inspector with such variables.

"myTrigger" is a reference to the trigger that will be associated with this enemy object.

"exploding" is a boolean that says whether the enemy's OnCollisionEnter function is running or not.

"@HideInInspector" means "prevent the next variable from appearing in the Inspector". That's because "ship" is a reference to the player's ship, which we need to be public so the EnemyTrigger script (see next section) can talk to the Enemy script about it. This variable is only used internally by the script, so we don't want to see it in the Inspector, but it can't be private because another script needs to access it. Hence the use of "@HideInInspector".

function Start() {
    myTransform = transform;
 
    var others = GameObject.FindGameObjectsWithTag("trigger");
    for (other in others) {
        Physics.IgnoreCollision(collider, other.collider);
    }
 
    myTrigger = Instantiate(trigger, myTransform.position, myTransform.rotation);
    myTrigger.enemy = myTransform;
 
    this.enabled = false;
}

The first line in this function may seem pointless--why not just use "transform.Translate", for example, instead of making this assignment and then doing "myTransform.Translate"? That's because "transform" is actually shorthand for "GetComponent(Transform)", and doing GetComponent calls takes a bit of time. Therefore if we only make this GetComponent call once in the Start function and store the result, the script will run faster. In the case of this particular game, the difference almost certainly won't be noticeable, but it's a simple enough optimization that I've gotten into the habit of doing.

In the next four lines, we first find all GameObjects with the tag "trigger", and store that in an array of GameObjects called "others". Then we loop through all the items in the "others" array and ignore collisions with each one. This is because we want the enemy to be on the same side as the turrets...otherwise the enemy would activate the turret triggers and make the turrets rotate toward it. (But not fire, unless you modify the TurretTrigger script).

Then we make an instance of the TurretTrigger prefab and call it "myTrigger", and put it in the same spot as the enemy object, with the same rotation. (The rotation hardly matters since the trigger is spherical, but we need to put something here, so we might as well use the same rotation.) The script on the TurretTrigger prefab will need a reference to the enemy object that spawned it (in other words, the enemy that's running this script), and the variable will be called "enemy", as you'll see in the next section. So we assign our own transform to this variable. It may help to re-read this section after reading the next section, so you can better see how the two scripts are interrelated, since they both refer to each other.

Finally we disable the script on the enemy object. This isn't strictly necessary, but does cause the Update function to stop running (until the player enters the trigger, which will cause the trigger script to re-enable this script). Again, it's unlikely this would be noticeable in the game unless you had a huge number of enemies all at once, but we might as well save a few processor cycles with something simple like this. Another way to get the same effect would be "GetComponent(Enemy).enabled = false;", but that takes longer to type....

function Update() {
    if (ship) {
        myTransform.LookAt(ship);
        myTransform.Translate(Vector3.forward * moveSpeed * Time.deltaTime);
    }
}

The first thing we want to check is if the "ship" variable is null or not. It will be null until the EnemyTrigger script assigns it a value (see next section). But we've disabled the Update function until the player's ship enters the trigger anyway, and the Update function is again disabled when the player's ship leaves the trigger (and in the event that the player gets blown up while still inside the trigger, it sends an OnTriggerExit message to all triggers). So why bother? Well, when referring to other objects, it's good practice to always check for the actual existence of that object, unless you're 100%, completely, totally sure that the other object will exist no matter what (and maybe even then!). A tiny bit of possibly unnecessary code is better than the chance of a null reference exception error, which, even if it happens for just one frame, will probably crash the program.

So if the ship variable does exist, first we want to look in its direction. Then we want to actually move in that direction. In a Translate function, Vector3.forward refers to the object's local forward direction, which we multiply by the moveSpeed variable, and then multiply by Time.deltaTime (the time since the last frame), so the ship will move at the same speed no matter what the actual framerate of the game is.

function OnCollisionEnter() {
    if (exploding) {return;}
    exploding = true;
    Instantiate(explosion, transform.position, transform.rotation);
    Destroy(gameObject);
    Destroy(myTrigger.gameObject);
}

The first line sees if this function has already been called by checking the "exploding" variable. The reason for this is because it's possible for collision calls to be made more than once, if the object collides with more than one other object that frame. In this case, we only care about any one collision at a time. So we set "exploding" to true, so if it happens that this function gets called again, it will just return without doing anything. Then we make an instance of the explosion prefab, wherever the enemy happens to be. Finally we get rid of the enemy object, and also the trigger that we instantiated in Start. Remember that the trigger variable was defined with the type of its script, so we need to add ".gameObject" to get rid of the whole trigger GameObject, and not just the script on it.

As a side note, if it detects a collision, the RaycastCheck script sends a message to any function called "OnCollisionEnter" on any script attached to the enemy prefab. This way raycasting collisions will behave like any other collision (with the wall's collider passed as a parameter, although none of the scripts currently need to know about the specifics of any collider they've hit, and thus it's ignored). It's worth remembering that the "special" functions like OnCollisionEnter also work like regular functions and can be called as such. For example, there's nothing stopping you from doing this:

function OnCollisionExit() {
    OnCollisionEnter();
}

That way you could have the same code run for OnCollisionExit that runs for OnCollisionEnter, if you wanted that to happen for whatever reason.

Note also that the enemy won't collide with turrets. This is because we checked Is Kinematic when making the rigidbody for the enemy, and kinematic rigidbodies only collide with non-kinematic rigidbodies, and the turrets don't have rigidbodies. If you wanted the enemy to collide with turrets, just turn off Is Kinematic in the prefab. This actually won't have any other effect on the ship in this case; checking Is Kinematic was just simpler than writing a bit of IgnoreCollision code.

The whole script looks like this:

var moveSpeed : float = 5.5;
var trigger : EnemyTrigger;
var explosion : GameObject;
private var myTransform : Transform;
private var myTrigger : EnemyTrigger;
private var exploding = false;
@HideInInspector
var ship : Transform;
 
function Start() {
    myTransform = transform;
 
    var others = GameObject.FindGameObjectsWithTag("trigger");
    for (other in others) {
        Physics.IgnoreCollision(collider, other.collider);
    }
 
    myTrigger = Instantiate(trigger, transform.position, transform.rotation);
    myTrigger.enemy = myTransform;
 
    this.enabled = false;
}
 
function Update() {
    if (ship) {
        myTransform.LookAt(ship);
        myTransform.Translate(Vector3.forward * moveSpeed * Time.deltaTime);
    }
}
 
function OnCollisionEnter() {
    if (exploding) {return;}
    exploding = true;
    Instantiate(explosion, transform.position, transform.rotation);
    Destroy(gameObject);
    Destroy(myTrigger.gameObject);
}

Once the script is saved, click on the Enemy prefab in the Project pane, and go to the Component menu and choose Scripts -> Enemy. Find the EnemyTrigger prefab in the Project pane, and drag that onto the EnemyTrigger slot in the Enemy script in the Inspector. Also drag an explosion prefab onto the Explosion slot. You can use the TurretExplosion prefab, or the ShipExplosion prefab, or make your own.

Now we just need to script the trigger.


2D Shooter Index : Previous Part : Next Part

Personal tools
Namespaces

Variants
Actions
Navigation
Extras
Toolbox