【Unity備忘録】クラス図設計して個人的によかった点

f:id:coffee_ryo:20180715035936p:plain

前提

  • 設計よくわかってない人間、コーダーとかスクリプターとか
  • PlantUMLでクラス図書く
  • ZenjectとUniRx使ってみる
  • 開発で使える期間は1年間と決まっている
  • 個人ゲーム開発

良かった点

予定を立てられる
  • エターなりたくない、ならば、いつ開発終わるの?の自問自答に対して答えられるようにしたい
  • クラス数最低〇〇個みたいなのが見えてくる
  • 技術的に難しそうな箇所の早期発見、早い段階で検証すれば精神的不安も解消
  • 「コーディングだけで最低半年かかる」とか分かれば早期に仕様の削り落としとかにも繋がる
重複コードの発見
  • 似たようなメンバ変数の構成のコンポーネントが出てくる
  • ボタンクリック時とかの処理とか、よく見るとメソッドの引数が違うだけとか
  • 通化しても問題なさそうなところはしていく、コード行数少なければ開発後半の苦しみが減る
  • 行き当たりばったりで開発していると気づきにくいかも
自分が書いたコードがわからなくなる現象の回避
  • 1個のクラスの役割「do」は1つ〜2つ程度でおさまっているか?
  • たくさんあるとシンドイ。プレイヤーの移動&ダッシュ&スーパーアーマー&死亡とかあるとスパゲッティー化
  • 役割が複数あるのは分割する
  • 逆に役割が0のやつとかがあったりする、そういうのは設計段階でなくす
  • クラス図を残しておくと、なんでこれ作ったか思い出すヒントになる

悩みどころ

個人開発でどこまで設計する?
  • 事前に設計したとしても仕様の変更とかが出てきてしまう。今の所見た目やアニメーションに関わる部分は変更が激しそうなためあまり設計してない。
  • クラス図以外では必要なのか?

【Unity】ゲームのちょっとした会話場面にFungusを使ってみる

f:id:coffee_ryo:20180711223526g:plain

アクションゲームでボスキャラとの戦闘前に会話を挟みたい

  • ゲームの操作を一時中断させることで休憩してもらい疲れないようにする
  • 強敵がくるという危機感や期待感を持ってもらう

という狙いで、会話シーンを作りたい。

会話シーンを作る手段としては考えたのは、

  • 自前スクリプト組む

  • ノベルゲームを作るためのAssetを使用する

Twitterで見かけたFungusというAssetのが良さそうだったのでこちらを使って見る。 ノベルゲーム開発で大活躍しそう。使い方備忘のため書き残しておく。 このAsset、無料です。

まずは会話ダイアログを出してみる

使っているUnityのバージョン

2018.1.0f2 Personal

AssetをInportする

assetstore.unity.com

Flowchartコンポーネントがアタッチされたオブジェクトを作成

メニューのToolsにFungusの項目が追加されているので、こちらから作成。

f:id:coffee_ryo:20180711194539p:plain

ヒエラルキーにきのこのマークが付いているオブジェクトが作られます。

f:id:coffee_ryo:20180711194728p:plain

FlowchartのWindowを確認

きのこマークが付いているオブジェクトのインスペクターをみて、「Open Flowchart Window」をクリック。

f:id:coffee_ryo:20180711195158p:plain

こんな画面が出ることを確認。あとで使います。

f:id:coffee_ryo:20180711195414p:plain

SayDialogコンポーネントがアタッチされたオブジェクトを生成

メニューのTools>Fungusから、SayDialogをCreate。

f:id:coffee_ryo:20180711195454p:plain

Gameビューを見てみると、会話ダイアログが作られます。

f:id:coffee_ryo:20180711195556p:plain

会話テキストを打ち込む

FlowchartのWindowで、「New Block」をクリックして選択。選択すると枠が緑色になる。

f:id:coffee_ryo:20180711211339p:plain

インスペクターの「+」マークを押して、Narrative>Sayをクリック。

f:id:coffee_ryo:20180711211730p:plain

