LoginSignup
13
13

More than 5 years have passed since last update.

11/29-30に行われた「Oculus Game Jam in Japan 東京西新宿ニフィティ会場」に参加してきた。
その感想や、どういう流れで開発を進めていったか書いていきたいと思う。

そもそもゲームジャムとは?

一言で言ってしまえば「ゲーム開発のハッカソン」であり、即席でチームを作り30-48時間程度でゲームをゼロから完成させるイベントである。

今回自分が参加したゲームジャムの概要は以下の通り。

  • 開発時間は30時間
  • 1チーム2~6名程度
  • OculusRiftを使用すること
  • 開発プラットフォームは自由
  • そのVR系デバイスの使用も自由

完成したゲーム

ユニティちゃんゴーストマンション
タイトル

Light_Silhouette.jpg
ユニティちゃんゴーストマンションは「ユニティちゃんライセンス」で提供されています。

当日の流れ

11/29 09:00


新宿駅からはるばる歩いて西新宿会場のニフティに到着。
とりあえず1Fのコンビニでコーヒーを買う。

10:00


予定では9:30にチーム編成、10:00開発開始だったがいきなりの時間押し。
10:00過ぎにチーム編成を開始となった。

今回は事前に作りたい物をアイデアスプレッドシートに記入しておき、現地でそのアイデアの中からやりたい人を募ってチーム編成するといった流れだった。
今回は以前自分が作り頓挫した探索ゲームをアイデアとして掲げたところ、何人か集まりチームを組むことができた。
全体での人数の偏りを調整した結果、「プログラマ3人、写真家1人」というメンツに。

11:30


全体のチーム編成が終わり、場が落ち着いたところでいざ作るゲームについての方針を決める事に。
以前自分が1人で作っていたものをベースに、30時間で実現可能そうな物を考えることに。
その結果、以下の様な感じで決まった。

  • ユニティちゃん vs ゴーストの対戦ゲーム
  • ユニティちゃんはOculusRiftをつけたプレイヤで主観視点
  • ゴーストは俯瞰視点
  • Photonを使ったネットワーク通信を使う

特にこの時点では「ゲームの終了条件」は決めておらず、とりあえず作っていくうちに模索していこうという結論に。

12:00


お昼ということで最初の外出可能時間に。
すぐ近場にもうやんカレーがあったのでチームメンバでお昼を食べに向かうことに。
全員食べ過ぎた。もうやんカレーでコーヒーを飲み、会場の1Fでまたコーヒーを買う。

13:00


会場に戻りいざ作業を開始……、と思ったが全員の開発環境を揃える必要がある。
今回は以下の環境で統一することとした。

  • Unity 4.6 Pro
  • Oculus SDK 0.4.3
  • Asset Server

今回はゲームジャム用にUnityProライセンスが支給されており、そのライセンス内にAssetServerが含まれていたので使用することにした。

AssetServerはUnity用のリソース共有やバージョン管理機能である。
gitやsvnと比べると機能は少ないが、GUI操作でとても簡単に利用できmetaファイル共有の罠も回避できる。
また通信もLAN内で完結するため、同期が高速で終了するというメリットもあり今回は非常に助かった。
(git + bitbucketで管理していたチームもあったが、ネットワークが遅く同期に数分以上待たされる事が頻発していたようだった)

14:00


AssetServerの導入が上手く行かず多少gdgdしたがとりあえず全員の開発環境の構築が完了。
(自分の環境ではインストールが途中で失敗したため、メンバの別のPCにインストールした)

開発を開始するにあたり、役割分担をすることに。
あまり細かく作業を分割するとタスクのオーバーヘッドが大きくなってしまうので、大まかにやることを4つに分割した。

役割分担

  • シーン遷移周り(Photonログイン、Room入室、ゲーム開始などの処理)
  • ゲームキャラの処理(メインロジック、Photonを使った同期など) ←自分担当
  • 入出力周り(OculusRiftの処理やキー入力周りの処理)
  • ステージ制作

