Siki_Unity_1-9_Unity2D游戲開發_Roguelike拾荒者


Unity 1-9 Unity2D游戲開發 Roguelike拾荒者

任務1:游戲介紹

Food:相當於血量:每走一步下降1,吃東西可以回復(果子10葯水20),被怪物攻擊會減少
中間的障礙物可以打破,人走兩步僵屍走一步;走到Exit進入下一關

最外圈的過道會保證是空的,其他的隨機生成(--確保主角能夠到達出口)

任務2:創建工程、素材

pan.baidu.com/s/1kTYS8ez

Unity5.2.1

2D Project -- Roguelike
  導入Assets.unitypackage

Sprites->所有切好的圖片 -- 主角/地形/出口/圍牆/僵屍等

任務3:創建游戲主角

Sprites中找到6個主角Scavengers的sprite圖片,拖入Hierachy中
  會自動在游戲物體Scavengers_SpritesSheet_0中創建一個動畫Animator,並將Animation
  和AnimationController保存在Sprites文件夾中,重命名控制器為Player/ 動畫為PlayerIdle
  並創建文件夾分類Assets->Animations->Animation/AnimatorController

此時,運行游戲,會自動播放Idle動畫

雙擊AnimatorController打開動畫編輯器,修改速度為0.5

創建攻擊動畫:
  將兩個攻擊Sprites拖入Player物體,會自動在Player中創建一個新的動畫PlayerAttack

創建受攻擊動畫:相似 -- PlayerUnderAttack

創建主角的Prefab

任務4:創建敵人

與創建主角相似

創建完Enemy1,創建Enemy1的Prefab

因為Enemy2的動畫控制器AnimatorController與Enemy1相同
  在Project視窗中右鍵Create->Animator Override Controller
  將Enemy1的Animator賦值給新建的Animator -- 表示兩個Enemy共用一個狀態機
  將Hierarchy中的Enemy1->GameObject->Break prefab instance,重命名為Enemy2
  將Enemy2 Animator賦值給Enemy2
  創建Enemy2的兩個動畫,將動畫拖入Enemy2 Animator賦值即可

相當於:Enemy1和Enemy2共用了Enemy1的狀態機(Animator),但是使用了不同動畫

創建Enemy2的Prefab

任務5:創建地板/ 圍牆/ 食物

創建地板:

把8種地板和出口分別拖入,並重命名Floor1~8/ Exit

把8中障礙物拖入,重命名為Wall1~8

做成Prefab 

創建圍牆:三種圍牆,重命名為OutWall1~3並做成Prefab

創建食物:拖入Soda和Food,做成Prefab

任務6:生成地圖

創建空物體GameManager

創建MapManager.cs
  // 地圖左下角設置為(0, 0)
  public GameObject[] outWallArray/ floorArray;
  // 創建根物體
  private Transform mapHolder;
  mapHolder = new GameObject("Map").transform;

  // 初始化圍牆
  for x for y(...) {
    GameObject cell;
    if (x==0 || y==0 || x==cols-1 || y==rows-1) {
      int index = Random.Range(0, outWallArray.Length);
      cell = GameObject.Instantiate
        (outWallArray[index], ...(x, y, 0), Quaternion.identity) as GameObject;
      // as GameObject -- .Instantiate()生成的為Object類型,強制轉化為GameObject
      // 在(x,y,0)處生成一個旋轉為0的圍牆
    } else ...
    cell.transform.SetParent(mapHolder);
  }

  // 初始化地板
  else {
    int index = Random.Range(0, floorArray.Length);
    cell = GameObject.Instantiate
      (floorArray[index], ...(x,y,0), Quaternion.identity) as GameObject;
  }

此時生成的地圖並不位於相機正中心
  -- 地圖長10寬10 -- (每一格長寬為1,從(0,0)開始,即畫面從(-0.5, -0.5開始))
  --> Camera位置:(4.5, 4.5, 0),顏色改為黑色

