zoukankan      html  css  js  c++  java
  • 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

     

     

     

     

     

     

  • 相关阅读:
    分别针对Customers表与Order表的通用查询操作
    类的继承
    kubernetes service 原理解析
    k8s生命周期-钩子函数
    深入理解Pod-初始化容器
    为 Pod 或容器配置安全性上下文
    Docker四种网络模式
    python中__new__方法详解及使用
    浅析python析构函数
    k8s中的网络
  • 原文地址:https://www.cnblogs.com/FudgeBear/p/8082034.html
Copyright © 2011-2022 走看看