😸

第二回ゆるゲームジャムトライ

2024/12/08に公開

はじめに

概要

  • 第二回ゆるゲームジャム「攻撃ができるアイテム」に参加する記録を残します。
  • この記事は2024年Cluster Script Advent Calendar 2024で公開しています

ワールドを作るきっかけ

経緯

  • 11月19日ハロークラスターで企画発表を受ける
  • 12月05日何か絡めないか考え始める(記録をとり始める)
  • (どうしよう、ゴール設定がなにも用意できない)

イベントのテーマや狙い

  • 「攻撃ができるアイテム」を一つ作り、「攻撃を受けるアイテム」を多数作りたいと思っています

作りたいワールドのイメージ

  • 「攻撃を受けるアイテム」、ってうけとってなにをするかということですよ、、、どうしよう。

実現可能性の調査

ゴール設定

イベントワールドの完成形のビジョン

2024年12月05時点でない

どのような体験を提供したいか?

2024年12月05時点でない

ターゲット層(誰に向けたワールドか?)

2024年12月05時点でない

ワールドの具体的な目標

2024年12月05時点でない

見た目(デザインやアート)

2024年12月05時点でない

機能(インタラクションやゲーム要素)

「攻撃を受けるアイテム」で、、なにか、、、、(つらい)
とりあえずChatGPTにアイディア出しをしてもらって、まぁやってもいいかなくらいの項目を記述していく

ピアノ・ギター

  • ["damage", X:int]というメッセージを受けて、Xを10で割ったあまりに分ける
    0:"ド", 1:"レ", 2:"ミ"...というように、余りと対になる音を出すのはどうでしょう(本当は裏で音楽イベントワールド制作を進めていました。その影響でイメージしやすかったです)

重力相撲

  • ["damage", X:int]というメッセージを受けたプレイヤーの重力を変える
    • 天井に当たったら負け、みたいな
    • ダメージ範囲を変える武器とか、いろいろチートとか考えること多そう。つらい。

フラクタル

  • 当たるたびにフラクタル構造が更新される。。。つらそう

環境構築

開発環境(ワールド作り)

  • Unity 2021.3.4f1
  • Windows10
  • Cluster Creator Kit 2.29.0
    • 特に決めていない、最新であればよい

開発環境(空間づくり)

設計と制作

ワールドの設計

  • Clusterでは2024年12月下旬まで、ワールドでクラフトモードを有効にすることができます。
    • クラフトアイテムは許可したほうが、プレイヤーの自由度が高まる
    • 自作のクラフトアイテムを、クラフトモード有のワールドに召還して遊ぶことができます。

スケッチやレイアウトのプロトタイプ

2024年12月05時点でない

試行錯誤の記録

テンプレートワールド(おまじない)

  • テンプレートワールドを開き[ウィンドウ]>[パッケージマネージャ]でCluster Creator Kitのバージョンを更新する
  • Despawn Heightコンポーネントを持つオブジェクトを用意する
  • Spawn Point(スクリプト)コンポーネントでSpawn TypeをEntranceにしたオブジェクトを用意する
  • (初回アカウントログインしたかもしれない)
  • [Cluster]>[設定]のベータ機能を有効にするをonにする
  • [Cluster]>[ワールドアップロード]で新規作成ボタンをおして、ワールドアップロードする

技術的な問題点とその解決1

送信(Sender, Attacker)

空オブジェクトを作成し、Grabbable item コンポーネントとScriptable Itemコンポーネントをつける

Scriptにはsender_sample.jsを設定する。(後述)

Overlap Detector Shapeコンポーネントをつけると、Box ColliderコンポーネントのTriggerにするがonになる

これだけでアップロードすると、地面をすり抜けていく(1敗)
地面をすり抜けないためのcubeをつけておく
z軸マイナス方向が、持った時に正面に伸びる方向である。そのため、Z軸プラス方向にオブジェクトを作ると自身のアバターとぶつかる(N敗)

受信(Receiver, Defender)

空オブジェクトを作成し、Scriptable Itemコンポーネントをつける

Scriptにはreciever_sample.jsを設定する。(後述)
とりあえずCubeをつけておく

動作確認

Cluster内で、Attackerをつかみ、使うを実行すると、
① AttackerとDefenderが接触すると、Defenderはダメージ値を受信した

② AttackerとDefenderが接触していないと、Defenderはダメージ値を受信しない

サンプルスクリプト(sender_sample.js)

