先看游戏演示效果

 

游戏主界面

 

 

商城界面

 

选择关卡界面

 

选择小鸟界面

 

游戏界面

 

游戏结算界面

 

一、准备工作

 

1. 下载 Unity Hub

Unity Hub 是 Unity 的官方管理工具,用于安装和管理不同版本的 Unity 编辑器。

  1. 访问 Unity 官网:https://unity.com

  2. 点击右上角的 “Get Started” 按钮。

  3. 选择 “Individual” 个人版,然后点击 “Get started”

  4. 在下载页面,选择适合你操作系统的 Unity Hub 版本(Windows 或 macOS)。

  5. 下载完成后,运行安装程序并按照提示完成 Unity Hub 的安装。


2. 安装 Unity 编辑器

  1. 打开 Unity Hub。

  2. 登录你的 Unity 账号。

  3. 点击左侧菜单中的 “Installs”,然后点击右上角的 “Install Editor” 按钮。

  4. 选择你想要安装的 Unity 版本(建议选择最新的 LTS 版本,稳定性更高)。

  5. 在 “Modules” 页面,选择你需要的平台支持模块(如 Windows Build Support、Android Build Support 等)。

  6. 点击 “Done”,然后点击 “Install” 开始安装。

  7. 安装过程可能需要一些时间,具体取决于你选择的模块和网络速度。


3. 创建或打开项目

  1. 安装完成后,点击 Unity Hub 左侧菜单中的 “Projects”

  2. 点击 “New Project” 创建一个新项目,或者点击 “Open” 打开已有项目。

  3. 选择项目模板(如 3D、2D、URP 等),设置项目名称和存储路径,然后点击 “Create”

  4. Unity 编辑器将自动启动并加载你的项目。


4. 激活 Unity 许可证

  1. 首次启动 Unity 时,系统会提示你激活许可证。

  2. 选择 “Personal” 个人免费版(适用于年收入低于 10 万美元的个人或小型团队)。

  3. 登录你的 Unity 账号,完成激活。


5. 安装额外模块

代码编辑环境IDE必须安装

如果你需要支持其他平台(如 iOS、Android、WebGL 等),可以通过 Unity Hub 安装额外的模块:

  1. 在 Unity Hub 中,点击已安装的 Unity 版本右侧的 “Settings” 图标。

  2. 选择 “Add Modules”

  3. 勾选你需要的模块(如 Android SDK、iOS Build Support 等),然后点击 “Install”

 

创建一个2D工程

 

二、UI部分

MapLevelUI

1.字段定义

public GameObject mapListGo;
public GameObject levelListGo;

[Header("地图卡片UI")]
public List<MapCardUI> mapCardUIs;

public GameObject levelListPrefab;

private MapScriptObject[] mapSO;

/// <summary>
/// 初始点击时生成的关卡列表
/// </summary>
private List<GameObject> levelListGos = new List<GameObject>();

变量托拽赋值如下

事先准备一个LevelListPrefab预设体用来动态创建加载每一关的的信息

2.业务逻辑方法

 

显示地图列表

public void ShowMapList(MapScriptObject[] mapSO)
{
    //保存mapSO
    this.mapSO = mapSO;

    mapListGo.SetActive(true);  //打开地图列表UI
    levelListGo.SetActive(false);   //关闭关卡列表UI

    //加载地图列表UI用到的数据
    UpdateMapListUIData(mapSO);
}

需要传一个MapScriptObject对象,该对象存储着每一张地图的本地数据(获得的星星数),将地图列表打开,关卡列表关闭,并加载mapSO中的数据

 

加载地图列表数据

private void UpdateMapListUIData(MapScriptObject[] mapSO)
{
    //展示每一个地图卡片UI用到数据
    for (int i = 0; i < mapSO.Length; i++)
    {
        mapCardUIs[i].Show(mapSO[i].starNumOfMap, this, i);
    }
}

遍历mapSO对象,对持有mapCardUI引用的对象调用其身上的Show方法

 

处理被点击地图的业务逻辑

MapCardUI部分,用来处理每张地图卡片UI的业务逻辑(每张地图卡片上都要挂载这个脚本)

public class MapCardUI : MonoBehaviour
{
    public GameObject lockUI;       //未解锁状态
    public GameObject UnLockUI;     //解锁状态
    public TextMeshProUGUI starCountText;

    private MapLevelUI mapLevelUI;
    /// <summary>
    /// 地图索引号
    /// </summary>
    private int mapIndex;