「Commands」という項目に「Say」の項目が追加されます。

f:id:coffee_ryo:20180711212236p:plain

「Say」の部分をクリックして、「Story Text」に会話内容を入力。

f:id:coffee_ryo:20180711212619p:plain

ここまでで、▶︎実行をすると会話ダイアログに会話内容が表示されます。

f:id:coffee_ryo:20180711212857p:plain

キャラクターを表示させて会話させる

Characterコンポーネントがアタッチされたオブジェクトを生成

メニューのTools>Fungusから、Characterを2つCreate。
作成されたオブジェクトのコンポーネント上で、

  • キャラクター名→Name Text
  • 関連する会話ダイアログ→Set Say Dialog
  • キャラクターの画像→Portraits

を設定。

f:id:coffee_ryo:20180711215520p:plain

Stageコンポーネントがアタッチされたオブジェクトを生成

メニューのTools>Fungusから、Stageを2つCreate。Stageコンポーネントでは、キャラクターのデフォルトの位置を設定できるようです。1体のCharacterに対して1つのStageをつくるのがいいのかな?(あんまりわかってない)

f:id:coffee_ryo:20180711220106p:plain

Stageコンポーネントが取り付けられてある子オブジェクトに、デフォルトの位置と関連しているオブジェクトがあります。

f:id:coffee_ryo:20180711220131p:plain

今回はキャラクターが左右から入ってくるようにしたいと思ったので、「Defalut Position」に「Offscreen ●●」のオブジェクトを指定。

Flowchart Windowでキャラクターを出して会話させる

FlowchartのWindowで、「New Block」 をクリック、Narrative>Portraitを作成して、

  • Portrait Stage→先ほど作成したStageを指定
  • Display→Showのまま
  • Character→作成したCharacterを指定
  • Portrait→Characterコンポーネントの「Portraits」で指定した画像を選択
  • Move→チェックボックスをつける
  • From Position, To Position→キャラクターの移動開始位置、終了位置を指定
  • Wait Until Finished→アニメーションが完了したら次のCommandを行うか、すぐさま次のCommandを行うかのフラグ?

f:id:coffee_ryo:20180711221604p:plain

この状態で▶︎実行すると、左右からキャラクターが画面中央に移動しながら表示されます。

あとはNarrative>Sayしてどのキャラクターに会話させるか指定させテキストを打ち込み、 Narrative>Portraitして会話終了タイミングでキャラクターを左右にフェードアウトさせることでそれっぽくなります。 できたのがこちら。

f:id:coffee_ryo:20180711223526g:plain

会話処理を発火させる

FlowchartのWindowで、発火させる条件が指定できます。

f:id:coffee_ryo:20180711222837p:plain

または、スクリプトからFlowchartコンポーネントのExecuteBlockメソッドを使えば任意の場所で発火させることができました。

会話終了タイミングで処理を呼び出す

インスペクターの「+」で、Scripting>Invoke Eventでコマンド追加すれば、ButtonコンポーネントのOnClickのように実行するメソッドを指定できる。 これ以外でもできそうです。

f:id:coffee_ryo:20180711223437p:plain

その他

繰り返しますが無料です。すごい。

【ゲームジャム参加】対戦ゲー「居合斬り」 制作中考えていた事と得られた気づき

f:id:coffee_ryo:20180612220958p:plain

はじめに

過去に引き続き、1週間でUnityでゲームを作り完成したゲームをプレイし合うweb上のイベントに参加しました。 こちらのゲームジャムでは開催毎にお題が与えられ、それに沿った物を作ります。
今回は「ぎりぎり」というお題で、自分は間合いを見切って敵を倒す「居合斬り」というゲームを公開しました。
▼こんなんです▼

居合斬り | 無料ゲーム投稿サイト unityroom - Unityのゲームをアップロードして公開しよう

こちらの記事では、自分への備忘録や次回参加者の参考のためにどんなことを考えながら作ったかと気づきを書き残しておきます。

どんなゲームにするかのアイデア出しと選定

