2DShooter Part7

Author: Eric Haines (a.k.a. Eric5h5)

2. Acceleration (Not quite as easy)
Unless you've got the trigger for the enemy object set to a large enough radius that the enemy is always triggered off-screen, you can see that the enemy instantly goes from sitting still to full speed when the player enters the trigger (and from full speed to 0 instantly if the player dies or leaves the trigger). While you have to admire such acceleration and braking abilities, you also have to consider the whiplash the poor pilot must experience, not to mention that it doesn't look very realistic. How about fixing that so the enemy accelerates properly when the player enters the trigger, and decelerates when the player leaves?

Hint: Isn't "lerp" a silly word?

Discussion: While the first thing that comes to mind (at least for me) is making OnTriggerEnter a coroutine by using yield, and having the enemy accelerate over time using a loop, and then doing the same in reverse for OnTriggerExit, this method has a potential problem. What if the player manages to enter the trigger and then exit before the enemy has fully accelerated? You'd have the OnTriggerEnter and OnTriggerExit coroutines running simultaneously for a while, which would almost certainly result in wrong behavior. It's probably better to use Mathf.Lerp. Something like, in Update:

This way the enemy is always "accelerating", though Mathf.Clamp01 makes speedIndex clamped between 0 and 1, so the Lerp won't make it go faster than "moveSpeed" or slower than 0 (which would be moving in the opposite direction). The third parameter in Mathf.Lerp is always clamped between 0 and 1 anyway, but only for the purposes of the Lerp calculation--in this case we actually don't want "speedIndex" itself to go outside that range, or else the deceleration probably wouldn't happen at the right time since it might take a while to get within the 0..1 range. Therefore, with the above code, you can switch between acceleration and deceleration at any time simply by swapping the sign of the "acceleration" variable from positive to negative (and then back again as necessary).

Of course, there's a bit more to it. You'd have to define the global variables for "speedIndex" and "acceleration" (the value of which you'd want to set in the Inspector), and change the EnemyTrigger script so it made "acceleration" in the Enemy script positive on trigger enter, and negative on trigger exit.

One more thing you might consider is what happens if the player dies while the enemy is chasing him--the enemy will still stop instantly when this happens, and, moreover, will start up again instantly if the player (after respawning) enters the trigger again. To prevent this, you'd have to get a little more tricky. One way is to have the Enemy and EnemyTrigger scripts not get disabled on trigger exit in this case, and instead set up a boolean in Enemy called "coasting". Then, in Update, you can set "coasting" to false if the player's ship still exists, and true otherwise, and have the movement code work both if the ship exists or the enemy is coasting. You can still have the scripts be disabled if "currentSpeed" reaches 0, as well as setting "coasting" to false in that case, and only do the LookAt if the ship exists (so it will continue in the direction of the ship's last position until it stops).

3. Avoiding collisions (Not quite as easy)
Now you've got a pretty rockin' enemy. It's got smooth moves, it shoots, it...crashes into walls. Hmm. Well, nobody ever said it was a smart enemy! The simple way around this, of course, is to remove the RaycastCheck script or the rigidbody component. Then it just flies right through the walls, though, which is hardly better (unless it's supposed to be a ghost enemy). How about making it try to avoid the walls altogether?

Hint: Did somebody just say "raycast"?

Discussion: Check out the RaycastCheck script for ideas. You could make another script, which checks a point a little distance in front of the enemy by using raycasting. In this case, you'd always want to check in Update, since the enemy would probably not be in a wall trigger when the raycast point would detect a wall. You've already got a nice way to decelerate, so you could probably use that somehow...just make sure the raycast point is far enough ahead or the deceleration is fast enough that it doesn't still crash into the wall anyway! The enemy could still potentially hit walls in certain cases, though: since it would only check the one raycast point, it's possible there sometimes might be some wall in between the enemy and that point, which the enemy would cluelessly run into. You could solve that simply by using more raycast points in a line, instead of just one. (Unfortunately we do have to check multiple points, since the raycast is checking pixel values and not colliders.)

3a. Pathfinding (Even more advanced)
OK, you can get the enemy to stop crashing into walls (most of the time), but how about making it even smarter? How about making it actually try to follow the player, if the player is savvy enough to put some wall in between himself and the enemy, while still remaining within the enemy's trigger area? Or you might even make a "stalker" enemy, and have it chase the player around the level all the time, only using the trigger area for activating the shooting routine and aiming shots, instead of activating movement. At first that seems too hard--we don't want to do lots and lots of raycasts to try to determine a path, because that would be too slow. However, it just so happens that there's a convenient map of the level in array format--the "dataLines[2]" variable from the LoadLevel script. You could copy the contents of that array into a static array variable (which can easily be accessed from any script), converting from chars to ints in the process. Then the enemies could look into this level array, converting their world coordinates to array coordinates to see where they are in the map. Likewise, the player's world position would be converted to map coordinates. (Check the GetLocation function in the LevelEditor script for the formula to do this, though you don't need to use ScreenToWorldPoint since you'd already have the world points in this case.) That way it would be easier and faster to do some pathfinding code. What you'd probably do is have the "ship" transform set to the middle of an empty cell next to the cell the enemy is in, that best suits the direction the enemy thinks it has to go in order to reach the player, rather than the actual ship's transform (unless the ship is reasonably close).

If you get stuck, you can take a look at the updated example project (see the link on the intro page for this). A simple flood-fill algorithm is used for pathfinding--it's easy to find a description of the theory behind this with an internet search, if you're not quite following what the code is doing. The SimpleEnemy prefab is what's covered in the first part of the tutorial; replace that with the Enemy prefab in the "Object Prefabs" array on the Manager object if you want to use the more advanced enemy from the scripting suggestions. Here's some extra controls in the example project:

The Manager script has an "Allow Diagonals" checkbox--this allows enemies to go diagonally between cells when pathfinding instead of just left/right/up/down. This means they can chase after the player faster, but at least with the walls as I drew them, they have a tendency to crash when trying to cut corners. If you give the enemy a faster turning speed, this doesn't happen as much. There are ways to prevent this--see if you can implement one (it's not that simple though, or at least the ways I could think of were a little complex!).

