ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Unity 2D游戏开发实战:从零构建火柴人部落战争Demo

Unity 2D游戏开发实战:从零构建火柴人部落战争Demo

在实际游戏开发或同人创作项目中,将“火柴人”这一经典形象与“部落战争”的玩法机制相结合,并融入“武装铁道”这类特定主题元素,是一个充满挑战和乐趣的过程。这类项目通常涉及角色设计、战斗逻辑、场景构建和资源管理等多个核心模块。对于希望从零开始构建一个类似《火柴人部落战争》风格小游戏的开发者,或者想深入理解2D游戏基础逻辑的爱好者而言,掌握一套清晰的实现路径至关重要。

本文将以“武装铁道”作为核心场景示例,带你从游戏概念设计开始,逐步完成一个可运行的火柴人部落攻防战Demo。我们将使用一个通用的2D游戏开发框架(如Unity或类似结构的自制引擎)作为技术栈,重点讲解状态机控制战斗单位、铁道场景的交互逻辑以及部落间的资源争夺机制。你将了解到如何为火柴人设计简单的动画状态、如何实现基于铁道移动和部署单位的逻辑,以及如何构建一个最小化的资源生产与消耗循环。最终,你将获得一个包含基础攻防、单位生产和场景交互的微型策略游戏原型。

1. 理解项目核心:火柴人、部落战争与武装铁道

在动手写代码之前,我们需要明确几个核心概念,并理清它们在这个项目中的具体表现形式。这决定了后续代码结构和资源组织的方向。

1.1 火柴人作为游戏单位的优势与实现要点

“火柴人”是一种高度简化的角色形象,其优势在于动画制作成本低、辨识度高且易于表现夸张的动作。在游戏逻辑层面,一个火柴人单位通常由以下几个部分组成:

  • 碰撞体(Collider):用于物理交互和攻击范围判定,通常使用矩形(Box Collider)或胶囊体(Capsule Collider)。
  • 渲染器(Renderer):负责绘制火柴人的线条或简单色块。在自制引擎中可能是一组动态绘制的线段;在Unity中可能是一个Sprite Renderer,使用预先画好的序列帧或骨骼动画。
  • 状态机(State Machine):这是控制单位行为的核心。一个火柴人单位至少应包含Idle(待机)、Move(移动)、Attack(攻击)、Die(死亡)等几个基本状态。

1.2 “部落战争”玩法机制的抽象

“部落战争”类游戏的核心循环是“资源采集 -> 单位生产 -> 攻防对抗”。在我们的Demo中,可以将其简化为:

  1. 资源系统:至少有一种资源(如“金币”或“能量”),由地图上的固定点(如矿点)或建筑随时间自动生产。
  2. 生产系统:玩家消耗资源,从特定建筑(如兵营)或铁道站点生产火柴人单位。
  3. 目标系统:双方部落拥有一个需要被摧毁的核心建筑(如“大本营”),摧毁敌方核心即获胜。

1.3 “武装铁道”场景元素的交互设计

“武装铁道”是这个同人项目的特色场景。铁道在这里可以扮演多重角色:

  • 快速移动通道:单位在铁道上移动可以获得速度加成。
  • 战略部署点:铁道上的特定站点(如“站台”)可以作为临时的单位生产或集结地点。
  • 可交互环境:铁道本身或列车可能成为可被摧毁或占领的中立目标,为战场增加变数。

基于以上分析,我们的技术实现将围绕“单位控制”、“资源流转”和“场景交互”三条主线展开。

2. 环境准备与项目结构搭建

我们选择使用Unity进行演示,因为它提供了完善的2D物理、动画和UI系统,能快速验证想法。你也可以根据这些逻辑,使用其他引擎或框架(如Godot、自制Canvas游戏)进行实现。

2.1 基础环境配置

首先确保你的开发环境就绪:

  • Unity Hub & Unity Editor:建议使用一个稳定的LTS版本,如2022.3 LTS。
  • 项目设置:新建一个2D项目。在Edit -> Project Settings中,确认以下设置:
    • Player设置中,根据目标平台(如PC)设置分辨率。
    • Physics 2D设置中,可以根据需要调整重力(通常设为0)和碰撞检测参数。

