一文零基础教你制作Unity 2048 !【文末源码】

前言

玩游戏也能学习知识?还记得高中时的化学元素常见金属活动性属性表吗?一起来复习一下:钾K,钙Ca,钠Na,镁Mg,铝Al,锌Zn,铁Fe, 锡Ni,铅Sn,氢(H),铜Cu,汞Hg,银Ag,铂Pt,金Au。 一股很熟悉的味道有没有?一起来看看化学元素和游戏之间发生的碰撞吧~

一,游戏介绍和效果展示

2048 一款益智小游戏,游戏的规则十分简单,简单易上手的数字小游戏,闲来无事,自己制作一个,却怎么也到没有成功到2048。

老规矩,先看下按照此博文一步步操作完成的效果吧~


二,开发前的准备工作

2.1 创建工程

打开Unity Hub 点击“新建”,在弹窗中输入 --> “项目名称” --> 选择"项目位置" --> “项目版本控制系统” 选不选都行,不需要在云端备份的话就不要选(我一般不选)
,选了没用过的话,会自动帮你安装PlasticSCM到本地。最后点击创建即可。

2.2 导入素材

将提前准备好的素材(图片和声音)导入工程。(文末会提供下载地址)

  • 方式一: 在"Project"面板右键 --> “Improt Package” --> “Custom Package…” --> 选择自己下载下来的UnityPackage --> 最后点击"Import" 导入。

  • 方式二: 直接将下载还的UnityPackage,拖拽到项目中,然后点击"Import" 导入:

导入项目后工程目录如下:


三,游戏开发进行中

3.1 游戏场景搭建

  • 设置分辨率:点击Game视图分辨率选框,设置为(1080x1920),没有的话点击加号自行添加一个,或者使用一个竖屏的就可以。

  • 创建背景:右键UI --> Image 创建后,重命名为BG,将其锚点设置为铺满,源文件指定为素材"play_bg_forest_dark":

  • 修改Canvas:将自动创建出来的Canvas的Canvas Scaler 属性设置如下:

  • 创建格子: 在“Canvas”下创建Image,重命名为“MapBg”,将其宽高设置为(1720,1720),然后修改其颜色为浅蓝色,透明度相应调低,效果如下:(自己觉得好看就可以了)

    然后在“MapBg”下再次创建一个Image,重命名为“gezi”,将其宽高设置为(400,388),调整其颜色为(22,0,192,30),还是一样调整到自己喜欢的颜色即可。然后“Ctrl + D” 复制15个格子出来:

    最后在“MapBg”上添加"Grid Layout Group",属性设置如下图,将格子铺满到地图背景上:

  • 创建数字池: 右键创建一个空物体作为加载数字的父物体,并命名为“NumPool”,所有属性默认即可。

  • 创建数字预制体: 右键Image重命名为Num,宽高调整为和格子一样大(400,380),然后将其拽到Resources文件夹下,作为预制体,然后右键删除场景中Num即可:

  • 游戏结束面板:创建Image命名为“UIFinsh”,铺满背景。在“UIFinsh”下在创建一个Text和一个Button,作为游戏结束显示文本和重新开始按钮:


3.2 核心代码编写

场景终于搭建完了,下面开始编写脚本吧,完整代码在四中给出,这里只讲解实现思路,查看核心逻辑。

  • 在Project下创建Scripts文件夹,然后创建"Manager.cs" 和 “Number.cs” 脚本。

  • 基础逻辑就是:默认生成两个数字,接收用户输入,管理器触发移动逻辑,数字移动并校验合并,移动后校验是否游戏结束,若结束处理游戏结束摞,若未结束在自动生成一个数字:

结束
继续
默认生成数字
管理器接收用户输入
管理器触发数字移动逻辑
数字移动并校验合并
移动后校验游戏结束
处理游戏结束逻辑
  • Manager中接收用户输入的方式一种是在移动端的滑动,一种是在PC端的按下剪头或WASD上下左右移动:

  • Manager收到用户输入后的控制游戏数字移动逻辑:

  • Num类收到Manager的移动,合并逻辑处理:

  • 最后将Manager挂载到“BG”,并对公有变量’PoolManager’和’UIFinsh’赋值,如下图即可:

  • 将Number脚本挂载到上面制作的预制体“Num”上即可。

    此时所有的游戏逻辑就都完成,运行游戏就可以玩耍了~