お題公開された月曜日の午前0時からテキストエディタに思いついたものを書きました。

  • ホームレスがお金を集めて長く生きながらえる
  • 路上バンドマンがバンドしてお金集めて生きながらえる
  • ぎりぎりの間合いを見極めて相手を切りつけ倒すゲーム
  • ジョジョの水コップにお金入れるやつ
  • カイジの鉄骨渡り
  • バイクに乗って海に落ちるかどうかのチキンレース
  • 日曜ギリギリにゲーム投稿
  • etc

イデアの方向性としては3つあって、

  1. 何かのオマージュでネタ的にもギリギリなものをやる
  2. 現実の世界でぎりぎりを連想させるもの(お金とかお金とか)
  3. ゲーム内に登場するパラメタ(タイミングとか、距離など)がギリギリであれば良い評価を与える

を考えていました。強烈なネタがあれば1.2で行こうと思っていましたが、いいのが出なかったので3の「ゲーム内に登場するパラメタがギリギリであれば良い評価を与える」方向性で決定しました。

最終的には、以下理由で「ぎりぎりの間合いを見極めて相手を切りつけ倒すゲーム」で開発しようと決めました。

  1. 1プレイ20秒くらいで出来そうなところがGood。ゲームジャムでは100作品以上のゲームが並ぶので、短時間でプレイが完結できればストレスなく遊べると思った。
  2. 対戦の仕組みが受け入れやすい題材。(やっぱりゲームは友達と一緒にやりたい!)
  3. ギリギリにさせるパラメタについて、プレイヤーと敵が相互に影響し合えそうなもの。

3の理由についての補足ですが、
音ゲーのように、何かしらのオブジェクトが固定位置にいるプレイヤー目掛けて来るのに対しタイミングを合わせたり、
チキンレースのようにプレイヤーが何かしらの目標に向かって、閾値を超えないようにするゲームはたくさん出ると考えていて、
自分の場合グラフィックが非常に苦手で、ここらへんのゲームルールが被ると印象に残りにくかなと懸念していました。

ここまで決めたのが月曜日の午前1時頃です。 社会人なのですが、月曜の夜から作りこみができるようにしないと学生やフリーランスの方に敵わないと考えていたので、月曜日の午前中には絶対に企画を確定するように決めていました。
結果として遊べるレベルのものを作るのに繋がったと感じてます。

作るルールとか機能とかを決める

居合斬りをゲームとする時に、どんなものが必要かを考えていました。
このイベントで一番怖いのが、「遊べるレベルのものを作れずに終わる」事だと思っていたので、ゲームとして成り立たせる為に必ず作るもの・どうしても実装したいものと、余裕があれば作るもので分けました。
必ず作る部分でどれくらい時間がかかるのかを出して、金曜日くらいにはとりあえず遊べるようしよう〜と考えて動いた結果、なんとか日曜日までに作りきることができました。
工数だしの重要性。。。!

●必ず作ろうと考えていたもの

  • キャラクター移動
  • 剣を振る攻撃
  • 攻撃を当てると相手を撃破
  • スコア
  • 敵のAI
  • BGM付ける

●余裕があれば作ろうとしていたもの

  • ローカル対戦
  • 通信対戦
  • ゲームの説明方法を起動直後見れるようにする
    ファーストビューで見せないと、操作方法わからず次のゲームに移ってしまう
  • ツイッター投稿
  • ランキング機能
  • キャラクターのアニメーションを凝りまくる
  • UIをおしゃれでわかりやすく
  • BGM自作
  • キャラクターストーリー
  • 数種類の敵を作る(瞬間移動とか、分身とか、距離感をつかみにくくするスキル持っている奴)
  • 色々チューニングしてwebglの起動時間短縮したり、カクツキ抑えたり

あとは、unityを実際に触りながらイメージを膨らませて行きました。 この時点で、月曜日の午前2時半くらいでした。

(9時から仕事なのに1時に寝ますとか言っておきながら作業してました。)