The Enemy script has some extras: "Rotate speed": pretty obvious. "Acceleration": also obvious, except that this is also the deceleration speed. "Path Steps": number of tiles the enemy will travel when pathfinding, before trying to head for the player again. Turns out it's not hard to start pathfinding when the LookAhead script detects a wall, but not so simple to figure out when to stop! In a 3D game, you could use a linecast to see if there's anything in between the enemy and the player, and turn off pathfinding if not, but due to the way walls are implemented here, that's not possible. So the "path step" solution isn't ideal, but it was simple to implement. One problem is that the enemy can sometimes be intent on pathfinding, and neglect to start shooting at the player as soon as he could. Therefore, the "Shoot When Pathfinding" checkbox can fix that problem, so the enemy never stops shooting. Otherwise he stops shooting when pathfinding, since mostly he'd be shooting uselessly at the walls.... The "Pathfind Outside Trigger" checkbox means that if the enemy is pathfinding and the player leaves the trigger area, the enemy will not stop, but continue to go after the player (unless the player dies). Try using the "Pathfind Outside Trigger" option on the Enemy prefab, and then using the "maze" level that's in the TextAssets folder. Turn off Maximize on Play for the Game view and zoom the Scene view out so you can see the whole level, and watch the enemy solve the maze.

The LookAhead script has a "Do Pathfinding" checkbox. If this is checked, the enemy will engage the pathfinding routine when blocked by walls. Otherwise, it will simply stop, though it will continue to rotate in the player's direction if activated, so will start up again as soon as the LookAhead script give the all-clear.

4. Lots o' stuff (Variably difficult)
Of course, there's a nearly infinite number of other things you can do. Make the enemies have health, so it takes more than one shot to destroy the more advanced enemies (and then make upgradable weapons, so the player can eventually have an easier time destroying enemies with lots of health). Make enemies be animated with an array of textures, and leave smoke trails. Make enemies spawn smaller enemies. Make enemies shoot homing missiles instead of bullets. Make enemies shoot bullets again, but have them aim where the player is heading, not where the player is. Make enemies that move in other ways instead of just heading for the player--hang back at a certain distance, circle the player, etc. Make enemies composed of more than one part, that lose capability as the parts are destroyed. Make boss enemies that do all sorts of stuff...the sky's the limit, so have fun! A lot of this stuff has already been covered on the Unity forums in one way or another, so do a search there if you need some help.

2D Shooter Index : Previous Part