#はじめに
はじめまして、@tm_1219といいます。普段は株式会社qnoteで猫社員たちと戯れながら、片手間にAndroidアプリ開発をしています。
この記事は「qnote Advent Calendar 2020」23日目の記事になります。
私は2020年10月にリリースされたスクウェア・エニックスのオクトパストラベラー大陸の覇者というスマホゲームを最近やっているのですが、ドット絵って綺麗で良いものだなぁと再認識し、ドット絵描いてみたい、ゲームを作って動かしてみたい、と思ったのでそのゲーム制作の様子を記事にしました。
業務と全く関係無い内容で本当に申し訳ないのですが、反省はしていません。
本記事ではUnityの解説を入れながら、Unityでゲームを動かす仕組みと、徐々にゲームができていく様子を伝えられたら良いなと思います。
Unityはドットインストールで触ったことはありましたが自分でゲームを作ったのは今回が初めてなので、ご指摘もコメントでどしどし頂けると助かります!
ちなみにめちゃくちゃ長い記事になってしまったので、ゲームの雰囲気だけ知りたい方は途中途中のGIFアニメを流し読みして頂けたらと思います!
また、制作した以下のゲームはオンラインランキング機能があるので、遊んで頂けるだけでも泣いて喜びます。
https://unityroom.com/games/tapfly_mod
※PCのみ動作します。
#環境
macOS Catalina 10.15.7
Unity 2019.4.16f1
Aseprite v1.2.25
#Asepriteを触ってみる
まず私はドット絵を描いたことが無かったので、ひとまずAsepriteを触って感触を掴んでみようと思いました。
ちなみにAsepriteは、Windows,mac,Linuxで使える唯一無二の神ツールと呼ばれています。
体験版でも保存機能以外は全て使えるので皆さんもぜひ触ってみてください!
Aseprite 体験版
Aseprite 製品版
##Asepriteの使い方
以下の記事を参考にさせて頂きました。
とてもわかりやすい内容となっているので、本記事では解説は割愛させて頂きます。
Asepriteでドット絵を描くには ~最初の設定とAsepriteの使い方~
Asepriteでドット絵を描く
##お試しでドット絵を描いてみる
株式会社qnoteでは10匹の猫社員たちと共に仕事をしており、せっかくなのでキャラクターとして登場させたいと思いました。
ひとまず試しに猫社員ちゃこ氏のドット絵を描いてみました。
ジト目にハチワレに長いまつげとお髭という個性豊かな子だったので、その要素を取り入れるだけでそれっぽくなりました!
ちなみに当初の予定ではAseprite触ってみた、という記事にしようとしていたので何も考えずただドット絵を描いて楽しんでいました。
ひとまずゲーム作りに入って、あとから必要なドット絵を描くこととします。
#Unityを使ったゲーム制作の流れ
猫といえばジャンプ、ジャンプと言えばJump○ingということで、ジャンプしてどんどん上に登っていくようなゲームを作りたいと思います。
##1.Unityの導入
こちらもわかりやすい導入記事がありましたので、参考にさせて頂いた記事の紹介のみとさせて頂きます。
2020年度版Unityダウンロード方法
##2.SceneとGameObjectとComponent
Unityではキャラクターもステージも背景も全てGameObjectとして定義します。
そしてそのGameObjectに色々な種類のComponentを組み合わせて設定することで様々な制御を行うことができます。
SceneはGameObjectたちを格納する箱のようなものです。
###キャラクターの表示
まず操作するキャラクターを表示させます。
UnityはAssetStoreに有償・無償の素材が沢山あるので一旦以下の素材について画像だけ使わせて頂きました。
AssetStore Sprite Pack #1 Tap and Fly
上記ページのAdd to My Assetsでダウンロード、インポートすると以下のような画面になります。
下のProjectビューから表示させたい画像を複数枚選んでSceneビューにドラッグ&ドロップしてアニメーションを保存すると、Hierarchyビューにplayer_fly_f01というGameObjectが追加され、Sceneビューに画像が表示されます。
わかりやすいように名前はPlayerに変えておきます。
そして、右側のInspectorからTransform ComponentのPositionとScaleをいじって上部の再生ボタンをクリックすると・・・。
簡単にアニメーションさせることができました。
このように実際にどのようにゲームが動作するのかプレビュー表示させて確認することができます。
再生の他に、一時停止、1フレームだけ進める、といったことができるようになっているのでデバッグ時にも役に立ちます。
###RigidBody Componentの設定
物理法則に則ってオブジェクトを動かしたい場合は、RigidBodyというComponentを設定することで実現できます。
Add ComponentからPhysics 2D->Rigidbody 2Dを選択します。
色々と設定がありますが、ひとまずMass(質量)を20、GravityScale(重力のかかり具合)を6としてみます。
再生してみます。
落ちた!
###床の設置
次はステージの土台となる床を設置してみます。
左のHierarchyビューから、+マーク->3D Object->Cubeを選択しFloorという名前をつけます。
床についてもまずはTransformでPositionとScaleを調整し土台となるように設定します。
ここまで2Dで全て表示させていましたが、実際は↓のように3D空間にそれぞれのオブジェクトが存在しています。
ひとまず再生してみます。
すり抜けます。
###Colliderによる衝突判定
UnityにはColliderというComponentがあり、これを使って衝突判定を行います。
2Dオブジェクトと衝突判定させるため、Floorに元々設定されている3Dオブジェクト用のBox ColliderをRemoveし、InspectorのAdd ComponentからPhysics 2D->Box Collider 2Dを選択します。
見づらいですが、床全体が緑の線に囲われており、こちらが衝突判定に使われます。
Player側にもColliderが必要なのでCircle Collider 2Dを追加します。
Player側はこの緑の円が衝突判定に使われます。
再生してみます。
うまく乗せることができました。
##3.スクリプトによる制御
Unityではキー操作や、ある条件のときに何か処理をさせたい場合、C#でスクリプトを書くことで実現できます。
###キャラクターの操作
まずはスペースキーを押した時にジャンプさせてみます。
Projectビューで右クリック->Create->C# Scriptを選択して、↓のサンプルコードを実装します。
using UnityEngine;
using System.Collections;
public class Player : MonoBehaviour {
// 変数の定義と初期化
public float flap = 550f;
Rigidbody2D rb2d;
// Updateの前に1回だけ呼ばれるメソッド
void Start() {
// Rigidbody2Dをキャッシュする
rb2d = GetComponent<Rigidbody2D>();
}
// シーン中にフレーム毎に呼ばれるメソッド
void Update() {
// スペースキーを押したら
if (Input.GetKeyDown(KeyCode.Space)) {
// (0,1)の垂直方向に瞬間的に力を加えて跳ねさせる
rb2d.AddForce(Vector2.up * flap, ForceMode2D.Impulse);
}
}
}
1.最初にStartメソッドが一度だけ呼び出されるので、PlayerのRigidbody2Dを保持しておく。
2.1フレーム毎に毎回呼び出されるUpdateメソッドにおいて、Input.GetKeyDownメソッドにてスペースキーが押下されているかチェックする。
3.押下されていたらAddForceにてVector2.up(0,1)の垂直方向に一定の力を掛けて、瞬間的(ForceMode2D.Impulse)に力を加える。
スクリプトをHierarchyビューのPlayerにドラッグ&ドロップして設定し、再生します。
スペースキーを押すたびにジャンプしてくれました。
可愛い。
###キーの押下時間によってジャンプ力を変える
次はJump○ing的な感じでスペースを押した時間によってジャンプの高さを変えたいのでコードを修正してみます。
まず、スペースを離した時にジャンプさせるために、Input.GetKeyDownをInput.GetKeyUpに変更。
// スペースを離したら
if (Input.GetKeyUp(KeyCode.Space)) {
次に押している時間によってジャンプ力を変えたいので、Input.GetKey(KeyCode.Spece)でジャンプ力を累積して、AddForce時に考慮する。
public float chargePower = 0f;
// シーン中にフレーム毎に呼ばれるメソッド
void Update() {
// スペースキーを押している間
if (Input.GetKey(KeyCode.Space)) {
// ジャンプ力を累積する。
chargePower += 1f;
}
// スペースキーを離したら
if (Input.GetKeyUp(KeyCode.Space)) {
// (0,1)の垂直方向に瞬間的に力を加えて跳ねさせる
rb2d.AddForce(Vector2.up * flap * chargePower, ForceMode2D.Impulse);
// 一度ジャンプしたらpowerをリセットする
chargePower = 0;
}
}
これはAddForceにて適当にchargePowerを掛けてしまったのが原因のようです。
フレーム毎に加算する値を増やして、足すようにします。
if (Input.GetKey(KeyCode.Space)) {
// ジャンプ力を累積する。
chargePower += 5f;
}
// スペースキーを離したら
if (Input.GetKeyUp(KeyCode.Space)) {
// (0,1)の垂直方向に瞬間的に力を加えて跳ねさせる
rb2d.AddForce(Vector2.up * (flap + chargePower), ForceMode2D.Impulse);
// 一度ジャンプしたらpowerをリセットする
chargePower = 0f;
}
###左右へのジャンプ
次は、左右どちらかのカーソルを押したままジャンプした場合はその方向に飛ぶようにします。
Vector2は(x,y)で二次元のベクトルを扱える型なので、それぞれ以下の値を設定すれば良さそうです。
上方向:(0,1) ・・・ Vector2.upはnew Vector2(0,1)と同義
左斜め上方向:(-1,1)
右斜め上方向:(1,1)
// スペースキーを離したら
if (Input.GetKeyUp(KeyCode.Space)) {
// 初期値は上方向
Vector2 direction = Vector2.up;
// 左右キーが押されているかどうかチェックする。
if (Input.GetKey(KeyCode.LeftArrow) && Input.GetKey(KeyCode.RightArrow)) {
// どちらも押されていたら垂直に飛ぶ。
} else if (Input.GetKey(KeyCode.LeftArrow)) {
// 左が押されていたら左斜め方向に設定する。
direction = new Vector2(-1, 1);
} else if (Input.GetKey(KeyCode.RightArrow)) {
// 右が押されていたら右斜め方向に設定する。
direction = new Vector2(1, 1);
}
// directionに設定した方向に瞬間的に力を加えて跳ねさせる
rb2d.AddForce(direction * (flap + chargePower), ForceMode2D.Impulse);
トリさんが可哀想なので綺麗に着地できるようにします。
まず、Rigidbody 2Dの設定でFreeze Rotationにチェックを入れるだけで回転が抑止できるようです。
ゲーム性としては着地はビタ止めさせたいので、地面と接触したときにトリを止めます。
キャラクターのオブジェクトにRigidbody 2DとCollider 2D、接触判定させたい床のオブジェクトにCollider 2Dが設定されていれば接触したタイミングでOnCollisionEnter2Dメソッドが呼び出されるのでそこで速度をゼロにします。
// ゲームオブジェクト同士が接触したタイミングで実行
void OnCollisionEnter2D(Collision2D collision) {
// 着地した場合は速度をゼロにする。
rb2d.velocity = Vector2.zero;
}
###左右への移動
ジャンプだけではなく、左右に歩けるようにします。
移動についてはtransformで座標を設定する方法や、RigidBodyのAddForceで物理的に力を加える方法などがありますが、今回は慣性も必要なくただ等速に左右に動かしたいだけなのでtransformを使っていきます。
private Vector3 velocity = new Vector3(30f, 0, 0);
if (Input.GetKey(KeyCode.Space)) {
} else if (Input.GetKey(KeyCode.RightArrow)&&Input.GetKey(KeyCode.LeftArrow)) {
// 左右どちらも押下されていたら移動しない
} else if (Input.GetKey(KeyCode.RightArrow)) {
// velocityにTime.deltaTimeをかけてフレームレートによらず一定速度となるように調整
transform.position += (velocity * Time.deltaTime);
} else if (Input.GetKey(KeyCode.LeftArrow)) {
// -velocityにTime.deltaTimeをかけてフレームレートによらず一定速度となるように調整
transform.position += (-velocity * Time.deltaTime);
}
transform.positionに対してx軸を移動させたいので、Vector3で定義したvelocityを足しています。
ここで、動作環境のfpsによってUpdateメソッドが呼ばれる間隔がずれてしまうため、Time.deltaTime(フレーム間の経過時間)をかけることで同じ時間で同じ距離を移動するようにしています。
移動はできましたが、ずっと右を向いたままなのが不自然ですね。
###スプライト画像の差し替え
右を向いているとき、左を向いているとき、力を溜めている時、とそれぞれの画像を変えてみます。
まずAssetStoreから拾ってきた素材は全て右向きのため、以下の画像をコピーしてそれぞれ左向き用の画像を作成します。
(macであればプレビュー.appで左右反転できます)
左向き
player_fly_f01.png → player_fly_f01_left.png
player_fly_f02.png → player_fly_f02_left.png
player_fly_f03.png → player_fly_f03_left.png
力を溜める(右向き左向きで、それぞれ同じ画像を二枚用意しています)
player_idle_f02.png → player_idle_f02_02.png
→ player_idle_f02_left.png
→ player_idle_f02_left_02.png
上記で用意した左向きの画像三枚と、力を溜める(右向き)の画像二枚、力を溜める(左向き)の画像二枚を、それぞれまとめてSceneビューにドラッグ&ドロップします。(Hierarchyビューに生成される各オブジェクトは不要なので削除します)
ここで、力を溜める動作は一枚の画像だけで実現できると思うのですが、同じ画像を二枚使ってアニメーションを作ってしまうほうが手っ取り早かったのでそうしています。
アニメーションについてはまだまだよくわかっていないので、別の記事で詳しく取り上げたいと思います。
わかりやすいように元々あったflyアニメーションファイルをrightward,rightwardControllerにそれぞれリネームしておきます。
あとはスクリプトで切り替えたいタイミングでAnimatorのruntimeAnimatorControllerにそれぞれのAnimatorControllerを設定します。
using UnityEditor.Animations;
public class Player : MonoBehaviour {
Animator animator;
public AnimatorController leftwardController;
public AnimatorController rightwardController;
public AnimatorController chargeLeftController;
public AnimatorController chargeRightController;
bool isRightward = true;
void Start() {
// Rigidbody2Dをキャッシュする
rb2d = GetComponent<Rigidbody2D>();
// Animatorをキャッシュする
animator = GetComponent<Animator>();
}
// シーン中にフレーム毎に呼ばれるメソッド
void Update() {
〜省略〜
// スペースキーを押している間
if (Input.GetKey(KeyCode.Space)) {
// ジャンプ力を累積する
chargePower += 100f;
// 歩いていた方向によってスプライト画像を差し替える
animator.runtimeAnimatorController = (RuntimeAnimatorController)RuntimeAnimatorController.Instantiate(isRightward ? rightwardController : leftwardController);
} else if (Input.GetKey(KeyCode.RightArrow)&&Input.GetKey(KeyCode.LeftArrow)) {
// 左右どちらも押下されていたら移動しない
} else if (Input.GetKey(KeyCode.RightArrow)) {
// velocityにTime.deltaTimeをかけてフレームレートによらず一定速度となるように調整
transform.position += (velocity * Time.deltaTime);
// 左を向いていた場合に一度だけ右向きのスプライト画像に差し替える
if(!isRightward){
animator.runtimeAnimatorController = (RuntimeAnimatorController)RuntimeAnimatorController.Instantiate(rightwardController);
isRightward = true;
}
} else if (Input.GetKey(KeyCode.LeftArrow)) {
// -velocityにTime.deltaTimeをかけてフレームレートによらず一定速度となるように調整
transform.position += (-velocity * Time.deltaTime);
// 右を向いていた場合に一度だけ左向きのスプライト画像に差し替える
if(isRightward){
animator.runtimeAnimatorController = (RuntimeAnimatorController)RuntimeAnimatorController.Instantiate(leftwardController);
isRightward = false;
}
}
if (Input.GetKeyUp(KeyCode.Space)) {
// ジャンプする前の向きによって画像を差し替える
animator.runtimeAnimatorController = (RuntimeAnimatorController)RuntimeAnimatorController.Instantiate(isRightward ? rightwardController : leftwardController);
publicで変数設定を行うとInspectorから設定変更できるようになるので、それぞれAnimatorControllerを設定します。
再生してみると・・・。
それぞれの動作時に画像が切り替わってくれました。
##4.Prefabについて
PrefabはGameObjectの型のようなもので、予めこの型を作っておくとGameObjectの生成を楽に行うことができます。
また、スクリプトからも生成することができるようになるので、ある条件の時に動的に生成したい場合に大活躍します。
###台の設置
まずジャンプして上に登っていくゲームを考えたときに登るための台を設置する必要があります。
ひとまず床と同様にCubeオブジェクトをStepという名前でいくつか配置します。
また、忘れずにBox Collider 2Dを設定しておきます。
それっぽくなってきました。
###Prefabを使ったオブジェクトの動的生成
ステージを固定して台をあらかじめ決められた場所に設置しておいても良いのですが、台をランダムに生成してどれだけ登れたかを競うようなゲームにしようと思います。
そこでPrefabを使ってオブジェクトを動的に生成したいと思います。
Assetsフォルダ配下にResourcesフォルダを作成し、先程作成したStepオブジェクトをHierarchyビューからドラッグ&ドロップします。
そうするとStepというprefabファイルが作成できます。
※スクリプトからファイルを取得する際にはAssets->Resources配下にファイルが存在していることが必須なので注意。(サブフォルダがあるのは問題ない)
次にスクリプト上でこのファイルを参照しオブジェクトを生成するコードを加えます。
先程作成したStepオブジェクトのAdd ComponentからNew scriptでStepというスクリプトを作成します。
using UnityEngine;
using System.Collections;
public class Step : MonoBehaviour
{
private GameObject stepPrefab;
void Start() {
// StepのprefabファイルをGameObject型で取得
stepPrefab = (GameObject)Resources.Load ("Step");
}
void Update() {
// スペースキーを押したら
if (Input.GetKeyDown(KeyCode.Space)) {
// x,y軸はある範囲の中でランダムに座標を決める。
float x = Random.Range(-30.0f, 30.0f);
float y = Random.Range(10.0f, 30.0f);
// StepPrefabオブジェクトを生成する。
Instantiate(stepPrefab, new Vector3(x, y, 0f), Quaternion.identity);
}
}
}
1.Startメソッドで一度だけ、Resources.Loadメソッドを使ってResourcesフォルダ配下の”Step”ファイルをロードします。
2.Updateメソッドにおいてスペースキーを押されたときにInstantiateメソッドでStepオブジェクトを生成します。
またx,y座標はRandom.Rangeを使って、ある範囲の中でランダムに決めています。
スペースキーを押すたびに台が生成されました。(挟まれて可哀想・・・)
HierarchyビューでもStepオブジェクトがクローンされていることがわかります。
###台に着地したときに新たな台を生成する
先程はスペースキーを押すたびに台が生成されるようにしましたが、実際は台に着地できたら新たな台を生成させたいです。
Stepスクリプトの台の生成処理をOnCollisionEnter2Dに移動します。
using UnityEngine;
using System.Collections;
public class Step : MonoBehaviour{
private GameObject stepPrefab;
bool isFirstCollisionEnter = true;
Camera cam;
void Start() {
// StepのprefabファイルをGameObject型で取得
stepPrefab = (GameObject)Resources.Load ("Step");
// メインカメラオブジェクトを取得
cam = Camera.main;
}
void Update() {}
void OnBecameInvisible() {
Destroy(this.gameObject);
}
// ゲームオブジェクト同士が接触したタイミングで実行
void OnCollisionEnter2D(Collision2D collision) {
// 初めて着地したときのみ実行する
if(isFirstCollisionEnter){
isFirstCollisionEnter = false;
// x,y軸座標と幅はある範囲の中でランダムに座標を決める。
Vector3 stepPos = transform.position;
float x = Random.Range(stepPos.x * (-1), stepPos.x * (-1));
float y = Random.Range(stepPos.y + 10.0f, stepPos.y + 25.0f);
float width = Random.Range(5.0f, 20.0f);
stepPrefab.transform.localScale = new Vector3(width, stepPrefab.transform.localScale.y, stepPrefab.transform.localScale.z);
// StepPrefabオブジェクトを生成する。
Instantiate(stepPrefab, new Vector3(x, y, 0f), Quaternion.identity);
// メインカメラも上に移動する
Vector3 cameraPos = cam.gameObject.transform.position;
cameraPos.y = y+5f;
cam.gameObject.transform.position = cameraPos;
}
}
}
1.StartメソッドでStepのprefabファイルと、メインカメラのオブジェクトを取得します。
2.OnCollisionEnter2Dメソッドにおいて、初めて台に着地したときのみx,y座標と幅をある範囲の中で変動させて、新たなStepオブジェクトを生成します。
3.台に登ったら新たに生成した台のy座標をもとにメインカメラを移動します。
また、ここでOnBecameInvisibeメソッドで自分のオブジェクトをDestroyしていますが、これは画面外にオブジェクトが出た際に削除する処理になります。
この処理を入れないといつまでもStepオブジェクトが残り続けて負荷がかかってしまいます。
ひとまずどんどん上に登って行けるようになりました!
ただし・・・。
頭突きした場合や、台の横の部分に接触した場合でも着地したことになっていることに気がつきました。
###衝突判定を細かく設定する
しっかりと台の上に着地したときだけ新たな台が生成される様にします。
そのためにPlayerオブジェクトに対してEdge Collider 2Dを追加で設定します。
ここでIs Triggerにチェックを入れると衝突判定をしない(すり抜けるようになる)代わりに、オブジェクトが接触したかどうかを判定することができます。
矢印が指す足元部分にEdge Collider 2Dを配置しています。
そしてスクリプトにおいて、元々OnCollisionEnter2Dメソッドで処理していたものを、OnTriggerEnter2Dメソッドで処理することでPlayerの足元に床、もしくは台が接触したときだけ呼び出されるようになります。
private void OnTriggerEnter2D(Collider2D other) {
private void OnTriggerEnter2D(Collider2D other) {
##5.Sceneの追加
Unityでは場面が変わる場合、例えばタイトル画面、ゲームプレイ中、ゲームオーバー時などはそれぞれ別のSceneを作るのが一般的です。
###タイトル画面の実装
まず、新しくタイトル画面用のSceneを作成します。
File->New SceneでGameStartという名前をつけてSceneフォルダに保存します。
次にCanvasというオブジェクトを作ります
Canvasの子オブジェクトとしてPanel、Panelの子オブジェクトとしてButtonとImageを作成します。
今回Panelは一つだけなので無くても良いのですが、今後領域を分けて管理したい場合にPanelを作っておくと便利なので倣っておきます。
次にButtonのInspectorにおいて、Rect TransformでPosとWidth,Heightの設定、Image->Source Imageにスプライト素材のbutton_playをドラッグ&ドロップで設定します。
Imageについても同様に設定し、スプライト素材はtitleを設定します。
Panelについては、scene_01_skyを設定します。
素材のおかげでそれっぽくなりましたね!
次にスタートボタンを押したときにゲームが開始されるようにします。
元々のSampleSceneはGamePlaySceneにリネームしておきます。
まず、空のオブジェクトを作成し、Add ComponentでGameStartスクリプトを作成します。
using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;
public class GameStart : MonoBehaviour {
public void StartGame(){
SceneManager.LoadScene("GamePlayScene");
}
}
SceneManager.LoadSceneで指定したSceneを読み込んで遷移することができます。
このGameStartObjectをButtonに紐付けて、No FunctionとなっているところをGameStart->StartGameを選択します。
ここで、スクリプトをButtonに直接紐付けると関数が表示されないので、必ずGameObjectを紐付ける必要があります。
最後に作成したSceneをプロジェクトに追加する必要があります。
File->Build Settingsで設定を開き、Add Open Scenesを選択します。
GamePlaySceneを開き同様に追加します。
再生してみます。
うまく遷移させることができました。
###ゲームオーバー画面の実装
スタート画面と同様に新しくSceneを作り、名前はGameOverSceneとします。
同じようにCanvas,Panel,Button,Imageを作りますが、リトライとタイトル画面に戻る、の二つのボタンを配置することにします。
以下のスクリプトを作成し、GameObjectに紐付けて、それぞれのボタンにRetryGameとBackTitleを設定します。
using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;
public class GameOver : MonoBehaviour {
public void RetryGame(){
SceneManager.LoadScene("GamePlayScene");
}
public void BackTitle(){
SceneManager.LoadScene("GameStartScene");
}
}
そしてメインカメラの子オブジェクトとして空のGameObjectを作りGameOverColliderという名前にして、Edge Collider 2Dを以下のような形で追加します。
メインカメラの子オブジェクトとすることでカメラの移動にあわせてEdge Collider 2Dの判定も追従して移動してくれます。
GameOverColliderにおいてスクリプトを作成し紐付けます。
using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;
public class GameOverCol : MonoBehaviour {
private void OnTriggerEnter2D(Collider2D other) {
SceneManager.LoadScene("GameOverScene");
}
}
##6.その他
###効果音をつける
ボタン押下や、ジャンプ、台に乗れたときのスコア加算などについて、効果音をつけます。
効果音を鳴らしたいゲームオブジェクトについて、Audio Source Componentを追加します。
次にスクリプトにおいてAudioClipをpublicで宣言し、各アクション時にaudioSource.PlayOneShotメソッドを呼び出します。
public class Player : MonoBehaviour {
public AudioClip jump;
AudioSource audioSource;
void Start() {
// AudioSourceをキャッシュする
audioSource = GetComponent<AudioSource>();
}
if (Input.GetKeyUp(KeyCode.Space)) {
// ジャンプ音を鳴らす。
audioSource.PlayOneShot(jump);
最後にpublic宣言したAudioClipについて、サウンドファイルを紐付けます。
スコア加算と、ボタン押下、ゲームオーバー時も同様に対応します。
音については実際にゲームを遊んで頂いて確認してみてください。
###BGMをつける
まずは使用するBGMのサウンドファイルを追加して、GameObjectを生成します。
Loopにチェックを入れておきます。
そしてこのGameObjectにスクリプトを新規作成して追加します。
public class bgm : MonoBehaviour {
void Start() {
DontDestroyOnLoad (this);
}
}
ここで、DontDestroyOnLoadメソッドを呼んでおくと、シーンが切り替わってもオブジェクトが破棄されなくなり、BGMが鳴り続けます。
###スコアの実装
登った高さと、台の長さによってスコアを計算して表示するようにします。
0~9までの数字が一つの画像にまとまっている素材を使ってカスタムフォントを設定する方法で進めたのですが、ややこしくててこずりました・・。
以下の記事がとても参考になりました。
【Unity】自作テクスチャで文字を表示する(カスタムフォント)
まず、MaterialをScoreFontという名前で新規作成します。
ShaderはUnlit/Transparent、TextureをSprites->GUI->text_playScoreに設定します。
次に、ProjectビューでCustom FontをScoreFontという名前で作成します。
次に作成したScoreFontを選択し、Inspectorで以下のように設定していきます。
Line Spacingは行間で、今回は一行しかないので0にしています。
Ascii Start OffsetはASCIIコード表において数字の0から表示させる=48番目までオフセットさせるということで48を設定しています。
Trackingは同じ行における隣り合う文字の間隔で1を設定しています。
ややこしいのはCharacter Rectsの設定です。
これらは素材として使用しているtext_playScore.pngにおいて、0〜9までのそれぞれの数字がどの位置に、どのサイズで配置されているかを教えてあげる設定になります。
※Element0,1までしか見えていないですが、9まで設定しています。IndexとUv Xを同じ用に加算していけばOKです。
今回使用したtext_playScore.pngは260x38ピクセルの画像になるので、横に10分割してそれぞれのサイズを求めていきます。
ここでUvは全体を1としたときの設定、Vertは画像のピクセルサイズをもとに設定します。
各パラメータの解説は公式ドキュメントにも記載があるので参考にしてください。
https://docs.unity3d.com/ja/2019.4/Manual/class-Font.html
ここまでできたらHierarchyビューにCanvas,Panel,textを二つを配置して、それぞれScoreTitle,Scoreという名前にします。
ScoreのFontに先程作成したScoreFontを設定します。
Text欄に入力した数字がそれぞれの画像で表示されました!
そしてあとはスクリプトでスコアを計算して、textに動的に設定するようにしていきます。
public class Step : MonoBehaviour {
public Material normalMaterial;
public Material bonusMaterial;
void Start() {
〜省略〜
// 幅が短い場合はボーナスがつくため金色のマテリアルに変える
if(this.transform.localScale.x < 10.0f){
this.GetComponent<Renderer>().material = bonusMaterial;
} else {
this.GetComponent<Renderer>().material = normalMaterial;
}
}
private void OnTriggerEnter2D(Collider2D other) {
// 初めて着地したときのみ実行する
if(isFirstCollisionEnter){
isFirstCollisionEnter = false;
// スコアを加算する。
GameObject scoreObj = GameObject.Find ("Score");
Text textComponent = scoreObj.GetComponent<Text>();
// 台の長さが短いほどスコアが高くなるようにする。
int stepWidthScore = 50 - (int)this.transform.localScale.x;
if(this.transform.localScale.x < 10.0f){
// 幅が10.0f未満の場合は短さに応じて倍率をかける。
stepWidthScore *= 11 - (int)this.transform.localScale.x;
}
// 今のスコアに足してtextに設定する。
int scoreNum = int.Parse(textComponent.text) + (100 * stepWidthScore);
textComponent.text = scoreNum.ToString();
台については通常時と、幅が短かったときのボーナス用の二枚の素材を使います。
ノーマル用は元々あったscene_01_cradleを良い感じに切り出しました。
ボーナス用は金色っぽく新しく作ってみました。
上記の画像を設定したmaterialを作成します。
こっそり床の素材も変えておきました。
画像のWrap ModeをRepeatにして、materialのTilingを設定するとその個数分だけ並べて画像を表示してくれます。
スコアが加算されるようになりました。
###オンラインランキングの実装
以下のライブラリを使用させて頂き、オンラインランキングを導入します。
https://blog.naichilab.com/entry/webgl-simple-ranking
解説通り進めていけば実装できましたので、導入手順は割愛します。
スクリプトについては、スコアをPlayerオブジェクトにstatic変数で保持して、ゲームオーバー時にそのスコアを送信します。
public static int score = 0;
void Start() {
score = 0;
}
// ゲームオブジェクト同士が接触したタイミングで実行
private void OnTriggerEnter2D(Collider2D other) {
// 初めて着地したときのみ実行する
if(isFirstCollisionEnter){
〜省略〜
// Playerのscore変数にも格納しておく。(ゲームオーバー時に参照する)
Player.score = scoreNum;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
public class GameOver : MonoBehaviour {
void Start(){
// 最終スコアを加算する。
naichilab.RankingLoader.Instance.SendScoreAndShowRanking(Player.score);
}
##7.スプライト素材の作成
残念ながらゲーム作りに時間がかかってしまい、ボーナス台やボタンのデザインを修正するくらいしかできませんでした。。
今後、猫社員たちのドット絵や、背景なども自作して、また記事を投稿したいと思っています。
##8.ビルド、ゲームの公開
今回はWebGLで遊べるようにして、UnityRoomというUnity公式のプラットフォームで公開します。
以下の記事を参考にさせて頂きました。手順通りで問題なく公開できたので解説は割愛します。
unityroomに投稿する方法
こっそりハードモードを追加しました。
#今後の課題
・キャラクターや背景などのスプライト素材や効果音なども自作したい
背景については高く登っていくたびに宇宙に近づいている様子を背景で表したかった・・。
・ゲームバランス見直し
台の生成処理のランダム性、アイテムなどを出現させる、など。
・Unityのスクリプトについてリファクタリングする(ゴリ押し実装が多そう)
・スマホ対応(マルチタッチが大変そう)
・アニメーションについて理解を深める
・まったく別のゲーム作成もやってみる
などなど。。
これらは今後も継続的にQiitaに記事をあげて理解を深めていきたいと思っています。
#さいごに
最後まで読んで頂きありがとうございました。
とても長くなってしまいましたがUnityを使ったゲーム制作の雰囲気が伝わったでしょうか?
元々はアドベントカレンダーの記事投稿のために始めたことでしたが、記事作成とゲーム制作どちらもとても楽しめました。
また、記事を作成するためには自分が理解していないと記事が書けないので、ただUnityを触るよりも一つ一つしっかりと理解しながら進められたことはとても良かったと感じました。
みなさんも興味があったらぜひUnityやAsepriteを触ってみてください!!