1週間ゲームジャムに参加して得たこと

初めまして。「☕️りょう」と申します。
こちらのブログの用途ですが、
主にTwitter上で伝えきれないことを発信するのに運用していこうと思います。
よろしくお願いいたします。

何をやったのか

個人・チーム問わず1週間でUnityでゲームを作り完成したゲームをプレイし合うweb上のイベントにおいて、自身も参加してゲームを公開しました。

Unity 1週間ゲームジャム | 無料ゲーム投稿サイト unityroom - Unityのゲームをアップロードして公開しよう

「当てる」というお題を元にゲームを作ろうという話で、自分はリアルタイムで通信対戦ができるアイスホッケーゲームを作りました。1対1でボールをぶつけ合って、相手のゴールに入れた方が勝ちというものです。

得られたこと

  • 作り手側からしても、リアルタイム通信ゲームは人が集まるとめちゃくちゃ楽しい。
  • リアルタイム通信のゲーム開発にかかる工数は、通信なしのゲームに対し数倍掛かる。
  • 通信ラグをどう見せるかで、ゲームプレイの納得感に影響する。
  • 社会人がプライベートでゲームジャムに参加するときの心得。
  • 現職での新たな仕事。

簡単な自己紹介

ソーシャルゲームの開発&運用&カスタマーサポートをしている社会人3年目の会社員です。 プログラミングは4年前ほど前に就活がうまくいかないのをきっかけに独学ではじめました。職場では入社してからずっとphp使って仕事してます。
1週間ゲームジャムについては何度か参加しています。
あとコーヒーが好きです。抽出用機械と店員の動き見ればお店の良し悪し判断できます。

人が集まるとめちゃくちゃ楽しい件

ゲームジャムで作られたゲームを生放送する方がいまして、自分が作ったゲームをプレイしていただきました。

この放送前まではブラウザにずっと張り付いていて待機していたのですが、2時間おきに1人プレイされる程度でした。悲しかった。

どうしても自分が作ったゲームをプレイしてもらいたくて、勇気を出して対戦できるゲームがあることを書き込んだところ、視聴者や放送主さんが参加してくれて10人くらいの人と対戦して遊べました。 書き込みもそれなりにあり、楽しんで頂けたと思います。嬉しさのあまり家で生放送見ながらキタキタ踊りしてました。

リアルタイム通信のゲーム開発にかかる工数

通信なし、演出こだわらずの簡単なアイスホッケーであれば8時間程度で開発できそうかなと思っていましたが、通信ありきで作ったところ25~30時間ほど要しました。 通信についてはPhotonというサービスを利用してオンライン対戦を実現しました。 ローカルでプレイできるゲームに加えて、どれを同期対象とするかや同期の頻度・途中で通信が切断された時の仕組みを考えて実装する必要があって面倒臭かったです。

通信ラグの見せ方について

※完全同期を行えるらしいPhoton TrueSyncたるものがあるそうなのですが、よくわからなかったのでPhoton Unity Networking Freeをなんとなく使いました
オンライン通信についてはラグがついて回るので、ラグは発生するという前提で開発を進めました。 ちょっと面倒だったのが、ホッケーをして「ゴールした」という判定をどのように下すかです。

例えばAさんとBさんの端末間でラグが発生すると、
Aさんの端末から見てBさんコートにゴールインしているが、Bさんの端末からはラリー中に見える事象が発生し得ます。

このパターンについてゴールしたという判断を下すと、多分Bさんは到底納得いかないと思います。ラリーしていたのにいきなり敗北と言われるので。。。
逆にゴールしなかった判定を下すと今度はAさんが納得しないと思うのですが、先ほどのゴールを下す判断よりかはヘイトが低いと感じました。
僕が作ったゲームでは、自分のゴールにホッケーが入れられたタイミングで相手端末に参りましたという情報を送り、自分の端末で負け表示、相手の端末で勝利表示を行うようにしました。

ゲームジャムで、社会人が面白いゲームを作るための立ち回り方

過去参加した経験として、作業時間や思考時間・作るための体力やモチベーションがネックになって作るのが嫌になったり妥協点が多くなって納得感なく終わってしまうことがあったので
今回と前回は職場の上長にネゴる戦法をとって時間を確保しました。

具体的には、1週間ゲームジャムの日程が発表された直後に当週の残業時間をなるべく減らして欲しいとの要望を出しました。
代わりに当週の前は多めに残業し他メンバーの仕事を巻き取っていました。
結果的に残業はしてしまったのですが。。。ネゴっていなかったら多分遊べるレベルのゲームを作れていなかったです。

現職での新たな仕事について

職場の人に作ったゲームを見せまくってたら「Unityやってみる?」的な話になりまして、1日平均で30分だけUnityを触って開発を行えるようになりました。まだC#スクリプトを数行変える程度の作業ですが、いい成果を出して次に繋げられたらいいなと思います。

技術備忘録

  • プレイヤーの位置同期については、プレイヤーオブジェクトにPhoton Viewコンポートネントを付けた。
  • ボールの位置同期については、プレイヤーとボール衝突時にroom内のユーザーに衝突直後のボール速度と位置情報を送信するようにした。またタイムラグによるボールの瞬間移動にプレイヤーが追いつけるようにするため、コート中央にあるプレイヤーが侵入できないエリアの範囲を調整した。
  • ボール位置同期の実装は以下
using System.Collections;
using UnityEngine;

// ボールの位置同期を行う
public class BallSerializer : Photon.MonoBehaviour {

    private Rigidbody2D rigidbody;

    void Start () {
        rigidbody = this.GetComponent<Rigidbody2D>();   
    }

    // ルーム内の変数が変化した時に呼び出されるメソッド
    void OnPhotonCustomRoomPropertiesChanged(ExitGames.Client.Photon.Hashtable changedProperties ){
        
        // ルームプロパティからボール位置を取得
        object value = null;
        if (changedProperties.TryGetValue ("ballPosition", out value)) {
            this.transform.position = (Vector3)value;
        }

        // ルームプロパティからボール速度を取得
        value = null;
        if (changedProperties.TryGetValue ("ballVelocity", out value)) {
            rigidbody.velocity = (Vector2)value;
        }

        // ボールの動きをとめる
        value = null;
        if (changedProperties.TryGetValue ("ballstop", out value)) {
            rigidbody.bodyType = RigidbodyType2D.Static;
        }

    }

    void OnCollisionEnter2D (Collision2D other)
    {
        SoundManager.Instance.PlaySe("HIT");

        // 衝突した対象がプレイヤーかを取得
        var tag = other.gameObject.tag;
        if(tag != "Player"){
            Debug.Log("ball is not collision to player.");
            return;
        }

        // 衝突したプレイヤーが自分か取得
        var photonView = other.gameObject.GetComponent<PhotonView>();
        if(photonView.isMine == false){
            Debug.Log("ball is not collision to mine player.");
            return;
        }

        // 位置と速度をルーム内変数に設定
        var properties  = new ExitGames.Client.Photon.Hashtable();
        properties.Add("ballPosition", this.transform.position);
        properties.Add("ballVelocity", rigidbody.velocity);
        PhotonNetwork.room.SetCustomProperties(properties);
    }   
}