    public void Show(int starCount, MapLevelUI mapLevelUI, int mapIndex)
    {
        this.mapLevelUI = mapLevelUI;
        this.mapIndex = mapIndex;
        //星星为-1表示未解锁状态
        if (starCount < 0)
        {
            lockUI.SetActive(true);
            UnLockUI.SetActive(false);
        }
        else
        {
            lockUI.SetActive(false);
            UnLockUI.SetActive(true);
            starCountText.text = "×" + starCount.ToString();
        }
    }

    /// <summary>
    /// 处理地图卡片被点击的逻辑
    /// </summary>
    public void OnMapCardButtonClick()
    {
        mapLevelUI.OnMapCardButtonClick(mapIndex);
    }
}

当某张地图上的Button被点击后触发该事件,并且将该张地图的Index传给mapLevelUI

public void OnMapCardButtonClick()
{
    mapLevelUI.OnMapCardButtonClick(mapIndex);
}
public void OnMapCardButtonClick(int mapIndex)
{
    //记录下选择的地图索引
    LevelSelectManager.Instance.SetSelectMapIndex(mapIndex);
    ShowLevelList();
}

 

通过LevelSelectManager将选择的地图索引放入到GameScriptObject数据缓冲区里

public class LevelSelectManager : MonoBehaviour
{
    public static LevelSelectManager Instance {  get; private set; }

    private void Awake()
    {
        Instance = this;
        gameSO.SelectMapIndex = -1;
        gameSO.SelectLevelIndex = -1;
    }
    [Header("游戏数据对象")]
    public GameScriptObject gameSO;
    public MapLevelUI mapLevelUI;
    private void Start()
    {
        //选择关卡的场景加载完后,展示地图列表
        mapLevelUI.ShowMapList(gameSO.MapArray);
    }
    public void SetSelectMapIndex(int index)
    {
        gameSO.SelectMapIndex = index;
    }
    public int[] GetSelectedMapData()
    {
        return gameSO.MapArray[gameSO.SelectMapIndex].starNumOfLevel;
    }
    public void SetSelectedLevelIndex(int index)
    {
        gameSO.SelectLevelIndex = index;
        //加载下一个场景
        SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex + 1);
    }
}

显示关卡列表

private void ShowLevelList()
{
    mapListGo.SetActive(false);
    levelListGo.SetActive(true);

    //获得选中的地图对应的关卡数据
    int[] starNumOfLevel = LevelSelectManager.Instance.
        GetSelectedMapData();

    //刚开始时网格里没有关卡对象需创建并且放到列表里面
    if (levelListGos.Count == 0)
    {
        for (int i = 0; i < starNumOfLevel.Length; i++)
        {
            var levelGo = GameObject.Instantiate(levelListPrefab);
            levelListGos.Add(levelGo);
            //将动态生成的UI放levelListGo里
            levelGo.GetComponent<RectTransform>()
                .SetParent(levelListGo.transform.
                Find("LevelList").transform);
            //显示每个关卡卡片的UI
            levelGo.GetComponent<LevelUI>().ShowLevel(starNumOfLevel[i], i, this);
        }
    }
    //第二次时直接到列表里取就行了
    else
    {
        //遍历该数据动态生成UI
        for (int i = 0; i < starNumOfLevel.Length; i++)
        {
            var levelGo = levelListGos[i];
            levelGo.GetComponent<LevelUI>().ShowLevel(starNumOfLevel[i], i, this);
        }
    }
}

将地图列表关闭,关卡列表打开,通过LevelSelectManager获取到这张地图每一关获得的星星数,通过星星数来动态生成每一个关卡卡片对象

 

处理关卡按钮被点击后的业务逻辑,将levelIndex通过LevelSelectManager写入数据缓冲区

public void OnLevelButtonClick()
{
    mapLevelUI.OnLevelClick(this.levelIndex);
}
public void OnLevelClick(int levelIndex)
{
    LevelSelectManager.Instance.SetSelectedLevelIndex(levelIndex);
}

LevelUI是用来处理每一个关卡卡片槽的UI逻辑

public class LevelUI : MonoBehaviour
{
    [Header("锁定状态")]
    public GameObject lockStateGo;
    [Header("解锁状态")]
    public GameObject unLockStateGo;
    [Header("关卡数")]
    public TextMeshProUGUI levelNumText;
    [Header("显示星星数的图片")]
    public Image starImage;
    [Header("星星的精灵")]
    public Sprite[] starCountSprites;

    private MapLevelUI mapLevelUI;
    private int levelIndex;

