【Unity实现经典游戏--贪吃蛇】

作为一个将来想从事游戏行业的人来说,从一些经典游戏来学习Unity是最好不过的了,在这里我就介绍一下这次做的贪吃蛇。
贪吃蛇制作教学
详细的可以从上面链接进去学习,素材也从上方链接中取得。如果只是想了解整体框架和一些技术问题可以继续往下看。
我这篇博客更像是学习笔记,所以各位读者在一些细节的,不懂的地方,可以在评论区留言给我,我会尽我所能帮大家解决。

首先是场景的搭建:
我们需要两个场景,一个是开始界面场景,一个是游戏场景。

开始界面
开始界面
开始界面内容清单:
包含模式选择和皮肤选择(详细的处理在后文)
在这里插入图片描述
游戏场景:
在这里插入图片描述
游戏场景内容清单:
游戏场景内容
这里的Up,Down,Right和Left是四条边界
两个场景的ScriptsHolder是脚本控制器,分别用于控制不同的场景。

那么游戏场景就搭建好了,接下来是组件的一些注意事项:

1.开始界面的Skin和Model选择,需要让它们的子项属于同一个ToggleGroup中,这样就可以保证只有一个被选中。

2.记得给能碰撞到的物体都添加碰撞检测(BoxCollider)组件,而蛇头需要添加Ridbody2D和BoxCollider组件。

3.在File->Building Setting中添加这两个场景
在这里插入图片描述
4.预制体!
在这里插入图片描述
这四个预制体没什么好说的,一个是食物,一个是奖励物,一个是蛇头,一个是蛇身,需要注意的是都要添加碰撞检测组件。

接下来是最关键的脚本环节(所有脚本我都添加好注释了,直接挂载然后拖动元素赋值即可)

在开始场景的Scripts挂载StartUIController脚本

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class StartUIController : MonoBehaviour
{
    public Text lastText;
    public Text bestText;
    public Toggle white;
    public Toggle colors;
    public Toggle border;
    public Toggle noBorder;

    void Awake() {
        lastText.text = "上次:长度" + PlayerPrefs.GetInt("lastl",0) + "分数" + PlayerPrefs.GetInt("lasts",0);
        bestText.text = "最好:长度" + PlayerPrefs.GetInt("bestl",0) + "分数" + PlayerPrefs.GetInt("bests",0);
    }

    void Start() 
    {
        if(PlayerPrefs.GetString("sh","sh01")=="sh01")//这个是让数据随场景跳转传递的代码
        {
            white.isOn=true;
            PlayerPrefs.SetString("sh","sh01");
            PlayerPrefs.SetString("sb01","sb0101");
            PlayerPrefs.SetString("sb02","sb0102");
        }
        else
        {
            colors.isOn=true;
            PlayerPrefs.SetString("sh","sh02");
            PlayerPrefs.SetString("sb01","sb0201");
            PlayerPrefs.SetString("sb02","sb0202");
        }
        if(PlayerPrefs.GetInt("border",1)==1)
        {
            Debug.Log("边界模式启动");
            border.isOn = true;
            PlayerPrefs.SetInt("border",1);
        }
        else
        {
            Debug.Log("无边界模式启动");
            noBorder.isOn = true;
            PlayerPrefs.SetInt("border",0);
        }
    }

    //以下代码为模式和皮肤选择
    public void WhiteSelected(bool isOn)
    {
        if(isOn)
        {
            PlayerPrefs.SetString("sh","sh01");//存储皮肤文件的文件名,我这里都用SnakeHead
            PlayerPrefs.SetString("sb01","sb0101");
            PlayerPrefs.SetString("sb02","sb0102");
        }
    }
    public void ColorsSelected(bool isOn)
    {
        if(isOn)
        {
            PlayerPrefs.SetString("sh","sh02");
            PlayerPrefs.SetString("sh01","sb0201");
            PlayerPrefs.SetString("sh02","sb0202");
        }
    }
    public void BorderSelected(bool isOn)
    {
        if(isOn)
        {
            PlayerPrefs.SetInt("border",1);
        }
    }
    public void NoBorderSelected(bool isOn)
    {
        if(isOn)
        {
            PlayerPrefs.SetInt("border",0);
        }
    }
    public void StartGame()//开始游戏按钮
    {
        UnityEngine.SceneManagement.SceneManager.LoadScene(1);
    }
}

需要拖动置入的对象
在这里插入图片描述