開発しながら追加していった細かい調整について

ヒットエフェクト

居合い切りの瞬間のやりとりというのを表現出来たらいいなーということで、剣がヒットしたときに止まったりゆっくりになるのを考えていました。
後から気づいたのですがこの演出をいれたことで、ゲームとしてのカッコよさだけでなく自分が当てた・当てられたというのが明確に分かるようになりました。

駆け引きについて考える

見切って当てるゲームですが、見切りが失敗したときには何かしらのデメリットを与えようというのを考えていました。
いわゆるトレードオフとか、リスクとリターンとかの考えです。

まず考えたのが剣を振るアクションです。
ここは格闘ゲームの一部をイメージしていて、攻撃が失敗したら硬直する仕組みを入れてみました。

その後テストプレイしたところ、キャラクターが画面外まで逃げられてしまうので画面の端に壁を置きました。 なんとなくで置いたのですが、開発を進める途中でこの壁の存在が非常にいい駆け引きをもたらしてくれることに気が付きました。

剣硬直の仕様により、相手の攻撃を待ち空振りさせてからカウンターを狙う後出し作戦が安全なように思えます。 f:id:coffee_ryo:20180612214425j:plain

カウンターをしたいが、相手が前進をしつつ攻撃をしてこない場合は後退しないと相手の間合いに入ってしまい先制攻撃を受けるので、後退せざるを得ません。
ところが、壁の存在によりいつかは後退できなくなり、このプレイヤーが取れる手段は前進か攻撃しかできなくなります。
安全であることを保ち続けると、いつの間にかピンチになってしまうのです。 f:id:coffee_ryo:20180612215211j:plain

この話はスポーツのバスケの世界でよくあるのですが、ディフェンスの方針としてオフェンスの手段を減らせと言います。
オフェンスの行動パターンは主にパス・右ドリブル・左ドリブル・シュートの4種類なのですが、ディフェンス側はできるだけオフェンスに近づきシュートさせないようにし、右か左よりに体を寄せることで行動パターンをパス・片方のドリブルに絞ります。こうすることで相手の行動を読みやすくなり対策がしやすいです。ただし、オフェンスに近づくことでドリブル抜かれやすくなるので、ドリブルスピードに対応するための坂ダッシュが必要です!

では、前進しまくって端に追い詰めればいいかというと、これも結構危険で、相手が前進&攻撃を行って来る先制攻撃をして来ると反応できず被弾する危険性が高まります。

こんな感じで、様々なトレードオフが高速で行われる仕組みが出来上がり、プレイヤーに素早い判断を要求するゲームになりました。
うまく判断して狙い通り敵を撃破したときの爽快感・してやったぜ感は抜群です!

joycon対応

対戦モードの実装が1時間くらいで出来たのですが、2人で同じキーボードを使ってプレイするのは窮屈そうだなーというとに気づいたので、コントローラーで遊べるようにしてみました。 対応自体は以外にも簡単で、30分くらいでできました。
対応方法については、こちらの方の記事が大変参考になります。

taka8.hateblo.jp

ここまでで土曜日が終了で、日曜はひたすらバランス調整してました。

ゲーム自体を知ってもらう動き

ここは動きが足りなかったです。 ツイッターに進捗を上げることでとりあえず知ってもらえるところまでは持って行けたと思いますが、
「この人だからプレイする」みたいなものが足りなかったです。 この点は、ゲームジャム始まるずーーっと前から面白い発信をしていたトモぞヴPさんや、テラセネの森クマさんが強かったです。
もう師匠として見習っていきます。

https://twitter.com/kamiposo/status/1004232703309254658

終わりに

ゲームの駆け引きに関する箇所については完成度高く作れたと思います。
グラフィックであったり人を集める部分が弱かったので、次回開催以降改善していこうと思います。

unityroom作っていただいた@naichilabさんのおかげで、自分の作ったものが人に見られる機会を得ています。本当にありがとうございます。
あと、#weeyblegameに集まっていただいた開発者の皆さん、実際に対戦モードを試遊していただき感謝しています。
unityroomのランキング低く残念でしたが、皆さんのゲラゲラ笑っているところを見れて、ほんとに作って良かったです。