    public void ShowLevel(int starCount, int levelIndex, MapLevelUI mapLevelUI)
    {
        this.mapLevelUI = mapLevelUI;
        this.levelIndex = levelIndex;
        if (starCount < 0)
        {
            lockStateGo.SetActive(true);
            unLockStateGo.SetActive(false);
        }
        else
        {
            levelNumText.text = (levelIndex + 1).ToString();
            lockStateGo.SetActive(false);
            unLockStateGo.SetActive(true);
            starImage.sprite = starCountSprites[starCount];
        }
    }

    /// <summary>
    /// 处理关卡被点击事件
    /// </summary>
    public void OnLevelButtonClick()
    {
        mapLevelUI.OnLevelClick(this.levelIndex);
    }
}

 

ShopUI

1.字段定义

private Animator animator;
public GameObject levelViewport;
public GameObject shopButton;
public GameObject gameDescriptionButton;
public GameObject gameDescriptionUIPanel;

public TextMeshProUGUI playerScore;

public string[] birdPrices;
public TextMeshProUGUI[] birdPriceTexts;

public GameObject tipsView;
public TextMeshProUGUI tipsText;

 

2.方法

创建价格字典,用小鸟名字作为key

private Dictionary<string, int> birdPrice = new Dictionary<string, int>()
{
    { "RedBird",  14888 },
    { "YellowBird", 15888 },
    { "BigMouthBird", 16888 },
    { "LayEggBird", 27888 },
    { "BlackBird", 48888 },
};

显示小鸟价格

private void ShowPrice()
{
    playerScore.text = "当前积分:" +
        ShopManager.Instance.GetPlayerScore().ToString();
    for (int i = 0; i < birdPrices.Length; i++)
    {
        birdPriceTexts[i].text = "价格:"
        + ShopManager.Instance.GetBirdPrice(birdPrices[i]).ToString();
    }
}

通过ShopManager获取到小鸟价格

/// <summary>
/// 获取价格的接口
/// </summary>
/// <param name="type">鸟的类型</param>
/// <returns>字典里有没有这种鸟</returns>

public int GetBirdPrice(string type)
{
    try
    {
        return birdPrice[type];
    }
    catch
    {
        return -1;
    }
}

购买逻辑处理

public void OnBuyButtonClick(string birdType)
{
    //将提示面板打开
    tipsView.SetActive(true);
    //如果价格充足
    if (ShopManager.Instance.BuyBird(birdType))
    {
        tipsText.text = "购买成功!";
        ShowPrice();//更新界面
        //将数据写入本地
        DataManager.Instance.WriteData();
    }
    else
    {
        tipsText.text = "积分不足\r\n继续去消灭猪吧!";
    }
}

通过ShopManager检测当前的积分是否足以购买该种类型的鸟,并将反馈结果更新到Tips面板上反馈给玩家,如果购买成功更新界面

 

GameDescriptionUI

1.重置游戏数据解锁所有关卡

/// <summary>
/// 处理重置所有关卡数据按钮按下业务逻辑
/// </summary>
public void OnResetGameDataButtonClick()
{
    //关闭自身面板打开输入密码面板
    gameObject.SetActive(false);
    passwordInput.SetActive(true);
    flag = false;
}

/// <summary>
/// 处理解锁所有关卡按钮按下的业务逻辑
/// </summary>
public void OnUnLockAllLevelButtonClick()
{
    gameObject.SetActive(false);
    passwordInput.SetActive(true);
    flag = true;
}

这两种操作涉及到密码核验,密码核验部分如下

public void OnOKButtonClick()
{
    //核验密码
    if (gameScriptObject.VerifyPassword(inputField.text))
    {
        textMeshProUGUI.text = "操作成功";
    }
    else
    {
        textMeshProUGUI.text = "密码错误";
    }
    Invoke("CloseText", 2);
}

