尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Unity多单词顺序通关游戏开发:数据管理与进度逻辑详解

Unity多单词顺序通关游戏开发:数据管理与进度逻辑详解 1. 项目概述与核心价值最近在整理自己的游戏开发笔记翻到了一个几年前做的“字母拼词”小游戏项目。这个项目虽然体量不大但麻雀虽小五脏俱全它完整地覆盖了从UI设计、核心游戏逻辑、数据管理到最终打包发布的整个流程。更重要的是它实现了一个在单词学习类或休闲益智类游戏中非常经典的需求多单词顺序通关进度逻辑。简单来说玩家不是只拼一个单词就结束而是需要按照预设的顺序一个接一个地拼出多个单词每拼对一个才能解锁下一个最终完成一个关卡或一个章节。这种设计极大地增强了游戏的粘性和目标感。很多刚接触Unity的朋友做完打砖块、跑酷这类经典Demo后想尝试点更有逻辑性的小项目这个“字母拼词”游戏就是一个绝佳的练手选择。它不涉及复杂的物理引擎或3D模型核心在于对字符串的处理、UI的交互反馈以及游戏状态的管理。网上能找到的很多拼词游戏教程往往只做到单个单词的拼写和判定对于“如何管理一系列单词”、“如何保存和恢复游戏进度”这些实际开发中必然会遇到的问题讲得不够透彻。今天我就把这个项目的核心思路、关键代码实现尤其是那个“多单词顺序通关”的逻辑掰开揉碎了跟大家分享一下并附上完整的源码和效果GIF。2. 游戏整体设计与思路拆解2.1 核心玩法与功能定义在动手写代码之前我们先明确这个游戏要做什么。核心玩法非常简单屏幕上方显示一个单词例如“APPLE”但它的字母顺序被打乱了例如“PEPLA”并随机散落在游戏区域中。玩家需要用鼠标或手指拖动这些字母将它们按正确的顺序排列到下方的答案槽位中。当排列的单词与目标单词匹配时即判定成功。我们要实现的多单词顺序通关功能意味着游戏内会有一个单词列表比如[“CAT”, “DOG”, “BIRD”]。玩家必须首先正确拼出“CAT”系统记录该单词已完成然后才解锁并显示第二个单词“DOG”供玩家挑战依此类推。这听起来简单但涉及到几个关键的设计点数据层如何存储关卡数据单词列表、关卡ID等状态层如何记录当前正在挑战第几个单词如何记录每个单词的完成状态表现层UI如何根据当前状态动态更新例如显示“关卡1/3”高亮已完成的单词持久化玩家退出游戏再进来如何恢复到之前的进度2.2 技术方案选型与架构对于这样一个小型项目过度设计架构是没必要的。我采用的是一个清晰简单的单场景MVC模型-视图-控制器变体思路所有逻辑在一个场景内完成。模型 (Model)负责管理游戏的核心数据。这里我创建了两个关键的C#脚本。WordData这是一个可序列化的类用于定义单个关卡的数据结构。它包含关卡ID、该关卡包含的单词列表、以及每个单词是否已完成的布尔值数组。GameProgressManager这是一个单例模式Singleton的类负责管理整个游戏的进度。它加载和保存WordData记录当前游玩的关卡ID和当前关卡内正在挑战的单词索引。它不关心UI怎么显示只负责数据的增删改查和持久化使用PlayerPrefs或JSON文件。视图 (View)即Unity的UI系统。包括目标单词显示文本。字母卡片Prefab预制体。答案槽位一组UI占位符。进度显示UI如“单词 1/5”。单词列表预览可能用图标或文字显示哪些单词已完成、当前是哪个、哪些未解锁。控制器 (Controller)负责处理用户输入和协调模型与视图。主要脚本是GameManager。它监听玩家的拖拽操作调用GameProgressManager获取当前目标单词验证拼写结果并在拼写成功后通知GameProgressManager更新进度同时触发UI更新和播放音效/动画。选择PlayerPrefs进行数据存储对于小体量、结构简单的进度数据来说是快速且够用的。如果单词量很大或者有关卡地图等复杂结构我会推荐使用JSON或ScriptableObject进行存储但本项目以阐明逻辑为主PlayerPrefs更直观。3. 核心模块实现详解3.1 数据模型定义关卡与进度首先我们来构建游戏的“记忆中枢”——数据模型。WordData 类这个类就像一张关卡设计表。为了让它在Unity编辑器中可配置且能被序列化存储我们将其标记为[System.Serializable]。[System.Serializable] public class WordData { public int levelID; // 关卡编号 public Liststring wordList; // 该关卡的所有单词 public Listbool completionStatus; // 对应每个单词的完成状态与wordList索引对应 // 构造函数初始化时完成状态默认为false public WordData(int id, Liststring words) { levelID id; wordList new Liststring(words); completionStatus new Listbool(); foreach (var word in words) { completionStatus.Add(false); } } // 获取当前关卡第一个未完成的单词索引如果没有则返回-1 public int GetCurrentWordIndex() { for (int i 0; i completionStatus.Count; i) { if (!completionStatus[i]) return i; } return -1; // 全部已完成 } // 标记某个单词为已完成 public void CompleteWord(int index) { if (index 0 index completionStatus.Count) { completionStatus[index] true; } } }关键点解析completionStatus列表与wordList一一对应。这是实现顺序通关的核心。GetCurrentWordIndex方法确保了游戏逻辑总是指向下一个待完成的单词。GameProgressManager 类这个单例管理器是游戏进度的总指挥。public class GameProgressManager : MonoBehaviour { public static GameProgressManager Instance; private const string PROGRESS_KEY “PlayerGameProgress”; public WordData currentLevelData; public int currentLevelID 1; // 假设我们从第一关开始 void Awake() { if (Instance null) { Instance this; DontDestroyOnLoad(gameObject); // 跨场景保持 LoadProgress(); } else { Destroy(gameObject); } } void LoadProgress() { // 从PlayerPrefs加载进度 string savedJson PlayerPrefs.GetString(PROGRESS_KEY, “”); if (!string.IsNullOrEmpty(savedJson)) { // 这里简化处理实际可能存储多个关卡数据。本例假设只存当前关卡。 currentLevelData JsonUtility.FromJsonWordData(savedJson); currentLevelID currentLevelData.levelID; } else { // 首次游戏初始化第一关数据 InitializeNewLevel(currentLevelID); } } void InitializeNewLevel(int levelId) { // 这里可以从配置文件、ScriptableObject或硬编码读取单词列表 Liststring wordsForLevel new Liststring() { “UNITY”, “GAME”, “CODE”, “LEARN”, “FUN” }; currentLevelData new WordData(levelId, wordsForLevel); SaveProgress(); } public void SaveProgress() { string jsonData JsonUtility.ToJson(currentLevelData); PlayerPrefs.SetString(PROGRESS_KEY, jsonData); PlayerPrefs.Save(); } // 获取当前需要挑战的单词 public string GetCurrentTargetWord() { int currentIndex currentLevelData.GetCurrentWordIndex(); if (currentIndex ! -1) return currentLevelData.wordList[currentIndex]; return “”; // 所有单词已完成 } // 提交答案如果正确则更新进度 public bool SubmitAnswer(string playerAnswer) { string currentTarget GetCurrentTargetWord(); if (playerAnswer.Equals(currentTarget, StringComparison.OrdinalIgnoreCase)) { int indexToComplete currentLevelData.GetCurrentWordIndex(); currentLevelData.CompleteWord(indexToComplete); SaveProgress(); return true; } return false; } // 检查当前关卡是否全部完成 public bool IsLevelComplete() { return currentLevelData.GetCurrentWordIndex() -1; } }注意为了代码清晰本例将WordData直接序列化为JSON存储。在实际项目中如果有关卡选择界面GameProgressManager可能需要管理一个ListWordData并为每个关卡单独保存进度。使用PlayerPrefs存储复杂结构时务必注意键值对的大小限制和可读性对于更复杂的数据强烈建议使用JsonUtility或Newtonsoft.Json配合文件读写。3.2 游戏场景与UI搭建视图层是玩家直接交互的部分良好的UI布局能提升游戏体验。场景布局顶部区域放置一个TextMeshPro (TMP) 文本用于显示当前目标单词例如“拼出这个单词_ _ _ _ _”以及进度文本如“单词 2/5”。中部区域作为“字母池”。这里我们将使用Grid Layout Group组件来动态排列被打乱顺序的字母卡片。底部区域作为“答案区”。放置一排空的Image或Panel作为答案槽位数量等于当前目标单词的字母数。可以使用Horizontal Layout Group让它们等间距排列。侧边或底部可以添加一个单词列表预览栏用图标如锁、对勾、当前高亮直观显示所有单词的完成状态。字母卡片预制体 创建一个Button或Image作为字母卡片的预制体LetterCardPrefab。它上面挂载一个TMP文本组件用于显示字母并挂载我们接下来要写的DraggableLetter脚本。为其添加简单的悬停和点击效果改变颜色或缩放可以增加手感。3.3 核心交互逻辑实现这是游戏跳动的心脏负责处理拖拽和拼写判定。DraggableLetter 脚本这个脚本让字母卡片可以被拖动。public class DraggableLetter : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler { public char letter; // 这个卡片代表的字母 public TextMeshProUGUI letterText; [HideInInspector] public Transform originalParent; // 记录原始父物体用于放回 private CanvasGroup canvasGroup; // 用于在拖动时暂时屏蔽射线检测 private RectTransform rectTransform; void Start() { rectTransform GetComponentRectTransform(); canvasGroup GetComponentCanvasGroup(); if (canvasGroup null) canvasGroup gameObject.AddComponentCanvasGroup(); letterText.text letter.ToString(); originalParent transform.parent; } public void OnBeginDrag(PointerEventData eventData) { canvasGroup.alpha 0.6f; // 拖动时半透明 canvasGroup.blocksRaycasts false; // 防止阻挡射线 // 将自己设为Canvas的子物体确保在最上层拖动 transform.SetParent(transform.root); } public void OnDrag(PointerEventData eventData) { // 将屏幕坐标转换为RectTransform的局部坐标 RectTransformUtility.ScreenPointToWorldPointInRectangle(rectTransform, eventData.position, eventData.pressEventCamera, out Vector3 worldPoint); rectTransform.position worldPoint; } public void OnEndDrag(PointerEventData eventData) { canvasGroup.alpha 1.0f; canvasGroup.blocksRaycasts true; // 检查是否被拖到了答案槽位上 GameObject dropTarget eventData.pointerCurrentRaycast.gameObject; AnswerSlot slot dropTarget?.GetComponentAnswerSlot(); if (slot ! null slot.IsEmpty()) { // 成功放入槽位 transform.SetParent(slot.transform); rectTransform.anchoredPosition Vector2.zero; // 居中 slot.PlaceLetter(this); } else { // 放回原处 ReturnToPool(); } } public void ReturnToPool() { transform.SetParent(originalParent); transform.SetSiblingIndex(int.MaxValue); // 放回字母池末尾 // 可以添加一个简单的Tween动画让归位更平滑 } }AnswerSlot 脚本答案槽位负责接收字母卡片。public class AnswerSlot : MonoBehaviour { public int slotIndex; // 槽位序号从0开始 private DraggableLetter placedLetter null; public bool IsEmpty() { return placedLetter null; } public void PlaceLetter(DraggableLetter letter) { if (placedLetter ! null) { // 如果槽位已有字母先将其挤走返回字母池 placedLetter.ReturnToPool(); } placedLetter letter; // 通知GameManager更新当前拼写的单词 GameManager.Instance?.UpdatePlayerAnswer(slotIndex, letter.letter); } public void ClearSlot() { placedLetter null; } // 当卡片被从槽位中拖走时调用 public void OnLetterRemoved() { placedLetter null; GameManager.Instance?.UpdatePlayerAnswer(slotIndex, ‘\0’); // 用空字符表示移除 } }关键点AnswerSlot与GameManager通信实时更新玩家当前的拼写答案。这里我设计了一个UpdatePlayerAnswer方法它根据槽位索引和字母来更新一个内部的字符数组这个数组就代表了玩家当前拼出的单词。3.4 游戏流程控制与状态管理GameManager 脚本这是整个游戏逻辑的协调者采用单例模式便于访问。public class GameManager : MonoBehaviour { public static GameManager Instance; // UI引用 public TextMeshProUGUI targetWordText; public TextMeshProUGUI progressText; public Transform letterPoolParent; public Transform answerSlotsParent; public GameObject letterCardPrefab; private char[] currentPlayerAnswer; // 玩家当前拼写的单词 private ListAnswerSlot answerSlots new ListAnswerSlot(); void Awake() { if (Instance null) Instance this; else Destroy(gameObject); InitializeGame(); } void InitializeGame() { // 1. 从GameProgressManager获取当前目标单词 string targetWord GameProgressManager.Instance.GetCurrentTargetWord(); if (string.IsNullOrEmpty(targetWord)) { Debug.Log(“关卡已完成”); // 触发关卡完成UI return; } // 2. 更新UI显示 targetWordText.text $目标: {targetWord}; UpdateProgressUI(); // 3. 初始化答案槽位 InitializeAnswerSlots(targetWord.Length); // 4. 生成并打乱字母卡片 GenerateAndShuffleLetters(targetWord); } void InitializeAnswerSlots(int wordLength) { // 清空旧槽位如果有 foreach (Transform child in answerSlotsParent) Destroy(child.gameObject); answerSlots.Clear(); // 动态生成答案槽位 for (int i 0; i wordLength; i) { GameObject slotObj new GameObject($AnswerSlot_{i}); slotObj.transform.SetParent(answerSlotsParent); AnswerSlot slot slotObj.AddComponentAnswerSlot(); slot.slotIndex i; // 可以添加一个Image组件作为槽位背景 answerSlots.Add(slot); } currentPlayerAnswer new char[wordLength]; } void GenerateAndShuffleLetters(string word) { // 清空字母池 foreach (Transform child in letterPoolParent) Destroy(child.gameObject); // 创建字母卡片 Listchar letters new Listchar(); foreach (char c in word) { letters.Add(c); } // 打乱顺序Fisher-Yates洗牌算法 for (int i letters.Count - 1; i 0; i--) { int j Random.Range(0, i 1); char temp letters[i]; letters[i] letters[j]; letters[j] temp; } // 实例化卡片 foreach (char c in letters) { GameObject cardObj Instantiate(letterCardPrefab, letterPoolParent); DraggableLetter draggable cardObj.GetComponentDraggableLetter(); draggable.letter c; } } // 由AnswerSlot调用更新玩家答案 public void UpdatePlayerAnswer(int slotIndex, char letter) { if (slotIndex 0 slotIndex currentPlayerAnswer.Length) { currentPlayerAnswer[slotIndex] letter; CheckAnswer(); // 每次更新都检查一次 } } void CheckAnswer() { // 构建玩家拼出的字符串忽略空槽位‘\0’ StringBuilder sb new StringBuilder(); foreach (char c in currentPlayerAnswer) { if (c ! ‘\0’) sb.Append(c); } string playerWord sb.ToString(); // 只有当长度与目标单词一致时才进行判定 string targetWord GameProgressManager.Instance.GetCurrentTargetWord(); if (playerWord.Length targetWord.Length) { if (playerWord.Equals(targetWord, StringComparison.OrdinalIgnoreCase)) { OnWordCorrect(); } } } void OnWordCorrect() { Debug.Log(“拼写正确”); // 1. 播放正确音效和动画例如字母卡片跳动、粒子效果 // 2. 通知进度管理器 bool success GameProgressManager.Instance.SubmitAnswer(new string(currentPlayerAnswer).Replace(“\0”, “”)); if (success) { // 3. 短暂延迟后加载下一个单词或进入关卡完成状态 StartCoroutine(LoadNextWordWithDelay(1.5f)); } } IEnumerator LoadNextWordWithDelay(float delay) { yield return new WaitForSeconds(delay); // 清空当前答案和槽位 ClearCurrentAnswer(); // 重新初始化游戏会获取下一个单词 InitializeGame(); } void ClearCurrentAnswer() { foreach (var slot in answerSlots) { slot.ClearSlot(); } // 将所有字母卡片放回池中 foreach (DraggableLetter letter in letterPoolParent.GetComponentsInChildrenDraggableLetter()) { letter.ReturnToPool(); } Array.Clear(currentPlayerAnswer, 0, currentPlayerAnswer.Length); } void UpdateProgressUI() { WordData data GameProgressManager.Instance.currentLevelData; int currentIndex data.GetCurrentWordIndex(); int totalWords data.wordList.Count; int completedWords data.completionStatus.Count(status status); // 显示如“进度: 2/5 (已拼出: CAT, DOG)” progressText.text $进度: {completedWords 1}/{totalWords}”; // 1是因为索引从0开始 } }流程梳理GameManager在InitializeGame中串联了所有步骤获取单词 - 更新UI - 创建槽位 - 生成字母。CheckAnswer在玩家每次放置或移除字母时被触发实现了实时验证。当拼写正确时通过协程实现一个流畅的过渡然后重置场景并加载下一个单词。4. 多单词进度逻辑的强化与优化基础的顺序通关已经实现但一个健壮的系统还需要考虑更多边界情况和用户体验。4.1 进度可视化与反馈玩家需要清晰地知道自己的位置。我通常在游戏界面一侧添加一个“单词进度条”。创建进度条UI使用一个垂直或水平的Layout Group为关卡中的每个单词实例化一个小图标例如一个带边框的Image。图标状态管理为每个图标绑定一个脚本根据WordData.completionStatus和当前索引来更新其状态。已完成显示绿色对勾图标高亮。当前进行中显示特殊边框或呼吸动画效果。未解锁显示为灰色或带锁图标。动态更新在GameManager的UpdateProgressUI方法中不仅更新文本也遍历这个进度条更新每个图标的状态。4.2 容错与重置功能玩家可能会卡住需要提供重试当前单词或重置整个关卡的功能。重试当前单词在UI上添加一个“重试”按钮。点击后调用GameManager中的一个ResetCurrentWord方法。这个方法会public void ResetCurrentWord() { ClearCurrentAnswer(); // 清空答案槽位 // 重新打乱字母池中的字母卡片可以重新生成或者只是重新排列位置 ShuffleExistingLetters(); } void ShuffleExistingLetters() { ListDraggableLetter letters new ListDraggableLetter(letterPoolParent.GetComponentsInChildrenDraggableLetter()); // 随机打乱这些字母卡片在字母池中的顺序改变SiblingIndex for (int i 0; i letters.Count; i) { letters[i].transform.SetSiblingIndex(Random.Range(0, letters.Count)); } }重置整个关卡添加一个“重置关卡”按钮通常放在设置菜单里。点击后调用GameProgressManager.Instance.ResetLevel(currentLevelID)。这个方法需要清除该关卡的所有完成状态并重新保存。重要提示重置功能要谨慎最好有二次确认弹窗。同时确保重置后GameManager能正确刷新到该关卡的第一个单词。4.3 数据持久化的进阶思考PlayerPrefs虽然方便但在以下情况可能力不从心数据量较大存储多个关卡的复杂进度。需要云端备份玩家换设备后想继承进度。防止本地篡改PlayerPrefs存储在本地注册表或plist中容易被修改。进阶方案建议JSON文件存储将ListWordData序列化为一个JSON文件存储在Application.persistentDataPath下。这样结构更清晰也便于读写。string filePath Path.Combine(Application.persistentDataPath, “gameprogress.json”); string json JsonUtility.ToJson(allLevelsData); File.WriteAllText(filePath, json);ScriptableObject 资产将关卡数据单词列表本身设计成ScriptableObject在编辑器中配置。进度状态单独存储。这样数据和逻辑分离更专业。简易加密如果担心数据被轻易修改可以对存储的JSON字符串进行简单的XOR或AES加密后再存入PlayerPrefs或文件。5. 常见问题与调试技巧实录在实际开发这个项目的过程中我踩过不少坑也总结了一些调试技巧。5.1 拖拽交互的“坑”问题1字母拖拽时“卡顿”或无法拖动到某些区域。原因通常是Canvas的渲染模式或Raycast Target设置问题。如果场景中有多个Canvas拖拽物体的Canvas可能不是最上层那个。另外答案槽位或其他UI元素的Image组件如果勾选了Raycast Target可能会意外拦截拖拽事件。解决确保可拖拽物体所在的Canvas有足够的渲染层级。在OnBeginDrag中将物体设为transform.root的子物体通常能解决。仔细检查所有可能覆盖区域的UI元素的射线检测属性。问题2字母放入槽位后位置偏移没有居中。原因RectTransform.anchoredPosition没有正确归零或者槽位本身的锚点Anchor和轴心Pivot设置不是居中。解决确保答案槽位的RectTransform锚点设置为“居中”轴心为(0.5, 0.5)。在PlaceLetter方法中除了设置anchoredPosition为Vector2.zero有时还需要调用LayoutRebuilder.ForceRebuildLayoutImmediate来强制刷新布局。5.2 进度逻辑的“坑”问题玩家拼对单词后游戏没有正确加载下一个或者UI显示混乱。排查步骤打日志在OnWordCorrect和InitializeGame开始处添加Debug.Log打印当前目标单词和进度索引确认逻辑顺序。检查数据状态在SubmitAnswer方法前后打印GameProgressManager.Instance.currentLevelData.completionStatus列表看完成状态是否被正确更新。检查协程确认LoadNextWordWithDelay协程是否正常启动和执行。有时因为物体被意外销毁协程会中断。一个典型错误在InitializeGame中没有正确处理所有单词已完成的情况GetCurrentTargetWord返回空字符串导致后续生成字母时出错。务必添加空值判断。5.3 性能与内存小优化对象池频繁实例化和销毁字母卡片预制体Instantiate/Destroy会产生GC垃圾回收压力。对于这种重复使用的对象可以实现一个简单的对象池。在GameManager中维护两个列表一个存放活跃的卡片一个存放闲置的卡片。需要时从闲置池取用用完后放回而不是销毁。避免每帧查询不要在Update方法里频繁调用GameProgressManager.Instance.GetCurrentTargetWord()这类方法。只在状态改变时如单词拼对、重置后调用并缓存结果。UI重建优化动态生成大量UI元素如进度条图标时如果每帧都有变化可能引起不必要的UI网格重建。可以使用ContentSizeFitter和LayoutGroup的配合并注意在一次性批量修改UI后手动调用Canvas.ForceUpdateCanvases()谨慎使用。5.4 扩展思路这个基础框架有很大的扩展空间难度系统单词长度、字母相似度如B和D、时间限制、可用的提示次数。主题与皮肤不同的字母卡片美术风格、背景音乐和音效。数据驱动将关卡数据单词列表放到外部JSON或CSV文件中甚至可以从网络服务器加载实现动态更新关卡。发布到WebGL注意PlayerPrefs在WebGL平台的行为与本地略有不同。如果需要更稳定的存储可以考虑使用浏览器端的IndexedDB可以通过Unity的JS互操作来实现。这个“字母拼词”小项目核心价值在于它清晰地演示了一个有状态的、数据驱动的小游戏是如何构建的。从数据建模到UI交互再到状态管理和持久化每一环都紧扣实际开发需求。希望这份详细的拆解能帮助你不仅复现这个游戏更能理解其背后的设计思想并将其应用到你自己更复杂的项目中去。源码和演示GIF已整理好你可以在此基础上尽情发挥创意。
返回列表