用Unity製作一個簡單的大魚吃小魚遊戲
完成效果:
0.一些準備工作
簡單準備了一些魚的圖片素材和一張海底的背景圖
將這些魚的圖片拖動到遊戲中,建立一個擁有簡單動畫效果的遊戲物件。將它們的z軸都設為0。
將背景也拖入遊戲,並將z軸改為1。
將攝像機的Projection改為Orthographic。
1.控制魚的移動
接下來我們需要控制魚的移動。新建指令碼Fish,在update裡獲得玩家的輸入輸出。
void Update() { Vector3 playerInput = new Vector3(0, 0, 0); playerInput.x = Input.GetAxis("Horizontal"); playerInput.y = Input.GetAxis("Vertical"); transform.localPosition += playerInput; }
將指令碼掛在一條魚上,我們發現我們已經可以控制魚的移動了,但此時我們的移動過快了,需要做一些修改。
給Fish新增屬性speed,再將playerInput乘上speed和Time.deltaTime。
public float speed = 5f; void Update() { Vector3 playerInput = new Vector3(0, 0, 0); playerInput.x = Input.GetAxis("Horizontal"); playerInput.y = Input.GetAxis("Vertical"); transform.localPosition += playerInput * speed * Time.deltaTime; }
現在我們可以移動的更慢一些,每一幀不再移動那麼多的距離。同時,我們現在可以通過調節speed來控制移動速度。
再為我們的移動增加一個加速的過程。
增加兩個屬性velocity和desiredVelocity,表示魚當前的速度和我們期望的速度,並在awake裡將它們初始化。
Vector3 velocity;
Vector3 desiredVelocity;
void Awake()
{
velocity = new Vector3(0, 0, 0);
desiredVelocity = new Vector3(0, 0, 0);
}
增加屬性acceleration,表示魚的加速度。
public float acceleration = 1f;
修改魚的移動,將transform.localPosition += playerInput * speed * Time.deltaTime;
改為
desiredVelocity = playerInput * speed;
velocity = Vector3.MoveTowards(velocity, desiredVelocity, acceleration);
transform.localPosition += velocity * Time.deltaTime;
現在魚雖然已經可以移動,但還無法切換移動方向,會出現向後移動的情況。我們可以通過尤拉角改變魚的方向。將剛剛的移動修改為
desiredVelocity = playerInput * speed;
velocity = Vector3.MoveTowards(velocity, desiredVelocity, acceleration);
if (velocity.x > 0) transform.localEulerAngles = new Vector3(0, 0, 0);
else if (velocity.x < 0) transform.localEulerAngles = new Vector3(0, 180, 0);
transform.localPosition += velocity * Time.deltaTime;
魚的移動基本完成了。
2.創造更多的魚
在這個遊戲中,我們需要源源不斷的生成新的不同的魚,因此讓我們建立一個新的指令碼FishFactory,來為我們生產魚類。
FishFactory需要繼承ScriptableObject而不是MonoBehaviour,我們還要在類前面加上[CreateAssetMenu]。
[CreateAssetMenu]
public class FishFactory : ScriptableObject
現在,我們可以在Assets裡,右鍵->Create找到Fish Factory並建立它。
新增一個屬性fishes,來存放生產的魚類的預製體。
public Fish[] fishes;
新增get方法,輸入序號來獲得例項化的魚類。
public Fish Get(int id) {
Fish fish = Instantiate(fishes[id]);
return fish;
}
回到場景,將我們之前用素材製作的魚全部加上Fish指令碼,並新增碰撞體。我這裡是用的Box Collider 2D,並將邊緣調小了點。
當兩隻魚正面相遇時,我們希望它們穿過對方繼續向前(不考慮吃掉的情況下),所以我們將這些碰撞體都勾上Is Trigger。
為所有的魚新增剛體。將BodyType改為Kinematic,使其不受重力影響。
將所有的魚拖入Assets,儲存為預製體。
將其按大小從小到大拖入到Fish Factory中。
建立一個新的指令碼Control,來控制遊戲。
首先要新增一個FishFactory屬性。
public FishFactory fishFactory;
新增lastTime和delayTime屬性,來使魚能隔一段時間生成一次。
public float delayTime = 3f;
float lastTime = 0f;
先填寫update方法,來讓遊戲在(0,0,0)的地方生成新的魚。
void Update()
{
lastTime += Time.deltaTime;
if (lastTime >= delayTime){
lastTime = 0f;
Fish fish = fishFactory.Get(0);
fish.transform.localPosition = new Vector3(0, 0, 0);
}
}
建立空物體GameControl,並將Control指令碼掛在上面,將之前建立的FishFactory掛在指令碼上。
此時我們運行遊戲,已經可以看到螢幕中間不斷出現新的魚了。
3.魚的活動範圍
接下來,我們要把我們的魚的移動限制在我們的攝像機視野內。
修改Fish指令碼,我們要新增兩個屬性leftBottomPoint, rightTopPoint,來記錄攝像機投射到螢幕上的左下角和右上角的座標。
Vector3 leftBottomPoint, rightTopPoint;
在Awake中,使用ViewportToWorldPoint函式來將視口座標轉換為世界座標。
void Awake()
{
......
leftBottomPoint = Camera.main.ViewportToWorldPoint(new Vector3(0f, 0f,
Mathf.Abs(Camera.main.transform.position.z)));
rightTopPoint = Camera.main.ViewportToWorldPoint(new Vector3(1f, 1f,
Mathf.Abs(Camera.main.transform.position.z)));
}
編寫一個新的函式Clamp,來約束魚的移動。當魚的座標要超出邊框時,將魚的座標移到邊框上並把相應速度置0。
void Clamp() {
Vector3 position = transform.localPosition;
if (position.x > rightTopPoint.x) {
position.x = rightTopPoint.x;
velocity.x = 0;
}
else if (position.x < leftBottomPoint.x) {
position.x = leftBottomPoint.x;
velocity.x = 0;
}
if (position.y > rightTopPoint.y) {
position.y = rightTopPoint.y;
velocity.y = 0;
}
else if (position.y < leftBottomPoint.y) {
position.y = leftBottomPoint.y;
velocity.y = 0;
}
transform.localPosition = position;
}
在魚移動後,呼叫這個函式來約束魚的位置。
transform.localPosition += velocity * Time.deltaTime;
Clamp();
現在魚已經不能超出攝像機的視野範圍了。
4.魚的生成方式
在這之前,遊戲已經可以源源不斷的生成新的魚在螢幕中央了,並都由我們控制,但這顯然不是我們所期望的。我們希望能夠有魚在外界不斷生成並游到我們的視野範圍,而我們只需要控制一條魚來和它們互動。
所以,讓我們先移除Fish中的玩家輸入部分。將所有和playerInput有關的語句刪掉。
//Vector3 playerInput = new Vector3(0, 0, 0);
//playerInput.x = Input.GetAxis("Horizontal");
//playerInput.y = Input.GetAxis("Vertical");
//desiredVelocity = playerInput * speed;
給Fish新增一個SetDesiredVelocity函式,讓我們在外部僅給魚一個移動方向,魚就可以向該方向移動。
public void SetDesiredVelocity(Vector3 Input){
desiredVelocity = Input * speed;
}
如前文所說,這個輸入應該是一個方向。所以我們用ClampMagnitude函式約束它,讓它不大於1。
Vector3.ClampMagnitude(Input, 1f);
desiredVelocity = Input * speed;
在Control中新增屬性player,用來控制玩家操作的魚
Fish player;
在Awake函式中例項化它。
void Awake()
{
player = fishFactory.Get(1);
player.transform.localPosition = new Vector3(0, 0, 0);
}
新增PlayerUpdate函式,來讓玩家操控魚。
void PlayerUpdate() {
Vector3 playerInput = new Vector3(0, 0, 0);
playerInput.x = Input.GetAxis("Horizontal");
playerInput.y = Input.GetAxis("Vertical");
player.SetDesiredVelocity(playerInput);
}
將PlayerUpdate放入update函式中。
void Update()
{
PlayerUpdate();
...
}
現在我們只能控制遊戲最開始生成在螢幕中央的魚了。
接下來我們要修改其他魚的生成方式,首先刪掉原來測試時生成魚的程式碼。
if (lastTime >= delayTime) {
lastTime = 0f;
//Fish fish = fishFactory.Get(0);
//fish.transform.localPosition = new Vector3(0, 0, 0);
}
新增新的函式MakeFish
void MakeFish() {
Fish fish = fishFactory.Get(0);
}
之前說過我們希望其他魚一開始生成在邊框外。因此Control也需要獲得螢幕邊框的位置。和上面一樣,給Control新增leftBottomPoint, rightTopPoint屬性,並在Awake裡用ViewportToWorldPoint函式獲取座標。這裡就不貼程式碼了,畢竟和上面一樣。
回到MakeFish函式,我們用Random.Range函式來隨機決定新的魚是在螢幕左邊還是右邊,以及魚的座標。
Fish fish = fishFactory.Get(0);
if (Random.Range(0, 2) == 0) {
fish.transform.position = new Vector3(leftBottomPoint.x - 2f,
Random.Range(leftBottomPoint.y, rightTopPoint.y), 0f);
}
else {
fish.transform.position = new Vector3(rightTopPoint.x + 2f,
Random.Range(leftBottomPoint.y, rightTopPoint.y), 0f);
}
給左邊生成的魚一個向右的方向,給右邊生成的魚一個向左的方向。
Fish fish = fishFactory.Get(0);
if (Random.Range(0, 2) == 0) {
fish.transform.position = new Vector3(leftBottomPoint.x - 2f,
Random.Range(leftBottomPoint.y, rightTopPoint.y), 0f);
fish.SetDesiredVelocity(Vector3.right);
}
else {
fish.transform.position = new Vector3(rightTopPoint.x + 2f,
Random.Range(leftBottomPoint.y, rightTopPoint.y), 0f);
fish.SetDesiredVelocity(Vector3.left);
}
在update函式中呼叫MakeFish函式。
if (lastTime >= delayTime) {
lastTime = 0f;
MakeFish();
}
但還有一個問題。我們剛剛限制了魚的活動範圍,但現在我們又希望不由我們控制的魚可以從螢幕外進來並離開螢幕。我們可以利用Tag來區分它們。
在Awake中,將我們生成的魚的Tag設為“Player”
player = fishFactory.Get(1);
player.transform.localPosition = new Vector3(0, 0, 0);
player.tag = "Player";
來到Fish指令碼,在update函式裡的Clamp();
前面,新增限制條件
if(this.tag == "Player") Clamp();
我們還需要讓其他的魚在離開屏幕後死亡,防止生成的魚過多。因為這些魚只會往一個方向移動,方法有很多。我這裡的處理方法是讓魚在進入螢幕時標記,在離開螢幕時死亡。
使用一個屬性isInScreen,記錄是否進入螢幕。
bool isInScreen = false;
建立新的函式Die。當魚進入螢幕時,將isInScreen的值變為true。
void Die() {
float x = transform.position.x;
float y = transform.position.y;
if ((x >= leftBottomPoint.x && x <= rightTopPoint.x) &&
(y >= leftBottomPoint.y && y <= rightTopPoint.y)) {
isInScreen = true;
}
}
當魚進入螢幕並離開時,刪去它。
else {
if (isInScreen) {
Destroy(this.gameObject);
}
}
在update裡呼叫Die函式
if (this.tag == "Player") Clamp();
else Die();
現在這些魚會被刪去了,但它們在中心觸碰邊框後就立即消失了。我們不希望這樣。修改Die函式,讓它在2s後消失。
Destroy(this.gameObject, 2f);
這樣,魚的生成邏輯基本完成了。
5.魚的等級與互動
現在,我們要實現這個遊戲最關鍵的部分——“大魚吃小魚”。
首先,給Fish指令碼增加一個屬性level,表示魚的大小等級。
int level;
新增函式SetLevel,設定魚的等級。
public void SetLevel(int level) {
this.level = level;
}
新增函式GetLevel,獲得魚的等級。
public int GetLevel() {
return this.level;
}
來到FishFactory指令碼。之前放進這些魚的預製體時,我們是按照從小到大的順序放的,所以陣列中的編號就是魚的等級。我們在Get函式裡直接修改魚的等級。
public Fish Get(int id) {
Fish fish = Instantiate(fishes[id]);
fish.SetLevel(id);
return fish;
}
回到Fish。用OnTriggerEnter2D函式檢測碰撞。我們設定當玩家的等級小於等於這條魚的等級時,玩家會被吃掉;當玩家的等級大於這條魚的等級時,這條魚會被玩家吃掉。
private void OnTriggerEnter2D(Collider2D other)
{
if (other.tag == "Player") {
Fish player = other.GetComponent<Fish>();
if (player.GetLevel() <= level) {
Destroy(other.gameObject);
}
else {
Destroy(this.gameObject);
}
}
}
接下來,我們要新增魚的升級機制。
首先,在Control腳本里新增一個屬性score,來記錄吃魚的量。使用public和static識別符號,我們之後可以直接在Fish腳本里更改得分。
public static int score = 0;
我們還要新增一個屬性playerLevel,表示玩家等級。這個等級我們初始化為1,因為按我們的規則,玩家控制的魚只能吃比他小的魚,所以玩家的魚不能是最小的。
int playerLevel = 1;
新增一個屬性maxLevel,表示最大等級。它和FishFactory裡陣列的大小有關,我們可以在Awake裡初始化它。
int maxLevel;
void Awake() {
maxLevel = fishFactory.fishes.Length - 1;
...
}
建立新的函式MakePlayerFish,來根據玩家等級生成對應的魚。
首先我們要刪去舊的魚,刪之前要保留下它的位置和方向。
void MakePlayerFish() {
Vector3 playerPosition = new Vector3(0f, 0f, 0f);
Vector3 playerEulerAngles = new Vector3(0f, 0f, 0f);
if (player != null) {
playerPosition = player.transform.localPosition;
playerEulerAngles = player.transform.localEulerAngles;
Destroy(player.gameObject);
}
}
之後再根據等級生成新的魚。
player = fishFactory.Get(playerLevel);
player.transform.localPosition = playerPosition;
player.transform.localEulerAngles = playerEulerAngles;
player.tag = "Player";
現在可以將Awake裡原來生成玩家魚的程式碼換成MakePlayerFish函式。
//player = fishFactory.Get(0);
//player.transform.localPosition = new Vector3(0, 0, 0);
//player.tag = "Player";
MakePlayerFish();
新增屬性levelLine,表示每次升級需要的吃魚數。
public int levelLine = 20;
新增函式LevelUpdate,我們將(score/levelLine)與當前的playerLevel相比較,檢測是否需要升級。因為playerLevel開始是1,所以我們給(score/levelLine)+1。
void LevelUpdate() {
if ((score / levelLine + 1) > playerLevel) {
playerLevel++;
MakePlayerFish();
}
}
將LevelUpdate函式新增到update裡。
void Update()
{
LevelUpdate();
PlayerUpdate();
}
更改Fish腳本里的OnTriggerEnter2D函式。當魚被吃掉時,加一得分。
else {
Control.score++;
Destroy(this.gameObject);
}
現在魚已經可以升級了。但是還有一個隱患。玩家的等級可能超過最大等級。回到LevelUpdate函式,做一下修改。
void LevelUpdate() {
if ((score / levelLine + 1) > playerLevel && (playerLevel < maxLevel)) {
playerLevel++;
MakePlayerFish();
}
}
我們已經完成了玩家魚的部分,但隨機生成的魚依然是固定的。因此我們還需要修改MakeFish。
刪掉原來的Fish fish = fishFactory.Get(0);
,根據概率生成大魚小魚。我這邊設定是三分之二生成比玩家小一級的魚,六分之一生成比玩家大一級的魚(如果有的話),剩下的概率生成和玩家同級的魚。
//Fish fish = fishFactory.Get(0);
Fish fish;
if (Random.Range(0, 3) != 0) {
fish = fishFactory.Get(playerLevel - 1);
}
else if (Random.Range(0, 2) == 0 && playerLevel < maxLevel) {
fish = fishFactory.Get(playerLevel + 1);
}
else {
fish = fishFactory.Get(playerLevel);
}
至此,一個簡單的大魚吃小魚就完成了。雖然這裡只有三種魚,但只要素材足夠,按大小放入FishFactory中,就可以給遊戲加入更多等級的魚。