Unity初心者のマルチプレイゲーム制作格闘記の第二弾です。
前回の続きとなりますので、見たい方は下のリンクから是非!
プレイヤーを動かす
入力関係のコンポーネント作成
早速ですが、「PlayerAuthoring」を以下の通り編集しました。
・「OwnerPlayerTag」を追加:クライアントがどのプレイヤーオブジェクトを操作するか
・「PlayerStatus」へMoveSpeedを追加:プレイヤーの移動速度
・「PlayerInput」を追加:プレイヤーの入力値を格納するコンポーネント
・追加、編集したコンポーネントのベイク処理を追記
※オーサリング時に初期値をステータスに入れるように変更しました。
using Unity.Entities;
using Unity.Mathematics;
using Unity.NetCode;
using Unity.Rendering;
using UnityEngine;
// プレイヤー認識用(インスペクターでPlayerタグつけているようなもの)
public struct PlayerTag : IComponentData { }
// InitializePlayerSystemで初期化する用の認識タグ
public struct NewPlayerTag : IComponentData { }
// 自分が操作するオブジェクトか判定する用のタグ
public struct OwnerPlayerTag : IComponentData { }
// プレイヤーステータス
public struct PlayerStatus : IComponentData
{
// GhostFieldをつけることで、サーバーからクライアントに値が同期される
[GhostField] public int HP;
[GhostField] public float MoveSpeed;
}
// 入力関係のコンポーネントはAllPredicted
// 入力同期用のIInputComponentDataを継承
[GhostComponent(PrefabType = GhostPrefabType.AllPredicted)]
public struct PlayerInput : IInputComponentData
{
// Quantization = 0で正確なデータを同期する
[GhostField(Quantization = 0)] public float2 MoveValue;
}
public class PlayerAuthoring : MonoBehaviour
{
// プレハブのインスペクターから設定できるように変更
public int HP;
public float MoveSpeed;
public class PlayerAuthoringBaker : Baker<PlayerAuthoring>
{
public override void Bake(PlayerAuthoring authoring)
{
var entity = GetEntity(TransformUsageFlags.Dynamic);
AddComponent<PlayerTag>(entity);
AddComponent<NewPlayerTag>(entity);
AddComponent(entity, new PlayerStatus
{
HP = authoring.HP,
MoveSpeed = authoring.MoveSpeed,
});
// 動的に色を変更するため
AddComponent<URPMaterialPropertyBaseColor>(entity);
AddComponent<PlayerInput>(entity);
}
}
}
操作するプレイヤーオブジェクトの初期化とタグ付け
「InitializeLocalPlayerSystem」を作成し、クライアント(自分)が操作するプレイヤーオブジェクトに、先ほど追加した入力値受取用コンポーネントと操作対象であることを示すコンポーネントを付与します。
using Unity.Collections;
using Unity.Entities;
using Unity.Mathematics;
using Unity.NetCode;
// クライアントでのみ入力情報を受け取る
[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation | WorldSystemFilterFlags.ThinClientSimulation)]
public partial struct InitializeLocalPlayerSystem : ISystem
{
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate<NetworkId>();
}
public void OnUpdate(ref SystemState state)
{
var ecb = new EntityCommandBuffer(Allocator.Temp);
// GhostOwnerIsLocalコンポーネントはGhostOwnerのNetworkIdが自分のと一致する場合に有効化されるっぽいので、これが有効なエンティティにOwnerPlayerTagをつける
foreach(var (_ , entity) in SystemAPI.Query<PlayerTag>().WithAll<GhostOwnerIsLocal>().WithNone<OwnerPlayerTag>().WithEntityAccess())
{
ecb.AddComponent<OwnerPlayerTag>(entity);
ecb.SetComponent(entity, new PlayerInput { MoveValue = float2.zero });
}
ecb.Playback(state.EntityManager);
}
}
インプットアクションの作成
以下の通り「TestInputAction」を作成し、インスペクターから「Generate C# Class」にチェックを入れます。
プレイヤーの入力をコンポーネントへセット
プレイヤーの入力を受け取り、PlayerInputコンポーネントへセットするシステム「PlayerMoveInputSystem」を作成しました。
using Unity.Entities;
using Unity.NetCode;
using UnityEngine;
// このグループはクライアント側でのみ実行される
[UpdateInGroup(typeof(GhostInputSystemGroup))]
public partial class PlayerMoveInputSystem : SystemBase
{
// InputSystemで設定したアクション
private TestInputAction _inputActions;
private Entity _ownerPlayerEntity;
protected override void OnCreate()
{
RequireForUpdate<OwnerPlayerTag>();
_inputActions = new TestInputAction();
}
protected override void OnStartRunning()
{
_inputActions.Enable();
_ownerPlayerEntity = SystemAPI.GetSingletonEntity<OwnerPlayerTag>();
}
protected override void OnStopRunning()
{
_inputActions.Disable();
_ownerPlayerEntity = Entity.Null;
}
protected override void OnUpdate()
{
// プレイヤーの入力をコンポーネントにセット
EntityManager.SetComponentData(_ownerPlayerEntity, new PlayerInput
{
MoveValue = _inputActions.GameplayMap.PlayerMovement.ReadValue<Vector2>()
});
}
}
プレイヤーを動かす
PlayerInputから入力値を読み取り、実際にプレイヤーを動かすシステム「PlayerMoveSystem」を作成しました。折角なので、BurstとC#JobSystemを使ってみます。
using Unity.Burst;
using Unity.Entities;
using Unity.NetCode;
using Unity.Transforms;
// サーバー・クライアント両方で実行される
[UpdateInGroup(typeof(PredictedSimulationSystemGroup))]
[BurstCompile]
public partial struct PlayerMoveSystem : ISystem
{
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var deltaTime = SystemAPI.Time.DeltaTime;
new PlayerMoveJob
{
DeltaTime = deltaTime
}.Schedule();
}
[BurstCompile]
public partial struct PlayerMoveJob : IJobEntity
{
public float DeltaTime;
private void Execute(ref LocalTransform transform, in PlayerInput playerInput, in PlayerStatus playerStatus, in Simulate simulate)
{
transform.Position.xy += playerInput.MoveValue * playerStatus.MoveSpeed * DeltaTime;
}
}
}
プレイヤープレハブの設定
忘れずにPlayerプレハブにアタッチしてあるPlayerAuthoringのHPとMoveSpeedに任意の値を設定しておきます。また、他プレイヤーを接触した際に等速直線運動しないようにRigidbodyのDragの値なども少し調整しておきました。
カメラをプレイヤーに追従させる
このままだとカメラがデフォルト位置から動かないので、プレイヤーに追従するようにします。
方法は様々かと思いますが、今回はメインカメラをECS上で扱えるようにして動かしてみます。
メインカメラコンポーネントの作成
「MainCamera」コンポーネントを作成し、そこにメインカメラの参照を入れます。マネージドクラスを扱うため、MainCameraがクラスであることやAddComponentObjectでエンティティに追加していることに注意です。
using Unity.Entities;
using UnityEngine;
// メインカメラの参照を保持するコンポーネント
// Cameraはマネージドクラスのため、structではなくclassで定義
public class MainCamera : IComponentData
{
public Camera Value;
}
// メインカメラ認識用
public struct MainCameraTag : IComponentData { }
public class MainCameraAuthoring : MonoBehaviour
{
public class Baker : Baker<MainCameraAuthoring>
{
public override void Bake(MainCameraAuthoring authoring)
{
var entity = GetEntity(TransformUsageFlags.Dynamic);
// マネージドクラスはAddComponentObjectで追加する
AddComponentObject(entity, new MainCamera());
AddComponent<MainCameraTag>(entity);
}
}
}
メインカメラの参照をセット
先ほど作成したコンポーネントにメインカメラの参照をセットするシステム「InitializeMainCameraSystem」を作成しました。
using Unity.Entities;
using UnityEngine;
[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)]
public partial class InitializeMainCameraSystem : SystemBase
{
protected override void OnCreate()
{
RequireForUpdate<MainCameraTag>();
}
protected override void OnUpdate()
{
Enabled = false;
var mainCameraEntity = SystemAPI.GetSingletonEntity<MainCameraTag>();
EntityManager.SetComponentData(mainCameraEntity, new MainCamera { Value = Camera.main });
}
}
カメラを動かす
操作しているプレイヤーの現在位置へメインカメラを移動させるシステム「CameraMoveSystem」を作成しました。
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
using UnityEngine;
[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)]
public partial class CameraMoveSystem : SystemBase
{
private readonly float3 MAIN_CAMERA_OFFSET = new float3(0, 0, -10);
private Entity _ownerPlayerEntity;
private Camera _mainCamera;
protected override void OnCreate()
{
RequireForUpdate<OwnerPlayerTag>();
RequireForUpdate<MainCameraTag>();
}
protected override void OnStartRunning()
{
// 操作しているプレイヤーのエンティティをキャッシュ
_ownerPlayerEntity = SystemAPI.GetSingletonEntity<OwnerPlayerTag>();
//メインカメラをキャッシュ
var cameraEntity = SystemAPI.GetSingletonEntity<MainCameraTag>();
_mainCamera = EntityManager.GetComponentObject<MainCamera>(cameraEntity).Value;
}
protected override void OnStopRunning()
{
_ownerPlayerEntity = Entity.Null;
}
protected override void OnUpdate()
{
// 操作しているプレイヤーの現在位置を取得
var playerLocalPosition = EntityManager.GetComponentData<LocalTransform>(_ownerPlayerEntity).Position;
// オフセットを追加して
_mainCamera.transform.position = playerLocalPosition + MAIN_CAMERA_OFFSET;
}
}
メインカメラオブジェクトの作成
サブシーン上に空のオブジェクト「MainCamera」を作成し、MainCameraAuthoringコンポーネントをアタッチしました。
マルチプレイの動作確認とまとめ
マルチプレイヤーのテストのためにParrelSyncを導入していますので、普段はそちらで動作確認を行っていますが、このあたりで一度ビルドして動くかどうか見てみます。(UE5での開発では、ビルドしての動作確認を怠ってずいぶん苦労させられたのでこまめにします…笑)
少しわかりにくいかもしれませんが、プレイヤーとカメラの移動と当たり判定は正常に動作しているようです!
ここまでで動画の1/3くらいですね笑
通常の実装とは一癖も二癖もあるDOTSのマルチプレイですが、自分の知識不足で別途調べていることも多いのも相まって進捗は遅いですね…しかし、こうしてアウトプットすることで確実に理解が深まっていることを感じています。
次回は参考動画から少し離れてヴァンサバらしさを応用で実装していきます!
以下のリンクからどうぞ!