private void InitMap() {
    for (int x = 0; x < cols; x++) {
        for (int y = 0; y < cols; y++) {
            GameObject cell;
            if (x == 0 || x == cols - 1 || y == 0 || y == rows - 1) {
                int index = Random.Range(0, outWallArray.Length);
                cell = GameObject.Instantiate
                    (outWallArray[index], new Vector3(x, y, 0), Quaternion.identity);
            } else {
                int index = Random.Range(0, floorArray.Length);
                cell = GameObject.Instantiate
                    (floorArray[index], new Vector3(x, y, 0), Quaternion.identity);
            }
            cell.transform.SetParent(mapHolder);
        }
    }
}

任務7:控制障礙物的生成

在InitMap()中

private List<Vector2> positionList = new List<Vector2>(); // 用於取得中間部分的格子位置

positionList.Clear();

for(int x/y = 0 + 2; x/y < cols/rows - 2; x/y++) {  // 圍牆一列,空道一列
  positionList.Add(new Vector2(x, y));
}

// 從上面的positionList中隨機取得格子放入障礙物/食物/敵人
// 創建障礙物
public GameObject[] wallArray;
public int min/maxCountWall = 2/8;
int wallCount = Random.Range(minCountWall, maxCountWall + 1);
for(int i = 0~wallCount) {
  // 隨機取得格子位置
  int positionIndex = Random.Range(0, positionList.Count); // 隨機取得index
  Vector2 pos = positionList[positionIndex]; // 得到位置信息
  positionList.RemoveAt[positionIndex]; // 移除該位置 -- 保證一個格子只能有一個東西
  // 隨機取得障礙物
  int wallIndex = Random.Range(0, wallArray.Length);
  GameObject cell = GameObject(wallArray[...], pos, ...) as GameObject;
  cell.transform.SetParent(mapHolder);
}

避免地板將障礙物覆蓋,設置Layer:Background/ Items/ Roles

positionList = new List<Vector2>();
positionList.Clear();
for(int x = 0+2; x < cols - 2; x++) {
    for(int y = 0 + 2; y < rows - 2; y++) {
        positionList.Add(new Vector2(x, y));
    }
}
int wallCount = Random.Range(minCountWall, maxCountWall + 1);
for(int i = 0; i < wallCount; i++) {
    int positionIndex = Random.Range(0, positionList.Count);
    Vector2 pos = positionList[positionIndex];
    positionList.RemoveAt(positionIndex);
    int wallIndex = Random.Range(0, wallArray.Length);
    GameObject cell = GameObject.Instantiate
        (wallArray[wallIndex], pos, Quaternion.identity) as GameObject;
    cell.transform.SetParent(mapHolder);
}

任務8&9:敵人和食物的隨機生成 & 代碼優化

食物和敵人的數量與關卡有關 -- 數量成正比

創建GameManager.cs -- 控制游戲關卡
  public int level = 1;

在MapManager.cs中
  // 獲取GameManager
  private GameManager gameManager;
  gameManager = GetComponent<GameManager>();

創建食物 -- 數量2~level*2
  int foodCount = Random.Range(2, gameManager.level * 2 + 1);
  // 取得隨機位置 -- 重復代碼寫成Vector2 RandomPosition()
  Vector2 pos = RandomPosition();
  // 隨機取得物體 -- 重復代碼寫成GameObject RandomPrefab(GameObject[] prefabs);
  GameObject foodPrefab= Instantiate(RandomPrefab(foodArray)) as GameObject;
  foodPrefab.transform.setParent(mapHolder);

// get a random available position
private Vector2 RandomPosition() {
    int positionIndex = Random.Range(0, positionList.Count);
    Vector2 pos = positionList[positionIndex];
    positionList.RemoveAt(positionIndex);
    return pos;
}

