UnityECS CharacterControllerPackage
UnityECSにチャレンジしようとするとき、Unity既存のCharacterControllerと同じような機能を持つCharacterControllerPackageが存在します。
今回これを使ってモデルをアニメーションさせる方法と、RootMotionでCharacterControllerを動かす方法について紹介します。
※インストール方法や基本の使い方は割愛
サンプルを取り込む
CharacterControllerのPackageからサンプルをインストールします
このサンプルで基本の動く操作ができるようになっています。
今回はこのサンプルに付け足して、アニメーションモデルの表示、アニメーション、RootMotion対応をやっていきます。
※サンプルの使い方は割愛
HybridAnimation
Unityには自前ではECS用のアニメーションシステムがありません。
ですので今回はゲームオブジェクトを上に重ねる方法をとるHybridAnimationの方法をご紹介します。
UnityECSの世界といままでのUnityのゲームオブジェクトの世界はまるで別物で、両者同士は明示的に連携をさせないと一切関わりあうことができません。
しかし画面の上では、同じワールド座標に入れさえいればあたかも両者が同じ世界にいるように錯覚させることができるのです。(実際はただ別の世界線で重なってるだけ)
HybridAnimation Component Authoring
ということでコードになります。
まずThirdPersonCharacterComponent
を変更します
[Serializable]
public struct ThirdPersonCharacterComponent : IComponentData
{
+ public Entity MeshRootEntity;
+ public bool UseRootMotion;
public float RotationSharpness;
public float GroundMaxSpeed;
public float GroundedMovementSharpness;
public float AirAcceleration;
public float AirMaxSpeed;
public float AirDrag;
public float JumpSpeed;
public float3 Gravity;
public bool PreventAirAccelerationAgainstUngroundedHits;
public BasicStepAndSlopeHandlingParameters StepAndSlopeHandling;
}
[Serializable]
public struct CharacterControl : IComponentData
{
public float3 MoveVector;
+ public float3 RootMotionVelocity;
public float3 AccumulatedDeltaPosition;
public bool Jump;
}
自身のメッシュの位置情報をゲームオブジェクトに伝えるためのMeshRootEntity
ルートモーションを使うか否かのUseRootMotion
を追加します。
ルートモーションのヴェロシティの値を保持するRootMotionVelocity
を追加
また、ECSでアニメーションを動かすために必要なCharacterHybridData
コンポーネントとアニメーションを呼び出すために必要なCharacterHybridLink
を作成します。
[Serializable]
public class CharacterHybridData : IComponentData
{
public GameObject MeshPrefab;
public bool isCopyCollider;
}
[Serializable]
public class CharacterHybridLink : ICleanupComponentData
{
public GameObject Object;
public Animator Animator;
}
struct
ではなくclass
になるので注意してください。
Authoringは以下の通りです。
先ほど作成したCharacterHybridLink
はSystem
で動的に追加していきます。
[DisallowMultipleComponent]
public class ThirdPersonCharacterAuthoring : MonoBehaviour
{
public AuthoringKinematicCharacterProperties CharacterProperties = AuthoringKinematicCharacterProperties.GetDefault();
[Header("References")]
+ public GameObject MeshPrefab;
+ public GameObject MeshRoot;
+ public bool UseRootMotion;
public float RotationSharpness = 25f;
public float GroundMaxSpeed = 10f;
public float GroundedMovementSharpness = 15f;
public float AirAcceleration = 50f;
public float AirMaxSpeed = 10f;
public float AirDrag = 0f;
public float JumpSpeed = 10f;
public float3 Gravity = math.up() * -30f;
public bool PreventAirAccelerationAgainstUngroundedHits = true;
public BasicStepAndSlopeHandlingParameters StepAndSlopeHandling = BasicStepAndSlopeHandlingParameters.GetDefault();
public class Baker : Baker<ThirdPersonCharacterAuthoring>
{
public override void Bake(ThirdPersonCharacterAuthoring authoring)
{
KinematicCharacterUtilities.BakeCharacter(this, authoring.gameObject, authoring.CharacterProperties);
Entity entity = GetEntity(TransformUsageFlags.Dynamic | TransformUsageFlags.WorldSpace);
AddComponent(entity, new ThirdPersonCharacterComponent
{
+ MeshRootEntity = GetEntity(authoring.MeshRoot, TransformUsageFlags.Dynamic),
+ UseRootMotion = authoring.UseRootMotion,
RotationSharpness = authoring.RotationSharpness,
GroundMaxSpeed = authoring.GroundMaxSpeed,
GroundedMovementSharpness = authoring.GroundedMovementSharpness,
AirAcceleration = authoring.AirAcceleration,
AirMaxSpeed = authoring.AirMaxSpeed,
AirDrag = authoring.AirDrag,
JumpSpeed = authoring.JumpSpeed,
Gravity = authoring.Gravity,
PreventAirAccelerationAgainstUngroundedHits = authoring.PreventAirAccelerationAgainstUngroundedHits,
StepAndSlopeHandling = authoring.StepAndSlopeHandling,
});
AddComponent(entity, new ThirdPersonCharacterControl());
+ AddComponentObject(entity, new CharacterHybridData
+ {
+ MeshPrefab = authoring.MeshPrefab,
+ });
}
}
}
動かしたいモデルにアニメーションコントローラーを付けそれをプレハブとして保存しておきます。
このPackageではThirdPersonCharacterの位置がルートとずれているので、ルートの位置に合わせるようにThirdPersonCharacterの下の階層にMeshRoot
オブジェクトを追加します。
作成後上記で作成したモデルのプレハブとMeshRootをThirdPersonCharacterにアタッチします。
HybridAnimationSystem
HybridAnimationSystemとして以下を新規に追加します。
public partial class PlayerHybridSystem : SystemBase
{
protected override void OnUpdate()
{
EntityCommandBuffer ecb = SystemAPI.GetSingletonRW<EndSimulationEntityCommandBufferSystem.Singleton>().ValueRW.CreateCommandBuffer(World.Unmanaged);
//Create
// CharacterHybridDataを持つエンティティに対して処理を行う
// この処理の中で`CharacterHybridLink`を追加しており、WithNone<CharacterHybridLink>で
// 次回以降は呼び出さないようにしている
foreach (var (hybridData, entity) in SystemAPI.Query<CharacterHybridData>()
.WithNone<CharacterHybridLink>()
.WithEntityAccess())
{
// EditorでアタッチしたGameObject(モデル)をインスタンシエイトして、そのオブジェクトを取得
// そのオブジェクトが持っているAnimatorも取得しておく
GameObject tmpObject = GameObject.Instantiate(hybridData.MeshPrefab);
Animator animator = tmpObject.GetComponent<Animator>();
// 取得したゲームオブジェクトとAnimatorをCharacterHybridLinkに入れ、現在のEntityにAddComponetする
ecb.AddComponent(entity, new CharacterHybridLink
{
Object = tmpObject,
Animator = animator
});
}
// Update
// 上記のCharacterHybridLinkのコンポーネントを付けたことでこの処理が動き出す。
foreach (var (characterTransform, characterComponent, characterControl, hybridLink, entity) in SystemAPI.Query<
LocalTransform,
CharacterComponent,
RefRW<CharacterControl>,
CharacterHybridLink>()
.WithEntityAccess())
{
if (hybridLink.Object)
{
// RootMotion実装処理(内容は後述)
if (characterComponent.UseRootMotion)
{
// CharacterControlのRootMotionVelocityにAnimatorから受け取ったdeltaPositionを渡す
characterControl.ValueRW.RootMotionVelocity = hybridLink.Animator.deltaPosition;
}
// ゲームオブジェクトをmeshRootの位置に合わせる
// 上記でアタッチしたRootMeshの位置が取得できるので、その位置にゲームオブジェクトを持ってくる
LocalToWorld meshRootLTW = SystemAPI.GetComponent<LocalToWorld>(characterComponent.MeshRootEntity);
hybridLink.Object.transform.position = meshRootLTW.Position;
hybridLink.Object.transform.rotation = meshRootLTW.Rotation;
}
}
}
}
これでアニメーションを行うゲームオブジェクトとECSのCharacterControllerの位置が重ね合わさります。
RootMotionがOnの時、Animatorから取得できるdeltaPosition
を与えることで、CharacterControllerがアニメーションの移動速度に合わせることが常時できるようになっています。
次に入力に対してアニメーションを動かしていきます。
まずアニメーション用のコンポーネントを作成します。
public struct PlayerAnimation : IComponentData
{
// パラメーターを取得できたかどうか(アニメーションのパラメーターの取得は一回きりでいいため)
public bool ParameterLoaded;
//Hash値を保存
public int SpeedParameterHash;
//public int AttackParameterHash;
//Parameterの値を保存
public float SpeedMagnitude;
//public bool AttackTrigger;
}
入力を処理するPlayerAnimationSystem
を作成します。
public partial class PlayerAnimationSystem : SystemBase
{
protected override void OnUpdate()
{
// Create
// CharacterHybridLinkコンポーネントがEntityに対して処理を行う
foreach (var (playerAnimation, hybridLink, entity) in SystemAPI.Query<RefRW<PlayerAnimation>, CharacterHybridLink>().WithEntityAccess())
{
// パラメーターのハッシュ値を設定
if(!characterAnimation.ValueRO.ParameterLoaded)
{
Animator animator = hybridLink.Animator;
// Find the clipIndex param
for (int i = 0; i < animator.parameters.Length; i++)
{
// speedのパラメーターのハッシュを取得
if (animator.parameters[i].name == "speed")
{
playerAnimation.ValueRW.SpeedParameterHash = animator.parameters[i].nameHash;
}
// 今回割愛
//if (animator.parameters[i].name == "attack")
//{
// playerAnimation.ValueRW.AttackParameterHash = animator.parameters[i].nameHash;
//}
}
characterAnimation.ValueRW.ParameterLoaded = true;
}
}
var ecb = new EntityCommandBuffer(Allocator.Temp);
//Update
// CharacterControl,PlayerAniamtion,CharacterHybridLinkを持つエンティティに対して実行
foreach (var (playerAnimation,player, playerInputs, hybridLink, charactercontrol, entity) in SystemAPI.Query<
RefRW<PlayerAnimation>,
RefRW<PlayerComponent>,
PlayerInputs,
CharacterHybridLink,
CharacterControl>()
.WithEntityAccess())
{
if (hybridLink.Object)
{
// Animation
if (hybridLink.Animator)
{
// 入力から取得したMoveVectorを取得し絶対値を保持
playerAnimation.ValueRW.SpeedMagnitude = math.length(charactercontrol.MoveVector);
// PlayerAnimationHandlerを呼び出し、パラメーターの値を変える
// plyaerAniamtionなどに保存していた値をすべて投げる
PlayerAnimationHandler.UpdateSpeed(hybridLink.Animator, ref characterAnimation.ValueRW);
//if(characterAnimation.ValueRO.AttackTrigger)
//{
// ecb.AddComponent(entity, new AttackTrigger
// {
// Animator = hybridLink.Animator,
// AttackTriggerHash = characterAnimation.ValueRO.AttackParameterHash
// });
//}
}
}
}
}
}
このシステムではUpdateでプレイヤーからの入力ベクトルを読み取りそれをAnmator.SetFloat(hasmap,float);
の形に持っていきたいので全部PlayerAnimationHandler
に投げ入れてます。
以下がPlayerAnimationHandler
public static class PlayerAnimationHandler
{
// これを呼び出す
public static void UpdateSpeed(
Animator animator,
ref PlayerAnimation playerAnimation)
{
animator.SetFloat(playerAnimation.SpeedParameterHash, playerAnimation.SpeedMagnitude);
}
//public static void OnAttackTrigger(AttackTrigger trigger)
//{
// trigger.Animator.SetTrigger(trigger.AttackTriggerHash);
//}
}
これで入力した移動量によってアニメーションがなされるようになります。
RootMotion対応
先ほどAnimator.deltaPosition
はCharacterControl
のVelocityに入れていましたがそれをどう処理しているかについて解説します。
変更する場所は以下の通りです
CharacterAspect
のVariableUpdate()
とHandleVelocityControl()
の以下の部分です
[BurstCompile]
public void VariableUpdate(ref CharacterUpdateContext context, ref KinematicCharacterUpdateContext baseContext)
{
ref KinematicCharacterBody characterBody = ref KinematicAspect.CharacterBody.ValueRW;
ref CharacterComponent characterComponent = ref CharacterComponent.ValueRW;
ref CharacterControl characterControl = ref CharacterControl.ValueRW;
ref quaternion characterRotation = ref KinematicAspect.LocalTransform.ValueRW.Rotation;
// AccumulatedDeltaPositionにRootMotionVelcityを追加する
+ characterControl.AccumulatedDeltaPosition += characterControl.RootMotionVelocity;
// Add rotation from parent body to the character rotation
// (this is for allowing a rotating moving platform to rotate your character as well, and handle interpolation properly)
//KinematicCharacterUtilities.AddVariableRateRotationFromFixedRateRotation(ref characterRotation, characterBody.RotationFromParent, baseContext.Time.DeltaTime, characterBody.LastPhysicsUpdateDeltaTime);
// Rotate towards move direction
if (math.lengthsq(characterControl.MoveVector) > 0f)
{
CharacterControlUtilities.SlerpRotationTowardsDirectionAroundUp(ref characterRotation, baseContext.Time.DeltaTime, math.normalizesafe(characterControl.MoveVector), MathUtilities.GetUpFromRotation(characterRotation), characterComponent.RotationSharpness);
}
}
[BurstCompile]
private void HandleVelocityControl(ref CharacterUpdateContext context, ref KinematicCharacterUpdateContext baseContext)
{
+ float deltaTime = baseContext.Time.DeltaTime; //deltaTimeを取得
ref KinematicCharacterBody characterBody = ref KinematicAspect.CharacterBody.ValueRW;
ref CharacterComponent characterComponent = ref CharacterComponent.ValueRW;
ref CharacterControl characterControl = ref CharacterControl.ValueRW;
// Rotate move input and velocity to take into account parent rotation
if(characterBody.ParentEntity != Entity.Null)
{
characterControl.MoveVector = math.rotate(characterBody.RotationFromParent, characterControl.MoveVector);
characterBody.RelativeVelocity = math.rotate(characterBody.RotationFromParent, characterBody.RelativeVelocity);
}
// Move on ground
if (characterBody.IsGrounded)
{
// RootMotionを使うときの処理
if(characterComponent.UseRootMotion == true)//UseRootMotion
{
//先ほど追加したAccumulatedDeltaPosisonにdeltaTimeを割ったvelocityを渡している
+ float3 targetVelocity = characterControl.AccumulatedDeltaPosition / deltaTime;
CharacterControlUtilities.StandardGroundMove_Interpolated(ref characterBody.RelativeVelocity, targetVelocity, characterComponent.GroundedMovementSharpness, deltaTime, characterBody.GroundingUp, characterBody.GroundHit.Normal);
// Jump
if (characterControl.Jump)
{
CharacterControlUtilities.StandardJump(ref characterBody, characterBody.GroundingUp * characterComponent.JumpSpeed, true, characterBody.GroundingUp);
}
}
else//NoRootMotion
{
float3 targetVelocity = characterControl.MoveVector * characterComponent.GroundMaxSpeed;
CharacterControlUtilities.StandardGroundMove_Interpolated(ref characterBody.RelativeVelocity, targetVelocity, characterComponent.GroundedMovementSharpness, deltaTime, characterBody.GroundingUp, characterBody.GroundHit.Normal);
// Jump
if (characterControl.Jump)
{
CharacterControlUtilities.StandardJump(ref characterBody, characterBody.GroundingUp * characterComponent.JumpSpeed, true, characterBody.GroundingUp);
}
}
}
これでアニメーションの移動量に対してしっかりと追随してくれるようになります。
少し駆け足になりましたがこれで終わります。