Unity实战
U2D
角色
导入图片 > 纹理类型:Sprite(2D和UI) Sprite模式:单一/多个
输入控制:编辑 > 项目设置 > 输入管理器
输入检测:
1
2
3
var horizontal = Input.GetAxis("Horizontal"); // 有渐变 生输入用GetAxisRaw
var vertical = Input.GetAxis("Vertical");
transform.position += 4.0f * Time.deltaTime * new Vector3(horizontal, vertical);
为了后续支持,我们可以使用新版输入系统: 项目设置 > 玩家 > 其他设置 > 配置 > 活动输入处理 窗口 > 包管理器 > 安装Input System 项目 > 创建 > Input Actions(命名为GameControl
) 并编辑 创建输入映射(命名为Player
) 创建动作(命名为Move
) 对于移动来说 动作类型为Value 控制类型为Vector 2 给动作添加绑定 这里添加上下左右组合体 给四个方向分别绑定 在Input Actions 生成 C# 类 编写代码,命名有对应。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class GameInput : MonoBehaviour
{
private GameControl gameControl;
private void Awake()
{
gameControl = new GameControl();
gameControl.Player.Enable();
}
public Vector3 GetInputMovementDirection()
{
Vector2 inputVector = gameControl.Player.Move.ReadValue<Vector2>();
return new Vector3(inputVector.x, 0, inputVector.y).normalized;
}
}
创建事件处理器,生命周期相同的事件可以不销毁,静态事件和生命周期不同的时候记得销毁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/* 在GameInput中 */
private GameControl gameControl;
public event EventHandler OnInteract;
private void Awake()
{
gameControl = new GameControl();
gameControl.Player.Enable();
gameControl.Player.Interact.performed += Interact_Performed;
}
private void OnDestroy() // 换场景 销毁的时候也要一起销毁掉,不然再进来就会重复注册
{
gameControl.Player.Interact.performed -= Interact_Performed;
gameControl.Dispose();
}
private void Interact_Performed(UnityEngine.InputSystem.InputAction.CallbackContext obj)
{
OnInteract?.Invoke(this, EventArgs.Empty);
}
/* 在Player中 */
private void Start()
{
gameInput.OnInteract += GameInput_OnInteract;
}
private void GameInput_OnInteract(object sender, System.EventArgs e)
{
HandleInteraction();
}
1
2
3
4
5
// 如果是静态事件 也要记得取消注册
public static void ClearStaticEvent()
{
OnCut = null;
}
触发的方式:用户输入 -> gameControl.Player.Interact -> Interact_Performed -> OnInteract.Invoke() -> GameInput_OnInteract -> HandleInteraction()
设定帧率:
1
Application.targetFrameRate = 10;
渲染层级: Sprite Rneder > 其他设置 > 图层顺序:越大越靠前
按y排序层级: 项目设置 > 图形 > 摄像机设置 > 拖名都排序模式:自定义轴 透明度排序轴: (0, 1, 0) Sprite Rneder > Sprite排序点:轴心(pivot) 图片导入时 Sprite编辑器 设置轴心
预制体(Prefab):资源预制,即模板
角色的物理
刚体:Rigidbody 碰撞器:Collider 制止旋转:Rigidbody 2D > Constraints > Freeze Rotation 设置为触发器:Collider > Is Trigger
碰撞检测:
1
2
3
4
5
6
7
8
9
private void OnTriggerEnter2D(Collider2D collision)
{
var luna = collision.GetComponent<LunaController>();
if (luna != null)
{
luna.Change_health(1);
Destroy(gameObject);
}
}
用刚体移动
1
2
3
4
5
private void FixedUpdate()
{
var position = (Vector2)transform.position + walkSpeed * speedMultiplier * Time.fixedDeltaTime * lookDirection;
rigidbody2d.MovePosition(position);
}
检测范围的碰撞器:
1
2
3
4
5
Collider2D collider = Physics2D.OverlapCircle(rigidbody2d.position, 0.5f, LayerMask.GetMask("NPC"));
if (collider != null)
{
// 此处为语法
}
给NPC在检查器里添加Layer。
动画
给角色添加动画控制器(Animator) 挂载控制器文件(*.controller)
打开动画(ctrl+6)和动画器窗口 创建动画(*.anim) 添加要动画的属性 添加关键帧 修改属性值 也可以开启录制模式 拖到相应时间 直接在检查器里修改 简单的可以直接把图片拖到对象上 循环动画在动画的检查器里设置循环 调节速度可以修改采样,或者括起来就可以整体缩放
2D游戏不需要动画之间的过渡 在动画器里删除掉过渡 在动画器里添加状态切换时参考的参数 再检查器里添加切换条件 为了及时应用条件,删除退出时间 多个过渡可以设置优先级
混合树(BlendTree) 设置多个动画的混合
生成物体:
1
2
3
4
5
6
// 在某位置生成
Instantiate(effectGO, transform.position, Quaternion.identity);
// 这是直接给transform 然后把局部位置充值
Instantiate(healEffectGO, transform).transform.localPosition = Vector3.zero;
延迟销毁:
1
Destroy(gameObject, DestroyTimeSec);
摄像机
摄像机跟随,安装包: 窗口 > 包管理器 > 安装Cinemachine 在层级中创建Cinemachine > Virtual Camera 在检查器中设置跟随对象 在检查器中设置相机边界 Extensions > Add Extension > Confiner 2D 给地图增加Polygon Collider 2D 设置为触发器 添加给Confiner 2D的Bounding Shape 2D
调整摄像机位置到视角: 游戏对象 > 对齐到视图
UI
创建 > UI > 画布(Canvas) Rect Transform:更适合UI的transform Canvas:渲染模式:屏幕空间(布局)、屏幕空间(摄像机)、世界空间(注意一下,世界空间里的按钮,放背面了就点击不到了) Canvas Scaler:缩放模式
- 恒定像素大小
- 屏幕大小缩放
- 恒定物理大小
创建 > UI > 图片 子图片的锚点预设 在锚点预设中按下alt可以自动填充
图片、文本、面板等,在Godot里学过了,此处大同小异故,略。 九宫格的背景可以在精灵编辑器里设置border 应用九宫格的图片UI需要在检查器的Image > 图像类型 选择已切片(Tiled) 图片 把大小设置为原始大小:
1
characterImage.SetNativeSize();
UI的类,需要使用:
1
using UnityEngine.UI;
设置血条的函数:
1
2
3
4
5
public void SetHP(float fillPercent)
{
// hpMaskImage是血条遮罩
hpMaskImage.rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, fillPercent * hpBarWidth);
}
进度条的设计:图像,图像类型> 填充
永远面向摄像机:
1
2
3
4
void LateUpdate()
{
transform.LookAt(Camera.main.transform);
}
保持同向
1
transform.forward = -Camera.main.transform.forward;
图片布局组件:Grid Layout Group
DOTween
下载,然后随意拖到工程内
1
using SG.Tweening
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 创建Tween对象
Tween tween = transform.DOMove(target.position, moveTimeSec);
// 默认开启 可以暂停
tween.Pause();
tween.Play();
// 倒着播放
tween.PlayBackwards();
// ease函数
tween.SetEase(Ease.InOutSine);
// 结束后的回调
tween.OnComplete(() => { });
// kill掉tween
tween.Kill();
// tween序列
Sequence sequence = DOTween.Sequence();
sequence.Append(lunaSpriteTransform.DOLocalMoveY(1.5f, 0.25f));
sequence.Append(lunaSpriteTransform.DOLocalMoveY(0.55f, 0.25f));
sequence.Play();
音乐
1
2
3
4
5
6
7
8
9
10
11
12
13
public void playMusic(AudioClip music)
{
if (audioSource.clip != music)
{
audioSource.clip = music;
audioSource.Play();
}
}
public void playSound(AudioClip sound)
{
audioSource.PlayOneShot(sound);
}
U3D
新建工程
模板选择 3D URP(Universal Render Pipeline)即Universal 3D
删掉自带小教程:Readme:Remove Readme Assets 只保留最高质量: 编辑 > 项目设置 > 质量:只保留High Fidelity即可 对应的配置文件也可以删掉 SampleSceneProfile是默认创建的后处理文件 先删掉
后处理
Global Volume:Volume > 配置文件(Profile):创建配置文件 添加覆盖(Add Override) : Tonemapping:色调映射:模式:Neutral Color Adjustments:颜色调整: 泛光(Bloom) Vignette:相机四角的晕影
URP-HighFidelity(URP配置文件) > 质量 > 抗锯齿 在相机 > Camera > 渲染 > 抗锯齿也可以设置,不过我们还是用URP URP-HighFidelity-Renderer > 屏幕空间环境光遮挡(Screen Space Ambient Occlusion)
场景布置
创建 > 3D Object > Plane
中键:平移 右键:移动视角 Alt + 鼠标右键:以固定点旋转 Alt + 鼠标右键:放大缩小 Ctrl + 调整缩放、旋转、平移:以离散单位进行 左上角可以调节增量吸附
修改材质: Mesh Renderer > Material 默认是Lit
角色移动
方向的改变和插值。线性插值用Lerp
,方向向量(球面)差值用Slerp
1
2
3
4
5
transform.position += speed * Time.deltaTime * direction;
if (direction.magnitude > 0.1f)
{
transform.forward = Vector3.Slerp(transform.forward, direction, rotationSpeed * Time.deltaTime);
}
射线检测
在检查器里选择要检测的层。
1
2
[SerializeField]
private LayerMask layerMask;
1
2
3
4
5
6
7
if (Physics.Raycast(transform.position, transform.forward, out RaycastHit hitInfo, 2.0f, layerMask))
{
if (hitInfo.collider.gameObject.TryGetComponent<ClearCounter>(out ClearCounter counter))
{
counter.Interact();
}
}
数据对象
1
2
3
4
5
6
[CreateAssetMenu]
public class IngredientSO : ScriptableObject
{
public GameObject prefab;
public string ingredientName;
}
之后,可以在项目里创建IngredientSO
配方的数据对象
1
2
3
4
5
6
7
8
9
10
11
[Serializable]
public class CuttingRecipe
{
public IngredientSO input;
public IngredientSO output;
}
public class CuttingRecipesSO: ScriptableObject
{
public List<CuttingRecipe> list;
}
制作字体 窗口 > TextMeshPro > 字体资源创建工具
游戏切换场景
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// StartMenuUI.cs
public class StartMenuUI : MonoBehaviour
{
[SerializeField] private Button startButton;
[SerializeField] private Button quitButton;
private void Start()
{
startButton.onClick.AddListener(() =>
{
Loader.Load(Loader.Scene.GameScene);
});
quitButton.onClick.AddListener(() =>
{
Application.Quit();
});
}
}
// Loader.cs
public class Loader : MonoBehaviour
{
public enum Scene
{
GameMenuScene,
LoadingScene,
GameScene,
}
private static Scene targetScene;
public static void Load(Scene target)
{
// 先跳到Loading界面,再跳转到游戏界面
targetScene = target;
SceneManager.LoadScene((int)Scene.LoadingScene);
}
public static void LoadTargetScene() // 加载界面调
{
SceneManager.LoadScene((int)targetScene);
}
}
保存
1
2
3
4
5
6
7
8
9
private void SaveVolume()
{
PlayerPrefs.SetInt(MUSIC_VOLUME, settingsVolume);
}
private void LoadVolume()
{
settingsVolume = PlayerPrefs.GetInt(MUSIC_VOLUME, 5);
}
shader
创建 > Shader Graph > URP > Lit Shader Graph
粒子效果
发射器速度模式
多人模式
Netcode
窗口 > 包管理器 > Netcode for GameObjects
创建空对象、新建组件 > Network Manager Seletct transport > Unity transport Unity transport > Conection Data > 127.0.0.1 Network Manager > Player Prefab > 玩家的预制体 玩家预制体增加组件:Network Object 让玩家脚本并非继承MonoBehaviour而是NetworkBehaviour 现在可以运行,并创建主机
Network Manager > 日志级别
代码创建主机
1
2
3
4
5
6
startHostButton.onClick.AddListener(() =>
{
print("host");
NetworkManager.Singleton.StartHost();
Show(false);
});
文件 > 生成设置 > 玩家设置 > 分辨率和演示 > 在后台运行
现在可以生成,然后在两个游戏上分别进入主机和客户端。
从这里得知,运行日志在%USERPROFILE%\AppData\LocalLow\CompanyName\ProductName\Player.log
设置ip和端口
1
2
var transport = NetworkManager.Singleton.GetComponent<UnityTransport>();
transport.SetConnectionData(ipAddress, port);
同步
同步位置
1
2
3
4
5
6
7
8
private void FixedUpdate()
{
if (!IsOwner)
{
return;
}
HandleMovement();
}
服务端认证的方式:
给Player添加Network Transform组件
RPC(remote procedure call)
1
2
3
4
5
6
7
8
9
10
11
12
private void HandleMovementServerAuth()
{
Vector2 inputVector = GameInput.Instance.GetInputMovementDirection();
HandleMovementServerRpc(inputVector);
}
[ServerRpc(RequireOwnership =false)]
private void HandleMovementServerRpc(Vector2 inputVector)
{
// 执行运动逻辑
}
客户端认证的方式:
使用ClientNetworkTransform
新版本已经可以选了,这是旧版本的方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using Unity.Netcode.Components;
using UnityEngine;
namespace Unity.Multiplayer.Samples.Utilities.ClientAuthority
{
/// <summary>
/// Used for syncing a transform with client side changes. This includes host. Pure server as owner isn't supported by this. Please use NetworkTransform
/// for transforms that'll always be owned by the server.
/// </summary>
[DisallowMultipleComponent]
public class ClientNetworkTransform : NetworkTransform
{
/// <summary>
/// Used to determine who can write to this transform. Owner client only.
/// This imposes state to the server. This is putting trust on your clients. Make sure no security-sensitive features use this transform.
/// </summary>
protected override bool OnIsServerAuthoritative()
{
return false;
}
}
}
动画同步
玩家的PlayerAnimator脚本继承NetworkBehaviour,给玩家挂NetworkAnimator,然后只有IsOwner时执行。
同样,客户端授权模式:
1
2
3
4
5
6
7
public class OwnerNetworkAnimator : NetworkAnimator
{
protected override bool OnIsServerAuthoritative()
{
return false;
}
}
数据同步
客户端从服务端拿数据的方式:网络变量/rpc,支不支持用户中途加入
RPC方法使用:客户端只会执行ClientRpc
的代码,而主机会执行全部的代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
private void CreateOrder()
{
Order order = WeightedRandomSelection.GetRandomElement(availableOrders.list,
availableOrders.list.Select(it => it.weight).ToList());
AddNewOrderClientRpc(order);
}
[ClientRpc]
private void AddNewOrderClientRpc(Order order)
{
orders.Add(order);
OnOrderChanged?.Invoke(this, EventArgs.Empty);
}
处理报错:
1
Unity.Netcode.Editor.CodeGen.NetworkBehaviourILPP: Assets\Scripts\OrderManager.cs(98,9): error - AddNewOrderClientRpc - Don't know how to serialize Order. RPC parameter types must either implement INetworkSerializeByMemcpy or INetworkSerializable. If this type is external and you are sure its memory layout makes it serializable by memcpy, you can replace Order with ForceNetworkSerializeByMemcpy`1<Order>, or you can create extension methods for FastBufferReader.ReadValueSafe(this FastBufferReader, out Order) and FastBufferWriter.WriteValueSafe(this FastBufferWriter, in Order) to define serialization for this type.
Order无法序列化,这里可以换用更为简单的值类型。另外,List
1
2
3
4
5
6
7
[ClientRpc]
private void AddNewOrderClientRpc(int indexOfOrder)
{
Order order = orders[indexOfOrder];
orders.Add(order);
OnOrderChanged?.Invoke(this, EventArgs.Empty);
}
客户端要提交时,服务端会要求所有客户端执行代码,即,服务端会执行服务端和客户端的代码,客户端只会执行客户端的代码,所以:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 在客户端逻辑中,把要让所有人执行的逻辑换成:
DelieryCorrectOrderServerRpc()
[ServerRpc(RequireOwnership = false)] // 客户端调服务端需要权限
private void DelieryCorrectOrderServerRpc(int indexOfTrueOrder)
{
// 服务端独有的逻辑
// 同步所有的客户端
DelieryCorrectOrderClientRpc(indexOfTrueOrder);
}
[ClientRpc]
private void DelieryCorrectOrderClientRpc(int indexOfTrueOrder)
{
// 服务端和客户端都要执行的逻辑
}
生成预制体
每个同步生成的预制体,要在GameManager里,添加到NetworkPrefabs里。
我这个版本有一个DefaultNetworkPrefabs,似乎是不需要我做这一步了。
如何在网络实例化预制体:首先以值类型传递信息:
1
2
3
4
5
6
7
8
9
// 在客户端逻辑中
int ingredientIndex = GetIngredientIndex(ingredientSO);
CreateIngredientServerRpc(ingredientIndex, holderNO);
// 在ServerRpc中
var ingredientSO = IndexToIngredientSO(ingredientIndex); // 重新获得SO
var newIngredientGO = Instantiate(ingredientSO.prefab); // 生成对象
var newNetworkIngredient = newIngredientGO.GetComponent<NetworkObject>(); // 获得对象的NetworkObject
newNetworkIngredient.Spawn(true); // true表示切换场景时销毁 // 生成在网络上
如何传递网络引用:
1
2
3
4
5
6
7
8
9
10
11
12
// 在本地逻辑中
NetworkObject holderNO = holder.GetNetworkObject();
SomeFuncServerRpc(holderNO); // 自动类型转换
// 在ServerRpc中
[ServerRpc(RequireOwnership =false)]
private void SomeFuncServerRpc(NetworkObjectReference holderNORef)
{
holderNORef.TryGet(out NetworkObject holderNO);
var holder = holderNO.GetComponent<IngredientHolder>(); // 获得原类型
}
不能把动态生成的网络对象作为另一个网络对象的子对象,如果只是为了相对位置跟随,可以相应地修改相互关系的代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class FollowTransform : MonoBehaviour
{
private Transform target;
public Transform Target
{
get => target;
set => target = value;
}
private void LateUpdate()
{
if (target != null)
{
transform.position = target.position;
transform.rotation = target.rotation;
}
}
}
用网络变量来同步信息,网络变量只有服务端才能写
1
2
3
4
5
6
7
8
9
10
11
12
13
// 设置网络变量
private NetworkVariable<float> processTime = new(0);
// 每次更改时,会自动触发事件
public override void OnNetworkSpawn()
{
processTime.OnValueChanged += ProcessTime_OnValueChanged;
}
private void ProcessTime_OnValueChanged(float previousValue, float newValue)
{
// 核心逻辑
}
但是报错,他说这个变量是客户端所有,服务端没有权限修改? Write permissions (Server) for this client instance is not allowed!
检查了一下,实际问题是客户端无法修改服务端的变量。
客户端连接
监控玩家离线,现在更推荐用OnConnectionEvent
1
2
3
4
5
6
7
public override void OnNetworkSpawn()
{
if (IsServer)
{
NetworkManager.Singleton.OnClientDisconnectCallback += NetworkManager_OnClientDisconnectCallback;
}
}
也可以用来监听服务端掉线,这个事件只有服务端和掉线的客户端会收到。(返回的id总是客户端的id)
1
2
3
4
5
6
7
8
9
10
11
12
13
private void Start()
{
NetworkManager.Singleton.OnClientDisconnectCallback += NetworkManager_OnClientDisconnectCallback;
}
private void NetworkManager_OnClientDisconnectCallback(ulong id)
{
// 服务端或一个客户端有一方断开连接时,会向服务端和这个客户端发OnClientDisconnectCallback
if (id == NetworkManager.Singleton.LocalClientId)
{
// 服务端掉线的逻辑
}
}
在游戏开始后制止后来玩家加入。
Network Manager > Connection Approval 勾选
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public void StartHost()
{
NetworkManager.Singleton.ConnectionApprovalCallback += NetworkManager_ConnectionApprovalCallback; // 创建主机时,监听客户端加入
NetworkManager.Singleton.StartHost(); // 开启服务端
}
private void NetworkManager_ConnectionApprovalCallback(NetworkManager.ConnectionApprovalRequest request, NetworkManager.ConnectionApprovalResponse response)
{
if (SceneManager.GetActiveScene().name != Loader.Scene.CharacterSelectScene.ToString())
{
response.Approved = false;
response.Reason = "Game has already started";
}
else if (NetworkManager.Singleton.ConnectedClientsIds.Count >= 4)
{
response.Approved = false;
response.Reason = "Player is full";
}
else
{
response.Approved = true;
response.CreatePlayerObject = true;
}
}
主机似乎也会触发这个回调,这就不太知道该怎么办了
客户端显示拒绝原因
1
NetworkManager.Singleton.DisconnectReason;
在Unity Transport上设置连接次数和连接超时时间
踢出客户端,有时客户端断开连接的回调没有触发,可以显式调一下。
1
2
NetworkManager.Singleton.DisconnectClient(clientId, reason);
NetworkManager_Host_OnClientDisconnectCallback()
场景管理器
通过网络场景管理器来切换场景
在Network Manager里设置启用场景管理器
1
2
3
4
public static void LoadNetwork(Scene target)
{
NetworkManager.Singleton.SceneManager.LoadScene(target.ToString(), LoadSceneMode.Single);
}
客户端不需要自己切场景
1
2
3
4
5
6
7
8
9
10
createGameButton.onClick.AddListener(() =>
{
GameMultiplayer.Instance.StartHost();
Loader.LoadNetwork(Loader.Scene.CharacterSelectScene);
});
joinGameButton.onClick.AddListener(() =>
{
GameMultiplayer.Instance.StartClient();
});
不要在场景切换时销毁
1
DontDestroyOnLoad(gameObject);
手动生成玩家:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public override void OnNetworkSpawn()
{
state.OnValueChanged += State_OnValueChanged;
if (IsServer)
{
NetworkManager.Singleton.SceneManager.OnLoadEventCompleted += SceneManager_OnLoadEventCompleted; // 所有人场景加载完成
}
}
private void SceneManager_OnLoadEventCompleted(string sceneName, UnityEngine.SceneManagement.LoadSceneMode loadSceneMode, List<ulong> clientsCompleted, List<ulong> clientsTimedOut)
{
foreach (ulong clientId in NetworkManager.Singleton.ConnectedClientsIds)
{
Transform player = Instantiate(playerPrefab);
player.GetComponent<NetworkObject>().SpawnAsPlayerObject(clientId, true); // true表示切换场景时销毁
}
}
当前激活场景
1
SceneManager.GetActiveScene().name
序列化自定义类型
需要实现IEquatable
和
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public struct PlayerData : IEquatable<PlayerData>, INetworkSerializable
{
public ulong clientId;
public readonly bool Equals(PlayerData other)
{
return clientId == other.clientId;
}
public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
{
serializer.SerializeValue(ref clientId);
}
}
初始化需要在Awake()
。
Lobby
获得lobby包
项目设置>服务 跟着教程一步一步来,在unity dashboard注册,创建项目
初始化UnityServices
1
2
3
4
5
6
7
8
9
10
11
12
13
14
```cs
private async void InitializeUnityAuthentication()
{
// 初始化Unity Services,只运行一次
if (UnityServices.State != ServicesInitializationState.Initialized)
{
InitializationOptions options = new();
options.SetProfile(Random.Range(0, 10000).ToString());
await UnityServices.InitializeAsync(options);
await AuthenticationService.Instance.SignInAnonymouslyAsync(); // 以匿名方式登录
}
}
创建大厅
1
2
3
4
5
6
7
8
9
10
11
12
public async void CreateLobby(string lobbyName, bool isPrivate)
{
try
{
joinedLobby = await LobbyService.Instance.CreateLobbyAsync(lobbyName, 4, new CreateLobbyOptions { IsPrivate = isPrivate });
// 创建主机的后续逻辑
}
catch (LobbyServiceException e)
{
Debug.Log(e);
}
}
快速加入大厅
1
2
3
4
5
6
7
8
9
10
11
12
public async void QuickJoin()
{
try
{
joinedLobby = await LobbyService.Instance.QuickJoinLobbyAsync();
// 创建客户端的后续逻辑
}
catch (LobbyServiceException e)
{
Debug.Log(e);
}
}
通过房间码加入
1
joinedLobby = await LobbyService.Instance.JoinLobbyByCodeAsync(lobbyCode);
通过房间Id加入
1
joinedLobby = await LobbyService.Instance.JoinLobbyByIdAsync(lobbyId);
离开
1
await LobbyService.Instance.RemovePlayerAsync(joinedLobby.Id, AuthenticationService.Instance.PlayerId);
踢出玩家
1
await LobbyService.Instance.RemovePlayerAsync(joinedLobby.Id, playerId);
删除房间
1
2
LobbyService.Instance.DeleteLobbyAsync(joinedLobby.Id);
joinedLobby = null;
心跳
1
LobbyService.Instance.SendHeartbeatPingAsync(joinedLobby.HostId);
获取房间信息
1
QueryResponse queryResponse = await LobbyService.Instance.QueryLobbiesAsync();
判断是不是房主
1
2
3
4
private bool IsLobbyHost()
{
return joinedLobby != null && joinedLobby.HostId == AuthenticationService.Instance.PlayerId;
}
relay
同样在unity dashboard开启relay
1
2
3
4
5
6
7
8
9
10
Allocation allocation = await AllocateRelay();
string relayJoinCode = await GetRelayJoinCodeAsync(allocation);
await LobbyService.Instance.UpdateLobbyAsync(joinedLobby.Id, new UpdateLobbyOptions()
{
Data = new Dictionary<string, DataObject>()
{
{"RelayJoinCode", new DataObject(DataObject.VisibilityOptions.Member, relayJoinCode)}
}
});
NetworkManager.Singleton.GetComponent<UnityTransport>().SetRelayServerData(new RelayServerData(allocation, "dtls"));
1
2
3
string relayJoinCode = joinedLobby.Data[KEY_RELAY_JOIN_CODE].Value;
JoinAllocation joinAllocation = await JoinRelay(relayJoinCode);
NetworkManager.Singleton.GetComponent<UnityTransport>().SetRelayServerData(new RelayServerData(joinAllocation, "dtls"));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
private async Task<Allocation> AllocateRelay()
{
try
{
Allocation allocation = await RelayService.Instance.CreateAllocationAsync(4 - 1);
return allocation;
}
catch (RelayServiceException e)
{
Debug.Log(e);
return default;
}
}
private async Task<string> GetRelayJoinCodeAsync(Allocation allocation)
{
try
{
string relayJoinCode = await RelayService.Instance.GetJoinCodeAsync(allocation.AllocationId);
return relayJoinCode;
}
catch (RelayServiceException e)
{
Debug.Log(e);
return default;
}
}
private async Task<JoinAllocation> JoinRelay(string joinCode)
{
try
{
JoinAllocation joinAllocation = await RelayService.Instance.JoinAllocationAsync(joinCode);
return joinAllocation;
}
catch (RelayServiceException e)
{
Debug.Log(e);
return default;
}
}