今回はUnityで敵にダメージを与えた時に敵の近くにダメージテキストをただ表示するのではなく、ダメージテキストを一文字ずつアニメーションさせて表示する機能を作成したいと思います。
今回の機能はユニティちゃんのRPGを作ってみようの記事の中で作成したスクリプトで一文字毎のアニメーションをさせるのではなくあらかじめ一文字毎のテキストのアニメーションを作成しておき、それを再生するという流れになります。
難しいスクリプトを組まなくても済むのでこちらの記事の方が楽です。
今回の機能を作成すると以下のようになります。
今回はいつも使用しているUI→Textバージョンと、3D Object→TextMeshProを使用したバージョンの二つを作成してみます。
上のサンプルはUI→Textバージョンです。
UI→Textバージョンのダメージテキストアニメーションの作成
それではまずUI→Textバージョンの作成をしていきます。
UIの作成
Textの親となるCanvasを作成します。
ヒエラルキー上で右クリックからUI→Canvasを選択し、名前をDamageTextParentとします。
このDamageTextParentがダメージテキストの親となり、その子要素ダメージポイントの各桁のテキストを並べて表示するという形にします。
DamageTextParentゲームオブジェクトを選択し、インスペクタでRectTransformのWidthとHeightを100、ScaleのXYZを0.01とします。
CanvasのRender ModeをWorld Spaceにし普通のゲームオブジェクトと同じように3D空間に表示出来るようにします。
ここまで出来たらDamageTextParentをAssetsフォルダ上にドラッグ&ドロップしてプレハブにします。
次にヒエラルキー上のDamageTextParentを選択した状態で右クリックからUI→Textを選択します。
Textを選択しインスペクタのWidthを30、Heightを50にします。
Font Sizeを40、ParagraphのAliginmentを真ん中、Colorを赤色にします。
ここまで出来たらTextだけをAssetsフォルダにドラッグ&ドロップしてプレハブにします。
DamageTextParentとTextは単体でプレハブにする必要があるので注意してください。
Textのアニメーションを作成する
次にTextのアニメーションを作成していきます。
ヒエラルキー上のTextを選択した状態でAnimationタブを開きます。Animationタブが開いていない場合はUnityメニューのWindow→Animation→Animationを選択します。
ヒエラルキー上でアニメーションをさせるゲームオブジェクトが選択されていると、AnimationタブにCreateボタンが表示されるので押します。
新しいウインドウが開くのでDamageという名前を付けます。
この時点でAssetsフォルダにDamageアニメーションとTextのAnimatorControllerが作成され、TextにはAnimatorコンポーネントが設定されています。
Animationタブの赤い丸を押し(値等を変更するとキーフレームが打たれる)、0:05フレームに移動し、インスペクタのRectTransformのPosYを40を入力するかシーンビューでマニピュレータを使ってY軸方向に動かします。
するとキーフレームが0:00と0:05に作られます。
以下のように変更した箇所が赤くなっていればキーフレームが打たれます。
次にDamageの所を押し、Create New Clipを選択します。
名前をIdleとします。
Damageに戻り、0:00フレームを選択してCtrl+Cキーを押してコピーし、Idleの0:00フレームを選択してCtrl+Vキーを押して貼り付けます。
Damageアニメーションクリップを選択してインスペクタのLoopのチェックを外してアニメーションがループしないようにしておきます。
これでアニメーションクリップが出来ました。
アニメーターコントローラーの設定
Assetsフォルダに作成されたTextアニメーターコントローラーを選択し、今度はAnimatorタブを開きます。
AnimatorControllerに関しては
等を参照してください。
既に作成されたIdleアニメーションとDamageアニメーションが設定された状態が作成されていればそれをそのまま使っても構いません。
アニメーションパラメーターにTrigger型のDamageを作成します。
Idle状態、Damage状態、End状態を作成し、IdleにはIdleアニメーションクリップ、DamageにはDamageアニメーションクリップを設定します。
Idle→Damageの遷移条件はHas Exit Timeのチェックを外し、Damageをトリガーした時にします。
Damage→IdleはHas Exit Timeにチェックを入れ、その他の条件はなしとします。
次の文字のアニメーションへと移行させるビヘイビアスクリプトの作成
アニメーターコントローラーは一文字毎に設定されており、その文字のアニメーションが終わったら次のアニメーションへと移行させる必要があります。
なのでDamage状態を移動したら次の文字のアニメーションが開始されるようにビヘイビアスクリプトを作成し取り付けます。
Damage状態を選択し、インスペクタのAdd Behaviourボタンを押して名前をEndDamageAnimationという名前にします。
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 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class EndDamageAnimation : 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) { // アニメーション終了状態endに来たら次の文字のアニメーションをさせる animator.GetComponentInParent<DamageAnimation>().AnimateNextChar(); } // OnStateMove is called right after Animator.OnAnimatorMove() //override public void OnStateMove(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) //{ // // Implement code that processes and affects root motion //} // OnStateIK is called right after Animator.OnAnimatorIK() //override public void OnStateIK(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) //{ // // Implement code that sets up animation IK (inverse kinematics) //} } |
OnStateExitはその状態を抜ける時に呼び出されるのでそこで自身のAnimatorから親の要素を含めてDamageAnimatorスクリプトを取得し、そのAnimateNextCharメソッドを呼んで次の文字のアニメーションへ移行させます。
DamageAnimatorスクリプトは後で作成します。
これでアニメーターコントローラーの設定が終わりました。
ヒエラルキー上のDamageTextParentとその子要素のTextはもう使わないので削除します。
ダメージテキストをインスタンス化するスクリプトの作成
ダメージ用のテキストの親DamageTextParentプレハブとダメージポイントの一桁を表すTextのプレハブが出来たので、次はそれらをインスタンス化する処理を作成します。
今回はサンプルとしてヒエラルキー上にコライダを持つゲームオブジェクトをいくつか置いておいて、それをマウスクリックしたらそこにダメージテキストの親DamageTextParentをインスタンス化するようにします。
ヒエラルキー上のMain Cameraに新しくInstantiateDamagePointスクリプトを作成し取り付けます。
このスクリプト自体はDamageTextParentプレハブをクリックしたゲームオブジェクトの位置にインスタンス化するだけで、通常はダメージを与えた敵の位置等にDamageTextParentをインスタンス化する必要があります。
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 | using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class InstantiateDamagePoint : MonoBehaviour { // ダメージテキストの親のオブジェクトプレハブ [SerializeField] private GameObject damageTextParentPrefab; // Update is called once per frame void Update() { // マウスの左ボタンを押した時 if (Input.GetButtonDown("Fire1")) { Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; // マウスのある位置に何らかのコライダがあればその位置にダメージテキストの親オブジェクトを作成 if (Physics.Raycast(ray, out hit, 1000f)) { var ins = Instantiate<GameObject>(damageTextParentPrefab, hit.point, Camera.main.transform.rotation); // ダメージテキストの処理を行う ins.GetComponent<CreateDamageText>().CreateText(ins, (int)Random.Range(1f, 3000f)); } } } } |
damageTextParentPrefabにDamageTextParentプレハブをインスペクタで設定します。
Input.GetButtonDown(“Fire1”)でマウスの左ボタンが押されたかどうかが判定されますので、その時にCamera.main.ScreenPointRayメソッドを使って引数で与えたマウスの位置Input.mousePositionにレイを飛ばしその情報をrayに入れます。
Physics.Raycastを使ってrayの位置と方向からレイを1000m先まで飛ばしコライダを持つオブジェクトと接触したらそれがhitにデータが入ります。
damageTextParentPrefabをオブジェクトと接触した位置と、カメラの角度にしてインスタンス化します。
インスタンス化したゲームオブジェクトからCreateDamageTextスクリプトを取得しCreateTextメソッドに自身のゲームオブジェクトと、Random.Rangeで1~3000の間のint型の数値を渡して呼び出します。
どれだけのTextを作成するかの処理をするスクリプトの作成
DamageTextParentプレハブをインスタンス化する処理は出来ましたが、DamageTextParentはテキストの親の要素で子要素のTextはまだ作成していません。
DamageTextParentプレハブにこのTextを生成するスクリプトを取り付けます。
新しくCreateDamageTextスクリプトを作成しDamageTextParentプレハブに取り付けます。
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 48 | using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class CreateDamageText : MonoBehaviour { [SerializeField] private GameObject damageTextPrefab; // インスタンス化したダメージテキストリスト private List<GameObject> damageTextList = new List<GameObject>(); // 現在アニメーションをしているテキストの番号 private int charNum; // 全ての文字のアニメーションが終わってからゲームオブジェクトを消すまでの時間 [SerializeField] private float deleteTime = 1f; public void CreateText(GameObject parentObj, int damagePoint) { // ダメージテキストの幅を取得 var width = damageTextPrefab.GetComponent<RectTransform>().rect.width; GameObject damageTextIns; // ダメージポイントの桁数分のダメージテキストをインスタンス化 for (int i = 0; i < damagePoint.ToString().Length; i++) { damageTextIns = Instantiate<GameObject>(damageTextPrefab, parentObj.transform); // ダメージテキストの位置はダメージテキストの幅分右に移動させていく damageTextIns.GetComponent<RectTransform>().anchoredPosition = new Vector2(i * width, 0f); // ダメージポイントの桁毎の数値をダメージテキストに表示 damageTextIns.GetComponentInChildren<Text>().text = damagePoint.ToString()[i].ToString(); // アニメーションを管理する為にインスタンスをリストに保持しておく damageTextList.Add(damageTextIns); } // 最初の文字のアニメーションを開始 damageTextList[charNum].GetComponent<Animator>().SetTrigger("Damage"); } // 次の文字のアニメーションを開始させる public void AnimateNextChar() { // 全ての文字をアニメーションさせていなければ次の文字をアニメーションさせる if (charNum < damageTextList.Count - 1) { charNum++; damageTextList[charNum].GetComponent<Animator>().SetTrigger("Damage"); } else { // 全ての文字のアニメーションが終わっていたら1秒後に削除 Destroy(this.gameObject, deleteTime); } } } |
damageTextPrefabにはTextプレハブをインスペクタで設定します。
damageTextListは作成したTextを入れておくリストです。
charNumは今どの文字のアニメーションをしているかの番号です。
deleteTimeは全ての文字のアニメーション終了後に何秒経ったら親のゲームオブジェクト事消すかの秒数の設定です。
CreateTextメソッドはInstantiateDamagePointスクリプトでDamageTextPrefabをインスタンス化した後に呼び出しています。
damageTextPrefabのRectTransformを取得しそこからTextの幅を取得します。
その後、引数で受け取ったダメージ数の文字の数(桁数)分のdamageTextPrefabをインスタンス化します。
インスタンス化したdamageTextPrefabの位置は少しずつずらさないと同じ位置に作られてしまうので先ほど取得したwidthの幅だけ横にずらした位置にインスタンス化します。
i * widthとすることで何個目のTextの1個目は0、2個目は30、3個目は60と少しずつずれていきます。
インスタンス化したdamageTextPrefabの子要素のTextにその桁の数値を入れます。
その桁の数値はダメージをdamagePoint.ToString()で文字列にしているのでその中の何番目の文字かを[i]で取得しそれを入れます。
damageTextPrefabをダメージの桁数分インスタンス化したら最初のTextのAnimatorコンポーネントを取得しアニメーションパラメータのDamageをトリガーします。
AnimateNextCharメソッドはEndDamageAnimationビヘイビアスクリプトから呼び出していて、ダメージの桁数の文字を超えていなければ次の文字を指してアニメーションをさせ、全ての文字のアニメーションが終わっていたら親のゲームオブジェクトを削除します。
これでUI→Textのダメージテキストアニメーション機能が出来ました。
TextMeshProのダメージテキストアニメーション
TextMeshProはテキストをメッシュにして表示する機能です。
新しめのUnityであればTextMeshProは標準で搭載されているので、使うのに必要な他のファイル等をインポートするだけです。
UnityメニューのWindow→TextMeshPro→Import TPM Essential Resourcesを選択しインポートします。
TextMeshProを使ったダメージテキストアニメーションはUI→Textバージョンとほとんど同じなので同じ部分は説明を省いていきます。
ダメージテキストの親のゲームオブジェクトプレハブを作成
ダメージテキストの親のゲームオブジェクトのプレハブを作成します。
ヒエラルキー上で右クリックからCreate Emptyを選択し、名前をDamageTextMeshProParentとします。
DamageTextMeshProParentを選択し、インスペクタのAdd ComponentからLayout→Rect Transformを取り付けます。
Rect TransformのWidthを1、Heightを1とします。
DamageTextMeshProParentをAssetsフォルダにドラッグ&ドロップしてプレハブにします。
TextMeshProの作成
TextMeshProを使って通常の3D空間に置けるテキストを作ります。
ヒエラルキー上で右クリックから3D Object→Text – TextMeshProを選択し、名前をDamageTextMeshProTextとします。
RectTransformのWidthとHeightを0.5、Main SettingsのFont AssetにLiberationSansSDFを設定します。
MaterialsのElement0にLiberationSansSDFの子要素のマテリアルが設定されていなければ設定します。
Font Sizeに5を設定します。Alignmentは真ん中にします。
ダメージテキストアニメーションの作成
DamageTextMeshProTextダメージテキストが出来たので次はこれのアニメーションを作成します。
作り方はUI→Textバージョンと同じなのでやり方は省きますが、アニメーションクリップの名前をIdleTextMeshProとDamageTextMeshProという名前にしました。
ここまで出来たらDamageTextMeshProTextのみをAssetsフォルダにドラッグ&ドロップしてプレハブにします。
スクリプトの作成
スクリプトはUI→Textバージョンとほとんど同じですが、わたくしの都合で複製して作成します。
InstantiateDamagePointスクリプトを複製してInstantiateDamagePoint2スクリプトにします。
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 | using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class InstantiateDamagePoint2 : MonoBehaviour { // ダメージテキストの親のオブジェクトプレハブ [SerializeField] private GameObject damageTextParentPrefab; // Update is called once per frame void Update() { // マウスの左ボタンを押した時 if (Input.GetButtonDown("Fire1")) { Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; // マウスのある位置に何らかのコライダがあればその位置にダメージテキストの親オブジェクトを作成 if (Physics.Raycast(ray, out hit, 1000f)) { var ins = Instantiate<GameObject>(damageTextParentPrefab, hit.point, Camera.main.transform.rotation); // 親の位置を移動させるので最初は見えないようにしておく ins.SetActive(false); // ダメージテキストの処理を行う ins.GetComponent<CreateDamageText2>().CreateText(ins, (int)Random.Range(1f, 3000f)); } } } } |
ダメージテキストの親を非アクティブにしたり、CreateDamageTextをCreateDamageText2としているだけですね。
作成したらMain Cameraに取り付けます。
次はCreateDamageTextを複製してCreateDamageText2を作成します。
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 48 49 50 51 52 53 | using System.Collections; using System.Collections.Generic; using TMPro; using UnityEngine; using UnityEngine.UI; public class CreateDamageText2 : MonoBehaviour { [SerializeField] private GameObject damageTextPrefab; // インスタンス化したダメージテキストリスト private List<GameObject> damageTextList = new List<GameObject>(); // 現在アニメーションをしているテキストの番号 private int charNum; // 全ての文字のアニメーションが終わってからゲームオブジェクトを消すまでの時間 [SerializeField] private float deleteTime = 1f; public void CreateText(GameObject parentObj, int damagePoint) { // ダメージテキストの幅を取得 var width = damageTextPrefab.GetComponent<RectTransform>().rect.width; GameObject damageTextIns; // ダメージポイントの桁数分のダメージテキストをインスタンス化 for (int i = 0; i < damagePoint.ToString().Length; i++) { damageTextIns = Instantiate<GameObject>(damageTextPrefab, parentObj.transform); // ダメージテキストの位置はダメージテキストの幅分右に移動させていく damageTextIns.GetComponent<RectTransform>().anchoredPosition = new Vector2(i * width, 0f); // ダメージポイントの桁毎の数値をダメージテキストに表示 damageTextIns.GetComponentInChildren<TMP_Text>().text = damagePoint.ToString()[i].ToString(); // アニメーションを管理する為にインスタンスをリストに保持しておく damageTextList.Add(damageTextIns); } // 親の位置をダメージ数の桁数 / 2分を左に寄せる parentObj.GetComponent<RectTransform>().anchoredPosition -= new Vector2((width * damagePoint.ToString().Length) / 2f, 0f); // 親の位置を移動したので見えるようにする parentObj.SetActive(true); // 最初の文字のアニメーションを開始 damageTextList[charNum].GetComponent<Animator>().SetTrigger("Damage"); } // 次の文字のアニメーションを開始させる public void AnimateNextChar() { // 全ての文字をアニメーションさせていなければ次の文字をアニメーションさせる if (charNum < damageTextList.Count - 1) { charNum++; damageTextList[charNum].GetComponent<Animator>().SetTrigger("Damage"); } else { // 全ての文字のアニメーションが終わっていたら1秒後に削除 Destroy(this.gameObject, deleteTime); } } } |
ダメージポイントのText部分をTMP_Textと変更し、ダメージテキストの親のゲームオブジェクトの位置を子のテキストの幅の半分左に寄せてマウスクリックした位置にダメージテキストの真ん中がくるようにします。
その後、親のゲームオブジェクトをアクティブにしています。
次にEndDamageAnimationビヘイビアを複製し、EndDamageAnimation2を作ります。
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 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class EndDamageAnimation2 : 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) { animator.GetComponentInParent<CreateDamageText2>().AnimateNextChar(); } // OnStateMove is called right after Animator.OnAnimatorMove() //override public void OnStateMove(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) //{ // // Implement code that processes and affects root motion //} // OnStateIK is called right after Animator.OnAnimatorIK() //override public void OnStateIK(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) //{ // // Implement code that sets up animation IK (inverse kinematics) //} } |
CreateDamageTextをCreateDamageText2としているだけです。
これで機能が出来ました。
終わりに
ユニティちゃんのRPGを作ってみようカテゴリの記事でダメージポイントを表示しアニメーションをスクリプトを使って作成しましたが、なんだか難しい感じになっているので、アニメーション自体はAnimationウインドウで作成する方法を今回記事にしてみました。
こちらはこちらで個々のテキスト処理があるので面倒な感じもしますが・・・・(^_^;)