2Dアクションゲームを作るときのネット上に(なかなか)落ちていない情報
はじめに
みなさんこんにちは。10日目担当RiG++OB、5回生の妹チルです。なんでまだ大学にいるんだろうね、不思議だね。ダネフシャ!昔は総合部長やってたり2Dグラフィック課リーダーやってたりしてました。今の3回生がギリギリ知ってるかもね。部室の奥の方でずっと絵を描いてた人です。
あ、これ僕のオリキャラちゃんです。かわいいでしょ?
今回はアドカレを書く人が全然いないということで寄稿することにしました。参考になって楽しい記事が書ければいいな~というお気持ちです。
さて、この記事のテーマは【2Dアクションゲームを作るときのネット上に(なかなか)落ちていない情報】です! 今作っているこんなゲーム↓
で手に入れた知識を、コーディング課の人と2D課の人に向けて少し放流していきます。
見てわかる通り序盤すらまだ作れていないので僕も勉強の最中です。一緒に学んでいきましょう!
コーディング課の人へ向けて
【コードの構造】
まずはコードの理論的な構造の話。ゲームのコードではなるべくスクリプト毎に役割を分けた方がいいです。つまりスクリプトを量産しましょう!
例えば、プレイヤーを動かすスクリプトを考えます。このとき、プレイヤーキャラクターの機能としてはなにが考えられるでしょうか?
①動き
②アニメーション
とりあえずこの2つですかね。
ではこれを実装するとき、何が必要でしょうか。勝手に動かれては困るのでプレイヤーの入力を待つ必要がありますよね。というわけで
③入力判定
あとはゲームにもよりますが位置情報だったりプレイヤーの状態だったりを確認するための
④プレイヤーのデータ
データの中でも特別に処理したい
⑤HP
⑥お金
他には⑦、⑧、⑨……
と、こんな感じでどんどん分割していくことによって『どこにあの処理書いたっけ!?』という現象がある程度防げます。HPの処理はPlayerHPに書いてあるに決まってますよね。
未来の自分のためにも、スクリプトはどんどん分割していきましょう!
【横移動】
なんとゲーム制作って、プレイヤーを動かすにもプログラミングしないといけないらしいです。びっくりだ…。個人的にはRigidbody2Dを使ってvelocityを直接弄るのがオススメですが色々と流派のようなものがあるので紹介しておきます。実際にはこの4つを状況によって使い分ける必要があります。
①Transform.positionを書き換える
→個人的には『やっちゃダメコード』の最たるものです。なにがダメって当たり判定がまともに機能しないこと!(瞬間移動だからね) ただし物理的な当たり判定の存在しないゲームを作るときはこれでもいいです。
②Rigidbody2D.velocityを書き換える
→おすすめ!ちゃんと当たり判定をとってくれるので地面や壁に触れると止まります!(ちゃんと止まるかどうかはPhysicsMaterial2Dの設定次第)
③Rigidbody2D.AddForce()を使う
→物理的な力を加えて押し出す手法。現実で何かをドンッ!と押したときと同じような挙動をとります。ジャンプとかに使う方法もあるのですが…まあこれに関してはやってみればわかるのですが、2Dゲームのジャンプってめちゃくちゃ物理法則を無視しているので全然思った通りの動きしてくれないです。慣性の効いた動きを作りたいときにたまに使う。
④Rigidbody2D.MovePosition()を使う
→①の物理演算もちゃんとやってくれるバージョン。ワープ移動の時にその間を補間して障害物があったら考慮してくれるよ!②がベクトル指定なのに対して、こっちは絶対座標を指定できるので座標指定で動かしたいときに使える!②じゃなくてこっちを主流にしている人もいるみたい。
これらをプレイヤーの入力に合わせて操作します。
多分初めて作るときスティックの入力が全然反映されないとかRigidbody2Dアタッチし忘れてNullReferenceエラー出されたりとか色々あると思うけどそこらへんまで書いてると無限に記事が伸びてしまうので許して…!そのあたりは検索するとYouTubeとかに初心者用っていう名目で置いてあると思います!
【坂移動】
そんなこんなでプレイヤーの横移動を作ることができました。じゃあこれで坂道移動しようか!!
…あれ?上りが遅いし下りはなんか…落ちてる?
そう、横移動だけだと坂道は移動できません!!上り坂が登れるように見えているのも錯覚です。横に押し込まれてずり上がっているだけです。坂道では坂道用に、その坂の『壁ずりベクトル』をプレイヤーの移動ベクトルとしなくてはならないのです!
法線ベクトルと直行するのが壁ずりベクトルです。要は接線ですね。これを求めるにはレイを飛ばして法線ベクトルを求めてそれに直行するベクトルを求めっ…説明めんどくさいのでコード貼ります。
layerMaskで地面のみに当たるレイをposからdir方向にdis長で飛ばし、まずは当たった対象の法線ベクトルを取ります。
それからそのベクトルと進もうとしている方向のベクトル(toVector)の内積をとって法線ベクトルと掛け、toVectorとの差を…ってなにしてんだこれ?大分昔に書いたコードなのでなにもわかりません。なんかこれで壁ずりベクトルとれるみたいですね。知らんけど。
そしてこれだけだと始点と終点でめり込んだり飛び出したりするから坂道の判定早めて地面にプレイヤーを押し付けてうんにゃらかんにゃら…ポン!できた!
ここのうんにゃらかんにゃらに関してのヒントは『レイの射出開始位置』『プレイヤーのコライダーの形状』『法線ベクトル』です。4時間くらいあればいい感じにできると思います。
【ジャンプ】
ジャンプくらい勝手にしてくれよ…と思うんですがゲーム制作ではプログラミングをしないとジャンプボタンを押してもジャンプしてくれません。不親切ですね。
ジャンプの実装はvelocityやMovePosition()を利用するのが一番それっぽくなります。ボタンを押した瞬間にポン!と飛び上がって、時間とともに速度が落ちていき、やがて落下に転じる。これをプログラムで表現します。余談ですが、経験上、落下はRigidbody2Dの重力にまかせた方が色々と楽です。
他の処理も入っているのでわかりづらいですが…
要はgravityVelocityという段々減っていくベクトルをRigidbody2D.velocityに足しているってだけですね。で、y方向の速度が0以下になったら(=落下に転じたら)ジャンプ処理から脱して落下処理に移行させています(JumpReset())。
この処理だけだとvelocityを直接弄っている関係上上下移動しかしなくなってしまうので、横方向の入力も加味して横移動のベクトルも加算しています。
ネットだと圧倒的にAddForce()で実装しているのが多いのですがAddForce()は先に言った通りかなり現実っぽい挙動でゲームっぽくないのであまり使わない方がいいです。
【最適化】
ゲームは重い処理がいっぱいあります。だからキャストして処理の量を減らしたり非同期処理にしたり…。そんなことしてたらコードが長くなりますよね?そこでこの解決法!
『最適化をしない』
最適化なんてしなくていいです。…というのはちょっと嘘で、開発の最後には必ずしなければなりません。しかし逆に言うと最後以外はしなくていいです。最適化されたコードは読みにくく修正もしにくいので、最初から最適化されたコードを書こうとするとゲーム制作の難易度が5倍ほどに上がってしまいます。
なんなら重くなるのを怖がる必要もありません。3Dゲームが続々と出ているこの世の中、みなさんの持っているパソコンは2Dゲームごときでは重くなり得ません。レイをいくら飛ばしても重くならないし、全GameObjectにRigidbody2Dをつけても重くなりません。そんな心配をするよりも面白いゲームを作ろうとすることに全力を注ぎましょう。
当然1000個以上のオブジェクトに無茶な処理を毎フレームさせたり(GameObject.Find()を使うなど)したら重くなりますが、そうなったらその時に改善法を考えましょう。最適化は面白いゲーム作りの敵です。
2D課の人へ向けて
【画像サイズ】
2Dゲームを作る際、地味に困るのが素材の画像サイズです。ドット絵にはしたくないけどあんまり大きく描くと重いよね…どうしよう…と考えてしまうのは自然なことです。結論から言うと、これは『大きすぎなければなんでもいい』というのが正解です。オススメはプレイヤーの大きさが~500px(正方形の一辺の大きさ)くらいですかね。2のべき乗数が好ましいとかなんとかネット上には書かれていますが全部無視でいいです。どうせ2Dゲームなのでそんなに変わりません。
↑たとえばこの画像、1250*1000で描いてます。
自分のパソコンの環境で描くのに苦労するほど大きくなければなんでもいいと思います。ただ、確かUnityくんの最大許容サイズが8000pxくらいまでだからそれ以下にする必要はありますね。大き目のオブジェクトを描くことになるだろうことも考えるとやはり主人公は500px以内の方がいいでしょう。
【マップチップの描き方】
無限に続く画像の描き方がわからない…全部一枚絵でもいい?という問いに対しては正直なところNoと言わざるを得ません。全部一枚絵だとマップの構造を変更したいときにかなり面倒なのでやはりマップチップは用意したほうがいいでしょう。
今回は木の中を表現したいと考え、板目っぽいマップチップを描くことにしました。
まずは縦横各3倍にしたキャンバスを用意してベースカラーで塗りつぶします。
そして真ん中らへんに描きたいものを描きます。
色をつけた箇所に注目して、ここにはみ出した模様を真ん中の内部に入れていきます。(2番目の図の赤色のように。色のついた箇所4つすべて同様に処理する)
するとこんな感じになります。なにやらわけのわからない模様に見えますが、実はこれ上下左右がつながっています。右にはみ出したものを左にくっつけて、左にはみ出したものを右にくっつけているんですから当然ですよね。
そしてこれを描き込む…!!
ある程度書き込んだらまた同じ作業をして上下左右がつながる状態を常に作って…!!
完成!!!
上下左右につながってます。
そうです。めちゃくちゃごり押しです。自動的に左右を繋げてくれる便利機能なんてほんの一部のペイントツールにしかありません。それを用意できないのならこう描くしかないのです。少なくともCLIPSTUDIOPRO, SAIはこの描き方が最も効率的です。
【キャラクターアニメーション】
2Dゲームならどんなゲームにも必要になってくるのがこれ、キャラクターアニメーションです。まず1つ言いたいことがあります。
関節だけ動かすのはやめてくれ
最近めちゃくちゃ多いですよね。Live2DやSpriteStudioを使って関節部分だけ動くキャラクターアニメーション。あれ、普通の2Dアクションゲームに使うと本当に気持ち悪い見た目になります。
例えば、人って走るとき足だけが動くわけじゃないんですよね。足で地面を蹴って、もう一方の足を前に出すとき、体のバランスをとるために上半身も反対側に捻るんですよ。この『捻る』という動作が人間の動作の中で最も美しく見栄えがいいのです(独自研究、要出典)。美術用語でいうと『コントラポスト』とか言ったりしますね。
Live2D系の動きはこのコントラポストが完全に死にます!!!だから違和感が出てしまうのです。
Live2D系の動きが許されるのは前後の捻りが必要ないゲームだけ!『絵に動きもついている』というのがLive2D系であって、それは『動く絵』であるアニメーションではないのです。
…さて、そんな感じでキャラクターアニメーションをちゃんと描いてほしいのですが、問題はどうやったらそれっぽくなるかですよね。アニメーションは歴史が長いのでちゃんとした理論も数多く存在しますが、最も重要なことは『始』『中』『終』の3パートにわけて描くことです。
こういう動き↓を作りたいとしましょう。
この場合、攻撃モーションなので『始』は力を溜める動作です。手を頭上に掲げ力を溜めさせます。
そして『中』は攻撃中の動作です。溜めた風を一気に放つ!
最後に『終』。これは攻撃後の後隙をイメージして作ります。
実はしゃがみの終了モーションを流用しているのであんまり前の動作とつながっていません。
どうでしょうか?たったの5枚ですがちゃんとそれっぽく攻撃モーションになっていませんか?このように『始』『中』『終』の3パートにわけることで、少ない枚数でもしっかりと動作を描くことができます。
ちなみに枚数を見ればわかりますがかなり低いFPSで描いています。アニメーションは一般的に24FPSですが、私のゲームでは6FPSです。それでも意外と動いて見えるものです。6FPSなら大体1モーションにつき4~6枚で描けるので、あまり怖がらずにアニメーション作りにも是非チャレンジしてみてください!
最後に
え、終わり?2Dゲームを作るときに必要な知識はもっとあるんじゃ…という意見はわかりますが全部書いてたらマジで終わらないので妥協します。そもそもこの記事もともと『2D横スクロールアクションの作り方』って題で作り始めて全然書き終わらなくて妥協してこの題にしたので妥協の塊です。ゆるして!
でも妥協したおかげで書き終わったし記事を載せることもできました。ゲーム制作もこんな感じで適当に妥協していきましょう。完成さえすれば遊んでもらえる!
適当に行きましょう。
をわり。ぷり。
僕が書いた他の記事
①愛しき我が幻想郷
< https://rigpp.sakura.ne.jp/hp/blog/advent2018-4>
※めちゃくちゃ長いです
②お絵描きマン!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
< http://rigpp.sakura.ne.jp/hp/blog/advent2019-9>
③Unityで高解像度のTextを表示する方法
< http://rigpp.sakura.ne.jp/hp/blog/advent2019-5>
僕が書いた記事のほかにも面白い記事がいっぱい!結構暇つぶしになりますよ。おすすめ。
↓↓↓過去のアドカレ記事↓↓↓