3.3 游戏音效处理

音效是这一个游戏的很重要的部分,一个好的音效可以让用户得到更好的反馈,所以再简单的游戏也要有背景音和音效给用户一个好的交互体验。

  • 背景音乐:在“BG”上,添加组件“Audio Source”,将音频文件“bg_1”赋值给AudioClip并勾选Loop,即可完成自动循环播放背景音乐:

  • 数字音效:同理在“Num”预制体上添加组件“Audio Source”,将音频文件“num”赋值给AudioClip,其他属性默认即可。


四,游戏完成源码分享

4.1 Manager游戏管理类

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

public class Manager : MonoBehaviour
{
    public static Manager _isnstance;  //单例模式的引用

    public Transform poolManager;   //生成数字的池子
    private GameObject numPrefab;   //数字的预制体
    public Number[,] numbers = new Number[4, 4]; //保存方格中的数组
    //正在移动中的Num
    public List<Number> isMovingNum = new List<Number>();  
    public bool hasMove = false;   //是否有数字发生了移动
 
    public GameObject UIFinsh;  //游戏结束页面

    void Awake()
    {
        _isnstance = this;
    }
    
    void Start()
    {
        numPrefab = Resources.Load<GameObject>("Num");
        // 开始游戏
        ReStartBtn();
        // 游戏结束面板按钮监听,重新开始
        UIFinsh.GetComponentInChildren<Button>().onClick.AddListener(ReStartBtn);
    }

    // 重新开始
    void ReStartBtn()
    {
        isMovingNum.Clear();
        numbers = new Number[4, 4];
        for (int i = poolManager.childCount - 1; i >= 0; i--)
        {
            Destroy(poolManager.GetChild(i).gameObject);
        }
        hasMove = false;
        //游戏开始生成两个数字
        CreateNun();
        CreateNun();
        UIFinsh.SetActive(false);
    }

    void CreateNun()
    {
        GameObject go = Instantiate(numPrefab);
        go.transform.parent = poolManager;
        go.transform.localScale = Vector3.one;
    }

    #region 检测键盘和触摸输入
    void Update()
    {
        //--------- 移动端检测逻辑 ---------
        //有触摸点,且滑动
        if (isMovingNum.Count == 0)
        {
            if (Input.touchCount > 0 && Input.GetTouch(0).phase == TouchPhase.Moved)
            {
                int dieX = 0;
                int dieY = 0;
                //获取滑动的距离
                Vector2 touchDelPos = Input.GetTouch(0).deltaPosition;
                if (Mathf.Abs(touchDelPos.x) > Mathf.Abs(touchDelPos.y))
                {
                    //滑动距离
                    if (touchDelPos.x > 10)
                    {
                        dieX = 1;
                    }
                    else
                    if (touchDelPos.x < -10)
                    {
                        dieX = -1;
                    }
                }
                else
                {
                    if (touchDelPos.y > 10)
                    {
                        dieY = 1;
                    }
                    else if (touchDelPos.y < -10)
                    {
                        dieY = -1;
                    }
                }
                MoveNum(dieX, dieY);
            }
        }
        
        //--------- PC端检测逻辑 ---------
        if (isMovingNum.Count == 0)
        {
            int dieX = 0;
            int dieY = 0;
            if (Input.GetKeyDown(KeyCode.A) || Input.GetKeyDown(KeyCode.LeftArrow))
            {
                dieX = -1;
            }
            else
            if (Input.GetKeyDown(KeyCode.D) || Input.GetKeyDown(KeyCode.RightArrow))
            {
                dieX = 1;
            }
            else
            if (Input.GetKeyDown(KeyCode.W) || Input.GetKeyDown(KeyCode.UpArrow))
            {
                dieY = 1;
            }
            else
            if (Input.GetKeyDown(KeyCode.S) || Input.GetKeyDown(KeyCode.DownArrow))
            {
                dieY = -1;
            }
            MoveNum(dieX, dieY);
        }

        if (hasMove && isMovingNum.Count == 0)   //生成新的数字
        {
            CreateNun();
            hasMove = false;

            for (int i = 0; i < 4; i++)
            {
                for (int j = 0; j < 4; j++)
                {
                    if (numbers[i, j] != null)
                    {
                        numbers[i, j].OneMove = false;
                    }
                }
            }
        }
    }
    #endregion