VerifyPassword

 public bool VerifyPassword(string password)
 {
     if (this.password == password)
     {
         //解锁所有关卡
         if (GameDescriptionUI.flag)
         {
             foreach (var mapData in mapArray)
             {
                 mapData.starNumOfMap = 0;
                 for (int i = 0; i< mapData.starNumOfLevel.Length; i++)
                 {
                     mapData.starNumOfLevel[i] = 0;
                 }
             }
         }
         //重置所有关卡
         else
         {
             //积分重置
             playerGotScore = 50000;

             //数量重置
             birdOwnedCount["RedBird"] = 3;
             birdOwnedCount["YellowBird"] = 3;
             birdOwnedCount["BigMouthBird"] = 3;
             birdOwnedCount["LayEggBird"] = 3;
             birdOwnedCount["BlackBird"] = 3;

             foreach (var mapData in mapArray)
             {
                 mapData.starNumOfMap = -1;
                 for (int i = 0; i < mapData.starNumOfLevel.Length; i++)
                 {
                     mapData.starNumOfLevel[i] = -1;
                 }
             }
             mapArray[0].starNumOfMap = 0;
             mapArray[0].starNumOfLevel[0] = 0;
         }
         //将数据写入到磁盘
         DataManager.Instance.WriteData();
         return true;
     }
     return false;
 }

在GameScriptObject中核验密码,并修改游戏数据

 

BirdCountUI

public class BirdCountUI : MonoBehaviour
{
    public string[] birds;
    public TextMeshProUGUI[] textMeshProUGUIs;
    private Dictionary<string, TextMeshProUGUI> birdCountTexts
         = new Dictionary<string, TextMeshProUGUI>();

    private void Start()
    {
        for (int i = 0; i < birds.Length; i++)
        {
            birdCountTexts.Add(birds[i], textMeshProUGUIs[i]);
        }
        UpdateView();
    }

    public void UpdateView()
    {
        foreach (var item in birdCountTexts.Keys)
        {
            birdCountTexts[item].text = "×" + DataManager
                .Instance.GetBirdCount(item);
        }
    }
}

通过DataManager中的GetBirdCount方法获取到的数据加载到BirdCountUI上

public int GetBirdCount(string birdType)
{
    try
    {
        return gameSO.birdOwnedCount[birdType];
    }
    catch
    {
        Debug.LogError("字典里没存放这种类型的鸟");
        return -1;
    }
}

 

SelectBirdUI

1.字段定义

//记录所有类型的小鸟名字
public string[] birdNames;

//显示小鸟数量
public TextMeshProUGUI[] textMeshProUGUIs;

//这里需要考虑鸟的数量为0时将遮罩盖住并且将按钮禁用
public GameObject[] overlays;   //按钮的遮罩
public Button[] buttons;

//存放所有小鸟的精灵
public Sprite nullSprite;
public Sprite[] sprites;
private Dictionary<string, Sprite> spriteDictionary
    = new Dictionary<string, Sprite>();  //存放所有鸟的精灵

//选择框的精灵
public Image first;
public Image second;
public Image third;

//开始按钮的遮罩
public GameObject startButtonOverlays;
public Button startButton;

public GameObject tipsView;

//选择框存放的小鸟
private List<string> birds = new List<string>();

private int currSelectIndex = 0;

字段赋值如下

2.方法

Start

private void Start()
{
    //鸟数量不足时将提示框打开
    if (!DataManager.Instance.IsHasSufficientBirds())
    {
        tipsView.SetActive(true);
    }
    for (int i = 0; i < birdNames.Length; i++)
    {
        spriteDictionary.Add(birdNames[i], sprites[i]);
    }
    UpdateView();
}

UpdateView

 private void UpdateView()
 {
     for (int i = 0; i < birdNames.Length; i++)
     {
         int count = DataManager.Instance.GetBirdCount(birdNames[i]);
         textMeshProUGUIs[i].text = "×" + count.ToString();
         if (count <= 0)
         {
             overlays[i].SetActive(true);
             buttons[i].enabled = false;
         }
         else
         {
             overlays[i].SetActive(false);
             buttons[i].enabled = true;
         }
     }
     //当卡槽选满了就把按钮打开,否则关闭
     if (currSelectIndex == 2)
     {
         startButtonOverlays.SetActive(false);
         startButton.enabled = true;
     }
     else
     {
         startButtonOverlays.SetActive(true);
         startButton.enabled = false;
     }
 }

通过DataManager读取到小鸟数量,并将数据加载到视图,如果小鸟数量为0,则把按钮禁用,并打开灰色的按钮遮罩,并判断选择的小鸟数量是否达到了三,否则打开"开始游戏"的遮罩,禁用其身上的按钮

 

处理选择按钮按的业务逻辑

public void OnSelectButtonClick(string birdType)
{
    if (currSelectIndex == 3)
    {
        return;
    }
    switch (currSelectIndex)
    {
        case 0:
            first.sprite = spriteDictionary[birdType];
            break;
        case 1:
            second.sprite = spriteDictionary[birdType];
            break;
        case 2:
            third.sprite = spriteDictionary[birdType];
            break;
    }
    birds.Add(birdType);
    DataManager.Instance.UseBird(birdType);
    UpdateView();
    currSelectIndex++;
}