誰が幸せになるかわからないが、スクリプトを配置しておく
(プロトコルとは取り決めのことでであって、取り決めに何か情報を送るわけではない、、、とか読み直すと悲しくなる)

// プロトコル
let protocolDamage = "damage";
let protocolDamageValue = 10;

$.onStart(() => {
    $.state.using = false;
    $.log("プロトコルの送信値: " + protocolDamageValue);
    $.state.damage = protocolDamageValue;
});

$.onUse((isDown, player) => {
    if (!isDown) {
        return;
    }

    if ($.state.using) {
        return;
    }

    if ($.getGrabbingPlayer() === null) {
        return;
    }
    $.log("start using");
    $.state.using = true;
});

$.onUpdate((dt) => {
    if (!$.state.using) {
        return;
    }

    if ($.getGrabbingPlayer() === null) {
        return;
    }

    // OverlapDetectorShapeに重なっている、検知対象となる物体
    let overlaps = $.getOverlaps();
    let handles = [];
    for (let i = 0; i < overlaps.length; i++) {
        let handle = overlaps[i].handle;
        if (handle === null) {
            continue;
        }
        handles.push(handle);
    }

    $.log("damage プロトコルにdamage値を発火");
    $.log("handles.length: " + handles.length);
    for (let i = 0; i < handles.length; i++) {
        handles[i].send(protocolDamage, $.state.damage);
    }

    // using終了
    $.state.using = false;
});

サンプルスクリプト(receiver_sample.js)

// プロトコル
let protocolDamage = "damage";
let protocolDamageType = "number";

$.onStart(() => {
});

$.onUpdate((dt) => {
});

$.onReceive((protocol, body, _) => {
    $.log("プロトコル: " + protocol);
    $.log("プロトコルの受信値: " + body);
    if (protocol !== protocolDamage) {
        return;
    }
    if (typeof (body) !== protocolDamageType) {
        return;
    }

    let truncated = Math.trunc(body);
    $.log("受信値の整数値: " + truncated);
});

技術的な問題点とその解決2

damageプロトコルにnumberを送れることを確認できました。
この節ではnumberに意味付けしたいと思います。
およそHPというパラメータにおける、damageの値に充てることが多いと思います。
今回は音階というパラメータとして扱いたいと思います。

送信(Sender, Attacker)

  • 数字を8で割ったあまりでそれぞれ音を割り当てる。
    • ド - 0
    • レ - 1
    • ミ - 2
    • ファ - 3
    • ソ - 4
    • ラ - 5
    • シ - 6
    • ド - 7
  • 送信する値はuseことに変える
    • 送信する値-1, 0, 1.1, 9の値分用意する(テスト用)
      • -1 = 8 * -1 + 7 であるので、あまりは7とする
      • -1.1 は少数点切り捨てのはず。(JavaScriptのtruncに任せる)整数値として扱う
      • 9 = 8 * 1 + 1であるので、あまりは1とする

        送信側は新たに作成したスクリプトを更新するだけでいい

受信(Receiver, Defender)

  • 受信でピアノのドレミファソラシドと切り分けようとする
  • ヒエラルキーで、Defenderのアイテムの下に、Audio Sourceコンポーネントをつける
    • Sound###(###は数値)という名前は、スクリプトで名前参照するために変えないことをお勧めする
    • ###という数値はインデックスとしてスクリプトの利便性を考えている
    • Audio SourceコンポーネントはSound###で鳴らしたい音源(例はmp3ファイル)を指定する
      • 今回はSound0にド、Sound1にレ、…Sound6にシ、Sound7にド、という音源を指定している

サンプルスクリプト(sender_numbers.js)

  • sender_sampleに比べて、$.onUseで送信する値を更新するくらいしか追加作業はしていないはず
// プロトコル
let protocolDamage = "damage";

let protocolDamageNo = 0;
const protocolDamageValues = [
    -1,
    0,
    1.1,
    9
];
let protocolDamageIndexSize = protocolDamageValues.length;

$.onStart(() => {
    $.state.using = false;
    $.state.damage = 0;
});

$.onUse((isDown, player) => {
    if (!isDown) {
        return;
    }

    if ($.state.using) {
        return;
    }

    if ($.getGrabbingPlayer() === null) {
        return;
    }
    $.log("start using");
    $.state.using = true;

    ++protocolDamageNo;
    if (protocolDamageNo >= protocolDamageIndexSize) {
        protocolDamageNo = 0;
    }
    $.log("protocolDamageNo: " + protocolDamageNo);
    $.state.damage = protocolDamageValues[protocolDamageNo];
});

