LoginSignup
4
5

More than 3 years have passed since last update.

Unityで2Dアクションゲームを制作したときの知見

Last updated at Posted at 2020-06-04

はじめに

まずは公式リファレンスを見よう

一番言いたいことを最初に書きました。

UnderRocketというゲーム制作を通して得た知見を(自分用のメモとして)残しておきます。
Unityを学び始めて1年未満くらいの人向けの内容です。

特に以下のようなものを実装したい人向けです
・2Dのアクションゲーム
・WebGLビルドをし、公開する
・ハイスコアを保存、共有したい

(UnderRocketは以下のサイトで遊べるので、ぜひプレイしてみてください^^)
UnderRocket | unityroom

汚いですがコードはこちら
UnderRocket GitHub

TilemapとCinemachine

ステージ外を移さないカメラ移動

Cinemachineを利用します。
インストールの仕方を調べると、AssetStoreからインストールという記事がたくさんありますが、
現在はUnityエディタからインストールできます。

一番注意する点として、カメラの枠より小さい範囲を映そうとすると上手くいきません。
ステージの大きさや、映す範囲には注意してください。

Cinemachineインストール方法
Cinemachineの基本的な使い方

Tilemapを利用する

2Dゲームで素直にsceneにgameObjectを置いてステージを作成しようとすると、だいぶ大変です。
Tilemapを利用すると、簡単に描画・Collider配置ができます。

以下の2サイトを見れば、できると思うので、利用したことがない人はぜひ
コガネブログ
Qiita

Tilemapのtileひとつひとつの間隔が空いてしまう

tilemap.png

原因: SpritesのPixels Per Unitのサイズがタイルに合っていない
tilemap2.png

tilemap3.png

上の画像の場合は、Pixels Per Unitの値を16にする

TilemapとCinemachineを共存させる方法

Cinemachineの映す範囲を、Cinemachine ConfinerのBounding Shape2Dで指定します。
Cinemachine0.png

ここで、代入するものは、Tilemapとは別でステージの範囲に合わせて作成したCompositeCollider2D。

Cinemachine1.png

当然ですが、TilemapのcompositeColliderをカメラの枠にするとうまく動きません。
そのため、ステージの枠用のCompositeCollider2Dを別で用意する必要があるのです。

ただし、Tilemapと別ステージ枠を設定することで、本来狭すぎてCinemachineで映せなかった部分を、実際のステージより大きくすることで映すということができます(ステージ外が映ってしまいますが)。

NCMB

NCMBを利用し、オンラインランキング機能を実装する

今回オンラインランキング機能を実装するにあたり、
NCMB(Nifty Cloud Mobile Backend)というサービスを利用しました。
無料でひと月100万回APIリクエストできるすごいやつです。
NCMB公式サイト

公式のマニュアルや、記事を漁れば環境構築と基本的な実装は難しくないはずです。
クイックスタート(公式)
機能別コード例
コルーチンをいい感じに

前座: NCMBはUnityのWebGLビルドに対応しているのか

開発している途中で、実装は間違っていないはずでUnityエディタ上では動くのに、ブラウザ上では上手く動かないということがありました。
調べると「NCMBはUnityのWebGLに対応していない」とか「WebGL専用のスクリプトを用意してます」みたいなニュアンスの記事を目にします。

結論を言うと専用のスクリプトを使わなくても、WebGL版でもちゃんと動きます。
動かないとすれば、ビルドの設定やその他諸々の問題です。

createDateが参照できない

NCMBのデータストアを利用する際、最初からデータ格納日時である「createDate」とデータ更新日時である「updateDate」が用意されています。
しかし、このcreateDateが参照できなく...ない...ない...
...ありました。

自分で作成したDBでいうところのカラム(PlayerName, Scoreなど)はobj["PlayerName"]などと参照しているので、てっきりobj["createDate"]で参照できると思っていました。
obj.CreateDateでした。
この仕様は一般的なんですかね...私はわかりません。

private NCMBObject obj;

Debug.Log(obj["createDate"].ToString()); //error
Debug.Log(obj.CreateDate.ToString()); //ok

データストアへの登録が終わったかわからない