如果选择数已经到达了三,不做处理return,否则更新小鸟上场列表的精灵,并将该种类型的小鸟放入容器内

UseBird

public void UseBird(string birdType)
{
    try
    {
        if (gameSO.birdOwnedCount[birdType] > 0)
        {
            gameSO.birdOwnedCount[birdType] -= 1;
        }
        else
        {
            Debug.LogError("该种鸟数量不足");
        }
    }
    catch
    {
        Debug.LogError("不存在这种类型的鸟");
    }
}

 

处理清空按钮的业务逻辑

private void BackBird()
{
    //将列表内的鸟还回去
    foreach (var item in birds)
    {
        DataManager.Instance.AddBirdOwnedCount(item);
        UpdateView();
    }
}
public void OnClearButtonClick()
{
    //索引归零,并将选择框的精灵都设置为默认的
    currSelectIndex = 0;
    first.sprite = nullSprite;
    second.sprite = nullSprite;
    third.sprite = nullSprite;

    BackBird();
    birds.Clear();
}

点击清空按钮后,将小鸟出场的精灵全设置为null精灵,并返还未使用的鸟,小鸟容器清空

 

处理开始游戏按钮的业务逻辑

public void OnStartGameButtonClick()
{
    DataManager.Instance.ClearList();
    //将容器内的鸟都放到数据缓存区里
    foreach (var item in birds)
    {
        DataManager.Instance.AddEnterFieldBird(item);
    }

    //加载下个场景
    SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex + 1);
}

将容器内的鸟加入数据缓冲区,以便在游戏主场景中加载鸟,并加载下个场景

 

GamePauseUI

/// <summary>
/// 重启关卡按钮点击
/// </summary>
public void OnRestartButtonClick()
{
    GameManager.Instance.RestartLevel();
}

/// <summary>
/// 关卡列表的按钮点击
/// </summary>
public void OnLevelListButtonClick()
{
    GameManager.Instance.LevelList();
}

/// <summary>
/// 退出游戏按钮点击
/// </summary>
public void OnExitButtonClick()
{
    GameManager.Instance.ExitGame();
}

点击按钮后触发下面对应的逻辑

 

RestartLevel

public void RestartLevel()
{
    //将没用的鸟还回去
    gameSO.BackBirdToBirdOwnedCount();

    //重新加载选择鸟的场景即可
    SceneSwitch(SceneManager.GetActiveScene().buildIndex - 1);
    GameIsPause(false);
}

重启关卡后,将没用的鸟还回去,并加载选择鸟的场景,并暂停游戏

 

LevelList

public void LevelList()
{
    //将没用的鸟还回去
    gameSO.BackBirdToBirdOwnedCount();

    DataManager.Instance.WriteData();
    //加载上两个场景即可
    SceneSwitch(SceneManager.GetActiveScene().buildIndex - 2);
    GameIsPause(false);
}

选择关卡也是类似

 

GameoverUI

1.字段

private Animator animator;
private int starCount;

/// <summary>
/// 结算界面显示的图片
/// </summary>
public GameObject failImage;

public TextMeshProUGUI giveScoreNumText;
public StarUI star1;
public StarUI star2;
public StarUI star3;

赋值如下

 

2.方法

/// <summary>
/// 显示结算界面
/// </summary>
/// <param name="starCount">显示星星的个数</param>

public void Show(int starCount)
{
    //打开该UI
    gameObject.SetActive(true);
    this.starCount = starCount;
    animator.SetTrigger("IsShow");
}

/// <summary>
/// 显示获得的积分
/// </summary>

public void ShowGiveScore(int givesScoreNum)
{
    giveScoreNumText.text = "获得积分×" + givesScoreNum.ToString();
}

 

动画事件,显示星星

如果获得的星星数为0时显示失败界面,否则进入协程每隔0.5秒显示出一颗星

public void ShowStarEvent()
{
    if (starCount == 0)
    {
        failImage.SetActive(true);
        return;
    }
    //开启协程
    StartCoroutine(ShowStar());
}

 

协程函数,每隔半秒显示一颗星星

IEnumerator ShowStar()
{
    if (starCount >= 1)
    {
        star1.ShowStar();
    }
    //防止协程被Time.timeScale = 0卡住
    //用WaitForSecondsRealtime真实的世界时间模拟等待0.5秒
    yield return new WaitForSecondsRealtime(0.5f);
    if (starCount >= 2)
    {
        star2.ShowStar();
    }
    yield return new WaitForSecondsRealtime(0.5f);
    if (starCount >= 3)
    {
        star3.ShowStar();
    }
}

 