Unityは初学者が何も考えずにコードを書くと1つのComponentに機能が集中してしまいがちになる。
それを避けるために、今回はゲームキャラの処理をModel(メインロジック)-View(アニメーション管理)-Controller(入力管理)の3つのComponentに予め分割して作成することにした。(厳密なMVCパターンではないが、とりあえずそれっぽいので自分は勝手にMVC分割と呼んでいる。)

また「interfaceを先に定義しておき各componentはinterfaceに依存させる」とすることでクラスの依存性を切り離し、また並行での開発をしやすくした。

ICharacterInput.cs
using UnityEngine;

/// <summary>
/// キャラクタの操作用インターフェース 
/// </summary>
public interface ICharacterInput
{
    /// <summary>
    /// カメラの現在のForward
    /// </summary>
    /// <param name="cameraDirection">Camera direction.</param>
    void SetCameraDirection(Vector3 cameraDirection);

    /// <summary>
    /// プレイヤの移動方向(長さ1)
    /// </summary>
    /// <param name="moveDirection">Move direction.</param>
    void Move(Vector2 moveDirection);

    /// <summary>
    /// プレイヤに攻撃させる
    /// </summary>
    void Attack();
}

ICharacterStatus.cs
using UnityEngine;

/// <summary>
/// プレイヤキャラクタの状態取得
/// </summary>
public interface ICharacterStatus
{
    /// <summary>
    /// 現在座標
    /// </summary>
    /// <returns></returns>
    Vector3 GetCurrentPosition();

    /// <summary>
    /// 現在の顔の向き
    /// </summary>
    /// <returns></returns>
    Vector3 GetCurrentFaceDirection();

    /// <summary>
    /// 現在の体の向き
    /// </summary>
    /// <returns></returns>
    Vector3 GetCurrentBodyDirection();

    /// <summary>
    /// 前進中か
    /// </summary>
    bool IsWalkForward{ get; }

    /// <summary>
    /// 後退中か
    /// </summary>
    bool IsWalkBack{ get; }

    /// <summary>
    /// 死亡しているか
    /// </summary>
    bool IsDead{ get; }
}

interfaceを切るという作業は単純ながら、開発の見通しがよくなるので積極的に使っていくべきだと思う。
特に今回の様に、「入力デバイスが複数パターンある場合」や「ネットワーク同期を行う場合」はclassではなくinterfaceに依存させた設計にしておいた方が後で楽ができる。

とりあえず自分はユニティちゃん(プレイヤキャラ)のロジックの実装を進めることに。

17:00


実際にコーディングを開始してから3時間ほどが経過。
ユニティちゃんが同期して操作可能になる部分の実装を進めていた。
今回はPhotonSynchronizeComponentというComponentを作り、MVCっぽい流れの間に差し込むことにした。
Controller -> Model -> PhotonSynchronizeComponent -> View

PhotonSynchronizeComponent.cs
using UnityEngine;
using System.Collections;

/// <summary>
/// ModelとViewの橋渡し・同期を行う
/// </summary>
[RequireComponent(typeof(PhotonView))]
public class PhotonSynchronizeComponent : Photon.MonoBehaviour,ICharacterStatus
{
    /// <summary>
    /// Model側の持つ本当の値
    /// </summary>
    private ICharacterStatus realCharacterStatus;
    private Vector3 currentPosition;
    private Vector3 currentFaceDirection;
    private Vector3 currentBodyDirection;
    private bool _IsWalkForward;
    private bool _IsWalkBack;
    private bool _IsDead;

    void Start()
    {
        this.realCharacterStatus = GetComponent<AbstractPlayerModel>() as ICharacterStatus;
    }

    void Update()
    {
        if (photonView.isMine)
        {
            //ローカルオブジェクトなら値を直接更新
            this.currentPosition = realCharacterStatus.GetCurrentPosition();
            this.currentFaceDirection = realCharacterStatus.GetCurrentFaceDirection();
            this.currentBodyDirection = realCharacterStatus.GetCurrentBodyDirection();

            _IsWalkBack = realCharacterStatus.IsWalkBack;
            _IsWalkForward = realCharacterStatus.IsWalkForward;
            _IsDead = realCharacterStatus.IsDead;
        }

    }