    #region 游戏逻辑
    /// <summary>
    /// 数字移动方法
    /// </summary>
    /// <param name="directionX"></param>
    /// <param name="directionY"></param>
    public void MoveNum(int directionX, int directionY)
    {
        if (directionX == 1)  //向右移动  
        {
            //首先将空格填满   最右侧列不需做判断
            for (int j = 0; j < 4; j++)
            {
                for (int i = 2; i >= 0; i--)
                {
                    if (numbers[i, j] != null)  //格子中有物体(数字),,调用移动方法
                    {
                        numbers[i, j].Move(directionX, directionY);
                    }
                }
            }
        }
        else

        //===========向左移动==================
        if (directionX == -1)
        {
            for (int j = 0; j < 4; j++)
            {
                for (int i = 1; i < 4; i++)
                {   //最左侧的一列 [0,0] [0,1] [0,2] [0,3]
                    if (numbers[i, j] != null)
                    {
                        numbers[i, j].Move(directionX, directionY);
                    }
                }
            }
        }
        else

        //===========向上移动==================
        if (directionY == 1)
        {
            for (int i = 0; i < 4; i++)
            {
                for (int j = 2; j >= 0; j--)
                {
                    if (numbers[i, j] != null)
                    {
                        numbers[i, j].Move(directionX, directionY);
                    }
                }
            }
        }
        else

        //===========向下移动==================
        if (directionY == -1)
        {
            for (int i = 3; i >= 0; i--)
            {
                for (int j = 0; j < 4; j++)
                {
                    if (numbers[i, j] != null)  //有物体(数字)就移动
                    {
                        numbers[i, j].Move(directionX, directionY);
                    }
                }
            }
        }
    }


    /// <summary>
    /// 判断是否是空格的方法
    /// </summary>
    /// <param name="x">数组索引X</param>
    /// <param name="y">数组索引Y</param>
    /// <returns></returns>
    public bool isEmpty(int x, int y)
    {
        if (x < 0 || x > 3 || y < 0 || y > 3)
        {
            return false;
        }
        else if (numbers[x, y] != null)
        {
            return false;
        }
        return true;
    }

    /// <summary>
    /// 判断游戏是否结束
    /// </summary>
    /// <returns>返回true则游戏结束</returns>
    public bool isDead()
    {
        for (int i = 0; i < 4; i++)
        {
            for (int j = 0; j < 4; j++)
            {
                if (numbers[i, j] == null)
                {
                    return false;
                }
            }
        }

        for (int j = 0; j < 4; j++)
        {
            for (int i = 0; i < 3; i++)
            {
                if (numbers[i, j].value == numbers[i + 1, j].value)
                {
                    return false;
                }
            }
        }

        for (int i = 0; i < 4; i++)
        {
            for (int j = 0; j < 3; j++)
            {
                if (numbers[i, j].value == numbers[i, j + 1].value)
                {
                    return false;
                }
            }
        }
        return true;
    }

    #endregion
    
    /// <summary>
    /// 游戏结束
    /// </summary>
    /// <param name="isSuccess">false:输,true:赢</param>
    public void ShowUIFinsh(bool isSuccess)
    {
        UIFinsh.SetActive(true);
        if (isSuccess)
        {
            UIFinsh.GetComponentInChildren<Text>().text = "游戏成功";
        }
        else
        {
            UIFinsh.GetComponentInChildren<Text>().text = "游戏失败";
        }
    }
}

4.2 Number数字处理类

using System.Collections;
using System.Collections.Generic;
using System.Numerics;
using UnityEngine;
using UnityEngine.UI;
using Vector3 = UnityEngine.Vector3;

public class Number : MonoBehaviour
{
    //在二维数组中的位置X,Y
    public int posX; 
    public int posY;

    private int offsetX = -620;      //显示偏移,Y,,,
    private int offsetY = -620;
    private int space = 420;         // 间距

    private bool isMoving = false;   //动画是否播放过的计数
    public int value;                //产生数字是几
    private bool toDestroy;          //判断数字是否销毁  
    public bool OneMove = false;     //标识数字是否合并过一次
 
