Wednesday 30 December 2015

AI, Instantiation/ Destruction of an Object and Player Feedback/ Damage using Object Properties and Loops in my Unity Game

Today's blog post will cover examples of:

-AI
-Instantiation
-Destroying an object
-Visual and physical player feedback
-Damage
-Object properties/ physics behaviours
-And Loops

In my Unity game.

To begin: I have an intelligent Dinosaur enemy that incorporates AI, instantiation and the destruction of a harmful projectile, and also this instantiated fireball damages the player, which then shows visual feedback to the player as a damage warning and physical feedback where it knocks back the player in the direction they were moving, back onto the safer platform. This Dinosaur enemy is incredibly creative compared to many typical platformer enemies, and shows a range of techniques that have been incorporated into its AI. I also have a spikes game object as another way to injure the player, which are much more static than the Dinosaur enemy as they simply sit in place and hurt the player if the Bird touches them. I added these two as there needs to be more than one way for the player to get hurt in the game, since the Dino enemy alone would get boring, so there needs to be variety in the dangers of my game world (another way to strike a sense of fear and tension are the bottomless pits in my game if the player falls off the stage, talked about earlier in the collision detection blog post, these pose a much higher risk to the player. As if they fall down a hole they must restart the whole level since that's common convention many 2d platformers follow, whereas they can take 2 points of damage from the Dino/Spikes before the bird dies).

As an example of artificial intelligence used in my Unity game, I included an enemy Dinosaur with a very distinct attack pattern: Where when the player gets close (within a set distance inside the game object's properties, this means that I can freely change the wake range for any instance of a Dinosaur in the game, and it will attack from that set distance), the Dino will jump up out of the ground and surprise the player. When hiding only its eyebrows pop out of the ground as a clue to the more observant player, and when it wakes up the Dino then shoot fireballs at the Bird through instantiation (the fire rate and fireball speed can also be adjusted) that you must dodge or you'll take one point of damage. It is intelligent in the fact that the Dino can hide again when the player goes too far away, and it even turns around if the player goes either side of it. And since the player purposely can't kill the enemies, the game is much more stealth-based than the traditional platformer games. Here's an example of the code that allows it to work and a picture of the Dinosaur's sprite (created by me in Photoshop):


Sample of the Dino's code:

' {

distance = Vector3.Distance(transform.position, target.transform.position);

if(distance < wakeRange) //If the distance (as a numerical value) to the Bird is smaller (closer) than the wakeRange value (set to 6 in the Dino's properties inside the Inspector View), then wake up the Dino
{

awake = true;
Attack(lookingRight);

}

if(distance > wakeRange) //If the distance is bigger (further away) than the wakeRange, make the Dino continue to, or go back to, sleep
{

awake = false;

}

}'


This script basically checks to see the Bird's position from the Dino, and then compares it to the value set for the 'Wake Range' variable. If the player's number is smaller than the Wake Range, then they are nearby and the Dino can wake up then shoot. If the number is higher, then the Bird is still too far away and the Dino will carry on sleeping. I added this into the game as it helps make the AI feel more alive and cunning, and adds a good layer of strategy to the game where the player can't just blindly charge through a level since there are hidden traps to watch out for!

Also as an example of debugging I've used for my Unity game, I had to debug it since the Dino is supposed to be able to spot the player and jump out of hiding, but was never able to spawn a fireball to damage the player with. Meaning it couldn't injur the player at all! I used 'Debug.Log ("TEST");' for the Dino Turret to find the big problem, as that line of code writes 'TEST' into the console when it's been triggered depending on the portion of the C# code it's situated in. So I wrote it in the section of every major element of the code; such as detecting the player, rising up after spotting them, the bird colliding with it's collision cones to spot them and the attacking portion of the code. And I finally found the problem to lie in the attacking portion of the code, as the text 'TEST' printed into the console on every other step, showing it works fine, but didn't work for the attacking function. It meant that somewhere between the bird colliding with the Dino's field of vision, and instantiating a projectile to hurt the player, something went wrong. I got it to eventually instantiate fireball by adding the attack code after the Dinosaur wakes up (rather than in the attacking section that it references), as shown below:


Debugging code for the Dino waking up, and how Debug.Log helped find the problem:

'if(distance < wakeRange) //If the distance (as a numerical value) to the Bird is smaller (closer) than the wakeRange value (set to 6 in the Dino's properties inside the Inspector View), then wake up the Dino { awake = true; Attack(lookingRight);
Debug.Log("TEST"); }'


The Debug.Log message that I received in the Unity Console when everything is working as intended. The debug code essentially goes just below the section of your code you want to debug, so for example when you put it in the collision section of the code, if the player ever collides with that object, this message will appear in the console, so you know everything is working well! 