然后是游戏场景的脚本MainUiController,挂载到游戏场景的ScrptsHolder上

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class MainUIController : MonoBehaviour
{
    private static MainUIController _instance;//单例模式访问
    public static MainUIController Insatance
    {
        get
        {
            return _instance;
        }
    }

    public bool hasBorder = true;//边界标识位
    public bool isPause = false;
    public int score = 0;//存放分数
    public int length = 0;//存放长度
    public Text scoreText;
    public Text msgText;
    public Text lengthText;
    public Image bgImage;
    public Image pauseImage;
    public Sprite[] pauseSprites;
    private Color tempColor;

    void Awake() 
    {
        _instance = this;
    }

    void Start() {
       if(PlayerPrefs.GetInt("border",1)==0)//边界模式切换操作
       {
           hasBorder = false;
           foreach(Transform t in bgImage.gameObject.transform)//遍历bgImage下的所有子物体
           {
               t.gameObject.GetComponent<Image>().enabled = false;
           }
       }
   }
    

    void Update() 
    {
        switch(score)
        {
            case 10:
                ColorUtility.TryParseHtmlString("#CCEEFFFF",out tempColor);//解析16进制颜色组
                bgImage.color = tempColor;
                msgText.text = "阶段"+2;
                break;
            case 20:
                ColorUtility.TryParseHtmlString("#CCFFDBFF",out tempColor);//解析16进制颜色组
                bgImage.color = tempColor;
                msgText.text = "阶段"+3;
                break;
            case 30:
                ColorUtility.TryParseHtmlString("#EBFFCCFF",out tempColor);//解析16进制颜色组
                bgImage.color = tempColor;
                msgText.text = "阶段"+4;
                break;
            case 40:
                ColorUtility.TryParseHtmlString("#FFF3CCFF",out tempColor);//解析16进制颜色组
                bgImage.color = tempColor;
                msgText.text = "无尽阶段";
                break;
        }
    }

    public void updateUI(int s=1,int l=1)//加多少分数和多少长度
    {
        score +=s;
        length +=l;
        scoreText.text = "得分:\n" + score;
        lengthText.text = "长度:\n"+ length;
    }

    public void Pause()
    {
        isPause = !isPause;
        if(isPause)
        {
            Time.timeScale = 0;//时间冻结
            pauseImage.sprite = pauseSprites[1];
        }
        else
        {
            Time.timeScale = 1;//时间继续
            pauseImage.sprite = pauseSprites[0];
        }
    }

    public void Home()
    {
        UnityEngine.SceneManagement.SceneManager.LoadScene(0);
    }
}

同时也需要它控制食物的生成。
所以脚本FoodMaker也是挂载在这个结点上
以下是FoodMaker代码

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class FoodMaker : MonoBehaviour
{
    public int xlimit = 21;//从0,0到最右边限制21步(步数为测量后得出)
    public int ylimit = 11;//到上边的限制
    public int xoffset =  7;//向左为14  21-7=14
    public GameObject foodPrefab;//食物预设体
    public GameObject rewardPrefab;//随机奖励预制体
    public Sprite[] foodSprites;//食物样式的数组
    public static FoodMaker Instance//使用这个进行访问
    {
        get
        {
            return _instance;
        }
    }
    private Transform foodHolder;
    private static FoodMaker _instance;//使用单例模式被访问,用于食物被吃掉后生成


    void Awake()
    {
        _instance = this;//在脚本刚被调用时赋值为this
    }

    void Start()
    {
        foodHolder = GameObject.Find("FoodHolder").transform;//获得到名字为FoodHolder的物体
        MakeFood(false);
    }

    public void MakeFood(bool isReward)//需要被蛇头碰撞调用生成新食物,参数为本次生成是否需要生成奖励物
    {
        int index = Random.Range(0,foodSprites.Length);//随机获取食物样式
        GameObject food = Instantiate(foodPrefab);//用Instantiate函数实例化预制体
        food.GetComponent<Image>().sprite = foodSprites[index];//给食物样式,Image数组需要引入UI组件
        food.transform.SetParent(foodHolder,false);//设置参数为false让其保持坐标不变
        int x = Random.Range(-xlimit+xoffset,xlimit);//设置随机刷新位置X
        int y = Random.Range(-ylimit,ylimit);//设置随机刷新位置y
        food.transform.localPosition = new Vector3(x*30,y*30,0);//这里的30代表我们每分割一个步长是30
        if(isReward)
        {
            GameObject reward = Instantiate(rewardPrefab);//用Instantiate函数实例化预制体
            reward.transform.SetParent(foodHolder,false);//设置参数为false让其保持坐标不变
            x = Random.Range(-xlimit+xoffset,xlimit);//设置随机刷新位置X
            y = Random.Range(-ylimit,ylimit);//设置随机刷新位置y
            reward.transform.localPosition = new Vector3(x*30,y*30,0);//这里的30代表我们每分割一个步长是30
        }
    }
}