【Unity】LineRendererを使ってお絵かきソフトにある機能を実装する

f:id:coffee_ryo:20180506190912p:plain

概要

現在開発中の2Dアクションゲームにてお絵かきのシステムをギミックとして取り入れたかったので実装しました。

環境

作る機能

  • 線を引く
  • 対称定規で線を引く

LineRendererについて

Unityにあるコンポーネントです。2点以上の位置情報を設定することで、点と点を繋いで線を引いてくれます。 「1つの繋がった線につきLineRendererが取り付けられたオブジェクトが1つ必要」です。

大体の処理の流れ

【マウスクリック時】

  • 線オブジェクトを生成し、線の太さや色を設定する

【Update】

  • プレイヤーのマウスの位置を取得
  • LineRendererのSetPositionメソッドを使い位置情報を登録していく

線を引く機能を実装する

スクリプティングをしてコンポーネントを作ります。

using UnityEngine;
using System.Collections.Generic;
using System.Linq;

public class FreeHand : MonoBehaviour {

    /// <summary>
    /// 描く線のコンポーネントリスト
    /// </summary>
    private List<LineRenderer> lineRendererList;

    /// <summary>
    /// 描く線のマテリアル
    /// </summary>
    public Material lineMaterial;

    /// <summary>
    /// 描く線の色
    /// </summary>
    public Color lineColor;

    /// <summary>
    /// 描く線の太さ
    /// </summary>
    [Range(0,10)] public float lineWidth;


    void Awake () {
        lineRendererList = new List<LineRenderer>();
    }

    void Update () {

        // ボタンが押された時に線オブジェクトの追加を行う
        if (Input.GetMouseButtonDown(0)) {
            this.AddLineObject();
        }

        // ボタンが押されている時、LineRendererに位置データの設定を指定していく
        if (Input.GetMouseButton(0)) {
            this.AddPositionDataToLineRendererList();
        }
    }

    /// <summary>
    /// 線オブジェクトの追加を行うメソッド
    /// </summary>
    private void AddLineObject () {

        // 追加するオブジェクトをインスタンス
        GameObject lineObject = new GameObject();

        // オブジェクトにLineRendererを取り付ける
        lineObject.AddComponent<LineRenderer>();

        // 描く線のコンポーネントリストに追加する
        lineRendererList.Add(lineObject.GetComponent<LineRenderer>());

        // 線と線をつなぐ点の数を0に初期化
        lineRendererList.Last().positionCount = 0;

        // マテリアルを初期化
        lineRendererList.Last().material = this.lineMaterial;

        // 線の色を初期化
        lineRendererList.Last().material.color = this.lineColor;

        // 線の太さを初期化
        lineRendererList.Last().startWidth = this.lineWidth;
        lineRendererList.Last().endWidth   = this.lineWidth;
    }

    /// <summary>
    /// 描く線のコンポーネントリストに位置情報を登録していく
    /// </summary>
    private void AddPositionDataToLineRendererList () {

        // 座標の変換を行いマウス位置を取得
        Vector3 screenPosition = new Vector3(Input.mousePosition.x, Input.mousePosition.y, Camera.main.nearClipPlane + 1.0f);
        var mousePosition = Camera.main.ScreenToWorldPoint(screenPosition);

        // 線と線をつなぐ点の数を更新
        lineRendererList.Last().positionCount += 1;

        // 描く線のコンポーネントリストを更新
        lineRendererList.Last().SetPosition(lineRendererList.Last().positionCount - 1, mousePosition);
    }
}

空のゲームオブジェクトをヒエラルキー上で作って、上記のコンポーネントをアタッチします。 f:id:coffee_ryo:20180506184102p:plain

線の太さは「LineWidth」で変更できます。とりあえず0.2くらいで設定しておきます。 線の色は「LineColor」で変更できます。 線の色の変更の為に「LineMaterial」の設定が必要なので、プロジェクト上でCreate > Material、Shaderを「Unlit / Color」としてコンポーネントに取り付けます。