    // Use this for initialization
    void Start()
    {
        // 80%成2的概率,更改本身的Sprite名字,以更换图片
        value = Random.value > 0.2f ? 2 : 4;
        this.GetComponent<Image>().sprite = LoadSprite();
        do
        {
            posX = Random.Range(0, 4);
            posY = Random.Range(0, 4);
        } while (Manager._isnstance.numbers[posX, posY] != null);

        transform.localPosition = GetLocalPos();
        // 存放数字本身到数组中,表示此位置有数字不能生成新的数字
        Manager._isnstance.numbers[posX, posY] = this;
        if (Manager._isnstance.isDead())
        {
            // 游戏失败
            Manager._isnstance.ShowUIFinsh(false);
        }
    }

    // Update is called once per frame
    void Update()
    {
        //播放一次动画
        if (!isMoving)
        {
            if (transform.localPosition != GetLocalPos())
            {
                isMoving = true;
                StartCoroutine(MoveAni());
            }
        }
    }

    // 移动动画
    IEnumerator MoveAni()
    {
        Debug.Log("移动动画...");
        float t = 0;
        for (int i = 0; i < 10; i++)
        {
            transform.localPosition = Vector3.Lerp(transform.localPosition, GetLocalPos(), t);
            t += 0.1f;
            yield return new WaitForEndOfFrame();
        }
        // 移动结束的回调
        MoveOver();
    }