The Dinosaur originally was just going to wake up when the player approached from a set distance, and actually attack when the player touches the small collider cones either side of it as its field of view. I eventually scrapped this idea however, since it was visually strange for the Dino to jump up and stare-down the player, even being in possession of a ranged attack, but only choose to shoot the Bird when it gets much closer. I feel the final product for the Dino was a much better outcome, and even though the sprite of the Dino is completely static (which was created by me in Photoshop, as compared to many other sprites I've used in this project that are sourced royalty free online from Open Game Art), it is still actually animated using the Unity Animator, so that it jumps in and out of the ground using a spritesheet that is also made by me. The code for the collider cone went largely unused in the end, but still exists in the game, here's an extract of it to show how it works:


Collision cone code sample:

' void OnTriggerEnter2D(Collider2D col) //Conditional statements to decide what to do when the player comes in range
{
if(col.gameObject.tag == "Player") //See if the trigger that collides with the cone is the Bird or not, since it's tagged with 'Player'
{

if(isLeft)
{
dinoTurretAI.Attack(false); //Player is in the left trigger, not right and isLeft defaults to false
}

else
{
dinoTurretAI.Attack(true); 
}
}
}'

Essentially how it works is if the Bird (the only game object with the tag of 'Player', assigned in the Unity inspector used to make that game object individual, or part of a set that your code can reference), collides with the trigger cone either side of the Dino, then it will signal to attack, otherwise by default the attack script is set to false so the Dino doesn't constantly open fire. I changed this in the end for the better, as the code that runs now is more optimised to play since it only needs to run in one script, and not two.

The Dino's colliders, note the two cone shaped trigger boxes on either side of it. These used to be its 'line of sight'.


Next up is damage in my game. Both the Fireballs shot out of the Dino and Spikes will damage the player, who has a maximum health of 2 (I did this because they originally had 5 health, but upon testing the game felt too easy with 5 hit-points and posed less of a challenge. Where with 2 pieces of health the difficulty is there, but it's still slightly forgiving since it gives the player small amount of room for error and isn't completely brutal to play compared to if they only had 1 chance). It essentially removes 1 from the Bird's current health variable (which automatically sets itself to 2 when the game starts, since that's the value of the max health variable and the current health will continually drop from that number in-game). Here's the damage code they both share which is inside the 'Player' script, which essentially checks to see that if the object which collides with the Bird is tagged with 'Spike' or 'Bullet', and then it applies the damage to them by reducing the value of the Bird's current health (I put all of the collision scripts with various objects that hurt the Bird together in the Player script, as it simply saves space and is easier to find than a searching through lots of different scripts to find each object that can hurt the player):



Damage code upon collision with the Bird:

' public void OnTriggerEnter2D(Collider2D col){ //Much simpler and efficient damage code in just a few lines, where if the player collides with the object tagged with 'Spike', do damage to them
if (col.gameObject.tag == "Spike") {
curHealth -= 1; //Remove 1 piece of health from the player, so they take 2 hits to die (as the max is 2)
anim.Play ("Bird_Damage", -1, 0f); //gameObject.GetComponent<Animation>().Play ("Bird_Hurt"); //After the collision play the bird's hurt animation as visual feedback to the player that they have just been hit

//Player knockback from spikes 1
StartCoroutine (Knockback (0.02f, 250, transform.position)); //Time it takes to get the knockback is 0.02 seconds, with a power of 350. It affects the position of the player. Coroutine function is used as it's the only way you can start an IEnumerator function that's in the Player script. For testing I originally had StartCoroutine(player.Knockback(0.02f, 250, player.transform.position));, but after many errors in the console I removed the player text as I didn't need to reference a script the code is already in.

}

if (col.gameObject.tag == "Bullet") { //Same code but for the fire shot out by Dinos instead
curHealth -= 1;
anim.Play ("Bird_Damage", -1, 0f);

StartCoroutine (Knockback (0.02f, 250, transform.position));

Destroy (GameObject.FindWithTag("Bullet")); //When collided with destroy the game object tagged with 'Bullet' (which is the fireball)

}
}'

I originally had a separate damage script for the Spikes and Fireball, but as mentioned previously it was just much more convenient to keep them all together under one 'OnTriggerEnter' script. Which is why both of these objects are so similar, and even share the same public void as the collectable Coins I talked about previously! The Spikes and Fireball collisions contain code that play the Bird's 'damaged' animation and knock them back after being triggered, which is a form of visual and physical feedback I talk about with the knockback script. Also the Fireball has an extra line of code which is designed to destroy itself once it collides with the Bird, which is explained in further detail when I talk about instantiation.

 (These are the Fireball and Spike sprites used in my game, which match the pixel art style very well.)



