加载图片到Unity后,最好先设置要发布的平台,如果你的项目很大,到最后再换平台就会花费大量的时间
所以在开始就应该设计好
创建Player和Enemy 的预制体
使用 Animator override controller , 这个组件可是是不同的人物使用相同的动画控制器
对 Enemy1动画控制器右键,然后选择 Animator override Controller,
再选择重写的动画
为游戏添加Wall和其它Sprites
注意设置Tag和layers
Wall 的 Sorting layer 设置为 items
outerWall 的 Sorting layer 设置为Floor,因为它不会出现在人物的移动范围内,
而 Wall 需要出现在游戏中,作为阻挡人物前进障碍,并且要在 Floor的上方进行渲染
,不能被Floor层阻挡
BoardManager
创建随机地图
public class BoardManager : MonoBehaviour
{
// Using Serializable allows us to embed a class with sub properties in the inspector.
[Serializable]
public class Count
{
public int minimum; //Minimum value for our Count class.
public int maximum; //Maximum value for our Count class.
//Assignment constructor.
public Count (int min, int max)
{
minimum = min;
maximum = max;
}
}
public int columns = 8; //Number of columns in our game board.
public int rows = 8; //Number of rows in our game board.
public Count wallCount = new Count (5, 9); //Lower and upper limit for our random number of walls per level.
public Count foodCount = new Count (1, 5); //Lower and upper limit for our random number of food items per level.
public GameObject exit; //Prefab to spawn for exit.
public GameObject[] floorTiles; //Array of floor prefabs.
public GameObject[] wallTiles; //Array of wall prefabs.
public GameObject[] foodTiles; //Array of food prefabs.
public GameObject[] enemyTiles; //Array of enemy prefabs.
public GameObject[] outerWallTiles; //Array of outer tile prefabs.
private Transform boardHolder; //A variable to store a reference to the transform of our Board object.
private List <Vector3> gridPositions = new List <Vector3> (); //A list of possible locations to place tiles.
//Clears our list gridPositions and prepares it to generate a new board.
void InitialiseList ()
{
//Clear our list gridPositions.
gridPositions.Clear ();
//Loop through x axis (columns).
for(int x = 1; x < columns-1; x++)
{
//Within each column, loop through y axis (rows).
for(int y = 1; y < rows-1; y++)
{
//At each index add a new Vector3 to our list with the x and y coordinates of that position.
gridPositions.Add (new Vector3(x, y, 0f));
}
}
}
//Sets up the outer walls and floor (background) of the game board.
void BoardSetup ()
{
//Instantiate Board and set boardHolder to its transform.
boardHolder = new GameObject ("Board").transform;
//Loop along x axis, starting from -1 (to fill corner) with floor or outerwall edge tiles.
for(int x = -1; x < columns + 1; x++)
{
//Loop along y axis, starting from -1 to place floor or outerwall tiles.
for(int y = -1; y < rows + 1; y++)
{
//Choose a random tile from our array of floor tile prefabs and prepare to instantiate it.
GameObject toInstantiate = floorTiles[Random.Range (0,floorTiles.Length)];
//Check if we current position is at board edge, if so choose a random outer wall prefab from our array of outer wall tiles.
if(x == -1 || x == columns || y == -1 || y == rows)
toInstantiate = outerWallTiles [Random.Range (0, outerWallTiles.Length)];
//Instantiate the GameObject instance using the prefab chosen for toInstantiate at the Vector3 corresponding to current grid position in loop, cast it to GameObject.
GameObject instance =
Instantiate (toInstantiate, new Vector3 (x, y, 0f), Quaternion.identity) as GameObject;
//Set the parent of our newly instantiated object instance to boardHolder, this is just organizational to avoid cluttering hierarchy.
instance.transform.SetParent (boardHolder);
}
}
}
//RandomPosition returns a random position from our list gridPositions.
Vector3 RandomPosition ()
{
//Declare an integer randomIndex, set it's value to a random number between 0 and the count of items in our List gridPositions.
int randomIndex = Random.Range (0, gridPositions.Count);
//Declare a variable of type Vector3 called randomPosition, set it's value to the entry at randomIndex from our List gridPositions.
Vector3 randomPosition = gridPositions[randomIndex];
//Remove the entry at randomIndex from the list so that it can't be re-used.
gridPositions.RemoveAt (randomIndex);
//Return the randomly selected Vector3 position.
return randomPosition;
}
//LayoutObjectAtRandom accepts an array of game objects to choose from along with a minimum and maximum range for the number of objects to create.
void LayoutObjectAtRandom (GameObject[] tileArray, int minimum, int maximum)
{
//Choose a random number of objects to instantiate within the minimum and maximum limits
int objectCount = Random.Range (minimum, maximum+1);
//Instantiate objects until the randomly chosen limit objectCount is reached
for(int i = 0; i < objectCount; i++)
{
//Choose a position for randomPosition by getting a random position from our list of available Vector3s stored in gridPosition
Vector3 randomPosition = RandomPosition();
//Choose a random tile from tileArray and assign it to tileChoice
GameObject tileChoice = tileArray[Random.Range (0, tileArray.Length)];
//Instantiate tileChoice at the position returned by RandomPosition with no change in rotation
Instantiate(tileChoice, randomPosition, Quaternion.identity);
}
}
//SetupScene initializes our level and calls the previous functions to lay out the game board
public void SetupScene (int level)
{
//Creates the outer walls and floor.
BoardSetup ();
//Reset our list of gridpositions.
InitialiseList ();
//Instantiate a random number of wall tiles based on minimum and maximum, at randomized positions.
LayoutObjectAtRandom (wallTiles, wallCount.minimum, wallCount.maximum);
//Instantiate a random number of food tiles based on minimum and maximum, at randomized positions.
LayoutObjectAtRandom (foodTiles, foodCount.minimum, foodCount.maximum);
//Determine number of enemies based on current level number, based on a logarithmic progression
int enemyCount = (int)Mathf.Log(level, 2f);
//Instantiate a random number of enemies based on minimum and maximum, at randomized positions.
LayoutObjectAtRandom (enemyTiles, enemyCount, enemyCount);
//Instantiate the exit tile in the upper right hand corner of our game board
Instantiate (exit, new Vector3 (columns - 1, rows - 1, 0f), Quaternion.identity);
}
}
}
使用内部类或者内部结构创建的对象或者结构,一般不会显示在Inspector中,在内部类和结构的上方加上C#的特性
【Serializable】,就可以使创建的对象或者结构显示出来
Gridposition记录的位置如上,在这里面我们将放置敌人,墙壁,或者拾取物,这样就限制了我们不会在外墙生成不该有的物品
只有当 x == -1 || x == columns || y ==-1 || y == rows 的时候,才实例化一个 outerWall 对象,其它时候都不生成一个外墙
首先生成一个随机下标,用于记录生成的是gridPosition 的哪个位置
生成一个随机位置
为了保证不在同一个地方生成不同的东西,将 gridPosition中的已经生成物品的位置去除
再返回一个随机位置
sqrMagnitude() : 如果只是计算两个向量之间的大小,用这个比用 Magnitude效率更高
Epsilon : 返回一个很小的浮点值,非常小,几乎等于0
Mathf.Abs : 返回变量的绝对值
Input.GetAxisRaw :当在游戏运行的时候,按下你设置好的键盘就会返回 1和-1这两个值
DontDestoryOnload : 使对象在重新加载场景时,不被摧毁
Player
//Player inherits from MovingObject, our base class for objects that can move, Enemy also inherits from this.
public class Player : MovingObject
{
public float restartLevelDelay = 1f; //Delay time in seconds to restart level.
public int pointsPerFood = 10; //Number of points to add to player food points when picking up a food object.
public int pointsPerSoda = 20; //Number of points to add to player food points when picking up a soda object.
public int wallDamage = 1; //How much damage a player does to a wall when chopping it.
public Text foodText; //UI Text to display current player food total.
public AudioClip moveSound1; //1 of 2 Audio clips to play when player moves.
public AudioClip moveSound2; //2 of 2 Audio clips to play when player moves.
public AudioClip eatSound1; //1 of 2 Audio clips to play when player collects a food object.
public AudioClip eatSound2; //2 of 2 Audio clips to play when player collects a food object.
public AudioClip drinkSound1; //1 of 2 Audio clips to play when player collects a soda object.
public AudioClip drinkSound2; //2 of 2 Audio clips to play when player collects a soda object.
public AudioClip gameOverSound; //Audio clip to play when player dies.
private Animator animator; //Used to store a reference to the Player's animator component.
private int food; //Used to store player food points total during level.
private Vector2 touchOrigin = -Vector2.one; //Used to store location of screen touch origin for mobile controls.
//Start overrides the Start function of MovingObject
protected override void Start ()
{
//Get a component reference to the Player's animator component
animator = GetComponent<Animator>();
//Get the current food point total stored in GameManager.instance between levels.
food = GameManager.instance.playerFoodPoints;
//Set the foodText to reflect the current player food total.
foodText.text = "Food: " + food;
//Call the Start function of the MovingObject base class.
base.Start ();
}
//This function is called when the behaviour becomes disabled or inactive.
private void OnDisable ()
{
//When Player object is disabled, store the current local food total in the GameManager so it can be re-loaded in next level.
GameManager.instance.playerFoodPoints = food;
}
private void Update ()
{
//If it's not the player's turn, exit the function.
if(!GameManager.instance.playersTurn) return;
int horizontal = 0; //Used to store the horizontal move direction.
int vertical = 0; //Used to store the vertical move direction.
//Check if we are running either in the Unity editor or in a standalone build.
#if UNITY_STANDALONE || UNITY_WEBPLAYER
//Get input from the input manager, round it to an integer and store in horizontal to set x axis move direction
horizontal = (int) (Input.GetAxisRaw ("Horizontal"));
//Get input from the input manager, round it to an integer and store in vertical to set y axis move direction
vertical = (int) (Input.GetAxisRaw ("Vertical"));
//Check if moving horizontally, if so set vertical to zero.
if(horizontal != 0)
{
vertical = 0;
}
//Check if we are running on iOS, Android, Windows Phone 8 or Unity iPhone
#elif UNITY_IOS || UNITY_ANDROID || UNITY_WP8 || UNITY_IPHONE
//Check if Input has registered more than zero touches
if (Input.touchCount > 0)
{
//Store the first touch detected.
Touch myTouch = Input.touches[0];
//Check if the phase of that touch equals Began
if (myTouch.phase == TouchPhase.Began)
{
//If so, set touchOrigin to the position of that touch
touchOrigin = myTouch.position;
}
//If the touch phase is not Began, and instead is equal to Ended and the x of touchOrigin is greater or equal to zero:
else if (myTouch.phase == TouchPhase.Ended && touchOrigin.x >= 0)
{
//Set touchEnd to equal the position of this touch
Vector2 touchEnd = myTouch.position;
//Calculate the difference between the beginning and end of the touch on the x axis.
float x = touchEnd.x - touchOrigin.x;
//Calculate the difference between the beginning and end of the touch on the y axis.
float y = touchEnd.y - touchOrigin.y;
//Set touchOrigin.x to -1 so that our else if statement will evaluate false and not repeat immediately.
touchOrigin.x = -1;
//Check if the difference along the x axis is greater than the difference along the y axis.
if (Mathf.Abs(x) > Mathf.Abs(y))
//If x is greater than zero, set horizontal to 1, otherwise set it to -1
horizontal = x > 0 ? 1 : -1;
else
//If y is greater than zero, set horizontal to 1, otherwise set it to -1
vertical = y > 0 ? 1 : -1;
}
}
#endif //End of mobile platform dependendent compilation section started above with #elif
//Check if we have a non-zero value for horizontal or vertical
if(horizontal != 0 || vertical != 0)
{
//Call AttemptMove passing in the generic parameter Wall, since that is what Player may interact with if they encounter one (by attacking it)
//Pass in horizontal and vertical as parameters to specify the direction to move Player in.
AttemptMove<Wall> (horizontal, vertical);
}
}
//AttemptMove overrides the AttemptMove function in the base class MovingObject
//AttemptMove takes a generic parameter T which for Player will be of the type Wall, it also takes integers for x and y direction to move in.
protected override void AttemptMove <T> (int xDir, int yDir)
{
//Every time player moves, subtract from food points total.
food--;
//Update food text display to reflect current score.
foodText.text = "Food: " + food;
//Call the AttemptMove method of the base class, passing in the component T (in this case Wall) and x and y direction to move.
base.AttemptMove <T> (xDir, yDir);
//Hit allows us to reference the result of the Linecast done in Move.
RaycastHit2D hit;
//If Move returns true, meaning Player was able to move into an empty space.
if (Move (xDir, yDir, out hit))
{
//Call RandomizeSfx of SoundManager to play the move sound, passing in two audio clips to choose from.
SoundManager.instance.RandomizeSfx (moveSound1, moveSound2);
}
//Since the player has moved and lost food points, check if the game has ended.
CheckIfGameOver ();
//Set the playersTurn boolean of GameManager to false now that players turn is over.
GameManager.instance.playersTurn = false;
}
//OnCantMove overrides the abstract function OnCantMove in MovingObject.
//It takes a generic parameter T which in the case of Player is a Wall which the player can attack and destroy.
protected override void OnCantMove <T> (T component)
{
//Set hitWall to equal the component passed in as a parameter.
Wall hitWall = component as Wall;
//Call the DamageWall function of the Wall we are hitting.
hitWall.DamageWall (wallDamage);
//Set the attack trigger of the player's animation controller in order to play the player's attack animation.
animator.SetTrigger ("playerChop");
}
//OnTriggerEnter2D is sent when another object enters a trigger collider attached to this object (2D physics only).
private void OnTriggerEnter2D (Collider2D other)
{
//Check if the tag of the trigger collided with is Exit.
if(other.tag == "Exit")
{
//Invoke the Restart function to start the next level with a delay of restartLevelDelay (default 1 second).
Invoke ("Restart", restartLevelDelay);
//Disable the player object since level is over.
enabled = false;
}
//Check if the tag of the trigger collided with is Food.
else if(other.tag == "Food")
{
//Add pointsPerFood to the players current food total.
food += pointsPerFood;
//Update foodText to represent current total and notify player that they gained points
foodText.text = "+" + pointsPerFood + " Food: " + food;
//Call the RandomizeSfx function of SoundManager and pass in two eating sounds to choose between to play the eating sound effect.
SoundManager.instance.RandomizeSfx (eatSound1, eatSound2);
//Disable the food object the player collided with.
other.gameObject.SetActive (false);
}
//Check if the tag of the trigger collided with is Soda.
else if(other.tag == "Soda")
{
//Add pointsPerSoda to players food points total
food += pointsPerSoda;
//Update foodText to represent current total and notify player that they gained points
foodText.text = "+" + pointsPerSoda + " Food: " + food;
//Call the RandomizeSfx function of SoundManager and pass in two drinking sounds to choose between to play the drinking sound effect.
SoundManager.instance.RandomizeSfx (drinkSound1, drinkSound2);
//Disable the soda object the player collided with.
other.gameObject.SetActive (false);
}
}
//Restart reloads the scene when called.
private void Restart ()
{
//Load the last scene loaded, in this case Main, the only scene in the game.
Application.LoadLevel (Application.loadedLevel);
}
//LoseFood is called when an enemy attacks the player.
//It takes a parameter loss which specifies how many points to lose.
public void LoseFood (int loss)
{
//Set the trigger for the player animator to transition to the playerHit animation.
animator.SetTrigger ("playerHit");
//Subtract lost food points from the players total.
food -= loss;
//Update the food display with the new total.
foodText.text = "-"+ loss + " Food: " + food;
//Check to see if game has ended.
CheckIfGameOver ();
}
//CheckIfGameOver checks if the player is out of food points and if so, ends the game.
private void CheckIfGameOver ()
{
//Check if food point total is less than or equal to zero.
if (food <= 0)
{
//Call the PlaySingle function of SoundManager and pass it the gameOverSound as the audio clip to play.
SoundManager.instance.PlaySingle (gameOverSound);
//Stop the background music.
SoundManager.instance.musicSource.Stop();
//Call the GameOver function of GameManager.
GameManager.instance.GameOver ();
}
}
}
SoundManager
public class SoundManager : MonoBehaviour
{
public AudioSource efxSource; //Drag a reference to the audio source which will play the sound effects.
public AudioSource musicSource; //Drag a reference to the audio source which will play the music.
public static SoundManager instance = null; //Allows other scripts to call functions from SoundManager.
public float lowPitchRange = .95f; //The lowest a sound effect will be randomly pitched.
public float highPitchRange = 1.05f; //The highest a sound effect will be randomly pitched.
void Awake ()
{
//Check if there is already an instance of SoundManager
if (instance == null)
//if not, set it to this.
instance = this;
//If instance already exists:
else if (instance != this)
//Destroy this, this enforces our singleton pattern so there can only be one instance of SoundManager.
Destroy (gameObject);
//Set SoundManager to DontDestroyOnLoad so that it won't be destroyed when reloading our scene.
DontDestroyOnLoad (gameObject);
}
//Used to play single sound clips.
public void PlaySingle(AudioClip clip)
{
//Set the clip of our efxSource audio source to the clip passed in as a parameter.
efxSource.clip = clip;
//Play the clip.
efxSource.Play ();
}
//RandomizeSfx chooses randomly between various audio clips and slightly changes their pitch.
public void RandomizeSfx (params AudioClip[] clips)
{
//Generate a random number between 0 and the length of our array of clips passed in.
int randomIndex = Random.Range(0, clips.Length);
//Choose a random pitch to play back our clip at between our high and low pitch ranges.
float randomPitch = Random.Range(lowPitchRange, highPitchRange);
//Set the pitch of the audio source to the randomly chosen pitch.
efxSource.pitch = randomPitch;
//Set the clip to the clip at our randomly chosen index.
efxSource.clip = clips[randomIndex];
//Play the clip.
efxSource.Play();
}
}
GameManager
public class GameManager : MonoBehaviour
{
public float levelStartDelay = 2f; //Time to wait before starting level, in seconds.
public float turnDelay = 0.1f; //Delay between each Player turn.
public int playerFoodPoints = 100; //Starting value for Player food points.
public static GameManager instance = null; //Static instance of GameManager which allows it to be accessed by any other script.
[HideInInspector] public bool playersTurn = true; //Boolean to check if it's players turn, hidden in inspector but public.
private Text levelText; //Text to display current level number.
private GameObject levelImage; //Image to block out level as levels are being set up, background for levelText.
private BoardManager boardScript; //Store a reference to our BoardManager which will set up the level.
private int level = 1; //Current level number, expressed in game as "Day 1".
private List<Enemy> enemies; //List of all Enemy units, used to issue them move commands.
private bool enemiesMoving; //Boolean to check if enemies are moving.
private bool doingSetup = true; //Boolean to check if we're setting up board, prevent Player from moving during setup.
//Awake is always called before any Start functions
void Awake()
{
//Check if instance already exists
if (instance == null)
//if not, set instance to this
instance = this;
//If instance already exists and it's not this:
else if (instance != this)
//Then destroy this. This enforces our singleton pattern, meaning there can only ever be one instance of a GameManager.
Destroy(gameObject);
//Sets this to not be destroyed when reloading scene
DontDestroyOnLoad(gameObject);
//Assign enemies to a new List of Enemy objects.
enemies = new List<Enemy>();
//Get a component reference to the attached BoardManager script
boardScript = GetComponent<BoardManager>();
//Call the InitGame function to initialize the first level
InitGame();
}
//This is called each time a scene is loaded.
void OnLevelWasLoaded(int index)
{
//Add one to our level number.
level++;
//Call InitGame to initialize our level.
InitGame();
}
//Initializes the game for each level.
void InitGame()
{
//While doingSetup is true the player can't move, prevent player from moving while title card is up.
doingSetup = true;
//Get a reference to our image LevelImage by finding it by name.
levelImage = GameObject.Find("LevelImage");
//Get a reference to our text LevelText's text component by finding it by name and calling GetComponent.
levelText = GameObject.Find("LevelText").GetComponent<Text>();
//Set the text of levelText to the string "Day" and append the current level number.
levelText.text = "Day " + level;
//Set levelImage to active blocking player's view of the game board during setup.
levelImage.SetActive(true);
//Call the HideLevelImage function with a delay in seconds of levelStartDelay.
Invoke("HideLevelImage", levelStartDelay);
//Clear any Enemy objects in our List to prepare for next level.
enemies.Clear();
//Call the SetupScene function of the BoardManager script, pass it current level number.
boardScript.SetupScene(level);
}
//Hides black image used between levels
void HideLevelImage()
{
//Disable the levelImage gameObject.
levelImage.SetActive(false);
//Set doingSetup to false allowing player to move again.
doingSetup = false;
}
//Update is called every frame.
void Update()
{
//Check that playersTurn or enemiesMoving or doingSetup are not currently true.
if(playersTurn || enemiesMoving || doingSetup)
//If any of these are true, return and do not start MoveEnemies.
return;
//Start moving enemies.
StartCoroutine (MoveEnemies ());
}
//Call this to add the passed in Enemy to the List of Enemy objects.
public void AddEnemyToList(Enemy script)
{
//Add Enemy to List enemies.
enemies.Add(script);
}
//GameOver is called when the player reaches 0 food points
public void GameOver()
{
//Set levelText to display number of levels passed and game over message
levelText.text = "After " + level + " days, you starved.";
//Enable black background image gameObject.
levelImage.SetActive(true);
//Disable this GameManager.
enabled = false;
}
//Coroutine to move enemies in sequence.
IEnumerator MoveEnemies()
{
//While enemiesMoving is true player is unable to move.
enemiesMoving = true;
//Wait for turnDelay seconds, defaults to .1 (100 ms).
yield return new WaitForSeconds(turnDelay);
//If there are no enemies spawned (IE in first level):
if (enemies.Count == 0)
{
//Wait for turnDelay seconds between moves, replaces delay caused by enemies moving when there are none.
yield return new WaitForSeconds(turnDelay);
}
//Loop through List of Enemy objects.
for (int i = 0; i < enemies.Count; i++)
{
//Call the MoveEnemy function of Enemy at index i in the enemies List.
enemies[i].MoveEnemy ();
//Wait for Enemy's moveTime before moving next Enemy,
yield return new WaitForSeconds(enemies[i].moveTime);
}
//Once Enemies are done moving, set playersTurn to true so player can move.
playersTurn = true;
//Enemies are done moving, set enemiesMoving to false.
enemiesMoving = false;
}
}
Enemy
//Enemy inherits from MovingObject, our base class for objects that can move, Player also inherits from this.
public class Enemy : MovingObject
{
public int playerDamage; //The amount of food points to subtract from the player when attacking.
private Animator animator; //Variable of type Animator to store a reference to the enemy's Animator component.
private Transform target; //Transform to attempt to move toward each turn.
private bool skipMove; //Boolean to determine whether or not enemy should skip a turn or move this turn.
//Start overrides the virtual Start function of the base class.
protected override void Start ()
{
//Register this enemy with our instance of GameManager by adding it to a list of Enemy objects.
//This allows the GameManager to issue movement commands.
GameManager.instance.AddEnemyToList (this);
//Get and store a reference to the attached Animator component.
animator = GetComponent<Animator> ();
//Find the Player GameObject using it's tag and store a reference to its transform component.
target = GameObject.FindGameObjectWithTag ("Player").transform;
//Call the start function of our base class MovingObject.
base.Start ();
}
//Override the AttemptMove function of MovingObject to include functionality needed for Enemy to skip turns.
//See comments in MovingObject for more on how base AttemptMove function works.
protected override void AttemptMove <T> (int xDir, int yDir)
{
//Check if skipMove is true, if so set it to false and skip this turn.
if(skipMove)
{
skipMove = false;
return;
}
//Call the AttemptMove function from MovingObject.
base.AttemptMove <T> (xDir, yDir);
//Now that Enemy has moved, set skipMove to true to skip next move.
skipMove = true;
}
//MoveEnemy is called by the GameManger each turn to tell each Enemy to try to move towards the player.
public void MoveEnemy ()
{
//Declare variables for X and Y axis move directions, these range from -1 to 1.
//These values allow us to choose between the cardinal directions: up, down, left and right.
int xDir = 0;
int yDir = 0;
//If the difference in positions is approximately zero (Epsilon) do the following:
if(Mathf.Abs (target.position.x - transform.position.x) < float.Epsilon)
//If the y coordinate of the target's (player) position is greater than the y coordinate of this enemy's position set y direction 1 (to move up). If not, set it to -1 (to move down).
yDir = target.position.y > transform.position.y ? 1 : -1;
//If the difference in positions is not approximately zero (Epsilon) do the following:
else
//Check if target x position is greater than enemy's x position, if so set x direction to 1 (move right), if not set to -1 (move left).
xDir = target.position.x > transform.position.x ? 1 : -1;
//Call the AttemptMove function and pass in the generic parameter Player, because Enemy is moving and expecting to potentially encounter a Player
AttemptMove <Player> (xDir, yDir);
}
//OnCantMove is called if Enemy attempts to move into a space occupied by a Player, it overrides the OnCantMove function of MovingObject
//and takes a generic parameter T which we use to pass in the component we expect to encounter, in this case Player
protected override void OnCantMove <T> (T component)
{
//Declare hitPlayer and set it to equal the encountered component.
Player hitPlayer = component as Player;
//Call the LoseFood function of hitPlayer passing it playerDamage, the amount of foodpoints to be subtracted.
hitPlayer.LoseFood (playerDamage);
//Set the attack trigger of animator to trigger Enemy attack animation.
animator.SetTrigger ("enemyAttack");
}
}
Wall
public class Wall : MonoBehaviour
{
public AudioClip chopSound1; //1 of 2 audio clips that play when the wall is attacked by the player.
public AudioClip chopSound2; //2 of 2 audio clips that play when the wall is attacked by the player.
public Sprite dmgSprite; //Alternate sprite to display after Wall has been attacked by player.
public int hp = 3; //hit points for the wall.
private SpriteRenderer spriteRenderer; //Store a component reference to the attached SpriteRenderer.
void Awake ()
{
//Get a component reference to the SpriteRenderer.
spriteRenderer = GetComponent<SpriteRenderer> ();
}
//DamageWall is called when the player attacks a wall.
public void DamageWall (int loss)
{
//Call the RandomizeSfx function of SoundManager to play one of two chop sounds.
SoundManager.instance.RandomizeSfx (chopSound1, chopSound2);
//Set spriteRenderer to the damaged wall sprite.
spriteRenderer.sprite = dmgSprite;
//Subtract loss from hit point total.
hp -= loss;
//If hit points are less than or equal to zero:
if(hp <= 0)
//Disable the gameObject.
gameObject.SetActive (false);
}
}
MovingObject
//The abstract keyword enables you to create classes and class members that are incomplete and must be implemented in a derived class.
public abstract class MovingObject : MonoBehaviour
{
public float moveTime = 0.1f; //Time it will take object to move, in seconds.
public LayerMask blockingLayer; //Layer on which collision will be checked.
private BoxCollider2D boxCollider; //The BoxCollider2D component attached to this object.
private Rigidbody2D rb2D; //The Rigidbody2D component attached to this object.
private float inverseMoveTime; //Used to make movement more efficient.
//Protected, virtual functions can be overridden by inheriting classes.
protected virtual void Start ()
{
//Get a component reference to this object's BoxCollider2D
boxCollider = GetComponent <BoxCollider2D> ();
//Get a component reference to this object's Rigidbody2D
rb2D = GetComponent <Rigidbody2D> ();
//By storing the reciprocal of the move time we can use it by multiplying instead of dividing, this is more efficient.
inverseMoveTime = 1f / moveTime;
}
//Move returns true if it is able to move and false if not.
//Move takes parameters for x direction, y direction and a RaycastHit2D to check collision.
protected bool Move (int xDir, int yDir, out RaycastHit2D hit)
{
//Store start position to move from, based on objects current transform position.
Vector2 start = transform.position;
// Calculate end position based on the direction parameters passed in when calling Move.
Vector2 end = start + new Vector2 (xDir, yDir);
//Disable the boxCollider so that linecast doesn't hit this object's own collider.
boxCollider.enabled = false;
//Cast a line from start point to end point checking collision on blockingLayer.
hit = Physics2D.Linecast (start, end, blockingLayer);
//Re-enable boxCollider after linecast
boxCollider.enabled = true;
//Check if anything was hit
if(hit.transform == null)
{
//If nothing was hit, start SmoothMovement co-routine passing in the Vector2 end as destination
StartCoroutine (SmoothMovement (end));
//Return true to say that Move was successful
return true;
}
//If something was hit, return false, Move was unsuccesful.
return false;
}
//Co-routine for moving units from one space to next, takes a parameter end to specify where to move to.
protected IEnumerator SmoothMovement (Vector3 end)
{
//Calculate the remaining distance to move based on the square magnitude of the difference between current position and end parameter.
//Square magnitude is used instead of magnitude because it's computationally cheaper.
float sqrRemainingDistance = (transform.position - end).sqrMagnitude;
//While that distance is greater than a very small amount (Epsilon, almost zero):
while(sqrRemainingDistance > float.Epsilon)
{
//Find a new position proportionally closer to the end, based on the moveTime
Vector3 newPostion = Vector3.MoveTowards(rb2D.position, end, inverseMoveTime * Time.deltaTime);
//Call MovePosition on attached Rigidbody2D and move it to the calculated position.
rb2D.MovePosition (newPostion);
//Recalculate the remaining distance after moving.
sqrRemainingDistance = (transform.position - end).sqrMagnitude;
//Return and loop until sqrRemainingDistance is close enough to zero to end the function
yield return null;
}
}
//The virtual keyword means AttemptMove can be overridden by inheriting classes using the override keyword.
//AttemptMove takes a generic parameter T to specify the type of component we expect our unit to interact with if blocked (Player for Enemies, Wall for Player).
protected virtual void AttemptMove <T> (int xDir, int yDir)
where T : Component
{
//Hit will store whatever our linecast hits when Move is called.
RaycastHit2D hit;
//Set canMove to true if Move was successful, false if failed.
bool canMove = Move (xDir, yDir, out hit);
//Check if nothing was hit by linecast
if(hit.transform == null)
//If nothing was hit, return and don't execute further code.
return;
//Get a component reference to the component of type T attached to the object that was hit
T hitComponent = hit.transform.GetComponent <T> ();
//If canMove is false and hitComponent is not equal to null, meaning MovingObject is blocked and has hit something it can interact with.
if(!canMove && hitComponent != null)
//Call the OnCantMove function and pass it hitComponent as a parameter.
OnCantMove (hitComponent);
}
//The abstract modifier indicates that the thing being modified has a missing or incomplete implementation.
//OnCantMove will be overriden by functions in the inheriting classes.
protected abstract void OnCantMove <T> (T component)
where T : Component;
}
BoardManager
public class BoardManager : MonoBehaviour
{
// Using Serializable allows us to embed a class with sub properties in the inspector.
[Serializable]
public class Count
{
public int minimum; //Minimum value for our Count class.
public int maximum; //Maximum value for our Count class.
//Assignment constructor.
public Count (int min, int max)
{
minimum = min;
maximum = max;
}
}
public int columns = 8; //Number of columns in our game board.
public int rows = 8; //Number of rows in our game board.
public Count wallCount = new Count (5, 9); //Lower and upper limit for our random number of walls per level.
public Count foodCount = new Count (1, 5); //Lower and upper limit for our random number of food items per level.
public GameObject exit; //Prefab to spawn for exit.
public GameObject[] floorTiles; //Array of floor prefabs.
public GameObject[] wallTiles; //Array of wall prefabs.
public GameObject[] foodTiles; //Array of food prefabs.
public GameObject[] enemyTiles; //Array of enemy prefabs.
public GameObject[] outerWallTiles; //Array of outer tile prefabs.
private Transform boardHolder; //A variable to store a reference to the transform of our Board object.
private List <Vector3> gridPositions = new List <Vector3> (); //A list of possible locations to place tiles.
//Clears our list gridPositions and prepares it to generate a new board.
void InitialiseList ()
{
//Clear our list gridPositions.
gridPositions.Clear ();
//Loop through x axis (columns).
for(int x = 1; x < columns-1; x++)
{
//Within each column, loop through y axis (rows).
for(int y = 1; y < rows-1; y++)
{
//At each index add a new Vector3 to our list with the x and y coordinates of that position.
gridPositions.Add (new Vector3(x, y, 0f));
}
}
}
//Sets up the outer walls and floor (background) of the game board.
void BoardSetup ()
{
//Instantiate Board and set boardHolder to its transform.
boardHolder = new GameObject ("Board").transform;
//Loop along x axis, starting from -1 (to fill corner) with floor or outerwall edge tiles.
for(int x = -1; x < columns + 1; x++)
{
//Loop along y axis, starting from -1 to place floor or outerwall tiles.
for(int y = -1; y < rows + 1; y++)
{
//Choose a random tile from our array of floor tile prefabs and prepare to instantiate it.
GameObject toInstantiate = floorTiles[Random.Range (0,floorTiles.Length)];
//Check if we current position is at board edge, if so choose a random outer wall prefab from our array of outer wall tiles.
if(x == -1 || x == columns || y == -1 || y == rows)
toInstantiate = outerWallTiles [Random.Range (0, outerWallTiles.Length)];
//Instantiate the GameObject instance using the prefab chosen for toInstantiate at the Vector3 corresponding to current grid position in loop, cast it to GameObject.
GameObject instance =
Instantiate (toInstantiate, new Vector3 (x, y, 0f), Quaternion.identity) as GameObject;
//Set the parent of our newly instantiated object instance to boardHolder, this is just organizational to avoid cluttering hierarchy.
instance.transform.SetParent (boardHolder);
}
}
}
//RandomPosition returns a random position from our list gridPositions.
Vector3 RandomPosition ()
{
//Declare an integer randomIndex, set it's value to a random number between 0 and the count of items in our List gridPositions.
int randomIndex = Random.Range (0, gridPositions.Count);
//Declare a variable of type Vector3 called randomPosition, set it's value to the entry at randomIndex from our List gridPositions.
Vector3 randomPosition = gridPositions[randomIndex];
//Remove the entry at randomIndex from the list so that it can't be re-used.
gridPositions.RemoveAt (randomIndex);
//Return the randomly selected Vector3 position.
return randomPosition;
}
//LayoutObjectAtRandom accepts an array of game objects to choose from along with a minimum and maximum range for the number of objects to create.
void LayoutObjectAtRandom (GameObject[] tileArray, int minimum, int maximum)
{
//Choose a random number of objects to instantiate within the minimum and maximum limits
int objectCount = Random.Range (minimum, maximum+1);
//Instantiate objects until the randomly chosen limit objectCount is reached
for(int i = 0; i < objectCount; i++)
{
//Choose a position for randomPosition by getting a random position from our list of available Vector3s stored in gridPosition
Vector3 randomPosition = RandomPosition();
//Choose a random tile from tileArray and assign it to tileChoice
GameObject tileChoice = tileArray[Random.Range (0, tileArray.Length)];
//Instantiate tileChoice at the position returned by RandomPosition with no change in rotation
Instantiate(tileChoice, randomPosition, Quaternion.identity);
}
}
//SetupScene initializes our level and calls the previous functions to lay out the game board
public void SetupScene (int level)
{
//Creates the outer walls and floor.
BoardSetup ();
//Reset our list of gridpositions.
InitialiseList ();
//Instantiate a random number of wall tiles based on minimum and maximum, at randomized positions.
LayoutObjectAtRandom (wallTiles, wallCount.minimum, wallCount.maximum);
//Instantiate a random number of food tiles based on minimum and maximum, at randomized positions.
LayoutObjectAtRandom (foodTiles, foodCount.minimum, foodCount.maximum);
//Determine number of enemies based on current level number, based on a logarithmic progression
int enemyCount = (int)Mathf.Log(level, 2f);
//Instantiate a random number of enemies based on minimum and maximum, at randomized positions.
LayoutObjectAtRandom (enemyTiles, enemyCount, enemyCount);
//Instantiate the exit tile in the upper right hand corner of our game board
Instantiate (exit, new Vector3 (columns - 1, rows - 1, 0f), Quaternion.identity);
}
}
}