NCMBObjectに用意されているSaveAsyncを利用してデータストアへのセーブ作業をしているのですが、この関数、エラーは返しても結果は返してくれません。
SaveAsync

例えばスコアランキングを作っていたとして、
①プレイヤーのスコアをデータストアに登録
②データストアからスコアが高い順にn人取得
とやったときに①が完了する前に②をおこなうと、②の結果が変わる可能性があります。

この問題は、プレイヤーの送信予定のデータとデータストアから持ってきたデータを比較することで解決しました。
①データストアからスコアが高い順にn人取得
②n人のスコアと、プレイヤーのデータで比較、処理(クライアント側)。
③プレイヤーのスコアをデータストアに登録

実装

当たり判定を残したまま、壁で跳ね返す

これには様々な方法があると思いますが、今回はRigidbody2Dを利用した方法です。

・Rigidbody2DのBodyTypeをDynamic
・CircleCollider2DのIsTriggerをfalse
・動きの制御はRigidbody2Dで
これさえ守れば、PhysicsMaterialの値次第で様々な跳ね返りができますね。

しかし、「壁でだけ跳ね返ってほしいのに、他のgameObjectとも物理的な衝突処理がされてしまう」という問題になりがちです。

この問題の解決策のひとつとして、「壁衝突用、他衝突フラグ用でふたつのgameObjectを用意する」があります。
・壁衝突用...CircleCollider(IsTrigger = false)、Layerを壁用のLayerと検知(他不要なものは検知しない)
collider1.png

・他衝突フラグ用...見た目に合わせたCollider(IsTrigger = true)、Layerは特に指定なし(Layer単位で無視したいものがあれば)
collider0.png

Layer同士無視するかどうかは、Edit->ProjectSettings->LayerCollisionMatrixから設定できます
collider2.png

毎回データストアを参照しない

UnderRocketの話をすると、各ステージごとにクリアタイム順10名と最近クリア10名の名前とタイム(or日付)を表示します。
プレイヤーがミッションクリアしたときは、ランクインしている可能性があるのでデータストアから最新の情報を読み取ります。
しかし、ミッション失敗したときはどうでしょう?私は最新の情報である必要性は低いと考えました。
そのため、ランキングデータを初回取得時にクライアント側に格納し、ランキング一覧表示や失敗時には、すでにあるデータはクライアント側から、無いデータだけをデータストアからとってくるという処理にしました。

データはDictionaryとして格納
keyはミッション名(SceneManager.GetActiveScene().nameと一致)
valueはstring2つ(playerNameと、timeまたはdate)を持つクラス

プロパティもシリアライズ化したい

「プロパティだけでいいのに、SerializeFieldでInspector上から編集したいから、仕方なくフィールドを用意。プロパティの初期値をフィールドから参照。」

[SerializeField] private int hp;
public int Hp {get; private set;}
private void Awake(){
    this.Hp = this.hp;
}

こういうとき、ないですか?
プロパティの初期値をSerializeFieldで変えられたら...最高ですよね?

そんな魔法がこちら↓
コガネブログ

※公式で意図されたものかわからないのでご用心

フォント

WebGLのビルドでは、Unityのデフォルトのフォント(Arial)を利用していると日本語が表示されません。
日本語に対応したフォントに変更する必要があります。
また、フォントによって大きさが違うので、表示の変化に注意してください。

フォントを一括で変更するには、以下のサイトを参考にしました↓
フォント一括変更

ツイートボタン

WebGL版でツイートする場合、現在開いているウィンドウとは別ウィンドウで開く、という処理が必要になります。
「エディタ上ではできるけど、ブラウザ上からはツイートできない」みたいな人は必見↓

WebGLでツイートボタン

Maximum call stack size exceeded

maximum.png
⇈エラー画像

NCMBを利用して初めてのビルドをし、unityroomに反映させたら出たエラー。
当時、ロードシーンにNCMBSettings等のgameObjectや、BGMなどをロードするgameObjectを置いていました。
そしてロードシーンのStart()でタイトルシーンへ移動するようにしていたのです。

つまり、最初の処理でやることが多すぎ、ということだったんですね。
「NCMBがダメなのか?」と当時試行錯誤しましたが、NCMB関連のgameObjectをタイトルシーンに移動させることで解決しました。

