ミニゲームを作ってUnityを学ぶ![ひつじコレクション編]
###第1回目: プレイヤーキャラクターの作成
まず最初に、ゲーム内でプレイヤーが操作するキャラクターを作成していきます。
この記事では主に**「移動制御・アニメーション・アニメーションイベント・ビヘイビア」**を取り扱います。
#新規プロジェクト
Unityプロジェクトを新しく作成します。
3Dにチェックが入っていることを確認したら名前を決めて新規プロジェクトを作成。
最初にシーンを保存しておきます。
- プロジェクト名: CsgActionSheep
- 画面比率: FreeAspect
- エディタレイアウト: 2by3でProjectウインドウをOneColomnに変更
- シーン名: main.unity
↑このあたりは自分の使いやすいように変更してください。
#SDユニティちゃんのインポート
プレイヤーの操作するキャラクターとしてSDユニティちゃんをインポートします。
上記リンク先より、右上のDATA DOWNLOADから**「SDユニティちゃん 3Dモデルデータ」と「ユニティちゃんシェーダー (Unity 5.4/5.5β 対応版)」**をダウンロードして取得したファイルをダブルクリック。
現在開かれているプロジェクトにアセットをインポートします。
#SDユニティちゃんをシーンに配置
お勉強のために、予め作成されているプレハブは使用せずにモデルデータのみを使用します。
- 「UnityChan/SD_unitychan/Models/SD_unitychan_humanoid」をゼロポジションに配置
- ルートオブジェクト「SD_unitychan_humanoid」を「Player」に変更
- Playerオブジェクトをプレハブに書き出す
【ゼロポジション】
TransformのPositionとRotationが全て0でScaleの値が全て1のオブジェクトを
シーンに配置する場合に、記事内では「ゼロポジションに配置」と表記しています。
#アニメーションの設定
オブジェクトにアニメーションを実装するにはMecanim(メカニム)というUnityの機能を利用します。
配置したPlayerオブジェクトにはAnimatorコンポーネントがすでにアタッチされていますので、こちらを使って必要なアニメーションを実装していきます。
Animatorの設定
- Projectビューを右クリックでCreateメニューからAnimatorControllerを作成
- AnimatorControllerをPlayerAnimControllerという名前に変更
- PlayerにアタッチされているAnimatorのControllerに上記を設定
- 同じくAnimatorについてApplyRootMotionのチェックを外す
【ApplyRootMotion】
アニメーションによる位置や回転の動きを実際のオブジェクトのpositionやRotationに反映させるかどうかという設定。
今回のPlayerオブジェクトはスクリプトで位置や回転を制御していくためチェックを外しています。
###AnimationStateの作成とAnimationClipの設定
先ほど作成したPlayerAnimContollerをダブルクリックするとAnimatorウインドウが表示されます。
このウインドウ内にアニメーションクリップやアニメーション間の遷移条件を設定していくことでアニメーションを実装することができます。
- Animatorウインドウ内で右クリック - CreateState - Emptyで「Idle」という名前のAnimationStateを作成
- 同様にして「Run, Down」のステートを作成
- 各StateのInspectorビューで以下のようにMotionに対応するAnimationClipを設定
State | Clip |
---|---|
Idle | UnityChan/SD_unitychan/Animations/SD_unitychan_motion_humanoid/Standing@loop |
Run | ~/Running@loop |
Down | ~/KneelDown |
###トランジションを設定
作成したState間を遷移するために必要なトランジション(矢印)を設定します。
- 各Stateを右クリック - MakeTransitionで下図のように遷移先へ矢印を伸ばす
一番最初に作成したIdleにはEntryからすでに矢印が伸びています。
このEntryから続いているStateがデフォルトで再生されるアニメーションとなります。
###遷移条件を設定
続いて矢印の方向に遷移するための条件を設定していきますが、まずは条件式に必要なフィールドを先に作成してしまいます。
- AnimatorウインドウParametersタグの右下にある+ボタンからBoolを選択
- 「IsRun」という名前で確定し、Bool型のIsRunフィールドを作成
- 同様に+ボタンからTriggerを選択し、「DoDown」フィールドを作成
この2つのフィールドを使ってアニメーションの遷移条件を設定します。
- IdleからRunへ延びる矢印を選択
- InspectorビューからHasExitTimeのチェックを外す
- Settingsを選択し、FixedDurationを0.125に設定
- Conditionsの+ボタンを選択し、IsRun - trueを設定
【HasExitTime】
チェックを入れるとステート遷移の条件が「遷移前のアニメーションの実行回数」になる。
遷移までの実行回数はSettings内のExitTimeで設定することができる。
ex)
ExitTime = 0.5: アニメーションが半分再生されたタイミングで遷移
ExitTime = 2: アニメーションがループして2回目の再生が終了したタイミングで遷移
今回は遷移条件をIsRunがtrueになったタイミングにしているためチェックを外しています。
【FixedDuration】
後述のTransitionDurationについての設定。
チェックを入れた場合はTransitionDurationで扱う値が「秒数」となり
チェックを入れない場合は遷移前のアニメーションクリップの再生時間を1としたときの「割合」になる。
【TransitionDuration】
ステートの遷移が開始してから完了するまでにかける時間。
元のアニメーションAからアニメーションBへはここで設定した時間をかけて切り替わり、
その間の動きをUnityが補間してくれるため全く異なる動作でもスムーズに繋がっているように見える。
0ならば即座に切り替わるが、もちろん動作を補間しないため場合によってはギクシャクして見える。
今回はFixedDurationにチェックが入ったうえで0.125を設定することで
0.125秒かけてアニメーションの切り替えを行っています。
【Conditions】
ステートが遷移するための条件を設定する。
今回はIsRunがtrueに切り替わったタイミングでIdleからRunへ遷移します。
これでIdleステートからRunステートへ遷移する条件が設定できました。
続けてRunからIdleに戻る矢印を選択し、先ほどと同じ設定からConditionsの部分のみをIsRunがfalseになったときに変更して遷移条件を設定します。
さらにIdleとRunそれぞれからDownに伸びる2つの矢印について、下表のように設定します。
パラメータ | 値 |
---|---|
HasExitTime | チェック無し |
FixedDuration | チェック有り |
TransitionDuration | 0 |
Conditions | DoDown |
IdleまたはRunアニメーションが再生中にDoDownトリガーがONになったタイミングで即座にDownアニメーションが開始されます。
#スクリプトからアニメーションを制御する
AnimatorControllerの設定が完了しましたので、スクリプトからアニメーションを制御できるようにします。
- PlayerAnimationという名前のスクリプトを作成し、Playerオブジェクトにアタッチ
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerAnimation : MonoBehaviour
{
public enum ANIM_ID
{
IDLE = 0,
RUN,
DOWN
}
public ANIM_ID AnimId { get; private set; }
void Awake()
{
InitAnim();
}
private Animator mAnimator;
private int mIdIsRun;
private int mIdDoDown;
private void InitAnim()
{
mAnimator = GetComponent<Animator>();
mIdIsRun = Animator.StringToHash("IsRun");
mIdDoDown = Animator.StringToHash("DoDown");
}
public void Play(ANIM_ID id)
{
AnimId = id;
switch (AnimId)
{
case ANIM_ID.IDLE:
mAnimator.SetBool(mIdIsRun, false);
break;
case ANIM_ID.RUN:
mAnimator.SetBool(mIdIsRun, true);
break;
case ANIM_ID.DOWN:
mAnimator.SetTrigger(mIdDoDown);
break;
}
}
}
InitAnim()でアタッチされているAnimatorコンポーネントとAnimatorControllerで設定した遷移条件となるフィールドを取得しています。
またANIM_IDを定義し、Play()に与えることでそれぞれのアニメーションを再生する仕組みです。
この時点でプロジェクトを実行するとユニティちゃんがIdle状態のアニメーションを再生し続けることが確認できます。
#プレイヤーの入力を受け付ける
続いて、プレイヤーの入力によってユニティちゃんを操作できるようにします。
###移動機能を実装
- PlayerオブジェクトにRigidbodyをアタッチし、UseGravityのチェックを外す
- 同じくRigidbodyのConstraintsでPositionYとRotationX, Zにチェックを入れる
- PlayerオブジェクトにCapsuleColliderをアタッチして以下のように設定
- PlayerActionという名前でスクリプトを作成し、Playerオブジェクトにアタッチ
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
public class PlayerAction : MonoBehaviour
{
private Transform mTrans;
private Rigidbody mRigid;
private PlayerAnimation mAnim;
void Start()
{
mTrans = GetComponent<Transform>();
mRigid = GetComponent<Rigidbody>();
mAnim = GetComponent<PlayerAnimation>();
State = STATE.IDLE;
}
void Update()
{
switch (State)
{
case STATE.IDLE:
case STATE.RUN:
CheckInputMove();
break;
}
}
void FixedUpdate()
{
switch (State)
{
case STATE.IDLE:
case STATE.RUN:
UpdateMove();
break;
}
}
//--------
// 状態 //
//---------------------------------------------------------------------------------
public enum STATE
{
DEFAULT = 0,
IDLE,
RUN,
ROLL,
DOWN
}
public STATE State { get; private set; }
//-------------
// 入力の監視 //
//---------------------------------------------------------------------------------
/// <summary>
/// wasdの入力監視
/// 8方向移動
/// </summary>
private void CheckInputMove()
{
Vector3 velocity = Vector3.zero;
if (Input.GetKey(KeyCode.W)) velocity.z += 1.0f;
if (Input.GetKey(KeyCode.A)) velocity.x -= 1.0f;
if (Input.GetKey(KeyCode.S)) velocity.z -= 1.0f;
if (Input.GetKey(KeyCode.D)) velocity.x += 1.0f;
// 移動力が0でない場合は移動の初期化
if (velocity.magnitude > 0.0f)
{
OnMove(velocity);
return;
}
// 移動力が0の場合は移動停止の初期化
OffMove();
}
//--------
// 移動 //
//---------------------------------------------------------------------------------
private const float MAX_SPEED = 4.0f;
private const float DEC_SPEED_VALUE = 0.4f;
private const float ROTATE_SPEED = 0.2f;
private float mSpeed = MAX_SPEED; // 現在の速度
private Vector3 mMoveVelocity; // 入力によって決まる移動力を一時的に保持
/// <summary>
/// 移動の初期化
///
/// アニメーションを開始
/// 状態を遷移
/// 移動力を保持
/// </summary>
/// <param name="velocity"></param>
private void OnMove(Vector3 velocity)
{
mAnim.Play(PlayerAnimation.ANIM_ID.RUN);
State = STATE.RUN;
mMoveVelocity = velocity.normalized * mSpeed;
}
/// <summary>
/// 移動停止の初期化
///
/// アニメーションを開始
/// 状態を遷移
/// 移動力を保持
/// </summary>
private void OffMove()
{
mAnim.Play(PlayerAnimation.ANIM_ID.IDLE);
State = STATE.IDLE;
mMoveVelocity = Vector3.zero;
}
/// <summary>
/// 実際の移動処理
///
/// Rigidbodyに保持している移動力を反映する
/// </summary>
private void UpdateMove()
{
mRigid.velocity = mMoveVelocity;
// 移動力が0でない場合はその方向に向きを変える(スムーズに)
if (mMoveVelocity.magnitude > 0.0f)
{
mRigid.angularVelocity = Vector3.zero; // (重要)これが無いと壁と接触しながら移動する際に回転力が相殺される
mRigid.rotation = Quaternion.Slerp(mRigid.rotation, Quaternion.LookRotation(mMoveVelocity), ROTATE_SPEED);
}
else
{
// 移動終了後は地面との摩擦で回転する力がかかってしまう場合があるので、それをゼロにする
mRigid.angularVelocity = Vector3.zero;
}
}
}
PlayerActionによってユニティちゃんが移動する流れは以下のようになります。
- Awake()で5つのSTATEのうちIDLE状態へ初期化
- IDLEまたはRUN状態のときはUpdate()でWASDキーの入力を受付
- いずれかの入力がある場合はOnMove()を実行し、入力が無い場合はOffMove()を実行
- OnMove(),OffMove()では移動力を保持して対応する状態へ遷移すると同時にアニメーションの再生を開始
- FixedUpdate()から実行されるUpdateMove()によって保持している移動力をRigidbodyに適用
###ダウンアクションの実装
移動処理に加えてダウンするアクションを実装します。
こちらは本来ゲームオーバー時に使用する予定のアクションですので、今回は動きを確認するだけの簡単な実装にしておきます。
void Update()
{
追加 CheckInputMouse();
switch (State)
{
case STATE.IDLE:
case STATE.RUN:
CheckInputMove();
break;
}
}
private void CheckInputMouse()
{
if (Input.GetMouseButtonDown(1)) OnDown();
}
/// <summary>
/// ダウンアニメーションの初期化
/// </summary>
public void OnDown()
{
State = STATE.DOWN;
mRigid.velocity = Vector3.zero;
mRigid.angularVelocity = Vector3.zero;
mAnim.Play(PlayerAnimation.ANIM_ID.DOWN);
}
プロジェクトを実行するとWASDキーでユニティちゃんが画面を走り回り、右クリックすることでダウンアクションを行うことが確認できます。(ダウン後は操作できなくなります)
#アニメーションイベントに対応する
現時点で移動やダウンは実行されるものの、コンソールログに以下のようなエラーが出力されてしまいます。
'Player' AnimationEvent 'OnCallChangeFace' has no receiver! Are you missing a component?
これはOnCallChangFaceというアニメーションイベントについて、Playerオブジェクトにはそれを受け取る仕組みが無いというエラーです。
【アニメーションイベント】
アニメーションの再生時に特定のタイミングでメソッドを呼び出す仕組み。
アニメーションクリップのEvents項目から1フレーム毎に設定することができる。
インポートしたユニティちゃんアセットのアニメーションクリップにはイベントが設定されたものがいくつかあるようで、先ほどAnimationStateに設定した各クリップをProjectビューから確認してみるとRunning@loopとKneelDownには以下のようにイベントが設定されていました。
エラーを修正するために、このイベントに対応するコードをPlayerAnimationに追加します。
//------------------------
// フェイスアニメーション //
//---------------------------------------------------------------------------------
public const string DEFAULT_FACE = "default@sd_hmd";
[Tooltip("表情のアニメーションクリップを設定")]
public AnimationClip[] animations;
//アニメーションイベントから呼び出される表情切り替え用のコールバック
public void OnCallChangeFace(string str)
{
int ichecked = 0;
foreach (var animation in animations)
{
if (str == animation.name)
{
ChangeFace(str);
break;
}
else if (ichecked <= animations.Length)
{
ichecked++;
}
else
{
//str指定が間違っている時にはデフォルトの表情に設定
ChangeFace(DEFAULT_FACE);
}
}
}
private void ChangeFace(string str)
{
mAnimator.SetLayerWeight(1, 1); // レイヤーウェイト = そのレイヤーのアニメーションをどの程度反映させるかどうか0.0f~1.0f
mAnimator.CrossFade(str, 0);
}
イベントで呼び出されるOnCallChangeFace()はアニメーションの特定タイミングでユニティちゃんの表情を変更するメソッドです。
このメソッドを利用するには今回のコード修正に加えて、アニメーションレイヤーを実装する必要があります。
参考: Unityのレイヤー、アバターマスクを使って体の一部分を別のアニメーションにする
参考: Animatorの個人的な逆引きリファレンス
【アニメーションレイヤー】
1つのオブジェクトに対して2つ以上のアニメーションを同時に再生する仕組み。
今回はユニティちゃんが走るアニメーションを再生しながら、表情部分のみ別にアニメーションを再生させたりします。
- PlayerAnimControllerをダブルクリックでAnimatorウインドウを表示
- Layersタグを選択し、+ボタンからFace Layerを作成
- FaceLayerの歯車アイコンからMask横のアイコンを選択して「_faceOnly」を設定
_faceOnlyはユニティちゃんアセットに含まれている表情部分のアバターマスクです。
この設定によってFaceLayerは表情部分だけのアニメーションを定義するレイヤーとなります。
続いて以下のようにStateを作成し、motion部分にはStateと同じ名前のクリップを「UnityChan/SD_unitychan/FaceAnimations/Humanoid/~」から探して設定します。
加えて、先ほどコードを追加したPlayerAnimationについて、インスペクタ上からanimationsプロパティに今回使用する4つの表情アニメーションのクリップを設定します。
実際にアニメーションを再生しているのは以下のメソッドです。
private void ChangeFace(string str)
{
mAnimator.SetLayerWeight(1, 1); // レイヤーウェイト = そのレイヤーのアニメーションをどの程度反映させるかどうか0.0f~1.0f
mAnimator.CrossFade(str, 0);
}
まずSetLayerWeight()でFaceLayerのWeightを1に設定しています。
Weightはこのレイヤーのアニメーションをどのくらいの割合で他のレイヤーとブレンドするかという重みで、0~1の範囲で指定することができます。
ここではWeightが最大の1ですので、FaceLayerで設定しているアニメーションの100%がBaseLayerのアニメーションにブレンドされることになります。
そして次行のCrossFade()ではstrに指定されているStateに0秒の時間をかけて遷移しています。
この2つのメソッドによってアニメーションの再生を行っていますが、今回はブレンド手法をOverrideに設定しているためBaseLayerを上書きする形でFaceLayerのアニメーションが再生され、結果的に表情はFaceLayer、それ以外はBaseLayerのアニメーションが実行されているように見えます。
プロジェクトを実行して移動やダウンの際にエラーメッセージが出ないことと、ユニティちゃんの表情が切り替わることを確認します。
#アニメーションビヘイビアを実装する
移動やダウンに対する表情変化は実装できましたが、アイドル状態に遷移したタイミングでの表情変化がまだ実装されていないため、移動して止まったときもユニティちゃんは驚き顔のままになってしまっています。
こちらを元の表情に戻すためにアニメーションビヘイビアという機能を利用します。
【アニメーションビヘイビア】
アニメーションの再生時に特定のタイミングでメソッドを呼び出す仕組み。
アニメーションイベントがクリップに対して設定され、1フレーム毎にメソッド呼び出しのタイミングを取れる一方、
こちらはアニメーションステートに対して設定され、主に遷移の開始や終了などのタイミングでメソッドを実行する。
アニメーションイベントは設定されているフレームまで再生されて初めてメソッドが呼び出されるので
そのタイミングより前にアニメーションがキャンセルされた場合は呼び出しもキャンセルされてしまうのに対して、
ビヘイビアは必ず実行される。
- AnimatorウインドウのBase LayerからRunステートを選択
- インスペクタのAddBehaviourで「BehaviourPlayerBaseRun」という名前のスクリプトを作成
- Projectビューに作成したスクリプトが表示されるのでそれを開いて編集
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class BehaviourPlayerBaseRun : StateMachineBehaviour
{
// OnStateEnter is called when a transition starts and the state machine starts to evaluate this state
//override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
//
//}
// OnStateUpdate is called on each Update frame between OnStateEnter and OnStateExit callbacks
//override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
//
//}
// OnStateExit is called when a transition ends and the state machine finishes evaluating this state
override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
PlayerAnimation pa = animator.gameObject.GetComponent<PlayerAnimation>();
if (pa.AnimId == PlayerAnimation.ANIM_ID.IDLE) pa.OnCallChangeFace(PlayerAnimation.DEFAULT_FACE);
}
// OnStateMove is called right after Animator.OnAnimatorMove(). Code that processes and affects root motion should be implemented here
//override public void OnStateMove(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
//
//}
// OnStateIK is called right after Animator.OnAnimatorIK(). Code that sets up animation IK (inverse kinematics) should be implemented here.
//override public void OnStateIK(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
//
//}
}
ビヘイビアはMonoBehaviourを継承した通常のクラスとは異なりStateMachineBehaviourを継承していて、定義されているメソッドをオーバーライドすることで独自の処理を実装していきます。
また引数のAnimatorはオブジェクトにアタッチされているAnimatorコンポーネントの参照ですので、これを利用してGameObjectなどを取得することができます。
今回はRunステートから他のステートに遷移するタイミングで判定を行い、必要な場合はユニティちゃんの表情をデフォルトに変更しています。
これでユニティちゃんの表情が正しいタイミングで切り替わるようになりました。
#おまけ:ブレンドシェイプでまばたきを実装する
インポートしたユニティちゃんのアセットにはブレンドシェイプを用いたまばたきアニメーションを実装できるスクリプトAutoBlinkforSDが含まれています。
こちらのスクリプトをPlayerオブジェクトにアタッチし、Ref_faceプロパティに以下の_face(表情部分のメッシュ)を指定することでユニティちゃんが自動的にまばたきを行うようになります。
Player/Character1_Reference/Character1_Hips/Character1_Spine/Character1_Spine1/Character1_Spine2/Character1_Neck/Character1_Head/_face
参考: Unity-Chanは二度笑う~ユニティちゃん表情モーション研究
この作品はユニティちゃんライセンス条項の元に提供されています