重新开始和选择关卡按钮的逻辑处理,调用GameManager类中的方法

/// <summary>
/// 处理重新开始按钮的逻辑
/// </summary>
public void OnRestartButtonClick()
{
    GameManager.Instance.RestartLevel();
}

/// <summary>
/// 处理关卡列表的按钮逻辑
/// </summary>
public void OnLevelListButtonClick()
{
    GameManager.Instance.LevelList();
}

 

StarUI

private Animator animator;
private RectTransform rotateStarBg;
public float rotateSpeed;
private bool isRotate = false;

private void Start()
{
    animator = GetComponent<Animator>();
    rotateStarBg = transform.Find("StarBg").GetComponent<RectTransform>();
    
    //给个随机旋转,避免三颗星的旋转步调一样
    rotateStarBg.Rotate(new Vector3(0, 0, Random.Range(0, 360)));
}
private void Update()
{
    if (isRotate)
    {
        rotateStarBg.Rotate(new Vector3(0, 0, rotateSpeed)
        * Time.unscaledDeltaTime);
    }
}

用Animator组件实现星星的变大再变小效果,并且将星星背景按照给定的角速度旋转,初始时需要给个随机角度,这样就避免了所有星星旋转步调一致,过于单调

 

三、Manager类

 

单例模式(Singleton Pattern) 是一种常用的软件设计模式,属于创建型模式的一种。它的核心目的是确保一个类在整个应用程序中只有一个实例,并提供一个全局访问点来获取该实例。

所有的Manager类都会设置为单例模式

 

AudioManager

设置为单例模式

public static AudioManager Instance {  get; private set; }

private void Awake()
{
    Instance = this;
}

定义AudioClip字段

public AudioClip birdCollision; 
public AudioClip birdSelect;    //选择鸟时的音效
public AudioClip birdFlying;
public AudioClip[] pigCollisions;
public AudioClip woodCollision;
public AudioClip woodDestroyed;

 

提供对外播放接口

public void PlayBirdCollision(Vector3 position)
{
    AudioSource.PlayClipAtPoint(birdCollision, position, 1);
}
public void PlayBirdSelect(Vector3 position)
{
    AudioSource.PlayClipAtPoint(birdSelect, position, 1);
}
public void PlayBirdFlying(Vector3 position)
{
    AudioSource.PlayClipAtPoint(birdFlying, position, 1);
}
public void PlayPigCollisions(Vector3 position)
{
    int index = Random.Range(0, pigCollisions.Length);
    AudioSource.PlayClipAtPoint(pigCollisions[index], position, 1);
}
public void PlayWoodCollision(Vector3 position)
{
    AudioSource.PlayClipAtPoint(woodCollision, position, 1);
}
public void PlayWoodDestroyed(Vector3 position)
{
    AudioSource.PlayClipAtPoint(woodDestroyed, position, 0.5f);
}

AudioSource.PlayClipAtPoint()用来播放音效,第一个重载参数是音效片段,第二个是播放位置,第三个是音效大小(默认为1,取值范围为0-1f)

 

DataManager

 

public GameScriptObject gameSO;

对数据缓冲区持有引用

 

private void Awake()
{
    Instance = this;
    DontDestroyOnLoad(gameObject);
}

设置成在加载场景时不销毁该对象

 

方法

 

读取本地数据

public void ReadData()
{
    //读取分数
    gameSO.PlayerGotScore = PlayerPrefs.GetInt("PlayerGotScore", 50000);

    //读取地图数据
    for (int i = 0; i < gameSO.MapArray.Length; i++)
    {
        gameSO.mapArray[i].starNumOfMap = PlayerPrefs
            .GetInt($"starNumOfMap{i + 1}", -1);
        for (int j = 0; j < gameSO.MapArray[i].starNumOfLevel.Length; j++)
        {
            gameSO.mapArray[i].starNumOfLevel[j] = PlayerPrefs.GetInt($"starNumOfLevel{i + 1}_{j + 1}", -1);
        }
    }
    //如果一开始没写入数据就去读默认都给-1,再把第一个地图第一关开启
    if (gameSO.mapArray[0].starNumOfMap == -1)
    {
        gameSO.mapArray[0].starNumOfMap = 0;
        gameSO.mapArray[0].starNumOfLevel[0] = 0;
    }

    //读取每只鸟的数量
    foreach (string item in gameSO.birdOwnedCount.Keys.ToList())
    {
        gameSO.birdOwnedCount[item] = PlayerPrefs
            .GetInt(item, 3);
    }
}