フルスクリーンのときに文字の表示が違う(大きさ)

フルスクリーンにしたときに、Canvas内の見え方が変わるというお話。
今まで見えていたTextがフルスクリーンにすると範囲をはみ出て見えなくなってしまった、なんてこともありました。

Canvasの設定でなんとかできそうですが、今はどちらのサイズでも注意する、とだけしています。
どなたか知りませんか(小声)

停止処理をtimeScaleに頼らない

動作停止の簡単な実装方法としてTime.timeScaleを0にするというものがあります。
これによりUpdateは呼ばれたままで、Time.deltaTimeが0になったり、FixedUpdateが呼ばれなくなったりします。
つまり、プレイヤーの入力は受け取るまま、時間経過によるgameObjectの動きを止められます。

最初この方法を使っていたのですが、NCMBを利用するにあたって、主にコルーチンが動かしたいときに動かせない状態になってしまいました。
自作のものなら実時間で測るコルーチンを利用すれば、できなくはないのですが...。

他の機能も実装しづらくなる可能性があるので、timeScaleを使わずフラグによって処理する形にしました。
Update()の冒頭で、止まっているフラグならRigidbody2D.simulated = falseにするなどです。

Unityの便利機能

ColliderとRigidbodyのPhysicsMaterialの違い

ColliderとPhysicsの両方にPhysicsMaterialを代入できます。

ColliderとPhysicsのPhysicsMaterialには優先順位があります。
Colliderのほうが優先順位が高く、
基本となるPhysicsMaterialはRigidBodyに、特定の部分だけ変える場合はColliderに
という認識でよさそうです。

ちなみに、何も代入しない場合のデフォルトは
friction = 0.4、bounciness = 0

詳しくはこちら↓
UnityEngine.Rigidbody2D-sharedMaterial

UnityEvent

UnityEvent 公式リファレンス

Buttonで出てくるこれ
unityevent.png

Inspector上でボタンが押されたときにする処理を設定できて、便利ですよね。

注意する点
・UnityEventにInspector上から何も代入してないときに、Invoke()で呼ぶと、エラーにならず次の処理に進む。
・呼ぶ関数の引数は1つまで

サンプルコード

using UnityEngine.Events;

/*省略*/

[SerializeField] private UnityEvent haveDamaged; //攻撃を受けた際の処理
[SerializeField] private UnityEvent destroyMe; //自身が破壊されるときの処理
[SerializeField] private UnityEvent contactEnemy; //敵にあたったときの処理

private void OnTriggerEnter2D(Collider2D collision)
{
    if (collision.CompareTag("Enemy"))
    {
        this.contactEnemy.Invoke();
    }
    if (collision.CompareTag("AttackToPlayer"))
    {
        this.hp--;
        this.haveDamaged.Invoke();
        if (this.hp <= 0)
        {
            this.destroyMe.Invoke();
            Destroy(this.gameObject);
        }
    }
}

スクロールビュー

テラシュールブログ

Tilemap同様、使ってみたら意外と便利だったシリーズ。
大事なのは、contentにvertualGroupをいれることです。
contentの中に空のgameObject→その中に入れたいものを複数入れるという流れ
contentに入るContentSizeFitterは「Unconstrained」にしておくことで自由にwidth/heightを変えることができます

公式リファレンス

OnEnable()

gameObjectがactiveになったときに呼ばれます。
Awake()より後、Start()より前です。
OnDisable()もあります。
Update()等目立ちすぎて、影薄くなりがちですが、大変便利です。

呼ばれる順番

クリックされたときに呼ばれたいが、ボタンのように選択してほしくない

これはUnderRocketの右上歯車アイコンを押すと、設定が開く、といったときの話です。
Buttonの画像だけ変えるとそれっぽくなりますが、どうしてもゲームの入力中に間違って選択してしまい、そのままEnter/Spaceで押してしまったりします。

そこで、EventTriggerをアタッチしてPointerClickの処理を追加することで、簡単にマウスクリック時の処理ができます。
知ってるかどうかの問題ですが、本当便利ですね。
なお、クリックされたかの判定は、Imageの画像の範囲に自動で合わせてくれていました。
onClick.png

4
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
5