2.2 创建核心项目目录结构

一个清晰的项目结构有助于管理日益复杂的资源。在Unity的Assets文件夹下,创建如下目录:

Assets/ ├── _Scripts/ # 所有C#脚本 │ ├── Core/ # 游戏核心管理器(GameManager, ResourceManager) │ ├── Units/ # 单位相关脚本(UnitBase, MeleeUnit, RangedUnit) │ ├── Buildings/ # 建筑相关脚本(BuildingBase, ResourceGenerator, Spawner) │ ├── UI/ # 用户界面控制脚本 │ └── Utilities/ # 工具类(ObjectPool, Extensions) ├── _Prefabs/ # 预制体(预制好的游戏对象) │ ├── Units/ # 火柴人单位预制体 │ ├── Buildings/ # 建筑预制体 │ └── Environment/ # 铁道、站台等场景元素预制体 ├── _Art/ # 美术资源 │ ├── Sprites/ # 精灵图(火柴人序列帧、建筑图片等) │ └── Animations/ # 动画控制器和动画片段 ├── _Scenes/ # 游戏场景 └── _Settings/ # 可脚本化对象(ScriptableObject),用于配置平衡性数据

2.3 配置初始场景与基础预制体

  1. 创建场景:在_Scenes文件夹中新建一个场景,命名为MainGame
  2. 设置摄像机:将主摄像机Projection设置为Orthographic(正交),并调整Size以便能看到足够的战场区域。
  3. 创建基础预制体
    • _Prefabs/Units下,创建一个空物体,命名为Stickman_Base。为其添加Sprite Renderer(暂时用一个白色方块代替)、Box Collider 2D(用于碰撞)和Rigidbody 2D(将Body Type设为Kinematic,避免受物理重力影响)。
    • _Prefabs/Buildings下,同样创建BaseCamp(大本营)和Barracks(兵营)的基础预制体,包含碰撞体和渲染器。
    • _Prefabs/Environment下,创建RailwaySegment(铁道段)和RailwayStation(站台)的预制体。

3. 实现火柴人单位的基础逻辑

所有游戏单位(火柴人)都应继承自一个共同的基类,这有利于统一管理生命值、状态和寻路逻辑。

3.1 创建单位基类 UnitBase

_Scripts/Units/下创建C#脚本UnitBase.cs。这个类将包含单位的基础属性和状态机框架。