然后是这个结点需要拖拽设置的对象:
在这里插入图片描述
然后蛇头挂载的控制脚本SnakeHead

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using System.Linq;
public class SnakeHead : MonoBehaviour
{
   public List<Transform> bodyList = new List<Transform>();//使用LIST存储蛇身
   // 属性的Public和Private将决定在面板上能不能直接修改
   public float velocity=0.35f;//相当于帧率
   public int step;//蛇每一步走多少
   private int x;//X,Y代表增量值
   private int y;
   private Vector3 headPos;
   private Transform canvas;
   private static FoodMaker _instance;
   private bool isDie = false;

   public AudioClip eatClip;
   public AudioClip dieClip;
   public GameObject dieEffect;
   public GameObject bodyPrefab;
   public Sprite[] bodySprites = new Sprite[2];//用于控制蛇身样式


   void Awake()
   {
      canvas = GameObject.Find("Canvas").transform;
      //路径不需要写扩展名和Resources/
      //可以使用泛型Resources.Load<Object>()
      gameObject.GetComponent<Image>().sprite = Resources.Load<Sprite>(PlayerPrefs.GetString("sh","sh02"));//加载Resources文件夹里的资源
      bodySprites[0] = Resources.Load<Sprite>(PlayerPrefs.GetString("sb01","sb0201"));
      bodySprites[1] = Resources.Load<Sprite>(PlayerPrefs.GetString("sb02","sb0202"));
   }
   void Start()
   {
      InvokeRepeating("Move",0,velocity);//InvokeRepeating是重复调用,
                                       //第一个参数是在本脚本里调用方法的方法名,字符串传递
                                       //第二个参数是多久开始调用
                                       //第三个参数是每隔多少时间调用一次
      x = 0;y=step;//让贪吃蛇一开始向右走,不然会静止不动
   }

   void Update()
   {
      if(isDie)
      {
         return;//如果死亡就直接返回,但还不知道这样做有什么问题
      }
      //空格加速控制模块
      if(Input.GetKeyDown(KeyCode.Space) && MainUIController.Insatance.isPause==false&&isDie==false)//GetKeyDown()函数是获取按下的按键
      {
         CancelInvoke();//使得当前重复调用的函数暂停
         InvokeRepeating("Move",0,velocity-0.3f);//这里的加速方法是加快函数的调用达到刷新效果
      }
      if(Input.GetKeyUp(KeyCode.Space)&&MainUIController.Insatance.isPause==false&&isDie==false)
      {
         CancelInvoke();
         InvokeRepeating("Move",0,velocity);
      }


      //方向控制模块
      if(Input.GetKey(KeyCode.W)&&y != -step&&MainUIController.Insatance.isPause==false&&isDie==false)//y!=-step控制蛇不能直接回头
      {
         //调整图片使其跟随方向旋转↓
         gameObject.transform.localRotation = Quaternion.Euler(0,0,0);//需要用四元数Quaternion赋值
         //给位移量赋值↓
         x=0;y=step;
      }
      if(Input.GetKey(KeyCode.S)&&y != step&&MainUIController.Insatance.isPause==false&&isDie==false)
      {
         gameObject.transform.localRotation = Quaternion.Euler(0,0,180);
         x=0;y=-step;
      }
      if(Input.GetKey(KeyCode.A)&&x != step&&MainUIController.Insatance.isPause==false&&isDie==false)
      {
         gameObject.transform.localRotation = Quaternion.Euler(0,0,90);
         x=-step;y=0;
      }
      if(Input.GetKey(KeyCode.D)&&x != -step&&MainUIController.Insatance.isPause==false&&isDie==false)
      {
         gameObject.transform.localRotation = Quaternion.Euler(0,0,-90);
         x=step;y=0;
      }
   }

   void Move()
   {
      headPos = gameObject.transform.localPosition;//保存下来蛇头移动前的位置
      gameObject.transform.localPosition = new Vector3(headPos.x+x,headPos.y+y,headPos.z);//使用Vector3进行赋值,蛇头移动
      if(bodyList.Count>0)//要至少有一个蛇身才进入,不然Last会返回空指针
      {
         //第一种方法,将尾巴移动到头的位置
         //该方法有两个问题,第一是每一次吃都会在原点重新生成,会闪烁一次(可以将蛇身生成在屏幕外解决此问题,或者将Grow函数放进Move函数中)
         // 第二是如果蛇身颜色不一样,会看得出来排序出错
         //↓代码
         // bodyList.Last().localPosition = headPos;//Last方法需要引入System.Linq
         // bodyList.Insert(0,bodyList.Last());
         // bodyList.RemoveAt(bodyList.Count-1);

         //第二种方法,记载蛇身位置,整体移动(从后往前挪动,节省空间开销)
         for(int i=bodyList.Count-2;i>=0;i--)
         {
            bodyList[i+1].localPosition = bodyList[i].localPosition;
         }
         bodyList[0].localPosition = headPos;//第一节身体等于头的位置
      }

   }