// get a random gameobject
private GameObject RandomPrefab(GameObject[] prefabs) {
    int index = Random.Range(0, prefabs.Length);
    return prefabs[index];
}

創建敵人 -- 數量為level / 2
  int enemyCount = gameManager.level/2;
  for(0~enemyCount) {
    Vector2 pos = ...;
    GameObject enemyPrefab = GameObject.Instantiate(...) as GameObject;
    enemyPrefab.transform.setParent(mapHolder);
  }

創建出口 -- 位置固定在右上方
  GameObject exit =
    (Instantiate(exitPrefab, new Vector2(cols-2, rows-2), Quaternion.identity) as GameObject;
  exti.transform.SetParent(mapHolder);

-- 代碼優化

上面的創建代碼是可重用的 -- 寫成method

private void InstantiateItems(int count, GameObject[] itemArray) {
    for(int i = 0; i < count; i++) {
        GameObject item = GameObject.Instantiate(RandomPrefab
            (itemArray), RandomPosition(), Quaternion.identity) as GameObject;
        item.transform.SetParent(mapHolder);
    }
}

任務10:完善主角和敵人的動畫狀態機

Player的動畫:PlayerIdle/ PlayerUnderAttack/ PlayerAttack

PlayerIdle<-->PlayerUnderAttack
PlayerIdle<-->PlayerAttack

在Animator中添加觸發器Trigger分別稱為Damage和Attack

Idle->Attack/UnderAttack:
  將Has Exit Time取消勾選 -- 在Idle動畫的任何時候都可以隨時切換到另一個動畫
  Transition Duration = 0 -- 因為這里的動畫是幀動畫,因此可以進行瞬時切換成其他動畫
  Conditions 切換方式:添加Attack/Damage Trigger

拖動左邊的箭頭可以手動播放動畫;拖動右邊的兩個箭頭可以手動控制何時切換

Attack/UnderAttack->Idle:
  勾選Has Exit Time即可 -- 播放完Attack/UnderAttack后自動切換到Idle動畫
  Transition Duration = 0 -- 瞬時間切換
  Exit Time = 1 -- 退出時間(多久進行切換)

Enemy的動畫:EnemyIdle/ EnemyAttack

Idle->Attack:
  添加觸發器Attack
  Has Exit Time uncheck
  Transition Duration = 0

Attack->Idle:
  Has Exit Time check
  Exit Time = 1
  Transistion Duration = 0

檢測動畫:

運行游戲;選中Player/ Enemy;在Animator視窗中查看狀態機

點擊Trigger右邊的小圓點,即為觸發該Trigger

任務11:控制主角的運動

為Player添加剛體,用於控制移動,勾選Is Kinematic

為Player添加BoxCollider2D,用於檢測碰撞(大小不能設置為(1,1),0.9就好)

為Player添加Player.cs來控制移動
  private int posx/posy = 1; // 當前位置
  private int Vector2 targetPos = new Vector2(1,1); // 目標位置

  float h/v = Input.GetAxisRaw("Horizontal"/"Vertical"); // GetAxisRaw()的返回值為-1/0/1
  targetPos += new Vector2(h,v); // 按鍵后,目標位置發生改變
  private Rigidbody2D rigidbody = GetComponent<Rigidbody2D>(); // 得到剛體,用於移動
  rigidbody.MovePosition(Vector2.Lerp(transform.position, targetPos, smoothing*Time.deltaTime));
    // MovePosition(目標位置):向目標位置移動
    // Lerp(起點,終點,速度);這里設smoothing=1

發現此時按下按鈕會移動很長的距離 -- Update中不停調用targetPos += Vector2(); 

需要設置一個休息時間:
  public float restTime = 0.5f;
  public float restTimer = 0.5f; // 計時器

在Update()中
  restTimer += Time.deltaTime; // 每次增加時間間隔
  if(restTimer <= restTime) { // 還在休息間隔中
    return; // 不進行其他操作
  }

  當有按鍵按下時:
  if(h!=0 || v!=0) {
    targetPos += ...; // 更新目標位置
    rigidbody.MovePosition(...); // 移動
    restTimer = 0; // 重置計時器
  }

按下按鍵時,只能移動一點點距離
  將rigidbody.MovePosition()代碼移出if條件
  因為每一幀都需要調用而不是只有按鍵的時候調用

發現可以一次同時在水平和豎直移動 -- 和游戲規則有悖
  if(h!=0) v=0; // 優先控制豎直方向

void Update () {

    rigidbody.MovePosition(Vector2.Lerp(transform.position, targetPos, smooth * Time.deltaTime));

    if (restTimer < restTime) {
        restTimer += Time.deltaTime;
        return;
    }

    float h = Input.GetAxisRaw("Horizontal");
    float v = Input.GetAxisRaw("Vertical");
    if (h != 0) {
        v = 0;
    }

    if (h != 0 || v != 0) {
        // 有按鍵輸入時
        targetPos += new Vector2(h, v);
        restTimer = 0; // 計時器歸零
    }
}

任務12:控制主角對牆體的攻擊

給outWall添加BoxCollider2D,scale=0.9;添加tag = OutWall

給wall添加BoxCollider2D,scale=0.9;添加tag = Wall

在每次按鍵的時候,進行碰撞檢測:
  RaycastHit2D hit = Physics2D.Linecast(targetPos, targetPos + new Vector2(h, v));
    // Physics2D.Linecast(起點,終點) 從起點向終點發射射線做檢測,返回RaycastHit2D
  if(hit.transform == null) { // 沒有碰撞,可以行走
  } else { // 有碰撞
    switch(hit.collider.tag) {
      case "OutWall": // 不做處理
      case "Wall": // 進行攻擊
        // 向牆發送攻擊的通知
        // 在Wall里添加Wall.cs來處理行為 Wall.TakeDamage();
        hit.collider.SendMessage("TakeDamage");

public int hp = 2;
public Sprite damageSprite; // 受損后的牆體圖片
public void TakeDamage() {
    hp -= 1;
    GetComponent<SpriteRenderer>().sprite = damageSprite;
    if(hp<=0) Destroy(this.gameObject);
}

禁用自身的collider,因為可能射線會碰到自身的collider:
  private BoxCollider2D collider = GetComponent<...>();
  collider.enabled = false;
  ... // 射線檢測
  collider.enabled = true; // 檢測后再啟用

無論是移動還是攻擊,都需要休息 -- 只要按下了按鍵,restTimer = 0;

攻擊時,播放PlayerAttack動畫
  Player.cs
    private Animator animator = GetComponent<Animator>();
    animator.SetTrigger("Attack"); // 觸發觸發器

rigidbody.MovePosition(
    Vector2.Lerp(transform.position, targetPos, smooth * Time.deltaTime));
if (restTimer < restTime) {
    restTimer += Time.deltaTime;
    return;
}
float h = Input.GetAxisRaw("Horizontal");
float v = Input.GetAxisRaw("Vertical");
if (h != 0) {v = 0;}
if (h != 0 || v != 0) {
    // 有按鍵輸入時
    collider.enabled = false; // 禁用自身collider
    // 碰撞檢測
    RaycastHit2D hit = 
        Physics2D.Linecast(targetPos, targetPos + new Vector2(h, v));
    if (hit.transform == null) {
        // 沒有檢測到碰撞
        targetPos += new Vector2(h, v);
    } else {
        // 檢測到碰撞
        switch (hit.collider.tag) {
            case "OutWall":
                break;
            case "Wall":
                hit.collider.SendMessage("TakeDamage");
                animator.SetTrigger("Attack");
                break;
        }
    }
    restTimer = 0; // 計時器歸零
    collider.enabled = true; // 檢測完碰撞后 開啟自身collider   
}

任務13:控制主角吃食物

對食物進行碰撞檢測:

對食物添加BoxCollider2D;設置為Is Trigger,scale=0.9,tag=food

存儲當前食物 GameManager.public int food = 100;
  將GameManager設置為單例模式

private static GameManager _instance;
public static GameManager Instance {
    get{
        return _instance;
    }
}
private void Awake() {
    _instance = this;
}

增加/減少食物的method:public void Add/ReduceFood(int count)
  food +=/-= count;

case "Food":
  // 用if判斷hit.collider.name是Food(Clone)還是Soda(Clone)
  GameManager.Instance.AddFood(10 | 20);
  // 同時,需要移動Player位置 && 銷毀食物
  targetPos += new Vector2(h, v);
  Destroy(hit.collider.gameObject);

case "Food":
    targetPos += new Vector2(h, v);
    Destroy(hit.collider.gameObject);
    if (hit.collider.name == "Food(Clone)") {
        GameManager.Instance.AddFood(10);
    } else if(hit.collider.name == "Soda(Clone)") {
        GameManager.Instance.AddFood(20);
    }
    break;

任務14&15:控制敵人的移動

當Player移動兩步時,敵人移動一步

給Enemy添加BoxCollider2D進行碰撞檢測,size=0.9, 

給Enemy添加Rigidbody2D進行移動,勾選Is Kinematic:很重要
  如果沒有勾選,會導致后面Enemy被Player推走

給Enemy添加tag=Enemy

case "Enemy": // 說明所走的路徑不通,判斷失誤,浪費一步
  break; 

給Enemy添加Enemy.cs
  Enemy是被動移動 -- 提供移動方法,供其他對象調用
  public void Move() {
    // 判斷Player所在位置的方向
    // Transform player = GameObject.FindGameObjectWithTag(...) 
    Vector2 offset = player.position - transform.position;
    if (offset.magnitude < 1.9f) { // 距離小於一格
      // 攻擊
    } else { // 追 -- 哪個軸偏移大,就往哪里追
      if (Mathf.Abs(offset.x) > Mathf.Abs(offset.y)) { // x軸移動
        if(offset.x<0) x=-1; else x=1;
      } else { // y軸移動
        if(offset.y<0) y=-1; else y=1;
      }
      targetPos += new Vector(x, y);
    }

在GameManager.cs中統一管理游戲進程:
  public List<Enemy> enemyList = new List...;

在Enemy.cs中:
  GameManager.Instance.enemyList.Add(this);

在GameManager.cs中控制Enemy是否行走:
  bool sleepStep = true; // 是否為休息狀態
  OnPlayerMove() {
    if(sleepStep) sleepStep = false;
    else { foreach enemy in enemyList { enemy.Move();
      sleepStep = true; }
  }

在Player.cs中
  有按鍵按下的時候調用OnPlayerMove()
  GameManager.Instance.OnPlayerMove();

此時,Enemy會隨着主角移動而移動,但是Enmey沒有碰撞檢測

設置目標位置targetPos之前先做檢測

// 更新位置之前,做碰撞檢測
collider.enabled = false; // 否則會碰撞到自己的collider
RaycastHit2D hit = Physics2D.Linecast(targetPos, targetPos+new Vector2(x,y));
collider.enabled = true;
if (hit.collider == null) {
    // 沒有碰撞
    targetPos += new Vector2(x, y);
} else {
    // 有碰撞
    switch (hit.collider.tag)
    {
        case "Wall":
        case "OutWall": // 不可能出現外牆的情況
            break;
        case "Food":
            Destroy(hit.collider.gameObject);
            targetPos += new Vector2(x, y);
            break;
    }
}

任務16:控制敵人的攻擊

private Animator animator = GetComponent<...>();
animator.SetTrigger("Attack");
player.SendMessage("TakeDamage", lossFood);  // 不同敵人的傷害不同

public void TakeDamage(int lossFood) {
  GameManager.Instance.ReduceFood(lossFood);
  animator.SetTrigger("UnderAttack"); 
}

任務17:控制游戲食物數量UI的顯示

在Player每走一步就消耗一定量食物:
  GameManager.Instance.ReduceFood(1);

在屏幕上顯示食物:
  右鍵創建UI->Text,重命名FoodText
  字體劇中,字號25,字體為Font->PressStart2P-Regular,內容:Food: 100
  放置在屏幕下方,Anchor Presets設置為bottom-center

在GameManager.cs中控制UI的更新
  private Text foodText;
  在void Awake()中 加入 InitGame();
    void InitGame() {
      foodText = GameObject.Find("FoodText").GetComponent...; 
      UpdateFoodText(0);
    }

private void UpdateFoodText(int foodChange) {
    if (foodChange == 0) {
        foodText.text = "Food: " + food;
    } else if (foodChange > 0) {
        foodText.text = "+" + foodChange + "  Food: " + food;
    } else {
        foodText.text = foodChange + "  Food: " + food;
    }
}

在ReduceFood()/ AddFood(){} 中加上 UpdateFoodText(count/-count);

任務18:控制游戲的失敗  

游戲失敗:Food <= 0;
  Player每走完一步:檢測food的數量
  -- 我的優化:在GameManager中的ReduceFood()中檢測 -- 沒有必要每一步都檢測

  if(food<=0){ 
    GameObject.FindGameObjectWithTag("Player").SetActive(false); 
    // 顯示游戲失敗 -- UI->Text,居中,白色,字號,字體,GameOver等等
    gameOverText.enabled = true;
  }

任務19&20:游戲關卡的勝利判斷 && 下一關卡的加載

判斷Player是否到達了Exit的位置(8,8)

每次Player移動,判斷是否到達終點
  Player每次移動,都會調用GameManager中的OnPlayerMove()
  在OnPlayerMove()中加入
    // 需要得到player的targetPos,與destinationPos比較
    將targetPos改為[HideInInspector]public targetPos;
    Player player = GameObject.Find...Tag(...).GetComponent...;
    // 還需得到Exit的位置
    MapManager mapManager = GetComponent...;
    if(player.targetPos.x == mapManager.cols-2 && ...y==rows-2) {
      private bool missionCompleted = true;
      // 加載下一關卡
      Application.LoadLevel(Application.loadedLevel); // 重新加載本關卡 -- 已棄用
      SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex); 
      // 需要記住某些值 -- 使用系統函數OnLevelWasLoaded() 
    }

運行 -- 發現可以重置關卡了,但是所有數據會重置

GameManager實例不能銷毀 -- 否則數據會重置
  在GameManager.cs的Awake()中:
    DontDestroyOnLoad(gameObject);

運行 -- 發現原來的GameManager沒有銷毀,但是新建了另一個GameManager

不將GameManager放在場景中
  -- 將GameManager做成Prefab
  -- 刪除場景中的GameManager
  在MainCamera中添加GameLoader.cs

// 實例化GameManager
public GameObject gameManager;

void Awake() {
    if(GameManager.Instance == null) {
        GameObject.Instantiate(gameManager);
    }
}

任務21:控制天數UI的顯示

當前天數的顯示
  UI->Image->DayImage; alt+上下左右居中;顏色設置為黑色
  在DayImage中創建一個Text叫DayText;居中偏上;字號32;字體自定義;顏色白色

什么時候顯示呢? -- 初始化UI的時候需要顯示

GameManage.InitGame()中:
  private Image dayImage = GameObject.Find("DayImage").GetComponent<Image>();
  private Text dayText ...;
  dayText.text = "Day " + level;

顯示完天數,需要隱藏

void HideDayImage() {
    dayImage.gameObject.SetActive(false);
}

在InitGame()最后調用 Invoke("HideDayImage", 1); 即可  -- 過1s后調用

任務22:添加音效

GameManager.cs

 

 

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM