用Unity复刻童年经典游戏—愤怒的小鸟
Header("地图卡片UI")]/// 初始点击时生成的关卡列表变量托拽赋值如下事先准备一个LevelListPrefab预设体用来动态创建加载每一关的的信息//记录所有类型的小鸟名字//显示小鸟数量//这里需要考虑鸟的数量为0时将遮罩盖住并且将按钮禁用//按钮的遮罩//存放所有小鸟的精灵//存放所有鸟的精灵//选择框的精灵//开始按钮的遮罩//选择框存放的小鸟字段赋值如下。
先看游戏演示效果
游戏主界面

商城界面

选择关卡界面

选择小鸟界面

游戏界面

游戏结算界面

一、准备工作
1. 下载 Unity Hub
Unity Hub 是 Unity 的官方管理工具,用于安装和管理不同版本的 Unity 编辑器。
-
访问 Unity 官网:https://unity.com。
-
点击右上角的 “Get Started” 按钮。
-
选择 “Individual” 个人版,然后点击 “Get started”。
-
在下载页面,选择适合你操作系统的 Unity Hub 版本(Windows 或 macOS)。
-
下载完成后,运行安装程序并按照提示完成 Unity Hub 的安装。
2. 安装 Unity 编辑器
-
打开 Unity Hub。
-
登录你的 Unity 账号。
-
点击左侧菜单中的 “Installs”,然后点击右上角的 “Install Editor” 按钮。
-
选择你想要安装的 Unity 版本(建议选择最新的 LTS 版本,稳定性更高)。
-
在 “Modules” 页面,选择你需要的平台支持模块(如 Windows Build Support、Android Build Support 等)。
-
点击 “Done”,然后点击 “Install” 开始安装。
-
安装过程可能需要一些时间,具体取决于你选择的模块和网络速度。
3. 创建或打开项目
-
安装完成后,点击 Unity Hub 左侧菜单中的 “Projects”。
-
点击 “New Project” 创建一个新项目,或者点击 “Open” 打开已有项目。
-
选择项目模板(如 3D、2D、URP 等),设置项目名称和存储路径,然后点击 “Create”。
-
Unity 编辑器将自动启动并加载你的项目。
4. 激活 Unity 许可证
-
首次启动 Unity 时,系统会提示你激活许可证。
-
选择 “Personal” 个人免费版(适用于年收入低于 10 万美元的个人或小型团队)。
-
登录你的 Unity 账号,完成激活。
5. 安装额外模块
代码编辑环境IDE必须安装

如果你需要支持其他平台(如 iOS、Android、WebGL 等),可以通过 Unity Hub 安装额外的模块:
-
在 Unity Hub 中,点击已安装的 Unity 版本右侧的 “Settings” 图标。
-
选择 “Add Modules”。
-
勾选你需要的模块(如 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 的特点
-
简单易用:提供简单的 API 来存储和读取数据。
-
跨平台:数据存储在本地,支持跨平台(Windows、macOS、Android、iOS 等)。
-
数据类型支持:支持
int、float、string三种数据类型。 -
持久化存储:数据在游戏关闭后仍然保留。
写入本地数据
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);
}
这里是一个专注于游戏开发的社区,我们致力于为广大游戏爱好者提供一个良好的学习和交流平台。我们的专区包含了各大流行引擎的技术博文,涵盖了从入门到进阶的各个阶段,无论你是初学者还是资深开发者,都能在这里找到适合自己的内容。除此之外,我们还会不定期举办游戏开发相关的活动,让大家更好地交流互动。加入我们,一起探索游戏开发的奥秘吧!
更多推荐

所有评论(0)