   void Grow()//蛇身生长函数
   {
      AudioSource.PlayClipAtPoint(eatClip,Vector3.zero);//零点播放音效
      int index =(bodyList.Count%2==0) ? 0 : 1;//==0取0否则取1
      GameObject body = Instantiate(bodyPrefab,new Vector3(2000,2000,0),Quaternion.identity);//将生成的蛇身放在看不到的位置
      body.GetComponent<Image>().sprite = bodySprites[index];
      body.transform.SetParent(canvas,false);//将蛇身加入画布
      bodyList.Add(body.transform);
   }

   void Die()//蛇的死亡方法
   {
      AudioSource.PlayClipAtPoint(dieClip,Vector3.zero);//零点播放音效
      CancelInvoke();//取消移动
      isDie = true;
      Instantiate(dieEffect);//调用死亡特效
      PlayerPrefs.SetInt("lastl",MainUIController.Insatance.length);//记录了最后的长度
      PlayerPrefs.SetInt("lasts",MainUIController.Insatance.score);
      if(PlayerPrefs.GetInt("bests",0)<MainUIController.Insatance.score)//最佳成绩判断
      {
         PlayerPrefs.SetInt("bestl",MainUIController.Insatance.length);
         PlayerPrefs.SetInt("bests",MainUIController.Insatance.score);
      }
      StartCoroutine(GameOver(1.5f));//调用协同程序Coroutine
   }


// coroutine本身提供了一种机制,让开发者可以控制一个代码片段↓
// 让它暂停,然后在下次调用时继续从上次暂停的地方继续运行。
   IEnumerator GameOver(float t)//协同程序Coroutine
   {
      yield return new WaitForSeconds(t);//告诉游戏系统等待一段时间再执行下方代码
      UnityEngine.SceneManagement.SceneManager.LoadScene(1);//重载当前场景
   }
   private void OnTriggerEnter2D(Collider2D collision)//触发器方法,和碰撞器有点区别
   {
      //也可以写成collision.tag=="Food"
      if(collision.gameObject.CompareTag("Food"))//检测Tag是否等于Food
      {
         Destroy(collision.gameObject);//销毁碰撞器所在的游戏物体
         MainUIController.Insatance.updateUI();
         Grow();
         FoodMaker.Instance.MakeFood((Random.Range(0,100)<20)? true : false);//%20概率生成奖励物
      }
      else if(collision.gameObject.CompareTag("Reward"))
      {
         Destroy(collision.gameObject);
         MainUIController.Insatance.updateUI(Random.Range(1,5));
         Grow();
      }
      else if(collision.gameObject.CompareTag("Body"))
      {
         //要注意碰撞器大小或者是蛇头和蛇身的间隔,不然每一次调用Grow都会自毁
         Debug.Log("撞到身体了");
         Die();
      }
      else
      {
         if(MainUIController.Insatance.hasBorder)//边界模式下撞击死亡
         {
            Die();
         }
         else//无边界模式
         {
            switch(collision.gameObject.name)
            {
               case "Up"://自由模式撞到上边界传送到下边界
               transform.localPosition = new Vector3(transform.localPosition.x,-transform.localPosition.y+step,transform.localPosition.z);
                  break;//偏移值step是为了传送后不触发下边界的碰撞,让它多走一步
               case "Down"://自由模式撞到下边界传送到上边界
               transform.localPosition = new Vector3(transform.localPosition.x,-transform.localPosition.y-step,transform.localPosition.z);
                  break;
               case "Left":
               transform.localPosition = new Vector3(-transform.localPosition.x+(step*5),transform.localPosition.y,transform.localPosition.z);
                  break;
               case "Right":
               transform.localPosition = new Vector3(-transform.localPosition.x+(step*8),transform.localPosition.y,transform.localPosition.z);
                  break;
            }
         }
      }
   }
}

然后基本上就搭建完毕了,因为贪吃蛇大体上来说还是很简单的。脚本的大部分地方我都给予了注释,帮助大家理解。

接下来是我在这次实践中印象深刻的一些细节:

1.场景带数据的跳转,用的是PlayerPrefs的存储+Resouce文件夹的扫描加载

2.死亡后的场景延迟加载,用的是协同程序Coroutine

3.加速小蛇移动用的是增加单位时间内函数调用的次数(虽然个人觉得这样好像有点问题,但是还不知道具体的问题在哪)

4.由于项目简单,脚本之间的访问调用用的都是单例模式

这个游戏大体上还是很简单的。所以没有特别多让我很困惑的地方。
此外,以后我学习做游戏还是同步更新好了,这样一整个游戏的写总结还是太粗糙了。

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值