ヒエラルキー上のMainCameraのBackgroundを白色にして、動作させてみます。 f:id:coffee_ryo:20180506184419g:plain

対称定規で線を引く

スクリプティングをしてコンポーネントを作ります。 LineRendererの位置の設定をする際に、UnityのQuaternionを使います。

using UnityEngine;
using System.Collections.Generic;
using System.Linq;

public class SymmetricRuler : MonoBehaviour {

    /// <summary>
    /// 描く線のコンポーネントリスト
    /// </summary>
    private List<List<LineRenderer>> lineRendererList;

    /// <summary>
    /// 対称定規で1回に描く線の本数
    /// </summary>
    public int rulerLineCount;

    /// <summary>
    /// 描く線のマテリアル
    /// </summary>
    public Material lineMaterial;

    /// <summary>
    /// 描く線の色
    /// </summary>
    public Color lineColor;

    /// <summary>
    /// 描く線の太さ
    /// </summary>
    [Range(0,10)] public float lineWidth;


    void Awake () {
        lineRendererList = new List<List<LineRenderer>>();
    }

    void Update () {

        // ボタンが押された時に線オブジェクトの追加を行う(複数個作る)
        if (Input.GetMouseButtonDown(0)) {
            this.AddLineObject();
        }

        // ボタンが押されている時、LineRendererに位置データの設定を指定していく(複数のLineRendererにデータを設定)
        if (Input.GetMouseButton(0)) {
            this.AddPositionDataToLineRendererList();
        }
    }

    /// <summary>
    /// 線オブジェクトの追加を行うメソッド
    /// </summary>
    private void AddLineObject () {

        lineRendererList.Add(new List<LineRenderer>());

        for(int i = 0; i < rulerLineCount; i ++){
            // 追加するオブジェクトをインスタンス
            GameObject lineObject = new GameObject();

            // オブジェクトにLineRendererを取り付ける
            lineObject.AddComponent<LineRenderer>();

            // 描く線のコンポーネントリストに追加する
            lineRendererList.Last().Add(lineObject.GetComponent<LineRenderer>());

            // 線と線をつなぐ点の数を0に初期化
            lineRendererList.Last().Last().positionCount = 0;

            // マテリアルを初期化
            lineRendererList.Last().Last().material = this.lineMaterial;

            // 線の色を初期化
            lineRendererList.Last().Last().material.color = this.lineColor;

            // 線の太さを初期化
            lineRendererList.Last().Last().startWidth = this.lineWidth;
            lineRendererList.Last().Last().endWidth   = this.lineWidth;
        }
    }

    /// <summary>
    /// 描く線のコンポーネントリストに位置情報を登録していく
    /// </summary>
    private void AddPositionDataToLineRendererList () {

        // 座標の変換を行いマウス位置を取得
        Vector3 screenPosition = new Vector3(Input.mousePosition.x, Input.mousePosition.y, Camera.main.nearClipPlane + 1.0f);
        var mousePosition = Camera.main.ScreenToWorldPoint(screenPosition);

        for(int i = 0; i < rulerLineCount; i ++){

            // 線と線をつなぐ点の数を更新
            lineRendererList.Last()[i].positionCount += 1;

            // 描く線のコンポーネントリストを更新
            lineRendererList.Last()[i].SetPosition(lineRendererList.Last()[i].positionCount - 1, Quaternion.Euler (0f,  0f, i * 360 / rulerLineCount) * mousePosition);
        }
    }
}

RulerLineCountを適当に8くらいに設定して、線を描いてみます。

f:id:coffee_ryo:20180506190637g:plain

その他

  • ヒエラルキー上の線オブジェクトを消せば線単位での取り消しができますので、スクリプト組めばUndo機能も実装できると思います。
  • 後から引いた線をカメラ手前に持ってくる課題や、パフォーマンス面での懸念があります。

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);
    }   
}