using UnityEngine; public class UnitBase : MonoBehaviour { [Header("基础属性")] public int maxHealth = 100; public float moveSpeed = 5f; public int attackDamage = 10; public float attackRange = 2f; public float attackInterval = 1f; // 攻击间隔(秒) [Header("运行时状态")] protected int currentHealth; protected UnitState currentState = UnitState.Idle; protected Transform target; // 当前目标(敌人或移动目标点) protected float lastAttackTime; public enum UnitState { Idle, Moving, Attacking, Dead } protected virtual void Start() { currentHealth = maxHealth; } protected virtual void Update() { if (currentState == UnitState.Dead) return; switch (currentState) { case UnitState.Idle: UpdateIdleState(); break; case UnitState.Moving: UpdateMovingState(); break; case UnitState.Attacking: UpdateAttackingState(); break; } } protected virtual void UpdateIdleState() { // 待机状态,可以在这里搜索敌人或接收指令 // 示例:自动寻找最近敌人 FindNearestEnemy(); } protected virtual void UpdateMovingState() { if (target == null) { ChangeState(UnitState.Idle); return; } // 向目标移动 Vector2 direction = (target.position - transform.position).normalized; transform.Translate(direction * moveSpeed * Time.deltaTime); // 如果进入攻击范围,则转为攻击状态 if (Vector2.Distance(transform.position, target.position) <= attackRange) { ChangeState(UnitState.Attacking); } } protected virtual void UpdateAttackingState() { if (target == null) { ChangeState(UnitState.Idle); return; } // 检查目标是否仍在攻击范围内 if (Vector2.Distance(transform.position, target.position) > attackRange) { ChangeState(UnitState.Moving); return; } // 根据攻击间隔执行攻击 if (Time.time - lastAttackTime >= attackInterval) { PerformAttack(); lastAttackTime = Time.time; } } protected virtual void PerformAttack() { // 这里调用伤害逻辑,例如: // target.GetComponent<UnitBase>()?.TakeDamage(attackDamage); Debug.Log(gameObject.name + " 攻击了 " + target.name); } public virtual void TakeDamage(int damage) { currentHealth -= damage; if (currentHealth <= 0) { Die(); } } protected virtual void Die() { ChangeState(UnitState.Dead); // 播放死亡动画,销毁或回收对象 Debug.Log(gameObject.name + " 死亡"); Destroy(gameObject, 0.5f); // 简单销毁,实际应用对象池 } protected void ChangeState(UnitState newState) { currentState = newState; // 这里可以触发状态改变事件,用于动画控制 OnStateChanged(newState); } protected virtual void OnStateChanged(UnitState state) { // 子类重写此方法以切换动画 } protected virtual void FindNearestEnemy() { // 简单的查找逻辑:查找标签为“Enemy”的最近单位 // 实际项目中应有更高效的管理器 GameObject[] enemies = GameObject.FindGameObjectsWithTag("Enemy"); float shortestDistance = Mathf.Infinity; Transform nearestEnemy = null; foreach (GameObject enemy in enemies) { float distance = Vector2.Distance(transform.position, enemy.transform.position); if (distance < shortestDistance) { shortestDistance = distance; nearestEnemy = enemy.transform; } } if (nearestEnemy != null && shortestDistance < 20f) // 20为发现范围 { target = nearestEnemy; ChangeState(UnitState.Moving); } } // 外部调用,命令单位移动到指定位置 public void MoveToPosition(Vector2 position) { // 创建一个临时目标对象(或使用一个空对象) GameObject tempTarget = new GameObject("MoveTarget"); tempTarget.transform.position = position; target = tempTarget.transform; ChangeState(UnitState.Moving); // 注意:实际需要处理临时目标的销毁 } }

这个基类定义了一个简单的状态机循环和基础属性。UpdateIdleState,UpdateMovingState,UpdateAttackingState是状态更新的核心,子类可以重写它们来实现更复杂的行为(例如,远程单位在攻击状态下的逻辑会不同)。

3.2 创建近战单位与动画控制器

  1. 创建近战单位子类:创建脚本MeleeUnit.cs,继承自UnitBase。可以重写PerformAttack方法,实现具体的近战伤害判定(如使用射线检测或触发碰撞)。
  2. 关联动画:为Stickman_Base预制体添加Animator组件。在_Art/Animations文件夹中创建Animator Controller,并设置状态参数(如IsMoving,IsAttacking,IsDead)来控制状态切换。在MeleeUnitOnStateChanged方法中,根据传入的state参数设置这些Animator参数。

3.3 为铁道场景添加移动加成

为了实现单位在铁道上移动更快的效果,我们需要利用Unity的碰撞检测。

  1. 创建铁道区域:为RailwaySegment预制体添加一个Box Collider 2D,并勾选Is Trigger。再添加一个脚本RailwaySpeedZone.cs
  2. 编写速度区域脚本
public class RailwaySpeedZone : MonoBehaviour { public float speedMultiplier = 2.0f; // 在铁道上移动速度加倍 private void OnTriggerEnter2D(Collider2D other) { UnitBase unit = other.GetComponent<UnitBase>(); if (unit != null) { // 这里需要一种方式来临时提升单位的移动速度 // 方法1:在UnitBase中暴露一个方法 // unit.ApplySpeedModifier(speedMultiplier); // 方法2:使用一个状态组件(更推荐) SpeedModifier mod = other.GetComponent<SpeedModifier>(); if (mod == null) mod = other.gameObject.AddComponent<SpeedModifier>(); mod.AddModifier("Railway", speedMultiplier); } } private void OnTriggerExit2D(Collider2D other) { UnitBase unit = other.GetComponent<UnitBase>(); if (unit != null) { SpeedModifier mod = other.GetComponent<SpeedModifier>(); if (mod != null) { mod.RemoveModifier("Railway"); } } } } // 一个简单的速度修饰器组件,可以叠加多种效果 public class SpeedModifier : MonoBehaviour { private Dictionary<string, float> modifiers = new Dictionary<string, float>(); private UnitBase unit; void Start() { unit = GetComponent<UnitBase>(); } public void AddModifier(string id, float multiplier) { modifiers[id] = multiplier; UpdateFinalSpeed(); } public void RemoveModifier(string id) { if (modifiers.ContainsKey(id)) { modifiers.Remove(id); UpdateFinalSpeed(); } } private void UpdateFinalSpeed() { float finalMultiplier = 1.0f; foreach (var mod in modifiers.Values) { finalMultiplier *= mod; } // 假设UnitBase有一个baseMoveSpeed和一个currentMoveSpeed // unit.currentMoveSpeed = unit.baseMoveSpeed * finalMultiplier; } }

这个设计允许单位同时受到多种速度影响(如铁道加速、减速沼泽等),并且效果可以叠加或移除。

4. 构建部落资源与生产系统

没有资源采集和单位生产的“部落战争”是不完整的。我们将实现一个简化的资源系统。

4.1 创建资源管理单例

_Scripts/Core/下创建ResourceManager.cs。这是一个单例类,管理整个游戏的资源存量。

using UnityEngine; public class ResourceManager : MonoBehaviour { public static ResourceManager Instance { get; private set; } [SerializeField] private int initialGold = 1000; private int currentGold; public System.Action<int> OnGoldChanged; // 事件,当金币变化时通知UI private void Awake() { if (Instance != null && Instance != this) { Destroy(this.gameObject); } else { Instance = this; } currentGold = initialGold; } public bool SpendGold(int amount) { if (currentGold >= amount) { currentGold -= amount; OnGoldChanged?.Invoke(currentGold); return true; } Debug.Log("金币不足!"); return false; } public void AddGold(int amount) { currentGold += amount; OnGoldChanged?.Invoke(currentGold); } public int GetCurrentGold() { return currentGold; } }

4.2 创建资源生产建筑

创建脚本ResourceGenerator.cs并挂载到金矿等建筑预制体上。

public class ResourceGenerator : BuildingBase // 假设BuildingBase是一个建筑基类 { [Header("资源生产")] public int goldPerInterval = 10; public float generationInterval = 5f; // 每5秒生产一次 private float timer; protected override void Update() { base.Update(); // 调用建筑基类的Update(如果有) timer += Time.deltaTime; if (timer >= generationInterval) { GenerateResource(); timer = 0f; } } private void GenerateResource() { if (ResourceManager.Instance != null) { ResourceManager.Instance.AddGold(goldPerInterval); // 可以在这里播放粒子效果或UI提示 } } }

4.3 创建单位生产建筑(兵营)

创建脚本UnitSpawner.cs并挂载到兵营预制体上。这个脚本将处理单位的生产队列和冷却。

public class UnitSpawner : BuildingBase { [System.Serializable] public class SpawnableUnit { public GameObject unitPrefab; public int goldCost; public float trainTime; // 训练时间 } public SpawnableUnit[] trainableUnits; public Transform spawnPoint; // 单位生成的位置 private Queue<SpawnableUnit> trainingQueue = new Queue<SpawnableUnit>(); private float currentTrainTimer = 0f; private bool isTraining = false; public void TrainUnit(int unitIndex) { if (unitIndex < 0 || unitIndex >= trainableUnits.Length) return; SpawnableUnit unitToTrain = trainableUnits[unitIndex]; if (ResourceManager.Instance.SpendGold(unitToTrain.goldCost)) { trainingQueue.Enqueue(unitToTrain); Debug.Log($"单位已加入队列,当前队列长度:{trainingQueue.Count}"); if (!isTraining) { StartTrainingNextUnit(); } } } private void StartTrainingNextUnit() { if (trainingQueue.Count > 0) { isTraining = true; currentTrainTimer = 0f; // 这里可以更新UI,显示正在训练的单位 } else { isTraining = false; } } protected override void Update() { base.Update(); if (isTraining) { currentTrainTimer += Time.deltaTime; SpawnableUnit currentUnit = trainingQueue.Peek(); // 查看队列第一个 if (currentTrainTimer >= currentUnit.trainTime) { SpawnUnit(currentUnit.unitPrefab); trainingQueue.Dequeue(); // 移除已完成的单位 StartTrainingNextUnit(); // 开始训练下一个 } } } private void SpawnUnit(GameObject unitPrefab) { if (spawnPoint != null && unitPrefab != null) { Instantiate(unitPrefab, spawnPoint.position, Quaternion.identity); // 可以在这里设置单位的阵营、初始目标等 } } // 提供一个方法供UI按钮调用 public void OnUITrainButtonClicked(int index) { TrainUnit(index); } }

4.4 创建简单的UI进行交互

在Unity中创建一个Canvas,添加按钮和文本。

  • 为每个可训练单位创建一个按钮,按钮的OnClick()事件关联到UnitSpawner实例的OnUITrainButtonClicked方法,并传递对应的单位索引。
  • 创建一个Text组件,用于显示当前金币。在ResourceManagerOnGoldChanged事件中更新这个文本。

5. 整合与运行验证

将上述所有系统整合到场景中,并进行测试。

5.1 场景搭建步骤

  1. BaseCamp(双方各一个)、BarracksResourceGenerator(金矿)预制体拖入场景,并设置好位置。为双方单位设置不同的Tag(如PlayerEnemy),以便FindNearestEnemy逻辑能正确工作。
  2. 使用RailwaySegment预制体在场景中铺设一条铁道,连接双方基地。
  3. RailwayStation预制体放置在铁道沿线,未来可以扩展其功能(如作为临时兵营)。
  4. 设置UI,将金币文本与ResourceManager.Instance.OnGoldChanged事件绑定,将训练按钮与对应的UnitSpawner脚本绑定。

5.2 核心游戏循环验证

启动游戏后,按顺序验证以下环节是否正常工作:

  1. 资源生产:观察金矿建筑是否定期增加全局金币数(可通过UI或控制台日志查看)。
  2. 单位生产:点击UI上的训练按钮,检查是否扣除金币,并在训练时间结束后,在兵营的spawnPoint位置生成一个火柴人单位。
  3. 单位移动与寻敌:生成的我方单位应能自动寻找并走向最近的敌方单位(或建筑)。你可以先放置一个静态的敌方建筑作为测试目标。
  4. 铁道加速:控制或观察单位走上铁道时,其移动速度是否明显提升。
  5. 攻击与销毁:当单位进入攻击范围后,应开始“攻击”(目前是日志输出),持续攻击直到目标被摧毁(需要为目标实现TakeDamageDie逻辑)。

5.3 预期输出与调试

如果一切正常,你将看到一个基本的攻防循环:采集资源 -> 训练单位 -> 单位沿铁道快速移动 -> 自动攻击敌方目标。这是“火柴人部落战争”游戏最核心的玩法骨架。

6. 常见问题排查与优化

在实现上述流程时,你可能会遇到一些典型问题。下表列出了常见现象、可能原因及解决方案。

问题现象可能原因检查与解决方案
单位生成后一动不动1. 单位未设置目标。
2. 单位的FindNearestEnemy逻辑未找到敌人。
3. 单位的Rigidbody 2DBody Type设置不当,阻碍了移动。
1. 检查单位生成后target是否为null。
2. 确认场景中存在Tag为“Enemy”的对象。
3. 将Rigidbody 2DBody Type设为Kinematic
点击训练按钮无反应(不扣钱,不训练)1. UI按钮事件未正确绑定。
2.ResourceManager实例未创建或单例获取失败。
3. 金币不足但未给出明确提示。
1. 在Unity编辑器中检查按钮的OnClick列表,确认调用了正确对象的方法。
2. 确保场景中有一个挂载了ResourceManager脚本的GameObject。
3. 在SpendGold方法中添加日志或UI反馈。
单位在铁道上没有加速1.RailwaySpeedZone脚本未挂载或触发器未启用。
2. 单位没有SpeedModifier组件,或UnitBase中没有应用速度修饰器的逻辑。
3. 触发器碰撞层(Layer)设置错误,双方未发生碰撞。
1. 检查铁道预制体是否有RailwaySpeedZone脚本和Collider 2D(Is Trigger勾选)。
2. 确保UnitBasecurrentMoveSpeed属性,并且在SpeedModifier中能修改它。
3. 检查单位和铁道的Layer,确保不是“Ignore Raycast”等互斥层。
多个单位同时训练时逻辑混乱UnitSpawner的生产队列trainingQueue逻辑有误,或者计时器currentTrainTimer被重置。确保StartTrainingNextUnit只在开始训练新单位或上一个单位完成时调用。currentTrainTimer应在每个单位训练开始时重置为0。
游戏运行一段时间后变卡1. 单位死亡后未及时销毁,大量无效对象残留。
2. 每帧都在执行昂贵的查找操作(如GameObject.FindGameObjectsWithTag)。
1. 使用对象池(Object Pool)管理单位的生成与回收,而不是频繁InstantiateDestroy
2. 将单位管理交给一个专门的UnitManager,它维护两个单位的列表,避免每帧进行全局查找。

7. 扩展方向与生产环境考量

当前实现是一个用于学习和原型验证的简化版本。若要将其发展为一个更完整、更稳健的项目,需要考虑以下扩展和优化。

7.1 游戏性扩展

  • 更多单位类型:基于UnitBase创建远程单位(RangedUnit)、骑兵单位(移动速度更快)、攻城单位(对建筑伤害高)等,重写其PerformAttack和寻敌逻辑。
  • 建筑多样化:实现防御塔(自动攻击范围内敌人)、城墙(阻挡单位)、科技建筑(升级单位属性)。
  • 铁道战略点:扩展RailwayStation脚本,使其可以被占领,占领后每隔一段时间为占领方生产资源或提供单位生产能力。
  • 胜利条件:完善BaseCamp建筑的TakeDamageDie方法,在其被摧毁时触发游戏结束事件,并宣布胜利方。

7.2 代码架构优化

  • 使用事件系统:用C#事件或UnityEvent替代直接的函数调用和查找,降低模块间的耦合度。例如,单位死亡时发出事件,由资源管理器监听并奖励金币。
  • 引入对象池:创建ObjectPool类管理所有单位的生成与回收,极大提升性能。
  • 配置数据外置:使用ScriptableObject来定义单位属性(血量、伤害、造价等)和建筑数据。这样策划人员可以在不修改代码的情况下调整游戏平衡。
  • 实现真正的寻路:对于复杂地图,使用Unity的NavMesh系统或A* Pathfinding Project等插件替代简单的直线移动。

7.3 生产环境注意事项

如果这个Demo需要向更正式的项目演进,以下几点至关重要:

  • 输入处理:实现一个稳健的输入管理器,处理鼠标选择、框选、右键移动/攻击等RTS标准操作。
  • 网络同步:如果目标是多人游戏,早期就需要确定网络架构(权威服务器、P2P等),并使用像Netcode for GameObjects或Photon这样的网络库,所有游戏逻辑都需要考虑状态同步。
  • 性能分析:使用Unity Profiler监控CPU、GPU和内存占用。特别注意单位数量增多时的性能瓶颈(绘制调用、物理计算、AI更新)。
  • 资源管理:对Sprite、音效、预制体进行合理的打包(AssetBundle)与动态加载,控制初始包体大小。
  • 异常处理与日志:在关键逻辑点(如资源交易、单位生成、伤害计算)添加详细的日志,并建立统一的异常捕获机制,便于线上问题排查。

从“武装铁道”这个具体场景出发,你已经搭建了一个具备核心循环的火柴人部落战争框架。接下来,你可以选择任何一个扩展方向深入,例如完善单位的技能系统,或者为铁道添加更复杂的交互事件(如可被破坏的桥梁)。最重要的是保持迭代,每增加一个功能,都进行充分的测试,确保游戏的基础体验始终稳固。

返回列表