#はじめに
この記事はNitKit コンピュータ研究部 Advent Calendar 2019 22日目の記事です。
Cpawの記事に隠れてひたすらこの記事を書いてました。
今回は私がゲームを始めて作ったときのメイキング・軌跡みたいなものを思い出しながら、時々コードやスクショも載せながらゆるく解説していきます。それと共に私がこのゲーム作りで学んだことを少し書いてみたいと思います。よろしくお願いいたします。
#早速ですが
私はゲームが本当に好きです。でも実際に学校生活の中でゲーム開発を行ってきてはおらず、いざ就活を始めるときにふとこう思いました。
###「ゲーム作ったことなくったって学校のプログラミング経験でどうにかなるもんなのかな?」
これはまごうことなく偽です。プログラミング学習で学べるのはあくまでプログラミングのやり方であり、ゲームの作り方ではないからです。
この至極まっとうなことに気づくのがあまりにも遅すぎたのは本当に今でも悔やんでいるのですがこの事実に気づいた私はその時こう思いました。
###「よし、それじゃあ実際に簡単なゲームを作って会社側に意欲を見せてみよう!」
そこから私の初めてのゲーム作りが始まりました。
#ずぶの素人のゲーム作り
##~その1:仕様書づくり~
実はこの時までにDXlibやSiv3D、Xenkoなどで色々とゲーム作りにトライしていたことはあるんです。
あるんですが、その当時使っていたパソコンが本当にクソスペックだったので、何をどうしても何も動かない状態が続き、気づけばやる気を失っていました。
「じゃあコマンドライン上で動くものを作ればいいじゃない!」と普通のゲーム開発者の方々は至極当然のように思うはずですが、この時の私は3Dゲーム、フルグラフィックこそがゲームたるものだ、と信じて疑わず、コマンドライン上で動くゲーム開発には見向きもしませんでした。(この時の判断にも死ぬほど後悔してます、ゲームだったら何でも作ってみればよかったのに...)
まあそんなこんなで、しょうもないながらも何回か挫折を味わっていた私は「あてずっぽうで作るより、きちんと企画書・仕様書から作成して作るほうが挫折しにくいのでは?」と思い、実際に三時間くらい使って仕様書を完成させ、その仕様書の通りに作ろうと決心しました。
これは後から見ても成功でした。大枠での進捗管理が容易になったのもそうですが、自分が何をすべきなのかを明確化できたところも後々響いてきます。
結果、今回作るゲームのコンセプト・期間・としては以下のようになりました。(仕様書があったはずなんですが捨ててました)
コンセプト...一分間でやる鬼ごっこ
製作期間...一ヶ月
使用ツール...Unity
##~その2:Unityの操作方法を覚える~
さてとにかく自分が何をすべきか、どれくらいの期間で作るかは決まりました。
ただこの時点ではUnityの使い方を何も知らない状況です。技術選定だけして後は野となれ山となれ方式ではいずれ計画は破綻します。
ということでこのチュートリアルをやりました。Unityは偉大。でもチュートリアルに何十日もかける余裕はないので、とりあえずこのチュートリアルは3日で終わらせました。
はじめてのUnity
##~その3:ゲームの大枠を作ろうとするも想像以上にコードが書けない~
さて、なんとかUnityのチュートリアルは終了したので、さっそくゲーム制作に取り掛かりました。
ゲームに必要な要素として、
- 3Dゲームである
- 一分間でゲームを終了させる
- 鬼はフィールドのランダム座標に一定の間隔でポップする
- フィールドの外に落ちてもゲームを終了させる
- 鬼はプレイヤーの座標を毎秒取得し、追いかける
大体こんな感じにリストアップしていたので、あとはこれに沿ってプロジェクトを進めていけばよいのですが。
しかし、私はここで拗らせてしまいます。
###「どうせならできる限り自分で作りたい!」
まだUnityのチュートリアルを終えたばかりのひよっこがそんなことを思うもんですから、速攻でプロジェクトが頓挫しかけました。
どのように作ればいいのかという経験・知識もない状態の人間にはそこまで自作で作りこむことなんてできません。できるわけがありません。その時の私はうんうん唸った結果、こうすることにしました。
###「出来合いのモデルは嫌だし自分でモデル作るか、、、」
##~その4:もっともらしい理由を付けてモデリングに逃げた~
一見良さそうなこの考えも、期間が決まっているプロジェクトに扱ったことのないソフトをもう一つ扱うことが確定するモデリングの要素を導入するという点ではまごうことなき愚策です。しかし一度決まったことを曲げるのも自分の成長につながらないのでは、と思い自分なりに様々なサイトを閲覧しまくることによってblenderを使って大体二週間かけてモデルを完成させました。
足っぽいのをミラーで生成して、
何となく人っぽい形を作りました。でも手があまりリアリティがなかったので、
最後にUV展開をしてテクスチャを作成しました。ただこれに関してはホントにガバガバなのでクソみたいなテクスチャしか作れませんでした。
そのテクスチャを作ったモデルに貼り付けて、
なんとかUnity上にインポートすることができました。万歳。
ちなみにモデルの頂点数とかはこんな感じです。ほんとに初心者のモデリングだったので、パラメータは軒並みクソです。
モデリングは自分が思っている以上に意外と楽しかったです。ただ、
手の細かい成型ができなかったため、手がデスフェイサーみたいになってしまったり、
モデルがこんな感じの形になってからしばらく戻せなかったり、
変形を間違えて恐ろしい形になった挙句この状態で保存をかけてしまい泣く泣く作り直す羽目になってしまったりとかなり時間をかけてはしまいました。しばらくモデリングはやりたくないです。
##~その5:さあ動かすぞと思ったもののやっぱりコードが書けないので、他力本願で行こう!~
まあ何とかモデルは完成しましたが、モデルの作成にここまでで二週間もかけてしまっていました。残りの一週間前後でやるのは残ったコードの実装です。さすがにモデル自作の時に思い知ったのか、ここらへんで私はスクリプト全て自作することを諦め、ほかの人のコードをガンガン流用することに決めました。でもほかの方が作ったソースを読みとって、どういう意味かを理解する、自分の環境に落とし込む、そんな作業をすると決めた時点でどうなるかは明白でした。
##~その6:技術力不足で毎日徹夜のデスマーチ突入~
はい。
###「デスマーチ」の時間です。
学校から帰ってきたらすぐにパソコンの前に座ってうんうん苦しみながらほかの人のソースを読む。そのソースを自分のプロジェクトの環境に落とし込む。デバッグも忘れないでやる。
自分でアルゴリズムを考えて実装するというよりほかの人の考えをうまく自分と同じ領域に移しているだけであったため、かなり無為でつらい作業でした。でも時間がないとできない作業でしたので、生活リズムが毎日三時寝は当たり前みたいな、完全にリリース前のIT企業勤めのサラリーマンになってしまいました。
そんな感じで必死こいて作ったソースがこれらです。ほんとにリファクタリングのこととか何一つ考えていないため、正直クソコードの羅列にしかなりませんが、皆様の反面教師となるのなら本望ということで、公開します。
作成したコード集
UI上に残り時間を表示するコードです。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
public class TimerController : MonoBehaviour
{
// GameObject TimerTex;
public Text timerText;
public float totalTime;
float seconds;
// Use this for initialization
IEnumerator Start()
{
enabled = false;
yield return new WaitForSeconds(3); //三秒待ってUpdate()を有効化
enabled = true;
}
// Update is called once per frame
void Update()
{
totalTime -= Time.deltaTime;
seconds = totalTime;
timerText.text = "残り時間:" + seconds.ToString();
if(totalTime <= 0)
{
SceneManager.LoadScene("GameClear");
}
}
}
カウントダウン表示を行うソースです。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class CountDown : MonoBehaviour {
Text text;
void Start()
{
text = GameObject.Find("CountDown").GetComponent<Text>();
StartCoroutine(Count());
}
IEnumerator Count()
{
yield return new WaitForSeconds(1f);
text.text = ("2");
yield return new WaitForSeconds(1f);
text.text = ("1");
yield return new WaitForSeconds(1f);
text.text = ("Start!");
yield return new WaitForSeconds(1.0f);
text.gameObject.SetActive(false);
}
}
フィールドのランダム座標に敵を出現させるコードです。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Pop : MonoBehaviour {
// 出現させる敵を入れておく
[SerializeField] GameObject Enemy;
// 次に敵が出現するまでの時間
[SerializeField] float appearNextTime;
// この場所から出現する敵の数
[SerializeField] int maxNumOfEnemys;
// 今何人の敵を出現させたか
private int numberOfEnemys;
// 待ち時間計測フィールド
private float elapsedTime;
IEnumerator Start()
{
numberOfEnemys = 0;
elapsedTime = 0f;
enabled = false;
yield return new WaitForSeconds(3);
enabled = true;
}
void Update () {
// この場所から出現する最大数を超えてたら何もしない
if (numberOfEnemys >= maxNumOfEnemys)
{
return;
}
// 経過時間を足す
elapsedTime += Time.deltaTime;
// 経過時間が経ったら
if (elapsedTime > appearNextTime)
{
elapsedTime = 0f;
AppearEnemy();
}
Resources.UnloadUnusedAssets();
}
// 敵出現メソッド
void AppearEnemy()
{
float x = Random.Range(10f, 450f);
float y = 116;
float z = Random.Range(10f, 450f);
Vector3 position = new Vector3(x,y, z);
Instantiate(Enemy, new Vector3(x,y,z),Quaternion.identity);
numberOfEnemys++;
elapsedTime = 0f;
}
}
敵の挙動について記述したコードです。NavMeshAgentを用いて簡単にプレイヤー追跡を行っています。
using UnityEngine;
using UnityEngine.AI;
using System.Collections;
using UnityEngine.SceneManagement;
public class Enemy : MonoBehaviour
{
[SerializeField] public GameObject Player;
private NavMeshAgent navAgent = null;
IEnumerator Start()
{
GetComponent<NavMeshAgent>().enabled = true;
Player = GameObject.Find("Player");
navAgent = GetComponent<NavMeshAgent>();
enabled = false;
yield return new WaitForSeconds(3); //三秒待ってUpdate()を有効化
enabled = true;
}
private void Update()
{
navAgent.destination = Player.transform.position;//navMeshAgentの操作
}
}
ゲームオーバー時の挙動を記述したものです。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class GameOverSceneChanger : MonoBehaviour
{
GameObject player;
private ParticleSystem particle;
//Exploder Exploder;
void Start()
{
particle = GetComponent<ParticleSystem>();
//Exploder = player.GetComponent<Exploder>();
}
// Use this for initialization
IEnumerator OnControllerColliderHit (ControllerColliderHit other)
{
Debug.Log("Hit");
if (other.gameObject.CompareTag("Enemy"))
{
yield return new WaitForSeconds(0.3f);
SceneManager.LoadScene("GameOver");
}
if (other.gameObject.CompareTag("DeathZone"))
{
yield return new WaitForSeconds(0.01f);
SceneManager.LoadScene("GameOver");
}
}
}
プレイヤーの挙動について記述したコードです。このコードが一番他力本願です。当時は本当にどんなふうに実装したらいいかわからず、ほとんどがコピペみたいなソースになっています。
using UnityEngine;
using System.Collections;
public class PlayerController : MonoBehaviour
{
private CharacterController charaCon;
private Vector3 moveDirection = Vector3.zero;
public float MoveSpeed = 5.0f;
public float RotateSpeed = 3.0F;
public float RollSpeed = 1200.0f;
public float gravity = 20.0F;
public float jumpPower = 6.0F;
IEnumerator Start()
{
charaCon = GetComponent<CharacterController>();
animCon = GetComponent<Animator>();
enabled = false;
yield return new WaitForSeconds(3);
enabled = true;
}
void LateUpdate()
{
var cameraForward = Vector3.Scale(Camera.main.transform.forward, new Vector3(1, 0, 1)).normalized;
Vector3 direction = cameraForward * Input.GetAxis("Vertical") + Camera.main.transform.right * Input.GetAxis("Horizontal");
charaCon.Move(moveDirection * Time.deltaTime);
if (Input.GetAxis("Vertical") == 0 && Input.GetAxis("Horizontal") == 0)
{
animCon.SetBool("Running", false);
}
else
{
Rotate(direction);
animCon.SetBool("Running", true);
}
if (charaCon.isGrounded)
{
animCon.SetBool("Jumping", Input.GetKey(KeyCode.Space));
moveDirection.y = 0f;
moveDirection = direction * MoveSpeed;
if (Input.GetKey(KeyCode.Space) )
{
moveDirection.y = jumpPower;
}
else
{
moveDirection.y -= gravity * Time.deltaTime;
}
}
else
{
moveDirection.y -= gravity * Time.deltaTime;
}
}
void Rotate(Vector3 Hope_Rotate)
{
Quaternion q = Quaternion.LookRotation(Hope_Rotate);
transform.rotation = Quaternion.RotateTowards(transform.rotation, q, RollSpeed * Time.deltaTime);
}
}
そんな感じでコードを書いて、あとはこれを適応させるだけだったんですがまだいろいろと作業が終わってなかったのを思い出し、コードの詳細な適応と共に以下の作業を行いました。
インポートしたモデルにSAColliderBuilderというアセットを適応したり、
そういう作業を必死に行ったり、残りの数日でUIの部分を作成したり、実際にテストプレイしながらストレスのたまらないようなゲーム性を追求し、
何とか完成しました。今見るとボタンの所とか本当にUnity臭がすごいですね。
デモプレイの様子はこちらです。
完成したゲームのリンクはこちらです。クソゲーですが、もし気になったら触ってみてほしいです。(Windowsのみ)
1 Minutes Tag!!! ver1.01 ~for Windows - Google ドライブ
#当時を振り返って・自分が得たもの
その当時はわからなかったんですが、結果的にサウンドの実装を全くしないまま終わってしまっていました。この時はとにかく動くものを作りあげる、ってところしか頭になかったため、今では臨場感に欠けるものができてしまったと反省しています。また、アニメーションの設定、ジャンプの設定が甘く、スペースキー押しっぱなしで棒立ち人間が無限ロケットのように飛び上がるバグが発生していました。
あとこれ。
思いっきり衝突してるはずなんですが、シーン遷移しない、どう直したらいいかわからないし治せないといった謎のバグが発生したため時間が三日ほど食われました。今度はもっとシンプルなColliderを使おうと思います。(自分で実装可能なところはアセットに頼らず自分で頑張ろうと思いました。)
対して、得たものですが、これは単純に自信がついたのと、様々な機会が増えたことです。
一ヶ月という期間で自分なりの考えをアウトプットできた、というやりがいは本当に何物にも代えられないものでしたし、拙くてもそれらを遂行することができた自分の実行力に自信が付きました。
また、逆求人のイベントに行く際の明確な提示材料ができ、様々な企業の方とお話しする機会をいただいたり、自分が幼少期熱中していたゲームを製作していた会社の方とお話しする機会をいただいたり、最終選考の機会をいただいたりと、本当に様々なものを得ることができました。やっぱり自分から行動するのは大事です。
#最後に
今この記事を読んでるゲームを開発しようか迷ってる皆さん。
###「今すぐゲームを作りましょう」
拙くてもいいです。クソゲーでもいいです。いくらくだらなくてもいいです。作りましょう。
###「ゲームのアイデアはあるのに迷ってる?迷うくらいなら作りましょう」
私みたいにこんなゲームを作って平然としてる人間だっています。違いは行動するかしないかです。
###ゲーム作りのノウハウは学校で本当に学べますか?
学べませんよね?確かに基礎的なことは様々な部分で応用ができます。企画、計算、マネージメントなどには様々な教科で学んだことが活きたりします。そういう観点からすると学校の学習も本当に有意義なものです。
ですが、ゲーム作りのノウハウはゲーム専門学校でもない限り実際に自分で作ってみないことにはつかないのです。ゲーム作りが上手くなりたければゲームを作るしかないのです。そこを行動しないままにして丸く収まっているのは非常にもったいないです。
###いつかはビッグタイトルを作ってみたい!
と思っているならなおのこと自分で行動し、それを様々な場所にアウトプットしましょう。早いうちに恥をかいたり、失敗することこそが将来の大成功にきっとつながります。重ね重ね言いますが、今現在様々なシーンで活躍されているゲーム開発者とあなたの違いは
###「目的のために行動しているか」
これ一つのみです。
全力でゲーム開発に取り組むのであれば、私もそうですが、あなたの周りの方々も、他のゲーム開発者の方々もきっと全力で応援します。するはずです。ゲームを作れるようになりたければ
###とにかく自分で、自分の意思で行動しましょう
自分で簡単なゲームを最後まで作り上げること、それこそがあなたのゲーム作りの第一歩です。
ここまでの長文を読んで下さり、誠にありがとうございます。
これからのゲーム作り人生を、皆様がより有意義に過ごせますように。