在Unity开发中,我们经常看到这样的场景:一个PlayerController脚本里充斥着几十个public变量,Inspector面板被塞得满满当当;或者相反,所有成员都标记为private,导致其他脚本无法获取必要数据。这两种极端情况,都是缺乏对封装原则理解的典型表现。
封装不是简单的"隐藏数据",而是一种有策略的信息管理艺术。好的封装能让代码像乐高积木一样——模块间通过标准接口连接,内部实现可以自由变化。本文将结合Unity特有场景,带你重新思考访问修饰符的选择逻辑。
上周帮朋友review一个塔防游戏源码时,发现EnemyAI脚本里有这样一段代码:
csharp复制public class EnemyAI : MonoBehaviour {
public float health;
public float speed;
public Transform target;
public bool isDead;
// 还有15个类似的public变量...
}
这种写法带来了三个致命问题:
对比采用封装后的版本:
csharp复制public class EnemyAI : MonoBehaviour {
[SerializeField] private float _health;
private float _speed;
private Transform _target;
public float Health => _health;
public float Speed => _speed;
public void TakeDamage(float amount) {
_health = Mathf.Max(0, _health - amount);
if(_health <= 0) Die();
}
}
关键改进点:
在常规C#开发中,我们可能这样使用修饰符:
| 修饰符 | 常规用途 | Unity特殊考量 |
|---|---|---|
| public | 类间交互 | Inspector可见,易被滥用 |
| [SerializeField] private | 序列化需求 | 显示在Inspector但不开放代码访问 |
| protected | 继承体系 | MonoBehaviour慎用,可能破坏预制体 |
| internal | 程序集内 | 适合Editor脚本与运行时分离 |
实际案例:当需要在Inspector显示但不想公开给其他脚本时:
csharp复制[SerializeField]
private float _attackCooldown = 2f;
不要止步于简单的get/set,属性可以成为强大的逻辑网关:
csharp复制private float _moveSpeed;
public float MoveSpeed {
get => _moveSpeed;
set {
_moveSpeed = Mathf.Clamp(value, 0, maxSpeed);
OnSpeedChanged?.Invoke(_moveSpeed);
}
}
这种写法实现了:
很多开发者会这样获取组件:
csharp复制// 危险写法
public class Player : MonoBehaviour {
public Rigidbody rb;
void Start() {
rb = GetComponent<Rigidbody>();
}
}
更健壮的封装方式:
csharp复制public class Player : MonoBehaviour {
private Rigidbody _rb;
protected void Awake() {
_rb = GetComponent<Rigidbody>()
?? gameObject.AddComponent<Rigidbody>();
}
public void AddForce(Vector3 force) {
_rb.AddForce(force);
}
}
定义攻击接口:
csharp复制public interface IDamageable {
void TakeDamage(float amount);
Transform HitTransform { get; }
}
实现类:
csharp复制public class Enemy : MonoBehaviour, IDamageable {
public Transform HitTransform => transform;
public void TakeDamage(float amount) {
// 具体实现
}
}
调用方只需知道接口,无需关心具体实现:
csharp复制void OnTriggerEnter(Collider other) {
if(other.TryGetComponent<IDamageable>(out var damageable)) {
damageable.TakeDamage(10f);
}
}
虽然扩展方法不属于传统封装范畴,但能优雅地扩展已有类型:
csharp复制public static class TransformExtensions {
public static void LookAt2D(this Transform transform, Vector3 target) {
Vector3 dir = target - transform.position;
float angle = Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg;
transform.rotation = Quaternion.AngleAxis(angle, Vector3.forward);
}
}
使用时就像原生方法一样自然:
csharp复制// 任何Transform实例现在都有这个方法
turretTransform.LookAt2D(playerPosition);
注意事项:
在最近开发的RPG项目中,我们采用了分层封装策略:
数据层:全部private字段+序列化支持
csharp复制[System.Serializable]
public class CharacterStats {
[SerializeField] private float _baseHealth;
private float _currentHealth;
public float HealthPercent => _currentHealth / _baseHealth;
}
逻辑层:protected virtual方法供子类扩展
csharp复制protected virtual void HandleMovement() {
// 基础移动逻辑
}
接口层:明确的public API契约
csharp复制public interface IInteractable {
string InteractionPrompt { get; }
void Interact(Player actor);
}
这种结构使我们的代码在6个月开发周期中保持了良好的可维护性,新成员也能快速理解系统边界。