Unity 2D策略游戏开发实战:从零构建《明日方舟》同人Demo 在实际游戏开发中很多开发者都希望将喜爱的世界观与自己的技术实践相结合制作一款同人游戏。这不仅是对原作的热爱更是一次完整的游戏开发全流程演练。本文将以一个名为“SamiExpedition”的《明日方舟》同人游戏Demo为例从零开始详细拆解一个2D策略/探索类游戏Demo的开发过程。我们将使用Unity引擎和C#语言涵盖从项目初始化、核心系统设计、到资源管理、场景搭建、角色控制、UI交互以及最终打包发布的完整链路。无论你是Unity初学者想了解一个完整游戏项目是如何组织起来的还是有一定经验的开发者希望学习如何将想法转化为可运行的Demo这篇文章都将提供一条清晰的路径。我们将重点关注工程实践包括目录结构规划、脚本职责划分、数据驱动设计、以及开发中必然会遇到的坑和解决方案。最终你将获得一个可运行、可扩展的游戏Demo框架并掌握独立完成类似项目的能力。1. 理解同人游戏Demo的开发目标与技术选型在动手写代码之前明确开发目标和选择合适的技术栈至关重要。这决定了后续所有工作的方向和效率。1.1 定义“SamiExpedition”Demo的核心玩法与范围一个Demo版本Ver 0.1的目标不是实现所有功能而是验证核心玩法循环Core Gameplay Loop的可行性并搭建起可扩展的项目基础。基于“明日方舟”的世界观我们可以设定“SamiExpedition”的初步方向核心玩法结合策略部署与地图探索。玩家在网格化地图上部署干员角色干员拥有不同的属性生命、攻击、防御等和技能以应对地图上的事件或敌人。Demo范围一个可交互的主菜单场景包含开始游戏、退出等基本按钮。一个简单的关卡场景使用Tilemap绘制的基础网格地图包含可通行与不可通行区域。基础角色系统实现1-2个可部署的干员预制体包含移动、攻击或交互的动画和逻辑。基础UI系统角色属性显示、部署UI、简单的胜利/失败条件判定。基础数据管理使用ScriptableObject或JSON管理角色属性、关卡数据。明确范围后就能避免陷入“想要实现太多功能导致项目烂尾”的常见困境。1.2 Unity引擎与C#为何是2D同人项目的合理选择对于这类2D策略/探索游戏Unity是目前社区最成熟、资源最丰富的选择之一。成熟的2D工作流Unity内置了完善的2D渲染管线、Sprite渲染器、Tilemap系统用于快速搭建网格地图、2D物理系统等开箱即用。强大的C#脚本支持C#语言兼具性能与开发效率其面向对象特性非常适合构建复杂的游戏系统如角色系统、技能系统、状态机。丰富的资源与社区无论是解决特定Bug还是寻找Shader、插件Unity社区都能提供大量支持。这对于独立开发者或小团队至关重要。便捷的跨平台发布Unity可以相对轻松地将项目打包到PC、Mac、WebGL乃至移动平台为Demo的分享和测试提供了便利。注意技术选型没有绝对的对错。Godot、GameMaker Studio 2等引擎也是优秀的选择。选择Unity主要基于其生态和笔者经验便于展开具体教程。1.3 项目初始环境与版本控制准备在开始创建Unity项目前做好环境准备能避免后续很多路径和依赖问题。安装Unity Hub与Unity编辑器建议安装一个稳定的LTS长期支持版本如2022.3 LTS。避免使用过于前沿的版本以减少遇到未知Bug的风险。创建新项目打开Unity Hub点击“新建项目”。选择“2D核心模板”为项目命名例如SamiExpedition_Demo。选择项目存储路径确保路径中没有中文或特殊字符这是Unity项目的一个基本规范。点击“创建项目”。初始化版本控制强烈推荐即使是一个人开发使用Git进行版本控制也是最佳实践。在项目根目录打开命令行执行git init。创建.gitignore文件可以从Unity官方GitHub仓库获取针对Unity的.gitignore模板忽略Library、Temp、Obj等无需提交的文件夹。进行首次提交git add .然后git commit -m “Initial commit: Project setup with 2D core template”。2. 构建清晰可维护的Unity项目结构混乱的项目结构是后期开发维护的噩梦。在编写第一行代码前我们应该规划好Assets目录的组织方式。2.1 设计标准的Assets目录规范一个清晰的结构有助于快速定位资源也便于团队协作。以下是推荐的结构Assets/ ├── [email protected] # 存放第三方插件、工具包 ├── Art/ # 美术资源 │ ├── Sprites/ # 精灵图片角色、UI、道具等 │ ├── Tilemaps/ # 瓦片资源 │ ├── Animations/ # 动画控制器(.controller)和动画片段(.anim) │ └── Materials/ # 材质球 ├── Audio/ # 音效与音乐 │ ├── BGM/ │ └── SFX/ ├── Prefabs/ # 预制体 │ ├── Characters/ # 角色预制体 │ ├── UI/ # UI控件预制体 │ └── VFX/ # 特效预制体 ├── Scenes/ # 场景文件 │ ├── 0_MainMenu.unity │ ├── 1_Level_Test.unity │ └── (其他关卡场景) ├── Scripts/ # C#脚本 │ ├── Core/ # 核心系统游戏管理器、场景加载器、事件中心等 │ ├── Characters/ # 角色相关脚本移动、战斗、状态等 │ ├── UI/ # UI控制脚本 │ ├── Systems/ # 其他系统存档、音频管理、配置等 │ └── Utilities/ # 工具类、扩展方法、常量定义 ├── Settings/ # 项目设置文件 │ └── (ScriptableObjects) └── StreamingAssets/ # 流式资源用于存放运行时加载的AB包或配置你可以在Unity编辑器的Project窗口中右键创建这些文件夹。这个结构不是一成不变的但遵循“按类型和功能模块划分”的原则。2.2 核心管理器的创建GameManager与场景流游戏需要一个“大脑”来管理全局状态和流程。我们创建一个简单的GameManager。创建GameManager在Assets/Scripts/Core/下创建C#脚本GameManager.cs。将其设计为单例模式Singleton确保全局只有一个实例。using UnityEngine; using UnityEngine.SceneManagement; namespace SamiExpedition.Core { public class GameManager : MonoBehaviour { public static GameManager Instance { get; private set; } // 游戏状态枚举 public enum GameState { MainMenu, Playing, Paused, GameOver } public GameState CurrentState { get; private set; } private void Awake() { // 实现简单的单例模式 if (Instance null) { Instance this; DontDestroyOnLoad(gameObject); // 跨场景不销毁 } else { Destroy(gameObject); } CurrentState GameState.MainMenu; } // 开始游戏 public void StartGame() { CurrentState GameState.Playing; SceneManager.LoadScene(1_Level_Test); // 加载关卡场景 } // 返回主菜单 public void ReturnToMainMenu() { CurrentState GameState.MainMenu; SceneManager.LoadScene(0_MainMenu); // 这里可以添加清理关卡数据的逻辑 } // 暂停/继续游戏 public void TogglePause() { if (CurrentState GameState.Playing) { CurrentState GameState.Paused; Time.timeScale 0f; // 暂停游戏时间 } else if (CurrentState GameState.Paused) { CurrentState GameState.Playing; Time.timeScale 1f; // 恢复游戏时间 } } // 退出游戏 public void QuitGame() { // 在编辑器中停止播放在打包应用中退出 #if UNITY_EDITOR UnityEditor.EditorApplication.isPlaying false; #else Application.Quit(); #endif } } }创建场景在Assets/Scenes/文件夹中创建两个场景0_MainMenu.unity和1_Level_Test.unity。在0_MainMenu场景中创建一个空的GameObject重命名为“GameManager”然后将GameManager脚本挂载上去。在File - Build Settings中将这两个场景拖入“Scenes In Build”列表并确保0_MainMenu在索引0的位置。2.3 使用ScriptableObject进行数据驱动设计硬编码角色属性会让调整和扩展变得极其困难。Unity的ScriptableObjectSO是解决这个问题的利器它允许你将数据作为资源文件.asset存储在项目中。创建角色数据SO在Assets/Scripts/Characters/下创建C#脚本CharacterData.cs。在Assets/Settings/下创建文件夹CharacterData。using UnityEngine; namespace SamiExpedition.Characters { [CreateAssetMenu(fileName NewCharacterData, menuName SamiExpedition/Character Data)] public class CharacterData : ScriptableObject { [Header(基本信息)] public string characterName; public Sprite portrait; // 头像 public GameObject prefab; // 关联的预制体 [Header(基础属性)] public int maxHealth 100; public int attack 20; public int defense 5; public int moveRange 3; // 移动范围格子数 [Header(部署成本)] public int deployCost 10; // 可以在这里添加技能ID、职业类型等更多属性 } }创建数据资产在Project窗口中右键Assets/Settings/CharacterData/-Create-SamiExpedition-Character Data。将其命名为“Char_Guard_A”。在Inspector面板中填写测试用的属性如最大生命值150攻击力25等。用同样方法创建“Char_Sniper_B”。3. 实现游戏核心场景与角色系统有了管理框架和数据基础接下来我们构建可玩的关卡和角色。3.1 使用Tilemap构建2D网格地图Unity的Tilemap系统是构建2D网格关卡的快速工具。创建Tilemap打开1_Level_Test场景。右键Hierarchy -2D Object-Tilemap-Rectangular。这会自动创建一个包含Grid和Tilemap子物体的结构。Grid物体控制整个地图的网格布局单元格大小Tilemap是实际绘制层。准备与绘制瓦片将你的地图瓦片图片例如草地、岩石、道路导入Assets/Art/Tilemaps/确保Texture Type设置为“Sprite (2D and UI)”并应用“Multiple”精灵模式使用Sprite Editor进行切片。在Project窗口选中切片好的精灵右键 -Create-2D-Tiles-Rule Tile或Simple Tile。创建一个Palette调色板并将其保存。打开Window-2D-Tile Palette。将你的Tile Palette拖入窗口就可以用画笔在Scene视图的Tilemap上绘制地图了。设置碰撞体对于不可通行的区域如岩石需要添加碰撞体。选中对应的Tilemap在Inspector中点击“Add Component”添加Tilemap Collider 2D。为了优化性能可以再添加一个Composite Collider 2D并勾选Tilemap Collider 2D上的“Used By Composite”。将Composite Collider的Geometry Type设置为“Polygons”。这样所有相邻的碰撞体会合并减少物理计算量。3.2 创建可部署的干员角色预制体角色是游戏交互的核心。创建角色预制体在Assets/Prefabs/Characters/下创建一个空预制体命名为“PF_Character_Base”。将其拖入场景进行编辑。为其添加以下组件Sprite Renderer用于显示角色精灵。Animator用于控制角色动画稍后配置。Box Collider 2D用于点击检测和物理交互根据精灵大小调整。创建一个C#脚本CharacterBase.cs挂载到该物体上。编写基础角色逻辑using UnityEngine; using SamiExpedition.Core; // 引用核心命名空间以便访问GameManager namespace SamiExpedition.Characters { public class CharacterBase : MonoBehaviour { [SerializeField] private CharacterData _data; // 在Inspector中关联SO public CharacterData Data _data; private int _currentHealth; private bool _isDeployed false; // 属性访问器 public int CurrentHealth _currentHealth; public bool IsDeployed _isDeployed; private void Start() { if (_data ! null) { _currentHealth _data.maxHealth; } else { Debug.LogError($Character {name} has no CharacterData assigned!); } } // 部署角色 public void Deploy(Vector3 position) { if (_isDeployed) return; transform.position position; _isDeployed true; // 触发部署动画、音效等 Debug.Log(${_data.characterName} deployed at {position}); } // 移动角色简化版直接瞬移 public void MoveTo(Vector3 targetPosition) { if (!_isDeployed) return; transform.position targetPosition; // 这里可以加入路径查找如A*算法和移动动画 } // 受到伤害 public void TakeDamage(int damage) { int actualDamage Mathf.Max(damage - _data.defense, 1); // 至少造成1点伤害 _currentHealth - actualDamage; Debug.Log(${_data.characterName} takes {actualDamage} damage, HP: {_currentHealth}/{_data.maxHealth}); if (_currentHealth 0) { Die(); } } // 角色死亡 private void Die() { _isDeployed false; // 触发死亡动画、音效 gameObject.SetActive(false); // 简单处理实际可能需要对象池 Debug.Log(${_data.characterName} has been defeated.); } } }关联数据与预制体将之前创建的“Char_Guard_A” ScriptableObject拖到预制体Inspector中CharacterBase脚本的Data字段上。将精灵图片拖到Sprite Renderer的Sprite字段。编辑完成后点击Prefab顶部的“Overrides - Apply All”保存修改。然后将场景中的实例删除。3.3 实现简单的部署与点击移动交互现在我们需要一个系统来让玩家部署和指挥角色。创建部署管理器在场景中创建一个空GameObject命名为“DeploymentManager”。创建脚本DeploymentManager.cs并挂载。using UnityEngine; using SamiExpedition.Characters; namespace SamiExpedition.Core { public class DeploymentManager : MonoBehaviour { [SerializeField] private CharacterData[] _availableCharacters; // 可部署角色列表 [SerializeField] private LayerMask _groundLayer; // 地图所在层级 [SerializeField] private LayerMask _characterLayer; // 角色所在层级 private CharacterBase _selectedCharacter null; private Camera _mainCamera; private void Start() { _mainCamera Camera.main; } private void Update() { if (Input.GetMouseButtonDown(0)) // 左键点击 { HandleMouseClick(); } } private void HandleMouseClick() { Ray ray _mainCamera.ScreenPointToRay(Input.mousePosition); RaycastHit2D hit Physics2D.Raycast(ray.origin, ray.direction); if (hit.collider ! null) { // 点击到了角色 if (((1 hit.collider.gameObject.layer) _characterLayer) ! 0) { CharacterBase character hit.collider.GetComponentCharacterBase(); if (character ! null character.IsDeployed) { SelectCharacter(character); Debug.Log($Selected character: {character.Data.characterName}); } } // 点击到了地面且已选中角色 else if (((1 hit.collider.gameObject.layer) _groundLayer) ! 0 _selectedCharacter ! null) { Vector3 worldPos _mainCamera.ScreenToWorldPoint(Input.mousePosition); worldPos.z 0; // 确保Z轴为0 _selectedCharacter.MoveTo(worldPos); DeselectCharacter(); } } else { // 点击空白处取消选择 DeselectCharacter(); } } // 从UI按钮调用部署角色 public void DeployCharacter(int index) { if (index 0 || index _availableCharacters.Length) { Debug.LogWarning($Invalid character index: {index}); return; } // 简单实现在鼠标位置部署 // 实际项目中这里需要检查部署点合法性、消耗资源等 Vector3 spawnPos _mainCamera.ScreenToWorldPoint(Input.mousePosition); spawnPos.z 0; CharacterData data _availableCharacters[index]; if (data.prefab ! null) { GameObject charObj Instantiate(data.prefab, spawnPos, Quaternion.identity); CharacterBase character charObj.GetComponentCharacterBase(); character.Deploy(spawnPos); Debug.Log($Deployed {data.characterName}); } } private void SelectCharacter(CharacterBase character) { // 可以在这里添加选中视觉效果比如高亮轮廓 _selectedCharacter character; } private void DeselectCharacter() { // 取消选中视觉效果 _selectedCharacter null; } } }配置层级Layer打开Edit - Project Settings - Tags and Layers。在Layers中添加两个新层级例如“Ground”和“Character”。将Tilemap物体的Layer设置为“Ground”。将角色预制体的Layer设置为“Character”。回到DeploymentManager脚本的Inspector将Ground Layer设置为“Ground”Character Layer设置为“Character”。4. 搭建UI界面与完成游戏流程闭环UI是玩家与游戏交互的桥梁。我们将实现一个简单但功能完整的UI流程。4.1 创建主菜单界面搭建UI Canvas在0_MainMenu场景中右键UI-Canvas。这会自动创建Canvas和EventSystem。在Canvas下创建一个Panel作为背景然后创建几个Button作为菜单项“开始游戏”、“设置”、“退出游戏”。绑定按钮事件选中“开始游戏”按钮在Inspector的On Click()事件列表下点击“”号。将场景中的GameManager物体拖入对象框。选择函数GameManager.StartGame。同理将“退出游戏”按钮绑定到GameManager.QuitGame。4.2 创建游戏内UI部署面板与角色状态在关卡场景创建UI在1_Level_Test场景中创建Canvas。创建一个部署面板Panel里面放置几个按钮每个按钮对应一个可部署角色显示角色头像和名称。编写UI控制脚本创建脚本DeploymentUI.cs并挂载到部署面板上。using UnityEngine; using UnityEngine.UI; using SamiExpedition.Core; namespace SamiExpedition.UI { public class DeploymentUI : MonoBehaviour { [SerializeField] private DeploymentManager _deploymentManager; [SerializeField] private Button[] _deployButtons; // UI按钮数组 [SerializeField] private Text _infoText; // 用于显示信息的Text组件 private void Start() { if (_deploymentManager null) { _deploymentManager FindObjectOfTypeDeploymentManager(); } // 为每个按钮绑定点击事件 for (int i 0; i _deployButtons.Length; i) { int index i; // 闭包问题需要捕获局部变量 _deployButtons[i].onClick.AddListener(() OnDeployButtonClicked(index)); } } private void OnDeployButtonClicked(int characterIndex) { if (_deploymentManager ! null) { _deploymentManager.DeployCharacter(characterIndex); UpdateInfoText($Attempting to deploy character {characterIndex}); } } private void UpdateInfoText(string message) { if (_infoText ! null) { _infoText.text message; // 可以添加一个协程来几秒后清空信息 } } } }关联引用在Inspector中将DeploymentManager物体和对应的UI按钮、Text组件拖拽到DeploymentUI脚本的对应字段。4.3 实现简单的胜利/失败条件与场景切换一个完整的游戏循环需要有结束状态。创建关卡管理器在关卡场景中创建空物体“LevelManager”。创建脚本LevelManager.cs。using UnityEngine; using UnityEngine.SceneManagement; using SamiExpedition.Characters; namespace SamiExpedition.Core { public class LevelManager : MonoBehaviour { [SerializeField] private CharacterBase[] _playerCharacters; // 玩家所有角色 [SerializeField] private GameObject[] _enemyUnits; // 敌方所有单位 [SerializeField] private GameObject _victoryUI; [SerializeField] private GameObject _defeatUI; private bool _isLevelOver false; private void Update() { if (_isLevelOver) return; CheckVictoryCondition(); CheckDefeatCondition(); } private void CheckVictoryCondition() { // 简单条件所有敌人都被消灭 bool allEnemiesDefeated true; foreach (var enemy in _enemyUnits) { if (enemy ! null enemy.activeInHierarchy) { allEnemiesDefeated false; break; } } if (allEnemiesDefeated) { LevelOver(true); } } private void CheckDefeatCondition() { // 简单条件所有玩家角色都倒下 bool allPlayersDefeated true; foreach (var playerChar in _playerCharacters) { if (playerChar ! null playerChar.gameObject.activeInHierarchy) { allPlayersDefeated false; break; } } if (allPlayersDefeated) { LevelOver(false); } } private void LevelOver(bool isVictory) { _isLevelOver true; Time.timeScale 0f; // 暂停游戏逻辑 if (isVictory) { Debug.Log(Victory!); if (_victoryUI ! null) _victoryUI.SetActive(true); } else { Debug.Log(Defeat!); if (_defeatUI ! null) _defeatUI.SetActive(true); } // 可以在这里添加返回菜单或下一关的按钮逻辑 } // 由UI按钮调用 public void ReturnToMenu() { Time.timeScale 1f; GameManager.Instance?.ReturnToMainMenu(); } } }创建胜利/失败UI在Canvas下创建两个Panel分别命名为“VictoryPanel”和“DefeatPanel”上面可以放提示文字和一个“返回主菜单”按钮。将这两个Panel默认设置为不激活取消勾选Inspector顶部的复选框。将Panel物体拖拽到LevelManager脚本的对应字段。将“返回主菜单”按钮的点击事件绑定到LevelManager.ReturnToMenu方法。5. 项目优化、常见问题排查与发布完成核心功能后我们需要让Demo更稳定并准备好发布。5.1 性能优化与资源管理基础对于Demo优化主要集中在避免明显的性能卡点上。使用对象池Object Pooling频繁实例化Instantiate和销毁Destroy角色、子弹等物体会产生GC垃圾回收压力。对象池预先创建一组对象循环使用。简单实现思路创建一个ObjectPool类管理一个QueueGameObject。需要时从队列取出并激活不需要时放回队列并禁用。合理使用Sprite Atlas精灵图集将大量小精灵图打包成一张大图可以减少Draw Call显著提升2D渲染性能。操作在Window-2D-Sprite Atlas中创建图集将相关精灵拖入。确保精灵的Packing Tag一致。控制Update频率对于不需要每帧更新的逻辑如AI决策使用InvokeRepeating或协程Coroutine中的WaitForSeconds来降低更新频率。5.2 开发与打包过程中的常见问题排查以下是开发此Demo时可能遇到的典型问题及解决方案。问题现象可能原因检查与解决步骤场景切换后GameManager丢失或重复单例模式实现有误或未正确设置DontDestroyOnLoad1. 检查GameManager的Awake方法确保单例逻辑正确if (Instance null)。2. 确保只在主菜单场景初始化一次GameManager。点击地面角色不移动Layer设置错误或射线检测未命中1. 检查Tilemap和角色的Layer是否按3.3节正确设置。2. 检查DeploymentManager中_groundLayer和_characterLayer的Mask是否选对。3. 在HandleMouseClick方法中添加Debug.Log打印hit.collider信息确认射线击中了什么。角色属性不生效CharacterData ScriptableObject未关联1. 检查角色预制体上CharacterBase脚本的Data字段是否拖入了对应的.asset文件。2. 检查DeploymentManager的_availableCharacters数组是否填充。打包后UI显示错位或点击无效Canvas缩放模式或分辨率适配问题1. 检查Canvas的Canvas Scaler组件。对于PC项目UI Scale Mode通常设为“Scale With Screen Size”参考分辨率设为1920x1080。2. 确保EventSystem在场景中。动画状态机不切换Animator Controller参数未正确设置或动画条件不满足1. 在Window - Animation - Animator中打开状态机检查Parameters和Transitions条件。2. 在代码中使用Animator.SetBool(“IsWalking”, true)等语句后在Inspector中观察参数是否真的改变了。5.3 构建与发布你的Demo构建设置Build Settings打开File - Build Settings。确保Scenes In Build列表中包含了0_MainMenu和1_Level_Test且顺序正确。选择目标平台如PC, Mac Linux Standalone。玩家设置Player Settings点击“Player Settings”按钮。在Company Name和Product Name中填写你的信息如“SamiExpedition_Demo”。可以设置一个图标Default Icon。执行构建点击“Build”按钮选择一个空文件夹作为输出目录。Unity会开始编译项目。首次构建时间可能较长。构建完成后在输出文件夹中会找到可执行文件.exe以及相关的数据文件夹。运行游戏时需要同时保留可执行文件和整个数据文件夹。5.4 后续扩展方向建议至此一个具备核心循环的《明日方舟》同人游戏Demo已经完成。你可以在此基础上继续深化完善角色系统加入职业分类先锋、近卫、狙击等、技能系统主动/被动、等级成长。实现真正的策略战斗引入攻击范围、攻击间隔、索敌逻辑最近/最远/防御最低。丰富地图与关卡设计使用Rule Tile制作更复杂的地形加入可交互物件治疗点、防御装置设计关卡敌人波次。加入资源与经济系统部署干员需要消耗“费用”费用随时间自动回复击败敌人可能掉落资源。数据持久化使用PlayerPrefs或序列化如JSON保存玩家解锁的干员、关卡进度等。音频与特效为角色动作、UI交互添加音效为攻击、技能添加简单的粒子特效。记住迭代开发的关键是每次只专注于一个可完成的小功能并确保其与现有系统能稳定集成。从这个Demo框架出发你已经拥有了将想法逐步实现为可玩游戏的基础。