    /// <summary>
    /// 同期パラメータの送受信
    /// </summary>
    /// <param name="stream">Stream.</param>
    /// <param name="info">Info.</param>
    public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
    {
        if (stream.isWriting)
        {
            //本体側なら値を送信
            stream.SendNext(currentPosition);
            stream.SendNext(currentBodyDirection);
            stream.SendNext(currentFaceDirection);
            stream.SendNext(_IsWalkForward);
            stream.SendNext(_IsWalkBack);
            stream.SendNext(_IsDead);
        }
        else
        {
            //リモート側は値を反映
            this.currentPosition = (Vector3)stream.ReceiveNext();
            this.currentBodyDirection = (Vector3)stream.ReceiveNext();
            this.currentFaceDirection = (Vector3)stream.ReceiveNext();
            this._IsWalkForward = (bool)stream.ReceiveNext();
            this._IsWalkBack = (bool)stream.ReceiveNext();
            this._IsDead = (bool)stream.ReceiveNext();
        }
    }

    #region ICharacterStatusの実装

    public Vector3 GetCurrentPosition()
    {
        return this.currentPosition;
    }

    public Vector3 GetCurrentFaceDirection()
    {
        return this.currentFaceDirection;
    }

    public Vector3 GetCurrentBodyDirection()
    {
        return this.currentBodyDirection;
    }


    public bool IsWalkForward
    {
        get
        {
            return this._IsWalkForward;
        }
    }

    public bool IsWalkBack
    {
        get
        {
            return this._IsWalkBack;
        }
    }

    public bool IsDead
    {
        get
        {
            return this._IsDead;
        }
    }

    #endregion
}

先程も述べたが、各Componentはinterfaceに依存させる作りにしている。
そのためICharacterStatusを実装したPhotonSynchronizeComponentをModel(ICharacterStatusを実装している)とViewの間に差し込むことで、メインの処理には一切影響を与えずにPhotonの同期処理を簡単に組み込むことができた。

19:00


企画発表会ということで各チーム何をやるのか発表。
とりあえず全く関係ない流れで「UniRx便利だよ!」をプレゼン中にぶち込めたので満足。

20:00


ゴーストの実装ということでモデルに「ミクダヨーさん」を採用することに。
ミクダヨーさんモデルをMMD4Mecanimを使ってインポートした。
が、ミクダヨーさんのボーンはMecanimに対応できない形状をしていたため腕が胴体にめり込んだポーズで走るようになってしまい、メチャクチャ怖かった。

いろいろ試行錯誤したが上手くいかず、最終的にミクダヨーさんはポーズ固定でスィーっと移動するという仕様にした。ゴーストだし。

mikudayo.jpg
くっそ怖い

21:00


ゴーストのメインロジック実装を開始。といっても基本処理は全くユニティちゃんと変わらない。
そのため、ModelとViewをabstractな基底クラスに切り出して継承させるだけで済むという結論に。
というわけでユニティちゃんの処理を基底クラスに切り分ける作業をしていた。
最初から基底クラスに別けていない当たりツメが甘い

22:00


クラスの基底クラスへの切り分けが完了し、ゴーストの実装をしていた。
またここらへんでController周りの処理が完成していたので、実際にOculusRiftを使いつつ複数人同時プレイをして動くか確認。
いろいろバグが見つかったのでリストアップして共有。

23:00


一時帰宅開始時刻ということで、写真家の人が帰宅することに。プログラマ勢3人は徹夜を選択。
(ちなみに写真家の彼は帰ってこなかった……)

11/30 00:00


深夜の小休憩。とりあえずコンビニでコーヒーを買う。

01:00


若干眠気を感じつつ、ユニティちゃんの攻撃処理を作成開始。
目の前にColliderを出し、それ触れたオブジェクトがIDamageインターフェースを持っていたらダメージが通る様な実装で進めることに。