这里用的是Unity的PlayerPrefs将数据本地化,读取地图的数据,如果初始没创建key可以设置默认值

PlayerPrefs 是 Unity 提供的一个用于存储和读取简单数据的工具。它通常用于保存玩家的偏好设置、游戏进度或其他小型数据。PlayerPrefs 将数据存储在本地设备上,适合存储少量的键值对(Key-Value)数据。

PlayerPrefs 的特点

  1. 简单易用:提供简单的 API 来存储和读取数据。

  2. 跨平台:数据存储在本地,支持跨平台(Windows、macOS、Android、iOS 等)。

  3. 数据类型支持:支持 intfloatstring 三种数据类型。

  4. 持久化存储:数据在游戏关闭后仍然保留。

 

写入本地数据

public void WriteData()
{
    for (int i = 0; i < gameSO.MapArray.Length; i++)
    {
        PlayerPrefs.SetInt($"starNumOfMap{i + 1}", 
            gameSO.mapArray[i].starNumOfMap);
        for (int j = 0; j < gameSO.MapArray[i].starNumOfLevel.Length; j++)
        {
            PlayerPrefs.SetInt($"starNumOfLevel{i + 1}_{j + 1}",
            gameSO.mapArray[i].starNumOfLevel[j]);
        }
    }
    PlayerPrefs.SetInt("PlayerGotScore", gameSO.PlayerGotScore);

    //写入每只鸟的数量
    foreach (var item in gameSO.birdOwnedCount.Keys)
    {
        PlayerPrefs.SetInt(item, gameSO.birdOwnedCount[item]);
    }

    PlayerPrefs.Save(); //写入到硬盘中
}

 

对外接口

 

分数的读写

public int GetPlayerScore()
{
    return gameSO.PlayerGotScore;
}

public void AddScore(int score)
{
    gameSO.PlayerGotScore += score;
}

 

购买鸟

public void BuyBird(int score)
{
    gameSO.PlayerGotScore -= score;
}

添加已拥有鸟的数量

重载1:

public void AddBirdOwnedCount(string birdType)
{
    try
    {
        gameSO.birdOwnedCount[birdType] += 1;
    }
    catch
    {
        Debug.LogError("不存在这种类型的鸟");
    }
}

重载2:

public void AddBirdOwnedCount(string birdType, int count)
{
    if (count < 0) return;
    try
    {
        gameSO.birdOwnedCount[birdType] += count;
    }
    catch
    {
        Debug.LogError("不存在这种类型的鸟");
    }
}

使用鸟

public void UseBird(string birdType)
{
    try
    {
        if (gameSO.birdOwnedCount[birdType] > 0)
        {
            gameSO.birdOwnedCount[birdType] -= 1;
        }
        else
        {
            Debug.LogError("该种鸟数量不足");
        }
    }
    catch
    {
        Debug.LogError("不存在这种类型的鸟");
    }
}

获取鸟数量

public int GetBirdCount(string birdType)
{
    try
    {
        return gameSO.birdOwnedCount[birdType];
    }
    catch
    {
        Debug.LogError("字典里没存放这种类型的鸟");
        return -1;
    }
}

/// <summary>
/// 判断是否拥有充足的鸟来上场
/// </summary>
/// <returns></returns>

public bool IsHasSufficientBirds()
{
    int allBirdCounts = 0;
    foreach (var item in gameSO.birdOwnedCount.Values)
    {
        allBirdCounts += item;
    }
    return allBirdCounts >= 3;
}

 

/// <summary>
/// 向GameSO放缓存数据,以便切换场景时加载鸟
/// </summary>
/// <param name="birdName"></param>

public void AddEnterFieldBird(string birdName)
{
    gameSO.AddEnterFieldBird(birdName);
}

/// <summary>
/// 清空列表
/// </summary>

  public void ClearList()
  {
      gameSO.EnterFieldBirds.Clear();
  }

 

GameManager

 

1.字段

/// <summary>
/// 上场的小鸟集合
/// </summary>
private List<Bird> birdList = new List<Bird>();

/// <summary>
/// 鸟的生成点
/// </summary>
public Transform[] enterFieldPoint;
/// <summary>
/// 当前上场的索引
/// </summary>
private int index = 0;

