嘿,朋友!如果你正盯着屏幕上的 Unity 编辑器发呆,手里攥着刚下载的引擎,心里想着“这玩意儿真的能做出好玩的游戏吗?”,那我得先给你吃颗定心丸:当然可以! 而且,你不需要是数学天才,也不需要背下整本 C# 字典。
今天咱们不聊那些枯燥的理论,直接动手。我们要一起从零开始,搭建一个最经典、也最能体现核心逻辑的小游戏框架:一个能跑、能跳、能打怪、还能通过 UI 显示分数并切换场景的小英雄。
我会把这个过程拆解成几个看起来吓人但其实超级简单的模块。咱们就像搭乐高一样,一块一块地把这个游戏拼起来。准备好了吗?打开你的 Unity,新建一个 3D Core 项目,咱们开始!
第一步:理解角色的灵魂——PlayerController 脚本
在 Unity 里,所有的行为都是由脚本驱动的。对于我们的主角来说,最重要的就是“怎么动”。很多新手喜欢直接用 Transform.Translate 来移动角色,但这在物理模拟中是个大坑,会导致角色穿墙或者滑行。我们要做的,是符合物理规律的运动。
我们需要创建一个脚本来处理输入、计算速度和施加力。
1. 变量定义:给角色装上“传感器”
首先,我们需要告诉脚本,这个控制器需要知道什么。除了基本的移动速度,我们还需要知道角色现在是不是在地面上(为了限制跳跃),以及角色的刚体组件在哪里。
using UnityEngine;
[RequireComponent(typeof(Rigidbody))] // 强制要求此物体必须有 Rigidbody 组件
public class PlayerController : MonoBehaviour
{
[Header("移动设置")]
public float moveSpeed = 5f; // 移动速度
public float jumpForce = 7f; // 跳跃力度
[Header("地面检测")]
public Transform groundCheckPoint; // 地面检测点的位置
public float groundCheckRadius = 0.2f; // 检测半径
public LayerMask groundLayer; // 地面的层级,用于区分地面和其他物体
private Rigidbody rb; // 保存刚体引用
private bool isGrounded; // 标记是否在地面
}
这里有个小技巧:[RequireComponent] 是个好东西,它能自动帮你在挂载脚本时添加必要的组件,防止因为漏掉 Rigidbody 而导致游戏崩溃。groundLayer 是你需要在 Unity 编辑器里手动设置的,通常我们会创建一个名为 “Ground” 的新 Layer,然后把地面物体的 Layer 改成它。
2. 初始化:找到你的身体
在 Start 方法中,我们要获取刚体组件的引用。这是所有物理操作的入口。
void Start()
{
rb = GetComponent<Rigidbody>();
}
3. 核心逻辑:物理移动与跳跃
这是最关键的部分。我们不在 Update 里直接修改位置,而是在 FixedUpdate 里施加力。这是因为物理引擎的计算频率是固定的,而渲染帧率是波动的。混在一起会导致不同电脑上游戏速度不一致。
void FixedUpdate()
{
// 1. 获取输入
float moveX = Input.GetAxis("Horizontal"); // 左右键 A/D 或 箭头键
float moveZ = Input.GetAxis("Vertical"); // 前后键 W/S 或 方向键
// 2. 计算移动方向
Vector3 moveDirection = new Vector3(moveX, 0f, moveZ).normalized;
// 3. 应用移动力 (使用 Rigidbody.AddForce)
// ForceMode.VelocityChange 会忽略质量,让移动手感更直接
rb.AddForce(moveDirection * moveSpeed * Time.fixedDeltaTime, ForceMode.VelocityChange);
// 4. 检查是否在地面
CheckIfGrounded();
// 5. 处理跳跃
if (Input.GetButtonDown("Jump") && isGrounded)
{
Jump();
}
}
为什么用 VelocityChange?
想象一下,一个胖子(质量大)和一个瘦子(质量小)同时被推一下。如果用普通的力,胖子几乎不动,瘦子飞出去了。但在游戏中,我们希望玩家感觉控制的是“速度”而不是“力气”,所以 VelocityChange 能让不同质量的物体获得相同的加速度感,操作手感更一致。
4. 地面检测:射线法 vs 物理球
我们用了 Physics.CheckSphere 来检测脚下是否有地面。这是一种性能不错且直观的方法。
void CheckIfGrounded()
{
// 在 groundCheckPoint 位置画一个球,检查是否与 groundLayer 中的物体相交
isGrounded = Physics.CheckSphere(groundCheckPoint.position, groundCheckRadius, groundLayer);
}
void Jump()
{
// 清除当前的垂直速度,防止连续按键导致无限连跳或高度异常
rb.velocity = new Vector3(rb.velocity.x, 0f, rb.velocity.z);
// 施加向上的力
rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse);
}
现在,把你的 Player 模型拖进场景,给它加上 Capsule Collider 和 Rigidbody,再挂载这个脚本。调整一下 groundCheckPoint 的位置(就在角色脚下),设置好 groundLayer。按下播放键,你会发现角色听话地听从你的指挥了!
第二步:让敌人动起来——简单的 AI 与攻击判定
光会跑还不够,游戏得有挑战。我们来做一个简单的敌人:它会朝玩家移动,碰到玩家就算玩家输,玩家碰到敌人头部就算玩家赢(攻击)。
1. 敌人基础脚本
这个脚本负责敌人的巡逻和追踪。
using UnityEngine;
public class EnemyAI : MonoBehaviour
{
public Transform player; // 玩家的目标
public float speed = 2f; // 敌人移动速度
public float detectionRange = 10f; // 发现玩家的距离
void Update()
{
if (player == null) return;
// 计算到玩家的距离
float distance = Vector3.Distance(transform.position, player.position);
// 如果在范围内,就朝玩家移动
if (distance < detectionRange)
{
transform.LookAt(player); // 面向玩家
transform.Translate(Vector3.forward * speed * Time.deltaTime);
}
}
}
2. 碰撞检测:谁是赢家?
这里涉及到两个不同的概念:Trigger(触发器) 和 Collision(碰撞)。
- 玩家攻击敌人:通常我们希望玩家挥剑时,只有当剑刃“切到”敌人时才造成伤害。我们可以给武器加一个 Box Collider,勾选
Is Trigger。 - 敌人碰到玩家:如果敌人碰到玩家身体,游戏结束。
让我们写一个通用的碰撞处理脚本,挂在玩家身上,用来处理“被敌人碰到”的情况。
using UnityEngine;
using UnityEngine.SceneManagement; // 用于场景切换
public class DamageHandler : MonoBehaviour
{
public int playerHealth = 100;
public GameObject gameOverPanel; // UI 面板
void OnCollisionEnter(Collision collision)
{
// 检查碰撞的对象是否有 EnemyTag
if (collision.gameObject.CompareTag("Enemy"))
{
TakeDamage(20);
}
}
void TakeDamage(int amount)
{
playerHealth -= amount;
// 简单的血条更新逻辑(假设你有 UI 文本绑定)
// UpdateHealthUI();
if (playerHealth <= 0)
{
Die();
}
}
void Die()
{
Debug.Log("Game Over!");
// 显示 UI 面板
if (gameOverPanel != null)
{
gameOverPanel.SetActive(true);
}
// 暂停时间,让玩家看清结局
Time.timeScale = 0f;
}
}
注意:为了让 OnCollisionEnter 工作,确保玩家的 Collider 和敌人的 Collider 都没有勾选 Is Trigger,且至少有一个有 Rigidbody。
第三步:战斗系统——攻击与伤害反馈
现在玩家能跑了,也能挨打了。接下来,让玩家能反击。我们将使用射线检测(Raycast)来实现近战攻击,这样比简单的距离判断更精准。
攻击脚本
using UnityEngine;
public class AttackSystem : MonoBehaviour
{
public int attackDamage = 25;
public float attackRange = 1.5f; // 攻击范围
public LayerMask enemyLayer; // 敌人所在的层级
void Update()
{
// 当按下鼠标左键或空格键时发动攻击
if (Input.GetButtonDown("Fire1"))
{
PerformAttack();
}
}
void PerformAttack()
{
// 从摄像机中心向屏幕中心发射一条射线
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
// 或者更简单的:从角色前方发射射线
// Ray ray = new Ray(transform.position + transform.forward * 0.5f, transform.forward);
if (Physics.Raycast(ray, out RaycastHit hit, attackRange, enemyLayer))
{
// 击中了敌人
EnemyHealth enemy = hit.collider.GetComponent<EnemyHealth>();
if (enemy != null)
{
enemy.TakeDamage(attackDamage);
// 可选:添加打击特效或音效
Debug.Log($"Hit enemy! Remaining HP: {enemy.currentHealth}");
}
}
}
}
为了让这个工作,你需要给敌人创建一个新的 Layer,比如 “Enemy”,并在脚本中指定。同时,确保敌人的 Collider 没有勾选 Is Trigger(除非你想做穿透攻击,但通常近战需要有实体碰撞)。
敌人血量管理
using UnityEngine;
public class EnemyHealth : MonoBehaviour
{
public int maxHealth = 100;
public int currentHealth;
void Start()
{
currentHealth = maxHealth;
}
public void TakeDamage(int damage)
{
currentHealth -= damage;
// 简单的受击闪烁效果
Renderer rend = GetComponent<Renderer>();
if (rend != null)
{
Color originalColor = rend.material.color;
rend.material.color = Color.red;
Invoke(nameof(ResetColor), 0.1f);
}
if (currentHealth <= 0)
{
Die();
}
}
void ResetColor()
{
Renderer rend = GetComponent<Renderer>();
if (rend != null)
{
rend.material.color = Color.white;
}
}
void Die()
{
Debug.Log("Enemy Died!");
// 播放死亡动画
// 销毁对象或禁用
Destroy(gameObject);
}
}
第四步:UI 交互——让数据可视化
游戏有了,但玩家不知道自己的血量和分数。这时候 UI 就派上用场了。Unity 的 Canvas 系统是 2D 和 3D 的桥梁。
1. 创建 UI
在 Hierarchy 右键 -> UI -> Text (Legacy) 或 Text (TMP)。
- 创建一个 Health Bar:一个背景 Image,里面套一个填充 Image。
- 创建一个 Score Text:显示当前分数。
- 创建一个 Game Over Panel:包含 “Restart” 按钮和 “Quit” 按钮。
2. 绑定 UI 数据
我们需要一个脚本来实时更新这些 UI 元素。
using UnityEngine;
using UnityEngine.UI; // 必须引入 UI 命名空间
using TMPro; // 如果使用 TextMeshPro
public class UIManager : MonoBehaviour
{
public Slider healthSlider; // 拖入 Health Bar 的 Slider 组件
public TextMeshProUGUI scoreText; // 拖入分数文本
public GameObject gameOverPanel; // 拖入 Game Over 面板
private int score = 0;
// 这个方法由 PlayerController 或 DamageHandler 调用
public void UpdateHealth(float currentHP, float maxHP)
{
if (healthSlider != null)
{
// Slider 的值通常在 0 到 1 之间
healthSlider.value = currentHP / maxHP;
}
}
public void AddScore(int points)
{
score += points;
if (scoreText != null)
{
scoreText.text = "Score: " + score.ToString();
}
}
public void ShowGameOver()
{
if (gameOverPanel != null)
{
gameOverPanel.SetActive(true);
}
}
// 重启按钮的点击事件
public void RestartGame()
{
UnityEngine.SceneManagement.SceneManager.LoadScene(UnityEngine.SceneManagement.SceneManager.GetActiveScene().buildIndex);
}
// 退出按钮的点击事件
public void QuitGame()
{
#if UNITY_EDITOR
UnityEditor.EditorApplication.isPlaying = false;
#else
Application.Quit();
#endif
}
}
如何连接?
在 PlayerController 或 DamageHandler 中,获取 UIManager 的单例或引用,然后在受伤时调用 UpdateHealth。在敌人死亡时,调用 AddScore。
第五步:场景切换——从菜单到游戏
最后,我们需要一个主菜单场景,以及进入游戏后的切换逻辑。
1. 场景设置
- Scene 1: MainMenu
- 包含 “Play” 按钮。
- “Play” 按钮的 OnClick 事件绑定
SceneManager.LoadScene("GameScene")。
- Scene 2: GameScene
- 包含玩家、敌人、UI。
2. 单例模式管理全局状态(可选但推荐)
为了防止多个 UI 管理器冲突,或者方便其他脚本访问 UI,我们可以用一个简单的单例。
using UnityEngine;
public class GameManager : MonoBehaviour
{
public static GameManager Instance { get; private set; }
void Awake()
{
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject); // 场景切换时保留此对象
}
else
{
Destroy(gameObject);
}
}
}
在 MainMenu 的 Play 按钮点击事件中,你可以直接调用:
using UnityEngine.SceneManagement;
// ...
SceneManager.LoadScene("GameScene");
而在 GameScene 的 UI 管理器中,如果要返回主菜单:
public void ReturnToMenu()
{
SceneManager.LoadScene("MainMenu");
}
第六步:调试与优化——让游戏更顺滑
代码写完了,游戏能跑了吗?很可能有一些小问题。
- 物理穿透:如果玩家跑得太快,可能会穿过墙壁。
- 解决:在 Rigidbody 设置中,将
Collision Detection改为Continuous。
- 解决:在 Rigidbody 设置中,将
- 跳跃手感僵硬:
- 解决:调整
jumpForce和moveSpeed。可以在 Inspector 中实时调整,直到手感舒适。
- 解决:调整
- UI 遮挡:
- 解决:检查 Canvas 的 Render Mode,如果是 Screen Space - Overlay,确保 Z 轴顺序正确。
给新手的特别建议:从小做起
很多初学者一上来就想做《原神》或《魔兽世界》,结果连走路都调不好,最后放弃了。记住,成功的秘诀在于“最小可行性产品”(MVP)。
你刚才做的这个:
- 能跑能跳的角色
- 能互动的敌人
- 有血量和分数的 UI
- 有开始和结束的场景
这就是一个完整的游戏原型!在此基础上,你可以:
- 换掉方块模型,换上精美的 3D 角色(去 Mixamo 下载免费动画)。
- 添加音效(Jump, Hit, Background Music)。
- 添加粒子特效(Hit flash, Dust when landing)。
- 增加关卡设计(放置更多障碍物)。
结语:你已经是开发者了
看着屏幕上那个跟着你键盘指令跳动的小方块(或者模型),你应该感到自豪。因为你刚刚亲手构建了一个数字生命的骨架。
C# 脚本不是魔法,它是逻辑的延伸。if 是选择,for 是循环,Vector3 是空间。当你理解了这些基本概念,你就拥有了创造世界的钥匙。
不要害怕报错。红色的错误信息是 Unity 在和你对话,告诉你哪里出了错。学会阅读 Error Log,是成为高手的第一步。
现在,去尝试修改 moveSpeed 看看会发生什么?去试着加一个二段跳?或者做一个能发射子弹的武器?
游戏开发是一场漫长的马拉松,但每一步都充满乐趣。祝你玩得开心,期待看到你做出的第一个小游戏!如果有具体的代码报错,随时回来问我,我们一起解决。