当たり判定
攻撃の当たり判定

02:00


ユニティちゃんの攻撃ができたので今度はゴーストの攻撃を作成。
ゴーストがユニティちゃんに触れたらダメージにしたかったが、なんか不安定で衝突を検知してくれない。
いろいろいじってた結果、OnTriggerEnterを使えば良いということがわかった。

02:30


眠気が限界になったので一旦仮眠することに。
ここから06:00くらいまでは仮眠しつつ微妙に作業を進めることの繰り返し。
床で爆睡している人がいてすげぇと思った

06:00


しっかりした睡眠がとれたわけでもなく、ダルさと眠気を感じつつも作業再開。
ココらへんからゲームの開始・進行・終了を管理するManagerクラスの作成をしていた。

07:00


結局エレベーターホールのソファーで寝てた
絶対怒られると思ったが運営の方が見て見ぬふりをしてくれてよかった

09:00


買い出しの時間が来たので朝ごはんを買う事に。
ついでにコーヒーを買う

目が覚めたので真面目にどうするか考えることに。
ゲームの終了条件として、「ゴーストがやられる」または「ユニティちゃんが全滅する」と決めた。
実装はすぐできたが、終了条件のネットワーク同期がなかなか上手くいかずに苦戦。

10:00

実際に2,3名で動かしつつの動作確認。やはりゲーム終了の同期が上手くいかない。

11:00


ゲーム開始 -> ログイン -> 待合室 -> ゲーム開始 -> 終了
全体の流れができるようになったので実際にOculusRiftも含めて動作確認。
ちょこちょことバグが見つかったのでリストアップして修正作業に。

12:00


昼飯ということで今回は駅とは反対方向を探索。デニーズがあるじゃないか
あとまたコーヒー買った

作業風景
作業風景。コーヒー買いすぎ。

13:00


自分はずっとゲーム終了の同期バグと格闘。
結論としては、RPCの送り先とBufferの有無が原因だった。

14:00


全体的にバグも修正できて遊べるように。
追加したい機能はいくつかあったが、今からやると時間が足りないし現状でもゲームとしてまとまってるので下手なことはしないことに。
見栄えやGUIの調整を中心に作業するこ
とに。

調整

  • ユニティちゃんの向きがわかりやすいように俯瞰視点時は黄色いライトを生成する
  • ミクダヨーさんの移動に軌跡を付ける

などの調整を加えて遊びやすくしていた

15:00


最終発表プレゼンの作成。
多分この会場内で一番設計にこだわった班なのと、唯一Photonを使っていた気がしたのでそこら辺を主張するプレゼンを作成。

16:00


作業終了。最終プレゼン。
OculusRift+外部出力の組み合わせのデモは失敗する予感がしたので、実際のデモではあえてOculusRiftを使わない事にした。
実際OculusRiftと外部出力の組み合わせが上手くいかず全くデモができていないチームが幾つかあった。

18:00


最終プレゼン終了。プレイアブルタイム&懇親会(ピザと寿司!!!!)
懇親会
懇親会

play.jpg
一緒に展示されていたLittle Witch Pie Delivery、箒型デバイス(というか箒そのもの)に跨って操作する。
高得点でステッカーが貰えるっぽい。ちなみに自分が遊んだら評価「B」でステッカーがもらえた。

21:00


完全撤収。

全体を通しての感想

OculusRiftガチ勢がAmazon会場の方に集中していたため、自分が参加した西新宿会場の方は和やかムードだった気がした。
また、おそらく新宿勢の中では唯一PhotonCloudを使っていたチームだと思う。PhotonCloudは普段の趣味開発でも結構使っているので、そのへんのノウハウを使うことができて良かった。

やはりゲームジャムは体力的にも精神的にもかなり疲労する。だがやりきった充実感と満足感はなんとも言えない。
次もゲームジャムが近場であるなら参加したいと思った(小並感

13
13
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
13
13