Whenever the player takes damage, I have also included visual and physical feedback to the player so they are aware they've taken damage, in order to alert them and say what just happened was bad without directly saying it every time. When the Bird is damaged in-game, they will then show a 'shocked' expression (since the lore of my character is that they change colour and appearance depending on how they're feeling) and violently flash yellow looking shocked, then turn red looking angry, using the Unity Animator as a form of visual feedback to the player to show that what happened was a negative thing. (The code to play this animation is included in the Fireball and Spikes collision code.) I did this over-the-top animation through a form of games design that teaches the player by showing, not telling the player to watch out everytime they're hit after the initial tutorial.

Since I felt this was not enough, if the player gets hit they'll also fly backwards in the opposite direction they were moving in, so if they walk off a platform onto spikes they'll be flung backwards onto a safe surface as a safety-net type of feature (but the code is rigged so if the player lands directly on the spikes after a high jump, they'll die. It knows if the player walked off onto it or jumped onto it depending on the horizontal movement speed), to give them another chance before they have to restart the whole level again because of a mistake. Here's the an extract of my C# script for the knockback player feedback code using a loop (and how it modifies my Bird object's properties and physics behaviour depending on the movement speed):


Bird knockback code 2 (the 1st part is in the collision section of the script):

'//Player knockback from spikes 2
public IEnumerator Knockback(float knockDur, float knockbackPwr, Vector3 knockbackDir){ //Dur is how long the force is added, power is the force strength and direction is the agnle the bird will fly when hit

float timer = 0; //Counting the time

rb2d.velocity = new Vector2 (rb2d.velocity.x, 0);

while( knockDur > timer) { //When the knock duration is grater than the time on the timer

timer+=Time.deltaTime; //Counts using seconds and adds to the timer

rb2d.AddForce(new Vector3(knockbackDir.x * -100, knockbackDir.y + knockbackPwr, transform.position.z)); //Apply a force of knockback to the rigid body. To do this get the knockback in the opposite direction the bird is moving across the x axis, and plus the knockback power on the y axis for a constant upwards push. Z doesn't get used as it's a 2d game. The spikes are also slightly rigged, in the fact if a player is careless and just walk off the edge onto the spikes, they'll be bounced back onto the platform as a safety net feature, whereas if they purposedly jump down directly on top of the spikes, they will be killed instantly in-game

}

yield return 0; //A yield return is needed to end an IEnumerator loop

}'

This code uses a counting function to loop the code, in order to constantly repeat the code in a loop, so the Spikes can knockback at a constant distance on the Y axis, and uses negative numbers to push the player backwards along the X axis. It does this over a set period of time and repeats the code constantly. The code essentially counts up with the delta time line (which counts at exactly 1 second relative time) until it's greater than the timer value set in the Unity Inspector of the Spikes game object and it means the Spikes can apply the force to the Bird by modifying the object's properties, changing its physics behaviour and launches away them depending on the Bird's current situation/ position. The yield return is used to stop the looping code. All of this is done for a reason, to add a more varied feel to the Bird's knockback and not just have them get flung back in exactly the same way everytime, but instead add variety to it. The code also works to get the Bird knocked back onto a platform safely, as if they approach the Spikes at a faster speed then they're knocked back further to compensate for the time they spend travelling in the air, which is shown by the line that knocks the Bird back by -100, which is times in the opposite direction.


The Fireball game object itself is designed to instantiate into existence from the game objects attached to the Dino, called either ShootPointLeft or ShootPointRight, and destroy itself when it collides with the player. This is both to balance the game and for optimisation purposes. If the fireball didn't destroy itself when it hurt the player then they could be damaged twice in succession, causing the player to die instantly from their low health and the game would feel unfair to play. It also optimised the game because if each fireball that spawned didn't get destroyed, eventually if the Dino kept shooting there could be potentially hundreds of fireballs running at once which would slow down the game dramatically on PCs that are geared less towards gaming. This is following professional industry practises, since having projectiles get destroyed after they've served their purpose and now clog up the game's memory is a standard procedure in most games. Here is the code the Dino's script uses to spawn a Fireball into the game when attacking on the left: (The code for attacking left is identical but just reversed without the exclamation mark inside the conditional if statement.)


Portion of attacking code to instantiate Fireball:

' bulletTimer += Time.deltaTime; //Cooldown code, the timer increases by 1 every second. And if the timer is greater than or equals to the shoot interval, aim at the player

if (bulletTimer >= shootInterval)
{

Vector2 direction = target.transform.position - transform.position; //Direction the Dino will shoot (towards the player as they are the target), the code will return a direction
direction.Normalize();

if (!attackingRight) //Instantiation code for the left of the Dino, spawning the fireball into the game to hurt the player

{
GameObject bulletClone;
bulletClone = Instantiate(bullet, shootPointLeft.transform.position, shootPointLeft.transform.rotation) as GameObject; //Spawns the projectile to the left of the Dino (at the ShootPoint's position), and casts the fireball as a game object into the game world
bulletClone.GetComponent<Rigidbody2D>().velocity = direction * bulletSpeed; //Once the bullet clone (clone of the real object) is created, set the fireball's rigid body velocity in the direction of the player at the speed set by the variable (100)

bulletTimer = 0; //The code will loop, by counting up again and restarting the timer, and once it's reached the shoot interval, the Dino will spit out another fireball. Rinse and repeat, as the Dino shouldn't be able to shoot 100 bullets a second for example

}'

Basically this code is another example of using Loops in my prototype. A loop is incredibly useful here so the code can constantly run at a fixed rate until the conditional statement requirements stop being met (so if the Bird goes out of range). The code is designed to think: if the Bird is to the left of the Dino, instantiate a Fireball Bullet from the Prefab assigned in the Inspector (I use these to spawn multiple objects that have the same properties as each other, and it's also useful for placing Coins like I mentioned before). The Bullet clone is named bulletClone, and is coded to shoot in the direction and angle of the target (which is the Bird when assigned in the Inspector), and is spawned as an in-game object. When the Bullet is shot the timer will reset to 0, looping the code again and thus counting back up until the shoot interval value is met (so in this case in 1.8 seconds as assigned in the Inspector), and once it counts up to 1.8 seconds the Dinosaur will shoot out another Fireball, reset the timer and start all over again.

1.8 seconds was specificity chosen as upon testing with various values, it felt the most balanced. It means that the Dino isn't brutally unfair in difficulty, shooting out so many Bullets that you just can't avoid them, but it still shoots out enough consistently to be a real threat and allows the player to gradually learn the attack patterns, and eventually predict exactly when the next Fireball will instantiate and dodge accordingly. It's why I'd rather have a set shooting interval than say, a random shot every 1-3 seconds, as it it would take away some of the skill factor in my game. The Dinos hiding out of sight and shooting out Fireballs through the act of surprise is generally a great way to show off their personality within the limitations of the game. Since they don't move about very much as a way to show the species is lazy, and they even choose to fly around in airships that are dotted around the game (the sprite was made by me in Photoshop, shown below), rather than just choosing to walk.

The airship sprite that was created by me. It acts as a background element, but also as a prop to set-up the game's story (since it's the first thing you see when the game begins, as you're dropped out of a Dinosaur's airship to be stranded in the desert).

Okay! Now that I've covered every coding element from the introduction list, here are some images of the Dino running the code in-game that I discussed earlier in this blog post (the Spikes damage the Player in the same way the Fireballs do, so they're only shown below the Bird in the pictures), and each image is a step-by-step process of what happens, and each have an arrow pointing towards the main focus of the code in that picture as a visual cue! These pictures also work as proof that all my code is running as expected!

The Dinosaur is hiding underground because the Bird is out of sight. Note the eyebrows poking out of the ground as a clue to its location.

The Bird comes within the 'Wake Range' distance and the Dino plays the animation where it jumps out of the ground, ready to attack the player.

The Dino instantiating a Fireball every 1.8 seconds, at a movement speed of 5. This Bullet then heads towards the Bird's initial location (so it's possible to dodge out of the way), while also gradually falling downwards due to me adding a slight value of 0.1 to the gravity in the Fireball Prefab's Rigidbody component, affecting its Physics Behaviour to fall much slower than the player, adding a realistic spin on the game since fire is made of gas, which is naturally very light). This falloff is to keep the player on their toes, as it means if the Dino shoots a Fireball upwards the player must be careful to not get hit by it on the way back down!
The fireball collides with the Bird, dealing damage and applying the visual (flashing special effects and animation) and physical (horizontal and vertical knockback) feedback.

More knockback as the Bird continues to fly further backwards (as the Dino continuously fires Fireballs after the first one has been destroyed), and has changed its expression from 'shocked' to 'angry' as the animation progresses before defaulting to the regular idle animation again.


My research into these various topics in my individual research blog posts helped me greatly during the development of the content I've talked about in this blog post. For example my research into AI helped me layout my Dinosaur's decision making process, using a bit of fuzzy logic with conditional statements, as each part of the code can be segmented out step-by-step depending on the current scenario. Likewise for destroying the Fireball, the 'Destroy(gameObject);' line that I used as an example for researching the destruction of an object was also fitted into my game during the collision section, only it specifically destroys the game object with a tag.

To follow onto this I originally used a damage system similar to the example I showed in that research post. However throughout the game's development I feel using the max health and current health system I have now is a lot more versatile than the example I provided (since if I wanted to I could make the Bird spawn in a certain level with a lower amount of health after a story sequence). Also the script I use now is much more optimised, and after originally testing with the first damage code as a basis it didn't work as intended, whereas  the code I have now is also a lot more efficient to run, since it simply requires one line of code in every object collider section of the player script for the appropriate object I want to damage the player. (That one line is 'curHealth -= 1;').

No comments:

Post a Comment