$.onUpdate((dt) => {
    if (!$.state.using) {
        return;
    }

    if ($.getGrabbingPlayer() === null) {
        return;
    }

    // OverlapDetectorShapeに重なっている、検知対象となる物体
    let overlaps = $.getOverlaps();
    let handles = [];
    for (let i = 0; i < overlaps.length; i++) {
        let handle = overlaps[i].handle;
        if (handle === null) {
            continue;
        }
        handles.push(handle);
    }

    $.log("プロトコルdamageでdamage値を送信");
    $.log("handles.length: " + handles.length);
    for (let i = 0; i < handles.length; i++) {
        handles[i].send(protocolDamage, $.state.damage);
    }

    // using終了
    $.state.using = false;
});

サンプルスクリプト(receiver_sound.js)

  • ユーザーがdamageプロトコルにnumber値を送るわけですが、どんな値がやってくるかわからない問題がります。
    • 負のとき(-1)
      • -1 = 8 * -1 + 7と考えるとしています
      • 8でわったあまりはJavaScriptでは-1 (= 8 * 0 + (-1))となるようでした。
      • 得られたあまりに8を足してから、再度8で割るようにしています *
    • 想定値(0)
    • 小数点を持つ値(1.1)
      • 少数切り捨て、四捨五入、いくつか整数に丸める方法はあると思います。
      • 今回はtruncを使っています
    • 想定外(9)
      • 負の値の検討の通り、8であった余りを得ることで0~7に抑えるようにしています
// プロトコル
let protocolDamage = "damage";
let protocolDamageType = "number";

const soundNodes = [
    $.subNode("Sound0"),
    $.subNode("Sound1"),
    $.subNode("Sound2"),
    $.subNode("Sound3"),
    $.subNode("Sound4"),
    $.subNode("Sound5"),
    $.subNode("Sound6"),
    $.subNode("Sound7"),
]
const soundNodesSize = soundNodes.length;

$.onStart(() => {
    $.state.canGetUnityComponent = true;
});

$.onUpdate((dt) => {
});

$.onReceive((protocol, body, _) => {
    $.log("プロトコル: " + protocol);
    $.log("プロトコルの受信値: " + body);
    if (protocol !== protocolDamage) {
        return;
    }
    if (typeof (body) !== protocolDamageType) {
        return;
    }

    let truncated = Math.trunc(body);
    $.log("受信値の整数値: " + truncated);
    let soundNodesNo = ((truncated % soundNodesSize) + soundNodesSize) % soundNodesSize;
    $.log("soundNodesNo: " + soundNodesNo);
    if ($.state.canGetUnityComponent) {
        try {
            soundNodes[soundNodesNo].getUnityComponent("AudioSource").play();
        } catch (e) {
            $.state.canGetUnityComponent = false;
            $.log("canGetUnityComponent: " + $.state.canGetUnityComponent);
        }
    }
});

技術的な問題点とその解決3

今回は音階というパラメータを全部送れるようにします。
前節で書いたコードのうち、送信部分で送れる値を変えるだけです

送信(Sender, Attacker)

const protocolDamageValues = [
    -1,
    0,
    1,
    2,
    3,
    4,
    5,
    6,
    7,
    8,
];

このスクリプトをつけると、確かにド~シまでの音が鳴ることを確認できる

(コラム)入室音をつける

普通ワールド公開は自分が想定している完成度60%を達成したときに行います。
しかし、ゆるゲームジャムでは完成度5%程度(というよりワールドアップロードできたら公開)するようにします。このワールド公開の一方で開発を行っていると、その途中一般プレイヤーが様子見に来てくれるのですが、まぁ気が付きません。今回は
入室音をつけることを行ってみたいと思います。→すぐできた。

技術的な問題点とその解決4

送信側で曲を作ります
ドレミファソラシドの音階で演奏できる簡単な曲を調べると、「キラキラ星」や「ハッピーバースデートゥーユー」があるみたいです

キラキラ星

    ドドソソ ララソ  ファファミミ レレド
    ソソファファ ミミレ ソソファファ ミミレ 
    ドドソソ ララソ ファファミミ レレド

これを今回の音ー記号の対に合わせようとするとつらいです。(記号のデバッグはつらい)

サンプルスクリプト(sender_kirakira.js)

全部添付して幸せになる人がいるかわかりませんが、全体像を共有します。

// プロトコル
let protocolDamage = "damage";

