Unity 3D 基础 / Unreal Engine 基础
Unity 3D 基础
Unity 编辑器界面
Unity 编辑器由五个核心面板组成,构成了游戏开发的完整工作流。
Scene 视图
Scene 视图是编辑场景的可视化工作区,开发者可以在其中自由移动、旋转、缩放游戏对象,并通过 Gizmos 辅助线查看碰撞体、灯光范围、音频源等组件信息。Scene 视图提供 2D/3D 切换、渲染模式(Shaded/Wireframe/UV 等)以及视角对齐工具。
Game 视图
Game 视图模拟最终发布的游戏画面,显示主摄像机渲染的内容。提供不同分辨率/宽高比模拟、Maximize on Play 功能,以及 Gizmos 开关用于调试。
Project 窗口
Project 窗口管理项目的所有资源文件(Assets),采用文件夹和资源的树形结构。支持按类型筛选、搜索,并显示资源的预览缩略图。资源在 Project 窗口中的组织结构直接映射到构建后的资源加载路径。
Hierarchy 窗口
Hierarchy 窗口以树形列表展示当前场景中所有游戏对象(GameObject),支持父子层级嵌套。Hierarchy 中的对象顺序影响渲染顺序,父对象的变换(Transform)会传递给子对象。
Inspector 窗口
Inspector 窗口显示当前选中对象的详细属性,包括 Transform 组件以及所有挂载的脚本组件。开发者可在 Inspector 中直接修改序列化字段的值,并支持 Preset 预设保存。
游戏对象与组件
Unity 采用组件化架构,GameObject 是容器,Component 是行为单元。
Transform 组件
每个 GameObject 必有一个 Transform 组件,定义对象的位置(Position)、旋转(Rotation)和缩放(Scale)。Transform 支持父子级联变换。
// Transform 基本操作
transform.position = new Vector3(1, 2, 3);
transform.rotation = Quaternion.Euler(0, 90, 0);
transform.localScale = Vector3.one * 2;
transform.Translate(Vector3.forward * Time.deltaTime);
transform.Rotate(Vector3.up, 45 * Time.deltaTime);Mesh Renderer
Mesh Renderer 负责将网格(Mesh)数据渲染到屏幕上,结合 Material 定义外观。常用属性包括 Cast Shadows、Receive Shadows、Lightmap 参数等。
// 运行时修改材质
GetComponent<Renderer>().material.color = Color.red;
GetComponent<Renderer>().material.SetTexture("_MainTex", newTexture);Collider
Collider 定义对象的物理碰撞体积。Unity 提供 Box Collider、Sphere Collider、Capsule Collider、Mesh Collider 等基本类型,以及 Wheel Collider 等专用类型。Collider 可以是触发器(IsTrigger)用于非物理碰撞检测。
void OnCollisionEnter(Collision collision) {
Debug.Log($"碰撞到: {collision.gameObject.name}");
}
void OnTriggerEnter(Collider other) {
Debug.Log($"触发进入: {other.gameObject.name}");
}Rigidbody
Rigidbody 使 GameObject 受物理引擎控制,包括重力、碰撞响应和力的作用。
Rigidbody rb = GetComponent<Rigidbody>();
rb.AddForce(Vector3.up * 10, ForceMode.Impulse); // 瞬时力
rb.AddForce(Vector3.forward * 5, ForceMode.Force); // 持续力
rb.velocity = new Vector3(0, 5, 0); // 直接设置速度
rb.useGravity = true;
rb.isKinematic = false;Light
Unity 支持 Directional Light(方向光)、Point Light(点光源)、Spot Light(聚光灯)和 Area Light(面光源)。可设置颜色、强度、阴影类型、Cookie 等参数。
Camera
Camera 是玩家看到画面的出口,可设置视口矩形、裁剪平面(Near/Far)、投影方式(Perspective/Orthographic)、Clear Flags 等。支持多摄像机叠加实现分层渲染。
// 摄像机基本控制
Camera cam = Camera.main;
cam.fieldOfView = 60;
cam.orthographicSize = 5;C# 脚本开发
MonoBehaviour 是 Unity 脚本的基类,提供了丰富的生命周期方法。
using UnityEngine;
public class PlayerController : MonoBehaviour
{
[SerializeField] private float moveSpeed = 5f;
[SerializeField] private Rigidbody rb;
// 对象实例化时调用,即使脚本未启用
private void Awake()
{
rb = GetComponent<Rigidbody>();
Debug.Log("Awake called");
}
// 脚本启用时调用,用于初始化
private void Start()
{
Debug.Log("Start called");
}
// 每帧调用,用于帧率相关的更新(输入、动画等)
private void Update()
{
float h = Input.GetAxis("Horizontal");
float v = Input.GetAxis("Vertical");
transform.Translate(new Vector3(h, 0, v) * moveSpeed * Time.deltaTime);
if (Input.GetKeyDown(KeyCode.Space))
{
rb.AddForce(Vector3.up * 10, ForceMode.Impulse);
}
}
// 固定时间间隔调用(默认 0.02s),用于物理更新
private void FixedUpdate()
{
// 物理计算放在这里
}
// 每帧在 Update 之后调用,用于摄像机跟随等
private void LateUpdate()
{
// 摄像机跟随逻辑
}
// 对象被销毁时调用
private void OnDestroy()
{
Debug.Log("OnDestroy called");
}
}生命周期顺序总结
| 方法 | 调用时机 | 用途 |
|---|---|---|
| Awake | 对象实例化后立即调用 | 引用获取、变量初始化 |
| OnEnable | 对象启用时 | 注册事件、开启协程 |
| Start | 首次 Update 前 | 依赖其他对象的初始化 |
| Update | 每帧 | 输入检测、逻辑更新 |
| FixedUpdate | 固定时间步长 | 物理运算 |
| LateUpdate | 每帧 Update 后 | 摄像机跟随 |
| OnDisable | 对象禁用时 | 注销事件 |
| OnDestroy | 对象销毁前 | 资源清理 |
物理系统
刚体与碰撞检测
Rigidbody 提供了多种碰撞检测模式以适应不同场景:
- Discrete:默认模式,每帧检测一次,适合低速运动
- Continuous:连续检测,防止高速物体穿透静态碰撞体
- Continuous Dynamic:连续检测,防止高速物体穿透动态和静态碰撞体
- Continuous Speculative:推测性连续检测,性能优于 Continuous
// 碰撞事件
void OnCollisionEnter(Collision collision)
{
ContactPoint contact = collision.contacts[0];
Debug.Log($"碰撞点: {contact.point}");
Debug.Log($"碰撞法线: {contact.normal}");
Debug.Log($"相对速度: {collision.relativeVelocity.magnitude}");
}
void OnCollisionStay(Collision collision) { }
void OnCollisionExit(Collision collision) { }Collision vs Trigger
- Collision:物理碰撞,对象间会相互阻挡,产生力的交互。两个对象都需要 Collider,至少一个需要 Rigidbody。
- Trigger:触发器模式,启用 IsTrigger 后对象可穿透,仅触发事件回调而不产生物理力的交互。
// Trigger 事件(不需要有 Rigidbody,但至少一方有 Collider)
void OnTriggerEnter(Collider other)
{
if (other.CompareTag("Pickup"))
{
Destroy(other.gameObject);
ScoreManager.Instance.AddScore(10);
}
}
void OnTriggerStay(Collider other) { }
void OnTriggerExit(Collider other) { }射线检测 Raycast
射线检测是从一个点沿方向发射虚拟射线,检测碰撞体的技术,广泛用于射击、点击选择、视线检测等场景。
// 屏幕点击发射射线
void Update()
{
if (Input.GetMouseButtonDown(0))
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray, out hit, 100f))
{
Debug.Log($"击中: {hit.collider.gameObject.name},位置: {hit.point}");
// 可用于射击伤害、对象拾取等
}
}
}
// 3D 球体射线检测敌人
RaycastHit[] hits = Physics.SphereCastAll(transform.position, 2f, transform.forward, 10f);
foreach (var hit in hits)
{
if (hit.collider.CompareTag("Enemy"))
{
Debug.Log("检测到敌人");
}
}
// 可视化调试射线
void OnDrawGizmos()
{
Gizmos.color = Color.red;
Gizmos.DrawRay(transform.position, transform.forward * 10f);
}Unity 进阶
UI 系统
Unity UI(uGUI)基于 Canvas 渲染,采用 Screen Space 或 World Space 坐标系。
Canvas
Canvas 是所有 UI 元素的根容器,支持三种渲染模式:
- Screen Space - Overlay:UI 始终覆盖在 3D 场景之上,最常用
- Screen Space - Camera:UI 由指定摄像机渲染,支持 3D 效果
- World Space:UI 放置在 3D 世界中,适用于 VR/AR 或世界内 UI
// 动态创建 Canvas
GameObject canvasGO = new GameObject("MyCanvas");
Canvas canvas = canvasGO.AddComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
canvasGO.AddComponent<CanvasScaler>();
canvasGO.AddComponent<GraphicRaycaster>();Image
Image 组件显示 Sprite 图片,支持四种 Image Type:Simple、Sliced(九宫格拉伸)、Tiled、Filled(可用于进度条)。
Image img = GetComponent<Image>();
img.sprite = Resources.Load<Sprite>("UI/icon_heart");
img.color = Color.red;
img.fillAmount = 0.75f; // Filled 模式时有效Button
Button 是可交互组件,支持多种 Transition(Color Tint/Sprite Swap/Animation),通过 onClick 事件绑定响应方法。
Button btn = GetComponent<Button>();
btn.onClick.AddListener(() => {
Debug.Log("按钮被点击");
});
// 在 Inspector 中拖拽绑定方法,或代码动态绑定
public void OnStartGame()
{
SceneManager.LoadScene("GameScene");
}Text / TextMeshPro
Text 用于显示文本内容。推荐使用 TextMeshPro(TMP),支持更丰富的文字样式和字体渲染。
using TMPro;
TextMeshProUGUI tmpText = GetComponent<TextMeshProUGUI>();
tmpText.text = $"得分: {score}";
tmpText.fontSize = 36;
tmpText.color = Color.yellow;
tmpText.alignment = TextAlignmentOptions.Center;Toggle
Toggle 提供开关选择功能,常用于设置面板。
Toggle toggle = GetComponent<Toggle>();
toggle.isOn = true;
toggle.onValueChanged.AddListener((isOn) => {
AudioListener.volume = isOn ? 1f : 0f;
});Slider
Slider 用于范围值选择,如音量调节、进度条等。
Slider slider = GetComponent<Slider>();
slider.minValue = 0;
slider.maxValue = 100;
slider.value = 50;
slider.onValueChanged.AddListener((value) => {
Debug.Log($"Slider 值: {value}");
});ScrollView
ScrollView 提供可滚动的内容区域,支持水平和垂直滚动。
// 动态添加内容到 ScrollView
Transform content = scrollRect.content;
for (int i = 0; i < 50; i++)
{
GameObject item = Instantiate(itemPrefab, content);
item.GetComponentInChildren<TextMeshProUGUI>().text = $"Item {i + 1}";
}布局组件
- Horizontal Layout Group:子元素水平排列
- Vertical Layout Group:子元素垂直排列
- Grid Layout Group:网格排列
- Layout Element:控制单个元素的大小和弹性
// 添加布局组
HorizontalLayoutGroup hlg = go.AddComponent<HorizontalLayoutGroup>();
hlg.spacing = 10;
hlg.childAlignment = TextAnchor.MiddleCenter;
hlg.childForceExpandWidth = false;EventSystem
EventSystem 是 UI 事件处理的核心,管理输入模块(Standalone Input Module / Touch Input Module)和射线投射。每个 UI 场景必须包含一个 EventSystem。
// 自定义事件接口
public class ClickHandler : MonoBehaviour, IPointerClickHandler, IPointerEnterHandler
{
public void OnPointerClick(PointerEventData eventData)
{
Debug.Log($"点击: {eventData.button}");
}
public void OnPointerEnter(PointerEventData eventData)
{
Debug.Log("鼠标进入");
}
}动画系统
Animator 与 Animation Controller
Animator 组件配合 Animator Controller 实现复杂动画控制。Animator Controller 是一种状态机(State Machine),管理动画状态及其切换条件。
状态机基础
Idle → Run(条件: Speed > 0.1)
Run → Jump(条件: IsJumping = true)
Jump → Idle(条件: IsGrounded = true)[SerializeField] private Animator animator;
void Update()
{
float speed = new Vector3(rb.velocity.x, 0, rb.velocity.z).magnitude;
animator.SetFloat("Speed", speed);
animator.SetBool("IsGrounded", isGrounded);
animator.SetTrigger("Jump");
animator.SetInteger("WeaponType", weaponIndex);
}混合树(Blend Tree)
混合树允许在多个动画之间平滑过渡,常用于运动动画。按参数(如速度)在行走、跑步等动画间插值。
Blend Tree (参数: Speed)
├── Idle (Speed = 0)
├── Walk (Speed = 0.5)
└── Run (Speed = 1.0)IK(Inverse Kinematics)
IK 反向动力学使角色的末端(手、脚)自动适配目标位置,用于角色抓取物体、踩地形等。
void OnAnimatorIK(int layerIndex)
{
Animator animator = GetComponent<Animator>();
if (rightHandTarget != null)
{
animator.SetIKPositionWeight(AvatarIKGoal.RightHand, 1f);
animator.SetIKRotationWeight(AvatarIKGoal.RightHand, 1f);
animator.SetIKPosition(AvatarIKGoal.RightHand, rightHandTarget.position);
animator.SetIKRotation(AvatarIKGoal.RightHand, rightHandTarget.rotation);
}
// 脚部 IK 适配地面
animator.SetIKPositionWeight(AvatarIKGoal.LeftFoot, 1f);
animator.SetIKPosition(AvatarIKGoal.LeftFoot, footPosition);
}骨骼动画
骨骼动画通过控制骨骼链(Bone Hierarchy)驱动蒙皮网格(Skinned Mesh Renderer)变形。Unity 使用 Avatar 映射骨骼与动画数据。
预制体(Prefab)
Prefab 是可复用的游戏对象模板,所有实例与 Prefab Asset 保持关联。
// 实例化预制体
[SerializeField] private GameObject enemyPrefab;
void SpawnEnemy()
{
Vector3 spawnPos = new Vector3(Random.Range(-10, 10), 0, Random.Range(-10, 10));
GameObject enemy = Instantiate(enemyPrefab, spawnPos, Quaternion.identity);
enemy.transform.SetParent(enemyParent);
}Nested Prefab(嵌套预制体)
Unity 2018.3+ 支持预制体内嵌套子预制体,修改子预制体时所有引用同步更新。适用于角色装备、建筑物模块等复杂层级结构。
Prefab Variant
Prefab Variant 基于已有预制体创建变体,继承基预制体的所有属性并允许覆盖。适用于同类型敌人的不同变种、不同颜色的 UI 元素等。
// 在 Variant 中覆盖基类属性
public class EnemyVariant : MonoBehaviour
{
[SerializeField] private EnemyBase baseEnemy;
void Start()
{
// 覆盖血量
baseEnemy.health = 200;
}
}资源管理
Resources 系统
Resources 文件夹下的资源可通过 Resources.Load() 运行时加载,适合小型项目原型开发。缺点:所有 Resources 中的资源会随主程序打包,无法增量更新。
// 从 Resources 加载资源
GameObject prefab = Resources.Load<GameObject>("Prefabs/Player");
Material mat = Resources.Load<Material>("Materials/Steel");
Texture2D tex = Resources.Load<Texture2D>("Textures/ground");
// 批量加载
Object[] allEnemies = Resources.LoadAll("Prefabs/Enemies");
// 异步加载
ResourceRequest request = Resources.LoadAsync<GameObject>("Prefabs/Boss");
yield return request;
GameObject boss = request.asset as GameObject;AssetBundle
AssetBundle 是 Unity 官方的资源分包方案,支持按需下载和增量更新。适合大型项目和热更新场景。
// AssetBundle 加载流程
IEnumerator LoadAssetBundle()
{
string url = "http://example.com/assetbundles/characters";
UnityWebRequest request = UnityWebRequestAssetBundle.GetAssetBundle(url);
yield return request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
AssetBundle bundle = DownloadHandlerAssetBundle.GetContent(request);
GameObject character = bundle.LoadAsset<GameObject>("Knight");
Instantiate(character);
bundle.Unload(false); // false: 仅卸载资源包数据,保留已实例化资源
}
}
// AssetBundle 依赖管理
AssetBundleManifest manifest = bundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
string[] dependencies = manifest.GetAllDependencies("characters");Addressables
Addressables 是 Unity 现代资源管理系统,基于 AssetBundle 但提供更高级的 API。支持远程/本地资源的抽象寻址、依赖自动管理、引用计数和自动卸载。
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
// 加载并实例化
AsyncOperationHandle<GameObject> handle = Addressables.InstantiateAsync("Enemies/Boss");
handle.Completed += (op) => {
GameObject boss = op.Result;
boss.transform.position = spawnPoint;
};
// 加载并手动管理
AsyncOperationHandle<GameObject> loadHandle = Addressables.LoadAssetAsync<GameObject>("Prefabs/Player");
yield return loadHandle;
GameObject playerPrefab = loadHandle.Result;
Addressables.Release(loadHandle);
// 检查资源是否已加载
Addressables.GetResourceLocationsAsync("Enemies/Boss");三种方案对比
| 特性 | Resources | AssetBundle | Addressables |
|---|---|---|---|
| 加载方式 | 同步/异步 | 异步 | 异步 |
| 内存管理 | 手动 | 手动 | 引用计数 |
| 依赖管理 | 无 | 需 Manifest | 自动 |
| 远程加载 | 不支持 | 支持 | 支持 |
| 热更新 | 不支持 | 支持 | 支持 |
| 学习成本 | 低 | 高 | 中 |
| 适用场景 | 小项目/原型 | 大型项目 | 中大型项目 |
Unreal Engine 5 基础
编辑器界面
Viewport
Viewport 是 UE5 的 3D 场景编辑视口,支持透视/正交视图切换、光照模式(Lit/Unlit/Wireframe/Shading Models)、视口类型(Top/Front/Side/Perspective)。支持 LOD 预览、反射和环境光照可视化。
Content Browser
Content Browser 管理项目的所有资源,采用文件夹树 + 资源缩略图网格的展示方式。支持按资源类型、名称、标签搜索,以及资产迁移(Migrate)和引用查看(Reference Viewer)。
Outliner
Outliner(世界大纲)以树形列表展示关卡中所有 Actor,支持搜索、分组、类型过滤。拖拽可修改 Actor 的父子层级关系,右键菜单提供批量操作(锁定、隐藏、选择等)。
Details
Details 面板展示选中 Actor 或资源的全部可编辑属性,相当于 Unity 的 Inspector。支持属性搜索、类别折叠、数组编辑、蓝图和 C++ 属性的实时编辑。
蓝图可视化脚本
蓝图是 UE5 的可视化脚本系统,通过连接节点(Node)和引脚(Pin)实现逻辑。
EventGraph
EventGraph 是蓝图的默认事件处理区域,包含:
- Event BeginPlay:关卡开始运行时调用,对应 Unity 的 Start
- Event Tick:每帧调用,对应 Unity 的 Update
- Event ActorBeginOverlap:重叠开始事件
- Event Hit:碰撞事件
节点通过执行线(白色箭头)和数据线(彩色引脚)连接。典型的蓝图流程:
Event BeginPlay → Print String "Hello Unreal" → Delay 2s → Destroy ActorFunction
蓝图函数是可以重用的逻辑片段,可以在 EventGraph 和其他函数中被调用。支持输入/输出参数和纯函数(Pure Function,不修改状态的只读函数)。
Blueprint Class
Blueprint Class 是 UE5 最核心的脚本类型,继承自 UObject 或 AActor。可定义变量(Variable)、函数(Function)、事件(Event)以及自定义组件。
Macro
Macro(宏)类似于函数,但在编译时展开,不会产生函数调用开销。适用于简单的逻辑组合。Macro Library 提供全局宏集合。
蓝图通信方式
Cast(类型转换)
// C++ 等价逻辑
ABaseCharacter* Character = Cast<ABaseCharacter>(OtherActor);
if (Character)
{
Character->TakeDamage(50);
}在蓝图中使用 Cast To 节点,将对象引用转换为目标类型后访问其变量和方法。
Event Dispatcher
Event Dispatcher(事件分发器)是一种多播委托,允许一个对象广播事件,多个监听者接收。绑定和解绑灵活,适用于观察者模式。
// 定义 Event Dispatcher: OnPlayerDeath
// 绑定: Bind Event to OnPlayerDeath → UIBlueprint 中的 UpdateHUD 函数
// 触发: OnPlayerDeath.Broadcast()Interface
蓝图接口允许多个不相关的类实现相同的方法集合。通过接口调用无需 Cast,提高系统的松耦合程度。
// 定义接口: IInteractable
// 方法: Interact / GetInteractPrompt / CanInteract
// 实现: DoorBP、ChestBP、NPCBP 分别实现 IInteractableC++ 开发
UObject
UObject 是 UE5 所有对象的基类,提供反射、GC 垃圾回收、序列化、网络复制等基础设施。
UCLASS(Blueprintable)
class MYGAME_API UMyObject : public UObject
{
GENERATED_BODY()
public:
UPROPERTY(BlueprintReadOnly, Category = "Stats")
float Damage;
UFUNCTION(BlueprintCallable, Category = "Actions")
void ApplyDamage();
};AActor / APawn / ACharacter / AGameMode
- AActor:可以放入关卡的对象基类,是所有 Actor 的根。
- APawn:可被控制器(Controller)控制的 Actor,是玩家或 AI 的物理表现形式。
- ACharacter:继承自 APawn,自带 CharacterMovementComponent 和 CapsuleComponent,专为角色设计。
- AGameMode:定义游戏规则(获胜条件、生成规则、玩家状态等),每关一个实例。
// 自定义角色
UCLASS()
class MYGAME_API AMyCharacter : public ACharacter
{
GENERATED_BODY()
public:
AMyCharacter();
protected:
virtual void BeginPlay() override;
virtual void Tick(float DeltaTime) override;
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement")
float WalkSpeed = 600.0f;
UFUNCTION(BlueprintCallable, Category = "Combat")
void Attack();
private:
void MoveForward(float Value);
void MoveRight(float Value);
void LookUp(float Value);
};UProperty / UFUNCTION 反射系统
UE5 通过 UHT(Unreal Header Tool)处理标记了 UPROPERTY 和 UFUNCTION 的代码,生成反射元数据。这使得蓝图可访问 C++ 属性/方法,并支持序列化、网络复制、GC 跟踪。
UCLASS()
class AWeapon : public AActor
{
GENERATED_BODY()
public:
// 蓝图可读,任何地方可写
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
UStaticMeshComponent* Mesh;
// 编辑任何实例,蓝图可读写
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
int32 AmmoCount;
// 仅编辑器可见
UPROPERTY(VisibleInstanceOnly, Category = "Debug")
bool bIsEquipped;
// 网络复制
UPROPERTY(Replicated, BlueprintReadOnly, Category = "State")
float Health;
// 蓝图可调用
UFUNCTION(BlueprintCallable, Category = "Actions")
void Fire();
// 可在蓝图中重写
UFUNCTION(BlueprintNativeEvent, Category = "Events")
void OnHit(AActor* OtherActor);
// 纯虚函数,必须在蓝图中实现
UFUNCTION(BlueprintImplementableEvent, Category = "Events")
void OnPickup();
};反射标记常用规范
| 标记 | 用途 |
|---|---|
| EditAnywhere | 可在任何详情面板编辑 |
| VisibleAnywhere | 详情面板只读显示 |
| BlueprintReadWrite | 蓝图可读写 |
| BlueprintReadOnly | 蓝图只读 |
| Category | 详情面板分组名称 |
| Replicated | 网络复制 |
| Transient | 不序列化 |
| SaveGame | 可保存到存档 |
UMG UI
UMG(Unreal Motion Graphics)是 UE5 的 UI 系统,类似 Unity 的 uGUI。
Widget Blueprint
UI 通过 Widget Blueprint 创建,包含 Designer 面板(可视化布局)和 Graph 面板(逻辑绑定)。
Canvas Panel
Canvas Panel 是 UMG 的根布局容器,类似于 Unity Canvas。子控件通过 Anchors 和 Offsets 进行定位,支持屏幕适配。
Button
按钮控件,支持 Click 事件绑定,可通过 Styles 设置 Normal/Hovered/Pressed 三种状态的显示效果。
// C++ 中绑定按钮事件
UButton* StartButton = Cast<UButton>(GetWidgetFromName(TEXT("Btn_Start")));
if (StartButton)
{
StartButton->OnClicked.AddDynamic(this, &UMyHUD::OnStartClicked);
}Text Block
文本显示控件,支持自定义字体、大小、颜色、对齐方式和富文本(Rich Text)。
Image
图片显示控件,支持 Brush 设置纹理、着色、九宫格切片。
控件动画
UMG 支持在 Widget Blueprint 中创建动画,支持对任何属性的关键帧驱动(位置、旋转、缩放、透明度、颜色等)。可通过 Play Animation / Reverse Animation 节点控制播放。
UPROPERTY(Transient, meta = (BindWidgetAnim))
UWidgetAnimation* FadeInAnimation;
void PlayFadeIn()
{
if (FadeInAnimation)
{
PlayAnimation(FadeInAnimation, 0.0f, 1, EUMGSequencePlayMode::Forward);
}
}事件绑定
// 通过 BindWidget 自动绑定蓝图控件
UCLASS()
class UMyHUD : public UUserWidget
{
GENERATED_BODY()
public:
UPROPERTY(meta = (BindWidget))
UTextBlock* HealthText;
UPROPERTY(meta = (BindWidget))
UProgressBar* HealthBar;
void UpdateHealth(float Current, float Max)
{
if (HealthText)
{
HealthText->SetText(FText::AsNumber((int32)Current));
}
if (HealthBar)
{
HealthBar->SetPercent(Current / Max);
}
}
};Unity vs UE5 对比
编程语言
| 方面 | Unity | Unreal Engine |
|---|---|---|
| 主语言 | C# | C++ |
| 脚本系统 | Mono / IL2CPP | 蓝图 + C++ |
| 编译速度 | 快(增量编译) | 慢(C++ 全量编译,Hot Reload 有限) |
| 能力上限 | 受限于 .NET 分配 | 完全控制内存和指针 |
| 开发效率 | C# 语法简洁,上手快 | C++ 复杂,蓝图可视化降低入门门槛 |
| 热更新 | C# 反射 / ILRuntime / HybridCLR | C++ 需 DLL 热加载或蓝图 |
Unity C# 示例
public class Player : MonoBehaviour
{
public int health = 100;
public void TakeDamage(int damage)
{
health -= damage;
if (health <= 0) Die();
}
}UE5 C++ 等效代码
UCLASS()
class APlayerCharacter : public ACharacter
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 Health = 100;
UFUNCTION(BlueprintCallable)
void TakeDamage(int32 Damage)
{
Health -= Damage;
if (Health <= 0) Die();
}
void Die();
};渲染管线对比
| 方面 | Unity | Unreal Engine |
|---|---|---|
| 默认渲染 | Built-in Render Pipeline | Deferred Renderer |
| 可编程管线的渲染 | URP(通用渲染管线) | Custom Render Pass |
| 高端渲染管线 | HDRP(高清晰度渲染管线) | 默认 + Lumen + Nanite |
| 全局光照 | 烘焙光照贴图 / 探针 | Lumen(动态实时全局光照) |
| 几何体渲染 | LOD + 遮挡剔除 | Nanite(虚拟化微多边形几何体) |
| 后处理 | Post Processing Volume | Post Process Volume(更强大) |
| 体积效果 | HDRP 支持体积云/雾 | 原生体积云/雾 |
| 光线追踪 | HDRP 支持 DXR | 原生 DXR 支持 |
URP vs HDRP vs Built-in
- Built-in:经典管线,适合老旧项目,性能可预测但功能有限。
- URP:轻量级可编程管线,适合移动端、Switch、低端 PC,画质与性能平衡。
- HDRP:高端管线,适合 PC/主机,支持高级光照和着色器效果,性能开销大。
UE5 Lumen 和 Nanite
- Lumen:实时动态全局光照系统,无需烘焙光照贴图即可实现间接光照反射。支持 Emission、Sky Light、反射弹射等效果。
- Nanite:虚拟化微多边形几何体系统,直接导入高精模型(数百万三角面)而无需手动 LOD,像素级粒度渲染。
适用场景
| 场景 | Unity 优势 | UE5 优势 |
|---|---|---|
| 移动端游戏 | 大量移动游戏(原神使用 Unity 定制版) | 优化难,发热高 |
| 2D 游戏 | 原生 2D 支持(Sprite/Sprite Atlas/Tilemap) | 主要面向 3D,2D 需额外工作 |
| PC 独立游戏 | 中型项目,小团队开发效率高 | 适合高画质要求项目 |
| 主机/3A 大作 | 少数 3A(如《崩坏:星穹铁道》) | 行业标准(《黑神话:悟空》《堡垒之夜》) |
| VR/AR | 占据主要市场份额 | 效果好但硬件要求高 |
| 建筑/工业可视化 | 轻量级,快速原型 | 高保真渲染,Digital Twin |
| 影视动画 | 使用较少的 | 大量使用 MetaHuman / Sequencer |
| WebGL/小游戏 | 原生 WebGL 导出 | 不支持 |
学习曲线对比
| 阶段 | Unity | Unreal Engine |
|---|---|---|
| 入门 | C# 基础 + Inspector 操作,1-2 周可做原型 | 蓝图可视化入门快,C++ 门槛高 |
| 中级 | 掌握资产管线、光照烘焙、移动端优化 | 理解 GameplayAbilitySystem、动画蓝图、网络复制 |
| 高级 | SRP 编程、ECS/DOTS、IL2CPP 优化 | 引擎源码定制、渲染管线修改、多线程优化 |
| 学习资源 | 大量中文教程、Asset Store 资源 | Epic 官方 Learn 平台、社区活跃 |
生态与资源商店对比
| 方面 | Unity Asset Store | Unreal Marketplace |
|---|---|---|
| 资源数量 | 超 10 万资源 | 超 5 万资源 |
| 定价模式 | 免费 + 付费(开发者自主定价) | 部分免费 + 付费 |
| 收入分成 | 积累到一定门槛后抽取 | Epic 收取 12% |
| 资源质量 | 良莠不齐 | 官方审核,整体质量较高 |
| 特色资源 | DoozyUI、Odin、TextMeshPro(免费) | Paragon 角色包、MetaHuman、Megascans |
| 代码插件 | 丰富(AI、网络、工具类) | 较少(主要在 C++ 插件) |
| 编辑器扩展 | 大量免费/付费工具 | 较少,主要通过蓝图 |
服务器对接
Unity 与服务器通信
UnityWebRequest(HTTP/HTTPS)
UnityWebRequest 是 Unity 推荐的 HTTP 通信组件,替代了曾经的 WWW 类。
using UnityEngine;
using UnityEngine.Networking;
using System.Collections;
public class NetworkManager : MonoBehaviour
{
// GET 请求
public IEnumerator GetRequest(string url)
{
using (UnityWebRequest request = UnityWebRequest.Get(url))
{
request.SetRequestHeader("Authorization", "Bearer " + authToken);
yield return request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
string response = request.downloadHandler.text;
Debug.Log($"GET 成功: {response}");
}
else
{
Debug.LogError($"GET 失败: {request.error}");
}
}
}
// POST 请求(JSON 数据)
public IEnumerator PostRequest(string url, object data)
{
string jsonData = JsonUtility.ToJson(data);
using (UnityWebRequest request = UnityWebRequest.Post(url, "POST"))
{
byte[] bodyRaw = System.Text.Encoding.UTF8.GetBytes(jsonData);
request.uploadHandler = new UploadHandlerRaw(bodyRaw);
request.downloadHandler = new DownloadHandlerBuffer();
request.SetRequestHeader("Content-Type", "application/json");
yield return request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
string response = request.downloadHandler.text;
var result = JsonUtility.FromJson<LoginResponse>(response);
Debug.Log($"登录成功: {result.playerId}");
}
}
}
// 下载 AssetBundle
public IEnumerator DownloadBundle(string url)
{
using (UnityWebRequest request = UnityWebRequestAssetBundle.GetAssetBundle(url))
{
yield return request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
AssetBundle bundle = DownloadHandlerAssetBundle.GetContent(request);
// 使用 bundle 加载资源
}
}
}
}TCP Socket
对于需要长连接和低延迟的游戏(如 MMORPG、MOBA),使用 TCP Socket 维持持久连接。
using System;
using System.Net.Sockets;
using System.Threading;
using System.Collections.Concurrent;
public class TcpClientManager : IDisposable
{
private TcpClient client;
private NetworkStream stream;
private Thread receiveThread;
private bool isConnected;
private readonly ConcurrentQueue<byte[]> receiveQueue = new ConcurrentQueue<byte[]>();
private readonly object sendLock = new object();
public event Action<byte[]> OnDataReceived;
public event Action OnDisconnected;
public async void Connect(string host, int port)
{
try
{
client = new TcpClient();
await client.ConnectAsync(host, port);
stream = client.GetStream();
isConnected = true;
receiveThread = new Thread(ReceiveLoop) { IsBackground = true };
receiveThread.Start();
}
catch (Exception e)
{
Debug.LogError($"连接失败: {e.Message}");
}
}
private void ReceiveLoop()
{
byte[] headerBuffer = new byte[4];
while (isConnected)
{
try
{
// 先读取 4 字节消息头(消息长度)
int read = 0;
while (read < 4)
{
int bytes = stream.Read(headerBuffer, read, 4 - read);
if (bytes <= 0) throw new IOException("连接断开");
read += bytes;
}
int messageLength = BitConverter.ToInt32(headerBuffer, 0);
byte[] dataBuffer = new byte[messageLength];
read = 0;
while (read < messageLength)
{
int bytes = stream.Read(dataBuffer, read, messageLength - read);
if (bytes <= 0) throw new IOException("连接断开");
read += bytes;
}
receiveQueue.Enqueue(dataBuffer);
// 在主线程触发事件
if (OnDataReceived != null)
{
UnityMainThreadDispatcher.Instance.Enqueue(() =>
{
while (receiveQueue.TryDequeue(out byte[] data))
{
OnDataReceived?.Invoke(data);
}
});
}
}
catch (Exception e)
{
Debug.LogError($"接收错误: {e.Message}");
isConnected = false;
OnDisconnected?.Invoke();
break;
}
}
}
public void Send(byte[] data)
{
if (!isConnected || stream == null) return;
lock (sendLock)
{
try
{
byte[] lengthPrefix = BitConverter.GetBytes(data.Length);
stream.Write(lengthPrefix, 0, 4);
stream.Write(data, 0, data.Length);
}
catch (Exception e)
{
Debug.LogError($"发送失败: {e.Message}");
}
}
}
public void Disconnect()
{
isConnected = false;
stream?.Close();
client?.Close();
}
public void Dispose()
{
Disconnect();
receiveThread?.Abort();
}
}WebSocket
WebSocket 适用于需要双向实时通信的场景,Unity 中可使用原生 WebSocket 或第三方库(如 BestHTTP、NativeWebSocket)。
using System;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
public class WebSocketClient
{
private ClientWebSocket ws;
private CancellationTokenSource cts;
public event Action<string> OnMessage;
public event Action OnConnected;
public event Action OnClosed;
public async Task Connect(string url)
{
ws = new ClientWebSocket();
cts = new CancellationTokenSource();
try
{
await ws.ConnectAsync(new Uri(url), cts.Token);
OnConnected?.Invoke();
_ = ReceiveLoop();
}
catch (Exception e)
{
Debug.LogError($"WebSocket 连接失败: {e.Message}");
}
}
private async Task ReceiveLoop()
{
byte[] buffer = new byte[4096];
while (ws.State == WebSocketState.Open)
{
WebSocketReceiveResult result = await ws.ReceiveAsync(
new ArraySegment<byte>(buffer), cts.Token);
if (result.MessageType == WebSocketMessageType.Close)
{
await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None);
OnClosed?.Invoke();
break;
}
string message = Encoding.UTF8.GetString(buffer, 0, result.Count);
OnMessage?.Invoke(message);
}
}
public async Task Send(string message)
{
byte[] data = Encoding.UTF8.GetBytes(message);
await ws.SendAsync(new ArraySegment<byte>(data),
WebSocketMessageType.Text, true, cts.Token);
}
public void Close()
{
cts?.Cancel();
ws?.Dispose();
}
}JSON 序列化
[System.Serializable]
public class LoginRequest
{
public string username;
public string password;
}
[System.Serializable]
public class LoginResponse
{
public bool success;
public string token;
public string playerId;
public int gold;
}
// 使用 JsonUtility(内置,功能有限)
string json = JsonUtility.ToJson(loginRequest);
LoginResponse response = JsonUtility.FromJson<LoginResponse>(json);
// 使用 Newtonsoft.Json(功能更丰富)
using Newtonsoft.Json;
string json = JsonConvert.SerializeObject(data);
var obj = JsonConvert.DeserializeObject<LoginResponse>(json);Protobuf 序列化
// 1. 定义 .proto 文件
// message PlayerData {
// int32 playerId = 1;
// string name = 2;
// int32 level = 3;
// repeated int32 itemIds = 4;
// }
// 2. 使用 protoc 生成 C# 代码
// protoc --csharp_out=./Generated ./Protos/game.proto
// 3. 序列化与反序列化
using Google.Protobuf;
// 序列化发送
PlayerData player = new PlayerData
{
PlayerId = 1001,
Name = "Hero",
Level = 50
};
byte[] data = player.ToByteArray();
tcpClient.Send(data);
// 反序列化接收
PlayerData received = PlayerData.Parser.ParseFrom(receivedData);
Debug.Log($"玩家 {received.Name} 等级 {received.Level}");断线重连策略
public class ReconnectionHandler
{
private int maxRetryCount = 5;
private float baseDelay = 1f;
private float maxDelay = 30f;
private int currentRetry;
public IEnumerator TryReconnect(TcpClientManager client, string host, int port)
{
currentRetry = 0;
while (currentRetry < maxRetryCount)
{
float delay = Mathf.Min(baseDelay * Mathf.Pow(2, currentRetry), maxDelay);
Debug.Log($"等待 {delay}s 后重连 (第 {currentRetry + 1}/{maxRetryCount} 次)");
yield return new WaitForSeconds(delay);
client.Connect(host, port);
if (client.IsConnected)
{
Debug.Log("重连成功");
// 恢复会话(重新登录、同步状态)
yield break;
}
currentRetry++;
}
Debug.LogError("重连失败,已达最大重试次数");
// 返回主菜单或显示断开提示
}
}UE5 与服务器通信
FHttpModule(HTTP/HTTPS)
#include "HttpModule.h"
#include "Interfaces/IHttpRequest.h"
#include "Interfaces/IHttpResponse.h"
#include "Dom/JsonObject.h"
#include "Serialization/JsonReader.h"
#include "Serialization/JsonSerializer.h"
void UMyNetworkComponent::SendHttpRequest()
{
FHttpRequestRef Request = FHttpModule::Get().CreateRequest();
Request->SetURL(TEXT("https://api.example.com/login"));
Request->SetVerb(TEXT("POST"));
Request->SetHeader(TEXT("Content-Type"), TEXT("application/json"));
Request->SetHeader(TEXT("Authorization"), FString::Printf(TEXT("Bearer %s"), *AuthToken));
// 构建 JSON 请求体
TSharedPtr<FJsonObject> JsonObj = MakeShareable(new FJsonObject);
JsonObj->SetStringField(TEXT("username"), Username);
JsonObj->SetStringField(TEXT("password"), Password);
FString RequestBody;
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&RequestBody);
FJsonSerializer::Serialize(JsonObj.ToSharedRef(), Writer);
Request->SetContentAsString(RequestBody);
Request->OnProcessRequestComplete().BindUObject(this, &UMyNetworkComponent::OnResponseReceived);
Request->ProcessRequest();
}
void UMyNetworkComponent::OnResponseReceived(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful)
{
if (bWasSuccessful && Response.IsValid())
{
FString ResponseStr = Response->GetContentAsString();
int32 StatusCode = Response->GetResponseCode();
// 解析 JSON 响应
TSharedPtr<FJsonObject> JsonResponse;
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(ResponseStr);
if (FJsonSerializer::Deserialize(Reader, JsonResponse) && JsonResponse.IsValid())
{
bool bSuccess = JsonResponse->GetBoolField(TEXT("success"));
FString Token = JsonResponse->GetStringField(TEXT("token"));
int32 PlayerId = JsonResponse->GetIntegerField(TEXT("playerId"));
UE_LOG(LogTemp, Log, TEXT("登录成功,PlayerId: %d"), PlayerId);
}
}
else
{
UE_LOG(LogTemp, Error, TEXT("HTTP 请求失败"));
}
}FSocket(TCP Socket)
#include "Sockets.h"
#include "SocketSubsystem.h"
#include "HAL/Runnable.h"
#include "HAL/RunnableThread.h"
class FTcpSocketClient : public FRunnable
{
public:
FTcpSocketClient()
: Socket(nullptr)
, bRunning(false)
, Thread(nullptr)
{
}
bool Connect(const FString& Host, int32 Port)
{
ISocketSubsystem* SocketSubsystem = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM);
TSharedRef<FInternetAddr> Address = SocketSubsystem->CreateInternetAddr();
FString ResolvedAddress;
ESocketErrors ResolveError;
if (!SocketSubsystem->GetHostByName(TCHAR_TO_ANSI(*Host), ResolvedAddress))
{
UE_LOG(LogTemp, Error, TEXT("DNS 解析失败"));
return false;
}
bool bIsValid = false;
Address->SetIp(*ResolvedAddress, bIsValid);
Address->SetPort(Port);
Socket = SocketSubsystem->CreateSocket(NAME_Stream, TEXT("GameClient"), true);
if (!Socket) return false;
bRunning = Socket->Connect(*Address);
if (bRunning)
{
Thread = FRunnableThread::Create(this, TEXT("TcpReceiveThread"));
}
return bRunning;
}
uint32 Run() override
{
TArray<uint8> HeaderBuffer;
HeaderBuffer.SetNum(4);
while (bRunning)
{
int32 ReadBytes = 0;
// 读取 4 字节消息头
if (Socket->Recv(HeaderBuffer.GetData(), 4, ReadBytes))
{
if (ReadBytes == 0) break;
int32 MessageLength = *reinterpret_cast<int32*>(HeaderBuffer.GetData());
TArray<uint8> DataBuffer;
DataBuffer.SetNum(MessageLength);
int32 TotalRead = 0;
while (TotalRead < MessageLength)
{
if (Socket->Recv(DataBuffer.GetData() + TotalRead,
MessageLength - TotalRead, ReadBytes))
{
TotalRead += ReadBytes;
}
else break;
}
OnDataReceived.Broadcast(DataBuffer);
}
}
return 0;
}
bool Send(const TArray<uint8>& Data)
{
if (!Socket || !bRunning) return false;
int32 SentBytes = 0;
int32 MessageLength = Data.Num();
// 发送消息头(4 字节长度)
TArray<uint8> Header;
Header.AddZeroed(4);
FMemory::Memcpy(Header.GetData(), &MessageLength, 4);
bool bSuccess = Socket->Send(Header.GetData(), 4, SentBytes);
if (bSuccess)
{
bSuccess = Socket->Send(Data.GetData(), Data.Num(), SentBytes);
}
return bSuccess;
}
void Disconnect()
{
bRunning = false;
if (Socket)
{
Socket->Close();
delete Socket;
Socket = nullptr;
}
if (Thread)
{
Thread->Kill(true);
delete Thread;
Thread = nullptr;
}
}
DECLARE_MULTICAST_DELEGATE_OneParam(FOnDataReceived, const TArray<uint8>&);
FOnDataReceived OnDataReceived;
private:
FSocket* Socket;
volatile bool bRunning;
FRunnableThread* Thread;
};FWebSockets
UE5 的 WebSocket 支持由 WebSockets 模块提供。
#include "WebSocketsModule.h"
#include "IWebSocket.h"
class UWebSocketClient : public UObject
{
GENERATED_BODY()
public:
TSharedPtr<IWebSocket> WebSocket;
void Connect(const FString& Url)
{
if (!FModuleManager::Get().IsModuleLoaded(TEXT("WebSockets")))
{
FModuleManager::Get().LoadModule(TEXT("WebSockets"));
}
WebSocket = FWebSocketsModule::Get().CreateWebSocket(Url);
WebSocket->OnConnected().AddLambda([]()
{
UE_LOG(LogTemp, Log, TEXT("WebSocket 已连接"));
});
WebSocket->OnMessage().AddUObject(this, &UWebSocketClient::OnMessage);
WebSocket->OnConnectionError().AddLambda([](const FString& Error)
{
UE_LOG(LogTemp, Error, TEXT("WebSocket 连接错误: %s"), *Error);
});
WebSocket->Connect();
}
void OnMessage(const FString& Message)
{
// 解析 JSON 消息
TSharedPtr<FJsonObject> JsonMessage;
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Message);
if (FJsonSerializer::Deserialize(Reader, JsonMessage))
{
FString Type = JsonMessage->GetStringField(TEXT("type"));
if (Type == TEXT("chat"))
{
FString Content = JsonMessage->GetStringField(TEXT("content"));
OnChatMessage.Broadcast(Content);
}
}
}
void SendMessage(const FString& Type, const TSharedPtr<FJsonObject>& Data)
{
if (!WebSocket.IsValid() || !WebSocket->IsConnected()) return;
TSharedPtr<FJsonObject> Message = MakeShareable(new FJsonObject);
Message->SetStringField(TEXT("type"), Type);
Message->SetObjectField(TEXT("data"), Data);
FString Output;
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&Output);
FJsonSerializer::Serialize(Message.ToSharedRef(), Writer);
WebSocket->Send(Output);
}
DECLARE_MULTICAST_DELEGATE_OneParam(FOnChatMessage, const FString&);
FOnChatMessage OnChatMessage;
};JsonObject
// 构建 JSON
TSharedPtr<FJsonObject> PlayerData = MakeShareable(new FJsonObject);
PlayerData->SetStringField(TEXT("name"), TEXT("Hero"));
PlayerData->SetNumberField(TEXT("level"), 50);
PlayerData->SetBoolField(TEXT("isOnline"), true);
// 数组字段
TArray<TSharedPtr<FJsonValue>> Items;
Items.Add(MakeShareable(new FJsonValueNumber(1001)));
Items.Add(MakeShareable(new FJsonValueNumber(1002)));
PlayerData->SetArrayField(TEXT("items"), Items);
// 嵌套对象
TSharedPtr<FJsonObject> Position = MakeShareable(new FJsonObject);
Position->SetNumberField(TEXT("x"), 10.0);
Position->SetNumberField(TEXT("y"), 0.0);
Position->SetNumberField(TEXT("z"), 20.0);
PlayerData->SetObjectField(TEXT("position"), Position);
// 序列化为字符串
FString JsonString;
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&JsonString);
FJsonSerializer::Serialize(PlayerData.ToSharedRef(), Writer);
// 反序列化
TSharedPtr<FJsonObject> ParsedData;
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(JsonString);
if (FJsonSerializer::Deserialize(Reader, ParsedData))
{
int32 Level = ParsedData->GetIntegerField(TEXT("level"));
FString Name = ParsedData->GetStringField(TEXT("name"));
}Protobuf 集成
UE5 集成 Protobuf 需要编译 protobuf 库并引入插件。
// 1. 在 Build.cs 中添加依赖
// PublicDependencyModuleNames.AddRange(new string[] { "Protobuf" });
// 2. 使用 protoc 生成 C++ 代码
// protoc --cpp_out=./Source/Game/Protos ./Protos/game.proto
// 3. 序列化发送
#include "Game.pb.h"
void SendPlayerData()
{
game::PlayerData PlayerData;
PlayerData.set_player_id(1001);
PlayerData.set_name("Hero");
PlayerData.set_level(50);
PlayerData.add_item_ids(1001);
PlayerData.add_item_ids(1002);
std::string SerializedData;
PlayerData.SerializeToString(&SerializedData);
TArray<uint8> DataArray(
reinterpret_cast<const uint8*>(SerializedData.data()),
SerializedData.size()
);
TcpClient.Send(DataArray);
}
// 4. 反序列化接收
void OnDataReceived(const TArray<uint8>& Data)
{
game::PlayerData PlayerData;
if (PlayerData.ParseFromArray(Data.GetData(), Data.Num()))
{
int32 PlayerId = PlayerData.player_id();
FString Name = UTF8_TO_TCHAR(PlayerData.name().c_str());
UE_LOG(LogTemp, Log, TEXT("收到玩家数据: %s (ID: %d)"), *Name, PlayerId);
// 解析道具列表
for (int i = 0; i < PlayerData.item_ids_size(); i++)
{
int32 ItemId = PlayerData.item_ids(i);
UE_LOG(LogTemp, Log, TEXT(" 道具 ID: %d"), ItemId);
}
}
}