/// <summary>
/// 小猪个数
/// </summary>
private int pigCount;
private int pigDeadCount;

private FollowTarget cameraFollowTarget;

public GameoverUI gameoverUI;

/// <summary>
/// 持有本地数据对象的引用
/// </summary>
public GameScriptObject gameSO;

 

2.方法

 

Awake

private void Awake()
{
    Instance = this;
    LoadSelectedLevel();

    
    //依据缓存区存放的鸟,实例化鸟
    for (int i = 0; i < gameSO.EnterFieldBirds.Count; i++)
    {
        GameObject go = Resources.Load<GameObject>("Birds/" +
            gameSO.EnterFieldBirds[i]);
        var bird = GameObject.Instantiate(go,
            enterFieldPoint[i].position, Quaternion.identity);
        birdList.Add(bird.GetComponent<Bird>());
    }
}

先加载选择的地图,关卡,来决定后面动态生成游戏地图,再实例化上场的小鸟

 

Start

private void Start()
{
    pigDeadCount = 0;

    pigCount = FindObjectsByType<Pig>(FindObjectsSortMode.None).Length;
    cameraFollowTarget = Camera.main.GetComponent<FollowTarget>();

    LoadNextBird();
}

查找场景中的猪对象,再加载需要上场的鸟

 

LoadNextBird

public void LoadNextBird()
{
    //小鸟用完了, 游戏结束
    if (index >= birdList.Count)
    {
        Invoke("WaitToGameover", 2);
    }
    else 
    {
        //让小鸟从弹弓中心点上场
        birdList[index].GoStage(Slingshot.Instance.shootPoint);
        cameraFollowTarget.SetTarget(birdList[index].transform);
    }
    index++;
}

依据鸟的数量来判断游戏有没有结束,没结束的话将小鸟放到弹弓中心点等待发射

 

OnPigDead

小猪死亡后调用

public void OnPigDead()
{
    pigDeadCount++;
    //所有的小猪被消灭掉
    if (pigDeadCount >= pigCount)
    {
        Invoke("Gameover", 2f);
    }
}

如果小猪全死亡,异步调用游戏结束方法

 

Gameover

private void Gameover()
{
    GameManager.Instance.GameIsPause(true);
    int starCount = 0;
    
    float pigDeadPercent = pigDeadCount * 1.0f / pigCount;
    if (pigDeadPercent > 0.33f)
    {
        starCount = 1;
    }
    if (pigDeadPercent > 0.66)
    {
        starCount = 2;
    }
    if (pigDeadPercent > 0.99)
    {
        starCount = 3;
    }
    gameoverUI.Show(starCount);
    gameSO.UpdateLevel(starCount);

    //计算获得的积分渲染到视图上
    int giveScore = gameSO.GiveScoreComputed(starCount);
    DataManager.Instance.AddScore(giveScore);
    gameoverUI.ShowGiveScore(giveScore);
}

依据消灭小猪数量占关卡小猪总数量的比例来给予星星,并且计算关卡得分渲染在视图上,调用DataManager中的AddScore方法更新分数

 

GameIsPause

public void GameIsPause(bool isPause)
{
    Time.timeScale = isPause ? 0 : 1;
}

游戏暂停设置时间缩放即可

 

RestartLevel

/// <summary>
/// 处理重新开始该关按钮的逻辑
/// </summary>
public void RestartLevel()
{
    //将没用的鸟还回去
    gameSO.BackBirdToBirdOwnedCount();

    //重新加载选择鸟的场景即可
    SceneSwitch(SceneManager.GetActiveScene().buildIndex - 1);
    GameIsPause(false);
}

 

LevelList

/// <summary>
/// 处理关卡列表的按钮逻辑
/// </summary>
public void LevelList()
{
    //将没用的鸟还回去
    gameSO.BackBirdToBirdOwnedCount();

    DataManager.Instance.WriteData();
    //加载上两个场景即可
    SceneSwitch(SceneManager.GetActiveScene().buildIndex - 2);
    GameIsPause(false);
}

 

 

Logo

这里是一个专注于游戏开发的社区,我们致力于为广大游戏爱好者提供一个良好的学习和交流平台。我们的专区包含了各大流行引擎的技术博文,涵盖了从入门到进阶的各个阶段,无论你是初学者还是资深开发者,都能在这里找到适合自己的内容。除此之外,我们还会不定期举办游戏开发相关的活动,让大家更好地交流互动。加入我们,一起探索游戏开发的奥秘吧!

更多推荐