// 音の対応表
const noteToNumber = {
    "ド": 0,
    "レ": 1,
    "ミ": 2,
    "ファ": 3,
    "ソ": 4,
    "ラ": 5,
    "シ": 6,
    "ド'": 7 // 高いド
};

// きらきら星の楽譜
const sheetMusic = [
    "ド", "ド", "ソ", "ソ", "ラ", "ラ", "ソ",
    "ファ", "ファ", "ミ", "ミ", "レ", "レ", "ド",
    "ソ", "ソ", "ファ", "ファ", "ミ", "ミ", "レ",
    "ソ", "ソ", "ファ", "ファ", "ミ", "ミ", "レ",
    "ド", "ド", "ソ", "ソ", "ラ", "ラ", "ソ",
    "ファ", "ファ", "ミ", "ミ", "レ", "レ", "ド"
];

let protocolDamageNo = 0;
let protocolDamageIndexSize = sheetMusic.length;

$.onStart(() => {
    $.state.using = false;
    $.state.damage = 0;
});

$.onUse((isDown, player) => {
    if (!isDown) {
        return;
    }

    if ($.state.using) {
        return;
    }

    if ($.getGrabbingPlayer() === null) {
        return;
    }
    $.log("start using");
    $.state.using = true;

    ++protocolDamageNo;
    if (protocolDamageNo >= protocolDamageIndexSize) {
        protocolDamageNo = 0;
    }
    $.log("protocolDamageNo: " + protocolDamageNo);
    const note = sheetMusic[protocolDamageNo];
    $.log("note: " + note);
    const damageValue = noteToNumber[note];
    $.log("damageValue: " + damageValue);
    $.state.damage = damageValue;
});

$.onUpdate((dt) => {
    if (!$.state.using) {
        return;
    }

    if ($.getGrabbingPlayer() === null) {
        return;
    }

    // OverlapDetectorShapeに重なっている、検知対象となる物体
    let overlaps = $.getOverlaps();
    let handles = [];
    for (let i = 0; i < overlaps.length; i++) {
        let handle = overlaps[i].handle;
        if (handle === null) {
            continue;
        }
        handles.push(handle);
    }

    $.log("プロトコルdamageでdamage値を送信");
    $.log("handles.length: " + handles.length);
    for (let i = 0; i < handles.length; i++) {
        handles[i].send(protocolDamage, $.state.damage);
    }

    // using終了
    $.state.using = false;
});

変えたところは

// 音の対応表
const noteToNumber = {
    "ド": 0,
    "レ": 1,
    "ミ": 2,
    "ファ": 3,
    "ソ": 4,
    "ラ": 5,
    "シ": 6,
    "ド'": 7 // 高いド
};

// きらきら星の楽譜
const sheetMusic = [
    "ド", "ド", "ソ", "ソ", "ラ", "ラ", "ソ",
    "ファ", "ファ", "ミ", "ミ", "レ", "レ", "ド",
    "ソ", "ソ", "ファ", "ファ", "ミ", "ミ", "レ",
    "ソ", "ソ", "ファ", "ファ", "ミ", "ミ", "レ",
    "ド", "ド", "ソ", "ソ", "ラ", "ラ", "ソ",
    "ファ", "ファ", "ミ", "ミ", "レ", "レ", "ド"
];

let protocolDamageNo = 0;
let protocolDamageIndexSize = sheetMusic.length;


ダメージ値の更新箇所

    $.log("protocolDamageNo: " + protocolDamageNo);
    const note = sheetMusic[protocolDamageNo];
    $.log("note: " + note);
    const damageValue = noteToNumber[note];
    $.log("damageValue: " + damageValue);
    $.state.damage = damageValue;

ですね。
今回の変更によって別の曲したいときはsheetMusicの配列のみ変えるだけで済むようになりました。
と、言っても受信側は一オクターブ分しかないので、曲の選定が難しいかもです

聖者の行進

ということで10分程度調べた範囲では、聖者の行進も一オクターブで弾けるようでした。

// 聖者の行進の楽譜
const sheetMusic = [
    "ド", "ミ", "ファ", "ソ",
    "ド", "ミ", "ファ", "ソ",
    "ド", "ミ", "ファ", "ソ", "ミ", "ド", "ミ", "レ",
    "ミ", "ミ", "レ", "ド",
    "ド", "ミ", "ソ", "ソ", "ファ",
    "ファ", "ミ", "ファ", "ソ", "ミ", "ド", "レ", "ド",
];

技術的な問題点とその解決5