    #region 游戏核心移动算法
    /// <summary>
    /// 核心,移动方法(有空格,有物体是否一样)
    /// </summary>
    public void Move(int directionX, int directionY)
    {
        //Debug.Log("测试");
        
        //==========向右移动==================  
        if (directionX == 1) 
        {
            int index = 1;  // 空格标志
            while (Manager._isnstance.isEmpty(posX+index,posY))
            {
                index++;
            }
            // 有空格的移动
            if (index>1)
            {
                
                if (!Manager._isnstance.isMovingNum.Contains(this))
                {   // 保证不会重复添加物体(数字)到列表,
                    Manager._isnstance.isMovingNum.Add(this);
                }
                //移动一次,就生成两个数字的标志符
                Manager._isnstance.hasMove = true;
                //向空格位置移动
                Manager._isnstance.numbers[posX, posY] = null;
                posX = posX + index - 1;
                Manager._isnstance.numbers[posX, posY] = this;
            }
            //有相同数字的移动
            if (posX < 3 && value == Manager._isnstance.numbers[posX+1,posY].value &&
                !Manager._isnstance.numbers[posX+1,posY].OneMove)
            {
                // 只合并一次的标志
                Manager._isnstance.numbers[posX + 1, posY].OneMove = true;
                // 移动的标志,(生成新的物体(数字))
                Manager._isnstance.hasMove = true;
                // 动画播放的限定(有数字在列表中就不会重复播放第二次动画) 
                // 不会重复添加物体(数字)到列表,
                if (!Manager._isnstance.isMovingNum.Contains(this))
                {
                    Manager._isnstance.isMovingNum.Add(this);
                }
                // 碰到一样的数字,讲位置设为空 并销毁本身标识(true),
                // 再将其位置上的值变为2倍,(更换成新的数字)
                toDestroy = true;
                Manager._isnstance.numbers[posX, posY] = null;
                Manager._isnstance.numbers[posX + 1, posY].value *= 2;
                posX += 1;
            }
        }else

        //===========向左移动==================
        if (directionX == -1)
        {
            int index = 1;
            while (Manager._isnstance.isEmpty(posX - index, posY))
            {
                index++;
            }
            //有空格的移动
            if (index > 1)
            {
                Manager._isnstance.hasMove = true;
                if (!Manager._isnstance.isMovingNum.Contains(this))
                {
                    Manager._isnstance.isMovingNum.Add(this);
                }

                Manager._isnstance.numbers[posX, posY] = null;
                posX = posX - index + 1;
                Manager._isnstance.numbers[posX, posY] = this;
            }

            //碰到相同数字的移动
            if (posX > 0 && value == Manager._isnstance.numbers[posX - 1, posY].value && 
                !Manager._isnstance.numbers[posX - 1, posY].OneMove)
            {
                Manager._isnstance.numbers[posX - 1, posY].OneMove = true;
                Manager._isnstance.hasMove = true;
                if (!Manager._isnstance.isMovingNum.Contains(this))
                {
                    Manager._isnstance.isMovingNum.Add(this);
                }

                toDestroy = true;
                Manager._isnstance.numbers[posX, posY] = null;
                Manager._isnstance.numbers[posX - 1, posY].value *= 2;
                posX -= 1;
            }

        }else

        //===========向上移动==================
        if (directionY == 1)
        {
            int index = 1;   //空格标志
            while (Manager._isnstance.isEmpty(posX , posY + index))
            {
                index++;
            }
            //有空格的移动
            if (index > 1)
            {
                Manager._isnstance.hasMove = true;
                if (!Manager._isnstance.isMovingNum.Contains(this))
                {
                    Manager._isnstance.isMovingNum.Add(this);
                }

                Manager._isnstance.numbers[posX, posY] = null;
                posY = posY + index - 1;
                Manager._isnstance.numbers[posX, posY] = this;
            }
            //有相同位置的移动
            if (posY < 3 && value == Manager._isnstance.numbers[posX , posY + 1].value && !Manager._isnstance.numbers[posX, posY + 1].OneMove)
            {
                Manager._isnstance.numbers[posX , posY + 1].OneMove = true;
                Manager._isnstance.hasMove = true;
                if (!Manager._isnstance.isMovingNum.Contains(this))
                {
                    Manager._isnstance.isMovingNum.Add(this);
                }

                toDestroy = true;
                Manager._isnstance.numbers[posX, posY] = null;
                Manager._isnstance.numbers[posX , posY + 1].value *= 2;
                posY += 1;
            }

        }else

        //===========向下移动==================
        if (directionY == -1)
        {
            int index = 1;  //空格标志位
            while (Manager._isnstance.isEmpty(posX, posY - index))
            {
                index++;
            }
            //有空格的移动
            if (index > 1)
            {
                Manager._isnstance.hasMove = true;
                if (!Manager._isnstance.isMovingNum.Contains(this))
                {
                    Manager._isnstance.isMovingNum.Add(this);
                }

                Manager._isnstance.numbers[posX, posY] = null;
                posY = posY - index + 1;
                Manager._isnstance.numbers[posX, posY] = this;
            }
            //有相同数字的移动
            if (posY > 0 && value == Manager._isnstance.numbers[posX, posY - 1].value && !Manager._isnstance.numbers[posX, posY - 1].OneMove)
            {
                Manager._isnstance.numbers[posX, posY -1].OneMove = true;
                Manager._isnstance.hasMove = true;
                if (!Manager._isnstance.isMovingNum.Contains(this))
                {
                    Manager._isnstance.isMovingNum.Add(this);
                }

                toDestroy = true;
                Manager._isnstance.numbers[posX, posY] = null;
                Manager._isnstance.numbers[posX, posY - 1].value *= 2;
                posY -= 1;
                }
        }
    }

#endregion

    /// <summary>
    /// 动画结束,标志改为false
    /// </summary>
    public void MoveOver()
    {
        isMoving = false;
        //若碰到了相同的数字  销毁自己,和改变另一个图片(数字)
        if (toDestroy)   
        {
            Destroy(this.gameObject);
            value = Manager._isnstance.numbers[posX, posY].value;
            Manager._isnstance.numbers[posX, posY].GetComponent<Image>().sprite = LoadSprite();
            //游戏成功
            if (value == 4096)
            {
                Manager._isnstance.ShowUIFinsh(true);
            }
        }
        Manager._isnstance.isMovingNum.Remove(this);
    }

    Vector3 GetLocalPos()
    {
       return new Vector3(offsetX + posX * space, offsetY + posY * space, 0);
    }

    /// <summary>
    /// 根据数字加载对应图片
    /// </summary>
    /// <returns></returns>
    Sprite LoadSprite()
    {
        return Resources.Load<Sprite>(value.ToString());
    }
}

结语

本文从新建项目开始一步一步带你完成所有步骤,还在等什么呢?三连支持一下吧~

需要素材的小伙伴点击链接下载即可。没有积分的同学,关注我的VX公众号,回复“2048”即可。

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

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:博客之星2021 设计师:Hiro_C 返回首页
评论 54

打赏作者

陈言必行

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值