前回、地面を作成したので今回はその世界の飾りつけを行う。
水の上は歩けない、物とは衝突するなどゲームとして自然な挙動も実現していく。では早速やっていこう。
見え方の設定
Assets→Art→Sprites→EnvironmentにあるMetalCubeをヒエラルキーに配置し、Rubyとぶつからない位置となるようにPositionを調整する。
ゲームを実行してMetalCube周辺を歩いてみよう。
Rubyが箱の前にいる構図にも関わらず、箱の後ろに描画されてしまっている。
スプライトの前後関係はOrder in layerなどで制御できるのでまずはこれを調整しよう。
TileMapで作成した地面は最背面に描画したいのでTimemapを選択してOrder In Layerを-1にする。
その後、Rubyを選択してOrder in Layerを10にしてみよう。
Rubyが手前にいるときはいい感じだが、後ろに行ったときがおかしい、こういった構図で前後関係を表すにはOrder In Layerとかでは無理そうだ。
この場合、基準点(足元)が画面の下に描かれている方が前と解釈すれば良さそうだ。
まずは、RubyのOrder in Layerを0に戻して
Edit -> Project Settingsを開いて Graphicsの以下のように変更する。
これはオブジェクトの前後関係をY軸の値によって判定する設定だ。
実行してみよう。
だいぶいい感じなのだが、おしい。現在は画像の中心点のy軸の位置で前後関係を判定しているのだがこれをピボットで判定するようにし、さらにそのピボットを画像の底辺に設定することでうまくいく。調整しよう。
まずは、RubyとmetalCubeの2つに対してSprite Sort Point をPivotに設定する。
Pivotの設定方法には2つある。
1枚の画像を分割してSpriteを作成していない場合は簡単だ。
元画像を選択して、PivotをBottomに設定して、Applyボタンを押すだけだ。
1枚の画像からスライスで分割して複数のスプライトを作成している場合はスプライトエディタで行う。 Rubyの画像はこちらの方法で変更してみよう。
元画像を選択して、Sprite Editorを開く
スプライトエディタが開くのでここからPivotを選択してApplyを押す。
これで前後関係の設定が完了だ。実行してみよう。
OK!前後関係が良好に描画されている。
物理特性
衝突判定を作っていこう。まずはRubyを選択し、AddComponent->Rigidbody2Dを付与する。
◯重力は不要なので、GravityScaleを0にする。
◯動いていないときに物理判定が停止しないようにSleeping ModeをNever Sleepにする
◯物とぶつかったときに不要なz軸回転を行わないようにConstraintsのFreezeRotationのzをにチェックを入れる
続いて、衝突判定用にBoxCollider2Dコンポーネントを付与する。
コライダーの大きさの調整を画面を見ながらできるようにEditColliderを押す
Rubyの足元に衝突判定が起こるにように調節したら、EditColliderのボタンをオフにしておく
metalCube
障害物であるmetalCubeにも、衝突判定をつけよう。
こちらはBoxCollider2Dのみを付与し、Colliderを以下のように調整する。
実行してみよう、2つの物体の衝突判定が適切に行われることがわかる。
プレファブ化
RubyとmetalCubeの設定ができたので、これらをプレファブ化しておこう。
Assetsフォルダの直下にPrefabsフォルダを作成し、RubyとmetalCubeをドラッグ&ドロップで配置する。
プレファブ化しておくと、その設定をもったままオブジェクトを複製できる。
また、調整を行いときもオリジナルのプレファブを編集するだけで、すべてに適用できる。
プレファブ化したmetalCubeを画面に複数設置し位置を調整しよう。(プレファブをドラッグで配置するか、ヒエラルキーにあるMetalCubeを選択し、Ctrl + D で複製してもOK)
実行すると、それらにたいしても適切に衝突判定が行われることがわかる。
RubyControllerの変更
RubyにRigidbodyを付与したので、移動も適切な移動になるように以下のように変更しよう。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class RubyController : MonoBehaviour
{
Rigidbody2D rb;
void Start()
{
rb=GetComponent<Rigidbody2D>();
}
void Update()
{
float horizontal = Input.GetAxis("Horizontal");
float vertical = Input.GetAxis("Vertical");
Vector2 position = rb.position;
position.x = position.x + 3.0f * horizontal * Time.deltaTime;
position.y = position.y + 3.0f * vertical * Time.deltaTime;
rb.MovePosition(position);
}
}
transform.positionをいじっての移動は1フレーム毎にワープを繰り返しているようなもので、他者との衝突や接触を正しく判定することができない。
MovePositionを使うことで移動した際、適切に他者に影響を与えることができる。
tilemap collision
こんどは地形に対してコリジョンを発生させてみよう。具体的には池には入れないようにしていく。
まずは、tilemapを選択し、AddComponentからTilemapCollider2Dを付与する
続いて、Tilesフォルダに入っている水以外のすべてタイルをshiftを押しながら全て選択し、Collider TypeをNoneに設定する。
実行してみよう。これで水以外のタイルは歩けるようになった。
コライダーの最適化
現状のままでも目的は果たしているが、改善の余地がある。
現状は水タイルの数だけ、コライダーがあるのでマップの大きさによってや処理が重くなる。なので連続したコライダーはつなげてしまおう。
まず、tilemapを選択して、AddComponentからcomposite Collider 2Dを付与する。(Rigidbody2Dが自動的に付与される)
そして、TilempaCollider2DにあるUsed By Compositのチェックボックスをオンにし、Rigidbody2DのBodyTypeをStaticにする。動くことないRigidbodyはStaticにすると意図せぬ挙動を防げるだけでなく、処理負荷の軽減にもつながる。
実行してみよう。コライダーが連結されたことにより池に沿って歩いたときに引っかかることなくスムーズに移動できることがわかる。
体力回復アイテムの実装
HPの概念を導入し、拾うと体力が回復するアイテムを実装する。
まずはRubyControllerを以下のように修正する。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class RubyController : MonoBehaviour
{
public float speed=3.0f;
public int maxHealth=5;
int currentHealth;
Rigidbody2D rb;
void Start()
{
rb=GetComponent<Rigidbody2D>();
currentHealth=maxHealth;
}
void Update()
{
float horizontal = Input.GetAxis("Horizontal");
float vertical = Input.GetAxis("Vertical");
Vector2 position = rb.position;
position.x = position.x + speed * horizontal * Time.deltaTime;
position.y = position.y + speed * vertical * Time.deltaTime;
rb.MovePosition(position);
}
public void ChangeHealth(int amount){
currentHealth = Mathf.Clamp(currentHealth + amount,0,maxHealth);
Debug.Log(currentHealth + "/" + maxHealth);
}
}
Rubyの移動速度(speed)をフィールドに出して、インスペクターから調整できるよう変更。
また、currentHealthという変数を用意して、現在のHPを管理できるようにした。
値を変更させるためにはChangeHealthメソッドを呼ぶ必要がある。
回復アイテムの作成
Art->Sprites->VFXフォルダに入っているCollectibleHealthを選択して、インスペクタからPPUを300に変更し、下部Applyボタンを押す。
それをヒエラルキーに配置し、positionを調整する。
BoxCollider2Dを付与し、isTriggerにチェックをいれ、EditColliderを使っていちごの大きさにあうように調整する。
isTriggerのコライダーにしたので、実際の衝突は起こらないが、スクリプトから接触を感知できる。Assets->Scriptsフォルダ内に
HealthCollectableスクリプトを作成して、以下のように記述する。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class HealthCollectable : MonoBehaviour
{
void OnTriggerEnter2D(Collider2D other) {
Debug.Log("triggerと接触:"+other);
}
}
記述が終わったら、CollectableHealthにアタッチする。
実行して、いちごに接触してみよう。画面左下にRubyと接触したと出力されている。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class HealthCollectable : MonoBehaviour
{
void OnTriggerEnter2D(Collider2D other) {
//Debug.Log("triggerと接触:"+other);
RubyController rubyCon = other.GetComponent<RubyController>();
if(rubyCon != null){
rubyCon.ChangeHealth(1);
Destroy(gameObject);
}
}
}
実行してみよう。いちごと接触するといちごが消えることがわかる。
(HPはMaxなので変化なし)
プロパティ
HPがMaxの状態でアイテムに触った場合、アイテムは消えないという仕様もありうる。その場合現在のHPを外部から参照する必要がある。変数をpublicにするという方法もあるが、ここではプロパティを定義しよう。
RubyControllerを以下のように修正する。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class RubyController : MonoBehaviour
{
public float speed=3.0f;
public int maxHealth=5;
int currentHealth;
public int health{get{return currentHealth;}}
Rigidbody2D rb;
void Start()
{
rb=GetComponent<Rigidbody2D>();
currentHealth=maxHealth;
}
void Update()
{
float horizontal = Input.GetAxis("Horizontal");
float vertical = Input.GetAxis("Vertical");
Vector2 position = rb.position;
position.x = position.x + speed * horizontal * Time.deltaTime;
position.y = position.y + speed * vertical * Time.deltaTime;
rb.MovePosition(position);
}
public void ChangeHealth(int amount){
currentHealth = Mathf.Clamp(currentHealth + amount,0,maxHealth);
Debug.Log(currentHealth + "/" + maxHealth);
}
}
getterのみが定義されたhealthプロパティを定義することで、外部クラスからは現在のHPの取得はできるが、直接書き換えられないという仕組みができた。
(HPを変えるためにはChangeHealthメソッド呼ぶしかない)
このように変数に意図しない変更が加えられなくなるしくみをカプセル化といい、堅牢なシステムを作る際に有効となる。
では実際にHealthCollectableをプロパティを参照する形に変更しよう。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class HealthCollectable : MonoBehaviour
{
void OnTriggerEnter2D(Collider2D other) {
//Debug.Log("triggerと接触:"+other);
RubyController rubyCon = other.GetComponent<RubyController>();
if(rubyCon != null){
if(rubyCon.health == rubyCon.maxHealth){return;}
rubyCon.ChangeHealth(1);
Destroy(gameObject);
}
}
}
実行してみよう。HPがMaxな現状だと、いちごに触れてもアイテムが消えないことがわかる。
回復アイテムができたのでいちごをプレファブ化しておこう。
次回はダメージ処理を作成していく。
コメント