いま受信側は受け取った値0~7に対しド~ドを返すようにしています。
ピアノっぽくしたくなりますね。
ハ長調(?)として基底音をドとしたピアノデザインにしてみたいと思います。
このようにすると、送信側が基底値を変えるだけで転調できるはずです。簡単ですね。
(音源を理由にしたくありませんが、#や♭は扱えません。そのため歯抜けみたいな楽器になります)

サンプルスクリプト(receiver_sound_a1.js)

全部投稿するまでもないので、部分的に書きます
方針は、受け取った値に対して、ドからのシフトを加えるだけでよいです。(ドのときはシフト値は0にします)
シフト分の固定値を用意して

const nodeShift = 5;

受信値を整数にした後、シフト分足して余りを計算するだけです。

    $.log("受信値の整数値: " + truncated);
    let soundNodesNo = (((truncated + nodeShift) % soundNodesSize) + soundNodesSize) % soundNodesSize;

技術的な問題点とその解決6

ワールドの世界観づくりをようやく始めます。
最近世界観づくりは国土交通省が推し進めているPLATEAUを使って、都市データを利用したいと思います

PLATEAU SDK for Unityを導入

都市データ導入

  • Unityの[PLATEAU]>[PLATEAU SDK]を開き、ローカルにダウンロードしたCityGMLを入力フォルダに指定します。
    * 基準座標系の選択
    • 今回は東京の都市データを利用するので、基準座標系は09:東京、のままにします
  • 範囲選択
    • 今回は東京のどこかの街を選択します
      • 基本導入するスケールは現物と同じです。人が歩ける範囲で十分な広さとなります
      • もちろん後で地物やワールド移動速度を変えることで、カバーできます。
      • 六本木ヒルズ周辺を選択します
        • 六本木ヒルズは建物に含まれていませんでした(1敗)
      • 銀座周辺を選択します
  • 地物別設定
    • 基本的にLODの設定はMIN/MAX2あるいは3にしてよいと思います。詳しくはPLATEAUの資料を読んでください。
    • テクスチャーを含めるはoffにしてください。(onにした方でiOS向けのビルド成功する方法があれば教えて)
    • Colliderを含める場合はチェックをつけましょう
    • 災害リスクや土地利用は不要だと思います。

背景を変える

背景(天体球, Skybox, スカイボックス)も変えておきます。
デザインにこだわっていないので、とりあえず夜っぽい色のマテリアルを用意して、ShaderをSkyboxの設定にしています。

こだわる人は自分の好きな背景表現ができる!「Skybox(スカイボックス)」の紹介があるのでそちらで確認してください。

技術的な問題点とその解決7

アイテムがつかみにくい

World Runtime Settingを導入します。ワールドで「しゃがみ」をできるようにする。これを導入するとFキーで選択できるようになるはず

ジャンプ力変更

街並み導入によって、ジャンプ力を変えられるようにしておきたいと思います(ちょっとした段差はジャンプで超えたい)。落下すればスタート位置に戻るので、あまり丁寧にメカニクスの準備は不要だと思いますが。
【cluster】クラフトアイテムでワープ・移動速度等変更
走る速さやジャンプ力を変えてみよう
このあたりはパッションでUnityを実装しています。。。(最適な設定であるかわかりません)

  • jumpHigh.js
$.onInteract((player) => {
    player.setJumpSpeedRate(4)
});

サムネイル

canvaを使ってサムネイルを作ります。
つらい。。。(閉会式の5分前とかだったはず)

振り返りと今後の展望

できたワールド

運営さん聞こえますか? 2日間じゃバグ直せないです

完成したワールドへの感想

時間が足りない(金曜2時間と日曜8時間、計10時間くらい開発したと思います)

自分が楽しかった点、成長できた部分

メッセージ処理は基本デバッグがつらいです。。。

予想外の達成感

damageプロトコルのnumberをどうとらえるかに悩みました。
最近音楽について調べているのでちょうどいいテーマを見つけられたと思います。

今後のアップデートや他のプロジェクト

ベータ機能を使わない、音楽イベントワールド制作に活かしたいと思います。

おわりに

感謝の言葉

開発中に遊びに来ていただいた方ありがとうございます。
いいね!つまらないね!いずれもF/Bいただけると励みになります

余談

LT会で自分が作成した画像をClusterプラットフォームで共有する方法がわかっていませんでした。


ここにファイルの追加、があったのか…知らなかったです。(画面共有できるのに気が付かなかった…ずっとウェブのほう探してた)

Discussion