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

 

 

 

 

 

 

原文地址:https://www.cnblogs.com/FudgeBear/p/8082034.html