まえがき
皆さまこんにちは。モブ豆と申します。
前回の制作日記➀の建築編に続き、今回はゲームシステム編に入っていこうと思います。あくまで日記なので内容が冗長になるのはご了承ください。
※2023/10/27以前に制作日記➀を読んだ方へ
制作日記➀に関してですが、サイトのシステムの不具合によって投稿の更新が正常に行われず、不完全な状態で公開されていた期間がありました。サイト運営様の迅速な対応によって、10/28深夜帯には完全版に更新することができましたので該当の方は一度読み直していただくことをお勧めします。
日記の構成
日記の構成は以下を想定しています。
・建築編(前編)..建築の外装について
・建築編(後編)..建築の内装について
・ゲームシステム編 ..どういったコンテンツを作るかやミニゲームなどについて(多分他の奴全て合わせてもここが一番長くなると思います。)←今ここ!!(2023/10/30時点)
・土木編 .. 地形について
・ストーリー編 ..ストーリーについて
・デバッグ編 ..デバッグ作業について
(余談 内容に関わらずたまーに挟みます)
※内容的にカテゴライズが難しいものもあり構成を途中でコロコロ変更する可能性があるのでご了承ください。
制作中の配布ワールド概要
(現時点で完全に決まっている要素だけ記述しています。)
作品名:未定
推奨プレイ人数:1人- 完全マルチ対応
ストーリー:あり
謎解き:微あり(隙間なし)
戦闘:あり
アスレチック:あり
難易度:普通-難しい
ゲームシステム編
以前から言っていたように長編になるため章立てで書いていきます。内容の多さの問題で各章ごとに、さらに細かく目次を作る可能性もあります。(特に3章、5,6章)
目次
1章.コンテンツ概要
2章.ゲームデザイン
3章.ミニゲーム
4章.買い物
5章.アイテム合成
6章.ダンジョン、戦闘システム
7章.NPC、メインエリア演出
8章.釣り
9章.その他
1章.コンテンツ概要
1-1.そもそもゲームシステムとは?そしてコンテンツとは?
ここでのゲームシステムとは前提として必要なプレイ環境やコマンド等の処理が必要な部分だけでなく、どのようなコンテンツを作ってそれらをどのようにつなぎ合わせるのかといったゲーム設計の話も交えて書いていこうと思います。
制作日記➀の方に書きましたが、今作が目指している点は主に下記の二点
・ストーリーをベースとしてそこに様々なコンテンツが肉付けされたRPG、アドベンチャー
・1要素特化というよりも様々な要素をふんだんなく楽しめるようなゲーム設計
ここでのコンテンツというのは本作を遊ぶ際の楽しめるポイントのことで、上記の目標を意識しながら考えました。
1-2.登場予定のコンテンツ
1.レベリングシステム
プレイヤーにいわゆるステータスと言われるものを追加しようと考えています。
現在考えているステータスの要素は以下の通り↓
・基礎体力
・基礎攻撃力
・最大魔力量
・最大マナ
(・最大スタミナ量)
・自動体力回復
・自動マナ回復
(・自動スタミナ回復)
(・物理防御)
・魔法防御
・インベントリ容量
()内のものはゲームプレイ上における快適さ、またはゲーム設計における問題および技術的な問題から実装を検討段階のものになります。
所持装備品、特定の施設を利用した際やポーションを利用した際のバフに応じてステータスを変動させることが可能で、ビルドを組むのも楽しさの一つに仕上げていきたいです。敵モンスターに対してもこれらの値を設定して、魔法が効きやすい敵、物理攻撃が効きやすい敵など様々な特徴付けをしていきます。
レベル上げに必要な経験値はクエストクリアまたはダンジョン内でのモンスター討伐及びダンジョンクリア報酬として与えられるもので、レベルが上がるごとに次レベルまでに必要な経験値量は指数関数的に増えていきます。ストーリー進行度やダンジョンの難易度に応じてもらえる経験値量も増やして、1レベルを上げるために必要な時間がなるべく均一になるように目指していく予定です。各ステータスごとの効果などについては2章でほんの少し、戦闘システムの所でがっつり書いていきます。
2.戦闘、アスレチック(ダンジョン)
主力コンテンツの一つ。
制作日記➀の方で活動エリアについて書いたと思いますが、メインエリア外にはダンジョンを作ろうと思っています。ダンジョンは全て地下に存在していて、上層から順に下層にまで下りていくイメージです。
ダンジョン内では専用のステータス画面やUIが用意されていて(まだ作っていませんが)、メインエリア内で行使できないスキルや魔術を行使することが可能になります。
ダンジョン内の奥に進むためには簡単な謎解きギミックなどを解く必要があるため、単純な戦闘だけではありません。
ダンジョン内の危険なエリアには希少な植物が自生していたり、鉱床があったりと、冒険者協会で素材収集のクエスト等を受注していると何度も来ることになります。さらにダンジョン内で遭遇するモンスターからも後述のアイテム合成の素材がドロップするので、より効率的にダンジョンを周るための武器防具の素材を集めるのもいいですし、クエストで必要なアイテムを合成するために素材集めにダンジョンに潜るということもあるかもしれません。
3.ミニゲーム(金策)
主力コンテンツの一つ。
ストーリーやサブクエストで一度遊ぶことになってからはその後何度でも再挑戦できるようになり、スコアやミニゲーム内容に応じて特殊な報酬であったりお金を獲得することができます。金策かつやりこみ要素として取り入れるものになりますが、おそらく皆さんが想定しているよりもしっかりゲームになっているのでご期待あれ。(もしかしたら一つずつ今作から切り離して配布をするかもしれません)
該当の章に移ってから詳細はお話しますが、現時点で考えているのは以下の四つ
・カジノ(スロット、ホイールオブフォーチュン、ブラックジャック)
・的当て訓練(射的)
・鉱山採掘パニック(アイスクライマー的な)
・商会員認定試験(早押しゲーム)
ストーリーを深堀りする段になってから必要なものが頭に浮かんだら都度追加していく予定です。
4.買い物
魚屋、地図屋、鍛冶屋といったようにお店がありますから当然買い物もできます。場所によってはアイテムを売却して売ることもできますし、特定のアイテムを納品することで提供するアイテムが増えていくといった場所もあります。ストーリーや他コンテンツの進行具合と足並みがそろうようなバランス調整にすることによって良いゲーム体験ができるように目指していきます。
5.アイテム合成
ダンジョンや特定の店で購入したり入手したりした素材を使って様々なアイテムを合成することができます。自分一人で合成できるものもありますし、専門の職人に素材を渡すことでしか作れないものもあります。
特定のビルドを組むために必要な素材集めに奔走するというのはRPGらしいかと思います。
7.釣り
金策ややりこみ要素として追加するものになりますが、やっていた方が特定の行動の際の選択肢が広がるように調整していくつもりです。水がある場所では基本どこでも釣りをすることができますが、釣り堀やダンジョン内の水場ではレアなものを釣ることができます。アイテム合成の際の素材にもなりますが、収集しきると何がいいことが..?!
8.ストーリー(メインクエスト)
主力コンテンツの一つ。
ゲームデザインの章で触れることになりますが、今までに挙げた様々なコンテンツはストーリーに沿ったメインクエストによる誘導で行われるものが多数になります。詳細についてはストーリー編について言及するのでここでは割愛。
9.サブクエスト
メインストーリーにやや関連する(大体の場合必須ではない)、または関係ないやりこみ要素的なものです。メインストーリーを進めるにあたって必要なステータスであったりお金であったり強い装備品であったりを手に入れて有利に進めるためにはやった方がいいコンテンツになります。一つ一つごとに小さいストーリーを作ろうと考えているので今後大きな時間を取られること間違いなし。
10.隠しチェスト
今作のこだわりポイントとして、建築を細部まで作りこんでいる(あくまで私の感覚ですが)というものがあります。なので色んな角度からの景観であったり細部の作りこみを目で楽しんで欲しいのですが、そういった楽しみ方をしている時にさらに良いことがあったらハッピーですよね?!
ということでボーナスアイテムの入った隠しチェストをメインエリア内の色んな場所に配置することにしました。やりこみ要素として機能するのが良いですね。
現在の進捗状況は、ミニゲームのみほぼほぼシステムづくりが完了しているという状況です。
ただ、そのミニゲームも現在は四種類とはいえかなーり細かく作っているため執筆内容はまだまだあります。
2章.ゲームデザイン
1-1.フレームワーク
さて本章ではゲームデザインについて書いていきます。私は専門家でもなんでもないので、以下に挙げるフレームワークは勝手に考えたものであるということに留意してください。
前章で挙げたこの2点
・ストーリーをベースとしてそこに様々なコンテンツが肉付けされたRPG、アドベンチャー
・1要素特化というよりも様々な要素をふんだんなく楽しめるようなゲーム設計
について実際にどのようにコンテンツ同士をつないでいくかは下の画像を参考に。
全てのコンテンツを最初から使用可能な状態にするのもいいですが、今作ではストーリーをベースにしているため、時系列すなわちストーリの進行度に応じて遊べるものを増やしていく方がストーリーの存在感が出て理に適っています。
ストーリーの節々に新たなコンテンツが開放されるタイミング(画像では「ポイント」と表記)を設定して各ポイントを通過した後に対応したものが開放されていくイメージです。ただ、ストーリー進行が一定程度進むとポイント通過だけでなく既に開放した要素を一定水準までアップグレードしておくことも開放条件に追加されていきます。
また、各コンテンツや要素ごとに進行度的なものを追加しようと考えており(画像にはLv.1,Lv.2,Lv.3,…と表記)段階ごとに区切ることで他コンテンツの進行の条件にできるようにしています。
このような制限を付ける理由は、コンテンツが多いとどれか一つだけをずっと遊び続けるという遊び方でアンバランスな状態が生まれてしまう危険性があるためです。
例えば「レベル99まで上げて全くストーリー進行をしていない、冒険者等級をカンストしているのにダンジョンに一度も行っていない」という状態は制作当初から想定している様々な要素をふんだんなく楽しめるというコンセプトをガン無視した設計になっていると言わざるを得ません。なのでダンジョンは階層ごとに入れる基準となる等級を設定するのです。
もちろん進行に必要なステータスレベルが5であれば、レベル10までは上げれるようにしておくなど、プレイに余裕ができる範囲で上限を設定するつもりです。
1-2.今作のゲームデザイン構成
上の画像はまだ、最初期段階の部分のみ記したものになります。全部書こうとしてもまだ考えていないものもあったり、作っていなくて具体的な想像がつかない部分もあるため、このゲームシステム編で新たな部分に触れる度にこの画像に書き加えていく形で更新していこうと思います。ストーリーの長さや鍛冶屋などお店の販売品ラインナップアップグレードのことも書くことも考慮するとおそらくこの画像と比較して縦にも横にもとんでもなく広くなったものが最終形になると思います。
ゲームデザインに非常に大きく関わる要素・コンテンツ一覧
・ストーリー
まず進行において最も根本的な条件となるものがストーリー、ここは言うまでもありませんね。
・ステータス(プレイヤーレベル)
前章の登場予定のコンテンツの部分でレベリングシステムに触れたと思いますが、そこで言及したプレイヤー自体のレベルのことになります。ストーリーを進める上で戦闘要素が必要となりますが、難易度的に進行が可能な最低限のラインとして設定しているというのが他コンテンツ開放条件としても使用する理由になります。
・冒険者等級
冒険者協会の所でお話した等級のことですね。等級は砂利級(ネーミングというか字面が酷いですが仮置き)からネザライト級まで合計10段階の予定です。
砂利、土、石、銅、鉄、金、エメラルド、アメジスト、ダイアモンド、ネザライトといったようにマインクラフト内の希少価値の低いものから高いものへ遷移していくように設定しました。プレイヤーにはパッとしないネーミングからかっこいい名前に変わっていく過程に達成感を感じて欲しいです。
画像では冒険者等級を上げるためにはステータスのみ必要になるような書き方になっていますが、ある程度高い等級になるとダンジョンの特定階層クリアやサブクエストでの昇格試験なるものも出てくるようになります。
・ダンジョン
階層ごとに分かれており、より深層に挑戦するためには前述の冒険者等級が必要な条件として求められます。ダンジョンはストーリーと大きく関わってくるので、特定の階層クリアまたは特定階層での特殊な行動が鍵がストーリー進行のフラグとなることもあります。
・ミニゲーム
一部のミニゲームのみ画像のように遊べる内容が段階的に開放されていくようにするつもりです。ミニゲームは要素が欠けると面白味が低減してしまうため、このような対応をしています。
・サブクエスト
画像ではミニゲーム2をクリアしたらサブクエスト1解放となっていますが実際はもっと複雑な要素が条件としてか関わってくる想定です。主にストーリー進行をしていれば開放されるものが多くなるという印象でしょうか。
3章.ミニゲーム
3-1.商会員認定試験(早押しゲーム)
3-1-1.ゲーム紹介
・商会員認定試験とは
制作日記➀の建築編にて商会について触れましたが、ストーリー上でプレイヤーは商会を経営するある人物に会う必要があります。
その人物は商会の建物群のうち、商会員専用スペースの中にいるため通常では会うことが難しいんですね。そのため、商会員に自身を登録して会いに行く流れになり、そこで登場するのがこの商会員認定試験になります。
様々な町人が持ってくるアイテムに合ったアイテムを渡すという作業を的確にこなすことで商会員としての能力を計るんですね。
一度クリアしたのちは商会のアルバイトとして何度も再挑戦することができるようになります。お金稼ぎの手段としても使いましょう。
・ゲームのルール
町人がカウンターに持ってくるアイテムに応じて対応するボタンを押しましょう。
カウンターで受け付けるアイテムは合計5種類で金、鉄、レッドストーン、ネザライト、ラピスラズリになります。ボタンはそれぞれのブロックの色に似たものを採用していますが、ただ各ブロックの上のボタンを押せばよいだけです。
後述の町人の種類の所でも言及しますが、町人が受け付けできるアイテムを持っていない場合は道案内ボタンという特殊ボタンを押す必要があります。また、条件をつけてくるような町人もいるのでただ早く押せばいいというだけでなく頭を使って対応しなければなりません。
選択を正解するたびに加点、間違えるたびに減点されていき、最終的なスコアがゲームクリアかどうかの指標になります。
クリア後に挑戦する際はスコアの高低によって報酬が変動します。
・プレイヤー側のUI
プレイヤー正面に左から金ブロック、鉄ブロック、レッドストーンブロック、ネザライトブロック、ラピスラズリブロックの順にボタンが配置されています。道案内ボタンのみ正面ではなく左側に配置されています。
・お客さん町人の種類
1.通常客
手に持っているアイテムは上述の五種類のブロックのうちどれかで、対応するボタンを押せばよいだけです。それゆえに一番対応が簡単なお客さんとなっています。
2.シークレット客
頭上に?マークの吹き出しがついているのが特徴です。一定間隔で手に持っているアイテムが変化する町人です。数秒前まで金ブロックだったのにレッドストーンブロックに変わっているなんてことがあるので対応するブロックのボタンを押すタイミングが重要になってきます。ブロックが変わった直後にボタンを押すのがコツですね。
3.リバース客
頭上に双方向の矢印の吹き出しがついているのが特徴のお客さんです。手に持っているブロック以外が正解となるので、通常客と真逆の対応をすれば正解となります。例えばこのタイプのお客さんが鉄ブロックを手に持っている際はそれ以外の金ブロック、レッドストーンブロック、ネザライトブロック、ラピスラズリブロックに対応するボタンが全て正解となります。
4.シークレットリバース客
シークレット客とリバース客の性質を両方とも併せ持つ厄介なタイプのお客さんです。手に持っているブロックが一定間隔で変化しつづける上に、提示された以外のブロックを選ばないと不正解になってしまいます。確率的な問題で適当にボタンを押していれば正解になることが多いですが、このお客さんは間違った対応をした際に大幅な減点を食らうので要注意です。
5.間違え客
そもそも対応していないブロックをカウンターに持ってくるお客さんになります。シークレット、リバース、シークレットリバースなど特殊な条件で注文してくる場合もありますが全ての場合において道案内ボタンのみが正解となります。そもそもお店間違えてますよ!って言ってあげるんですね。
ややこしいことに対応しているブロックと見た目が似ているものを持ってくるので、気づかずに他のお客さんと同様の対応をしてしまう可能性があり要注意なお客さんになります。
ちなみにこのタイプのお客さんが持ってくるブロックは以下の通り↓
3-1-2.ボタン連打について
マインクラフト内でボタンというものは一度押してから元の状態に戻るまで一定時間待たなければなりません。そのため早押しゲームという性質上、ボタンを連打できるようにしたいわけです。
後述しますが、お客さん対応への正誤判定はボタンを押したことを検知してその瞬間に行うのでここに組み込む形でボタン連打できる仕組みを作ります。といってもボタンが押された直後に元の状態のボタンをその位置へクローンするだけですが。
ボタンを押した瞬間に押される前の状態のボタンを重ねると超連打が可能になります。ベコベコベコベコッ!って感じで結構気持ちいいのがプレイヤーにとってストレスフリーであれば幸いです。
3-1-3.町人の移動制御(入店、対応待ち(列づくり)、退店)
※次のミニゲーム紹介まではコマンドを交えた解説になりますので興味のない方は3-2.カジノまで飛ばしてください。
コマンドで使用するスコア、タグ一覧
コマンド紹介でちょくちょく登場するので先に一覧を記しておきます。
・スコア
timer..入店時からの経過時間管理
sectimer..シークレット客の手持ちブロックリール
customer..客の状態判別
button1-6..押したボタンと町人の持つブロックとの比較、正誤判定
button..客の持つブロック割り当て、判別
dummy..間違え客の持つブロック決定
type..間違え客以外の客特性割り当て、判別
・タグ
admin..参加者判定用のタグ、一名のみに付与
stop..町人の入店時に前方に先客がいた場合、足を止めるためのタグ
normal..通常客
reverse..リバース客
secret..シークレット客
reverse_secret..シークレットリバース客
入店時の移動制御
今から説明するのは簡易版として別マップに試験的に作ったものなので、後の「配布ワールドの移植」のところで実際にどのように運用するのかはお話します。
さて、この早押しゲームですが町人がカウンターにブロックを持ってくるまでの移動制御をしなければいけないわけです。
説明しやすいようにステージを切り取って上から見た図を使っていきます。干し草の俵の位置(見にくいですが床色と同化しています。)に新たな町人を召喚してカウンター正面まで細かいテレポートを重ねて移動しているように見せます。
仕組みとしては町人を基準に特定座標にあるブロックに応じて移動する方向を決めていきます。
先ほどの干し草の俵の上に防具立てを設置し、この防具立てからランダムにいずれか一つを選び、下の干し草の俵に町人を召喚する形です。上の画像は町人の入店時のルートを示したものになります。この簡易版ではコマンドの簡素化のために防具立てを使用していますが、実際の運用ではエンティティの使用はラグの原因となるので使用する予定はありません。
移動方向はブロックごとによって違い、鉄ブロックは下方向、ダイヤモンドは右方向、金ブロックは左方向、エメラルドブロックは上方向、ガラスブロックでは一時停止という風にしています。画像のブロックに方向性を持たせてみると全てカウンターの方向に集まっていることがわかると思います。
ルートがクネクネしているのは町人がドアだったり別のエリアから移動してきたように演出するためですね。
入店時のコマンドは以下の通り
#移動処理(入店時)
execute as @e[scores={customer=0..1},tag=!stop] at @s if block ~ -49 ~ iron_block run tp @s ~~~-0.15 180 0
execute as @e[scores={customer=0..1},tag=!stop] at @s if block ~ -49 ~ emerald_block run tp @s ~~~0.15 0 0
execute as @e[scores={customer=0..1},tag=!stop] at @s if block ~ -49 ~ gold_block run tp @s ~0.15~~ -90 0
execute as @e[scores={customer=0..1},tag=!stop] at @s if block ~ -49 ~ diamond_block run tp @s ~-0.15~~ 90 0
execute as @e[scores={customer=0..1},tag=!stop] at @s if block ~ -49 ~ glass run tp @s ~~~ 180 0
内容としてはスコアがcustomer=0または1かつ(0は入店中、1は対応待ちという状態を表しています)、stopというtagがついていない町人に対してその町人のいる位置のy座標-49に存在するブロックに応じて各方向に進むまたは一時停止するというものになっています。町人の移動スピードは1tick(1/20秒)につき0.15ブロックなので1秒間に1.5ブロック程度進む計算ですね。
列に並ぶときの一時停止
また、店にはお客さんとして町人が次から次へとくるので、対応待ちの列を作る必要があります。前方に他の町人がいたら強制的に足を止めるようにします。
#一時停止処理(前方に先客がいた場合)
tag @e[scores={customer=0..1}] remove stop
execute as @e[scores={customer=0..1}] at @s positioned ^^^1.2 if entity @e[scores={customer=0..1},r=1] run tag @s add stop
execute as @e[scores={customer=0..1},tag=stop] at @s run tp @s ~~~
入店-対応待ちの町人の視点から前方1.2ブロック先を中心に1マス以内に別の町人がいたときに足を止めるtagを付与します。コマンドの中で常に最初にstopのtagを削除しているので、前方に町人がいない場合はstopタグが付与されず先ほどの移動処理が適用され、いる場合はこのコマンド群でstopタグが付与され足が止まるという風になっています。
先ほどの入店時の移動処理でtagがstopではない町人を対象にしていたのは、こちらの一時停止処理のコマンドの方で前方に他の客がいる町人に対してstopというtagをつけて足を止めさせたかったからなんですね。
退店時の移動制御
上の鉱石系ブロックがやたら多い画像は町人の帰宅用の誘導路となっています。カウンター正面の対応が終わった町人だけを帰らせればよいわけですから本来ならばステージ全面をカバーするような範囲で誘導路は必要ありませんが後述のバグ対策のために全面に敷いています。
各ブロックごとに割り当てられた方向を重ねてみるとネザライトブロックの場所に誘導していることがわかると思います。ネザライトブロックに到達した時点でその町人は消滅させますが、カウンター視点で見えない位置に来てから処理されるように帰り道のルート設計をしています。
退店時のコマンドは以下の通り
#移動処理(退店時)
execute as @e[scores={customer=2}] at @s if block ~ -46 ~ gold_block run tp @s ~0.2~~ -90 0
execute as @e[scores={customer=2}] at @s if block ~ -46 ~ diamond_block run tp @s ~-0.2~~ 90 0
execute as @e[scores={customer=2}] at @s if block ~ -46 ~ iron_block run tp @s ~~~-0.2 180 0
execute as @e[scores={customer=2}] at @s if block ~ -46 ~ emerald_block run tp @s ~~~0.2 0 0
execute as @e[scores={customer=2}] at @s if block ~ -46 ~ netherite_block run tp @s ~~-1000~
特に入店時のものと変わり映えしませんが、変更した所は町人の現在の状態を表すスコアcustomerが2(カウンターでの対応が終わり退店処理中の状態)になっている町人を対象にしていることと、ルートが入店時と違うため参照するブロックのy座標が異なっていることが挙げられるでしょうか。ネザライトブロックのある位置に到達した時点で奈落に送られて消滅させるというものも追加されています。
帰りの誘導用ブロックの層があまりにも大きく他を全て覆い隠してしまっているのでどのように重なっているか見やすくしてみました。
実際はこのように各層を配置しています
3-1-4.客種決定
客種決定ゾーン
説明が楽な入店、退店の部分から先に説明しましたが、面倒なのはカウンターに並んでどんどんと対応待ちの列ができてからになります。章冒頭のゲーム商会の部分でどのようなお客さんがいるかを説明しましたが、お客さんの特性を決めるのは列に並び始めてから比較的前に来てからです。
列に並ぶ際の画像は先ほども示しましたが図示すると以下の画像の通りです。左の画像が上から見た図、右の画像が横から見た図です。
横から見た方の画像を見て頂けるとわかりますが、客種決定ゾーンとハイライトされているところの下にラピスラズリブロックが置いてあります。このラピスラズリブロックは町人の状態を入店している状態から対応待ち状態に移行する際に使うものとなっています。
手に持つブロックの決定・ブロックの配布
#店正面に並んだ時(アイテム決定(ダミー含む))
execute as @e[scores={customer=0}] at @s if block ~ -50 ~ lapis_block run scoreboard players random @s button 1 6
scoreboard players set @e[scores={customer=0,button=1}] button1 1
scoreboard players set @e[scores={customer=0,button=2}] button2 1
scoreboard players set @e[scores={customer=0,button=3}] button3 1
scoreboard players set @e[scores={customer=0,button=4}] button4 1
scoreboard players set @e[scores={customer=0,button=5}] button5 1
scoreboard players random @e[scores={customer=0,button=6}] dummy 1 10
tag @e[scores={dummy=1..,customer=0}] add dummy
上記のコマンド群は入店していてまだ対応待ち状態になっていない客(特性が決定されていない客)つまりスコアcustomerが0の客のみに実行されるものになります。町人の立つ位置のy座標-50のところにラピスラズリブロックを検知した時点で間違え客用のダミーブロック含む合計6種類のうちのどのブロックを持つことになるかが等確率でランダムに決定されます。
持つブロックはbuttonというスコアで管理していますが、ランダムに数値を決定したのちにbutton1~6という別のスコアに変換しているのは後に控える正誤判定の際にがこちらの方が都合がよいためです。
また、ダミーブロックに関しては10種類ほどバリエーションがあるためbuttonが6に決定された場合(道案内ボタンが正解の場合)はさらにdummyというスコアを用いて手にもつブロックをランダマイズする必要があります。
#割り振られたスコアに応じてアイテム付与
replaceitem entity @e[scores={customer=0,button1=1}] slot.weapon.mainhand 0 gold_block 1
replaceitem entity @e[scores={customer=0,button2=1}] slot.weapon.mainhand 0 iron_block 1
replaceitem entity @e[scores={customer=0,button3=1}] slot.weapon.mainhand 0 redstone_block 1
replaceitem entity @e[scores={customer=0,button4=1}] slot.weapon.mainhand 0 netherite_block 1
replaceitem entity @e[scores={customer=0,button5=1}] slot.weapon.mainhand 0 lapis_block 1
replaceitem entity @e[scores={customer=0,dummy=1}] slot.weapon.mainhand 0 yellow_concrete 1
replaceitem entity @e[scores={customer=0,dummy=2}] slot.weapon.mainhand 0 white_concrete 1
replaceitem entity @e[scores={customer=0,dummy=3}] slot.weapon.mainhand 0 red_concrete 1
replaceitem entity @e[scores={customer=0,dummy=4}] slot.weapon.mainhand 0 black_concrete 1
replaceitem entity @e[scores={customer=0,dummy=5}] slot.weapon.mainhand 0 blue_concrete 1
replaceitem entity @e[scores={customer=0,dummy=6}] slot.weapon.mainhand 0 honeycomb_block 1
replaceitem entity @e[scores={customer=0,dummy=7}] slot.weapon.mainhand 0 concrete_powder 1
replaceitem entity @e[scores={customer=0,dummy=8}] slot.weapon.mainhand 0 nether_wart_block 1
replaceitem entity @e[scores={customer=0,dummy=9}] slot.weapon.mainhand 0 mud 1
replaceitem entity @e[scores={customer=0,dummy=10}] slot.weapon.mainhand 0 coral_block 1
最初の5種類以外のブロックに関しては全て間違え客が持っている紛らわしいダミーブロック群ですので、それぞれ先ほどのコマンド群で割り当てられたブロックに対応するものを町人の手に持たしていると考えればいいです。
客種決定
手に持たせるブロックが決定した後は客の特性を決定していきます。要は通常客なのかシークレット客のような特殊な客なのか、ということですね。
#客種別決定
scoreboard players random @e[scores={button=1..,customer=0},tag=!dummy] type 1 10
scoreboard players random @e[scores={button=1..,customer=0},tag=dummy] type 1 2
tag @e[scores={type=1..4,customer=0},tag=!dummy] add normal
tag @e[scores={type=5..8,customer=0},tag=!dummy] add reverse
tag @e[scores={type=9,customer=0},tag=!dummy] add secret
tag @e[scores={type=10,customer=0},tag=!dummy] add reverse_secret
tag @e[scores={type=1,customer=0},tag=dummy] add normal
tag @e[scores={type=2,customer=0},tag=dummy] add reverse
scoreboard players set @e[scores={button=1..}] customer 1
スコア名buttonが1以上かつスコア名customerが0の時、つまり先ほどのブロック割り当て処理が行われていてかつ入店状態の客であることを前提に上記のコマンドは実行されます。
スコア名typeを使用して各特性ごとの客が発生する確率を調整します。
スコアの最小値と最大値を1から10に設定したのち、通常客は1-4なので発生確率は40%、リバース客は5-8なので40%、シークレット客は9のみで10%、シークレットリバース客も10のみなので同様に10%という風になっています。これらの確率はスコア上限を増やすことによってもっと細かく設定することができるため、ゲーム難易度に応じて調整していきます。
ここで重要なのは間違え客は手に持っているアイテムが通常の5種類のブロックとは異なるため、手持ちのブロックが変化するシークレット客、シークレットリバース客の特性がつかないように処理していることでしょうか。
ここで客種決定に関する処理は終わったので最後の一文で客の状態を表すスコア名customerの値を0から1に変化させています。
シークレット客、シークレットリバース客の手持ちブロックリール
これらの客の場合は常に手持ちのブロックの変化とともに正解となるボタンも変わりますからその処理を書いていく必要があります。
#ルーレット処理(シークレット客+リバースシークレット客)
scoreboard players add @e[scores={customer=1},tag=!normal,tag=!reverse] sectimer 1
scoreboard players random @e[scores={sectimer=20}] button 1 6
scoreboard players reset @e[scores={customer=1,sectimer=20}] button1
scoreboard players reset @e[scores={customer=1,sectimer=20}] button2
scoreboard players reset @e[scores={customer=1,sectimer=20}] button3
scoreboard players reset @e[scores={customer=1,sectimer=20}] button4
scoreboard players reset @e[scores={customer=1,sectimer=20}] button5
scoreboard players set @e[scores={customer=1,button=1,sectimer=20}] button1 1
scoreboard players set @e[scores={customer=1,button=2,sectimer=20}] button2 1
scoreboard players set @e[scores={customer=1,button=3,sectimer=20}] button3 1
scoreboard players set @e[scores={customer=1,button=4,sectimer=20}] button4 1
scoreboard players set @e[scores={customer=1,button=5,sectimer=20}] button5 1
replaceitem entity @e[scores={customer=1,button1=1,sectimer=20}] slot.weapon.mainhand 0 gold_block 1
replaceitem entity @e[scores={customer=1,button2=1,sectimer=20}] slot.weapon.mainhand 0 iron_block 1
replaceitem entity @e[scores={customer=1,button3=1,sectimer=20}] slot.weapon.mainhand 0 redstone_block 1
replaceitem entity @e[scores={customer=1,button4=1,sectimer=20}] slot.weapon.mainhand 0 netherite_block 1
replaceitem entity @e[scores={customer=1,button5=1,sectimer=20}] slot.weapon.mainhand 0 lapis_block 1
scoreboard players set @e[scores={sectimer=20}] sectimer 1
スコア名sectimerを用いて20tickごと、つまり1秒ごとにbuttonの数値をまた1から6までランダムに上書きする形で再設定します。残りの処理は全体のブロック割り当ての時とほとんど同じですが、正誤判定用のスコアbutton1~6に関しては複数のスコアを使っている関係上、上書きが面倒なので初期化してから同様の処理を施しています。
3-1-5.正誤判定
最前列判定
正誤判定をするにあたってまず前提となるのは、どの町人が一番先頭にいるかを判別することです。町人は列を作って並んでいるわけですから、順番をぐちゃぐちゃにしてはいけません。
#最前列のタグ管理
execute as @a[tag=admin] positioned -13 -60 17 unless entity @e[scores={customer=1},tag=front] run tag @e[type=villager,scores={customer=1},r=3,c=1] add front
ここで出てくるfrontというtagは最前列であることを表すものです。
tagがadminであるプレイヤー(ゲーム参加者)がカウンターの中心の座標にいるとみなした上で、スコアcustomerが1(対応待ちの状態)かつfrontというtagを持つ町人がいない場合、新たに対応待ち状態の町人かつ3マス以内の一番近い町人にfrontというtagを付与しています。
町人が入店する時のカウンターへの列の作り方を一直線にしているためカウンターの中心座標から近い順に選べば最前列の判定は問題なく行えます。
ボタン押し検知
連打できるようにするためにボタンを押した直後にボタンを置きなおすコマンドとボタンを押した直後に正誤判定を行うコマンドはセットで用意します。
#ボタン押し検知
execute as @a[tag=admin] at @s unless blocks -10.42 -60.00 8.53 -10.42 -60.00 8.53 -10.42 -59.00 16.39 all run scoreboard players set @s button1 1
execute as @a[tag=admin] at @s unless blocks -10.42 -60.00 8.53 -10.42 -60.00 8.53 -10.42 -59.00 16.39 all run clone -10.42 -60.00 8.53 -10.42 -60.00 8.53 -10.42 -59.00 16.39
execute as @a[tag=admin] at @s unless blocks -11.46 -60.00 8.61 -11.46 -60.00 8.61 -11.51 -59.00 16.45 all run scoreboard players set @s button2 1
execute as @a[tag=admin] at @s unless blocks -11.46 -60.00 8.61 -11.46 -60.00 8.61 -11.51 -59.00 16.45 all run clone -11.46 -60.00 8.61 -11.46 -60.00 8.61 -11.51 -59.00 16.4
execute as @a[tag=admin] at @s unless blocks -12.49 -60.00 8.58 -12.49 -60.00 8.58 -12.50 -59.00 16.47 all run scoreboard players set @s button3 1
execute as @a[tag=admin] at @s unless blocks -12.49 -60.00 8.58 -12.49 -60.00 8.58 -12.50 -59.00 16.47 all run clone -12.49 -60.00 8.58 -12.49 -60.00 8.58 -12.50 -59.00 16.47
execute as @a[tag=admin] at @s unless blocks -13.44 -60.00 8.51 -13.44 -60.00 8.51 -13.52 -59.00 16.48 all run scoreboard players set @s button4 1
execute as @a[tag=admin] at @s unless blocks -13.44 -60.00 8.51 -13.44 -60.00 8.51 -13.52 -59.00 16.48 all run clone -13.44 -60.00 8.51 -13.44 -60.00 8.51 -13.52 -59.00 16.48
execute as @a[tag=admin] at @s unless blocks -14.50 -60.00 8.50 -14.50 -60.00 8.50 -14.45 -59.00 16.47 all run scoreboard players set @s button5 1
execute as @a[tag=admin] at @s unless blocks -14.50 -60.00 8.50 -14.50 -60.00 8.50 -14.45 -59.00 16.47 all run clone -14.50 -60.00 8.50 -14.50 -60.00 8.50 -14.45 -59.00 16.47
execute as @a[tag=admin] at @s unless blocks -8.30 -60.00 8.42 -8.30 -60.00 8.42 -9.43 -59.00 14.49 all run scoreboard players set @s button6 1
execute as @a[tag=admin] at @s unless blocks -8.30 -60.00 8.42 -8.30 -60.00 8.42 -9.43 -59.00 14.49 all run clone -8.30 -60.00 8.42 -8.30 -60.00 8.42 -9.43 -59.00 14.49
コマンド内で指定されている座標は各ボタンに対応する位置の座標になっています。
一つのボタンにつき二行の処理を書いていますが一行目はボタンNを押したという正誤判定のためのスコアbuttonNの数値を1に設定し、二行目ではそのボタンNを別の場所から複製して元の押されていない状態に即座に戻すといったものです。
また、ボタンを連打した際に複数のボタンを一度に押したという風になってしまうと正誤判定がバグるので、どのボタンを押したかという情報は1tick以内で初期化しなければなりません。コマンド内容は以下の通り。
#ボタン検知リセット
scoreboard players reset @a[tag=admin] button1
scoreboard players reset @a[tag=admin] button2
scoreboard players reset @a[tag=admin] button3
scoreboard players reset @a[tag=admin] button4
scoreboard players reset @a[tag=admin] button5
scoreboard players reset @a[tag=admin] button6
プレイヤーから各ボタンを押したという判定のスコアをリセットしているだけですね。
正誤判定(通常客+シークレット客)
execute as @a[tag=admin,scores={button1=1}] at @s if score @s button1 = @e[tag=front,tag=!dummy,tag=!reverse,tag=!reverse_secret] button1 run function quick_push/p_scoring
execute as @a[tag=admin,scores={button1=1}] at @s if score @s button1 = @e[tag=front,tag=!dummy,tag=!reverse,tag=!reverse_secret] button2 run function quick_push/m_scoring
execute as @a[tag=admin,scores={button1=1}] at @s if score @s button1 = @e[tag=front,tag=!dummy,tag=!reverse,tag=!reverse_secret] button3 run function quick_push/m_scoring
execute as @a[tag=admin,scores={button1=1}] at @s if score @s button1 = @e[tag=front,tag=!dummy,tag=!reverse,tag=!reverse_secret] button4 run function quick_push/m_scoring
execute as @a[tag=admin,scores={button1=1}] at @s if score @s button1 = @e[tag=front,tag=!dummy,tag=!reverse,tag=!reverse_secret] button5 run function quick_push/m_scoring
execute as @a[tag=admin,scores={button1=1}] at @s if score @s button1 = @e[tag=front,tag=!dummy,tag=!reverse,tag=!reverse_secret] button6 run function quick_push/m_scoring
ミニゲーム参加者がbuttonNを押したとき、最前列のお客さんのbuttonNのスコアと参加者のbuttonNのスコアを比較して会っていたら正解、間違っていたら不正解の時の処理を実行しています。上記のコマンドはbutton1を押したときのみの行数なので実際はこの6倍長いです。というかこれ間違えている時の処理はunlessでやればいいので各ボタンごとに処理は2行で済みますね..
buttonNというスコアに変換する必要もなく、buttonというスコアだけで全然いける気がしてきました。
一行の最後でfunctionコマンドを使用していますが、ここではfunctionで別にp_scoring、q_scoringというものを用意しておきます。前者は選択があっていた時にスコアを加算するコマンド群で後者は選択が間違いだった際にスコアを減算するコマンド群になります。
正誤判定(リバース客+シークレットリバース客)
#正誤処理(リバース客+シークレットリバース客)
execute as @a[tag=admin] if entity @e[tag=front,tag=reverse] run function quick_push/detect/reverse
execute as @a[tag=admin] if entity @e[tag=front,tag=reverse_secret] run function quick_push/detect/reverse
ここから先のコマンド群は当時の私がおそらく1functionコマンドの中の行数を減らそうと検知と正誤判定を別々に分けていたようです。コマンドを上下で分けているのはそれぞれfunctionコマンドのファイルが違うからですね。
上のコマンドはリバース、またはシークレットリバース客がいた場合に下記のコマンド群が記してあるfunctionコマンドを実行して正誤処理を行うというものです。
#正誤処理(リバース客+シークレットリバース客)
execute as @a[tag=admin,scores={button1=1}] at @s if score @s button1 = @e[tag=front,tag=!normal,tag=!dummy] button1 run function quick_push/m_scoring
execute as @a[tag=admin,scores={button1=1}] at @s if score @s button1 = @e[tag=front,tag=!normal,tag=!dummy] button2 run function quick_push/p_scoring
execute as @a[tag=admin,scores={button1=1}] at @s if score @s button1 = @e[tag=front,tag=!normal,tag=!dummy] button3 run function quick_push/p_scoring
execute as @a[tag=admin,scores={button1=1}] at @s if score @s button1 = @e[tag=front,tag=!normal,tag=!dummy] button4 run function quick_push/p_scoring
execute as @a[tag=admin,scores={button1=1}] at @s if score @s button1 = @e[tag=front,tag=!normal,tag=!dummy] button5 run function quick_push/p_scoring
execute as @a[tag=admin,scores={button1=1}] at @s if score @s button1 = @e[tag=front,tag=!normal,tag=!dummy] button6 run function quick_push/m_scoring
正誤判定の仕組みは通常客の時と同じでコマンドのうち一部を抜粋している形ですが、リバース系の客は持っているものと異なるブロックに対応するボタンが正解となるので、どのボタンを押したかというスコアが異なっている場合に正解の処理をする点で違っています。違うファイルに分けていることもですが、正誤判定においても行数は大幅に削減できるので現時点では無駄が多すぎるといった印象でしょうか。本実装までに最適化しなければなりませんね。
正誤判定(間違え客)
#正誤処理(間違え客)
execute as @a[tag=admin] if entity @e[tag=front,tag=dummy] run function quick_push/detect/dummy
#正誤処理(間違え客)
execute as @e[tag=front,tag=dummy] if entity @a[tag=admin,scores={button6=1}] run function quick_push/p_scoring
execute as @e[tag=front,tag=dummy] if entity @a[tag=admin,scores={button1=1}] run function quick_push/m_scoring
execute as @e[tag=front,tag=dummy] if entity @a[tag=admin,scores={button2=1}] run function quick_push/m_scoring
execute as @e[tag=front,tag=dummy] if entity @a[tag=admin,scores={button3=1}] run function quick_push/m_scoring
execute as @e[tag=front,tag=dummy] if entity @a[tag=admin,scores={button4=1}] run function quick_push/m_scoring
execute as @e[tag=front,tag=dummy] if entity @a[tag=admin,scores={button5=1}] run function quick_push/m_scoring
間違え客に関する正誤処理は上のコマンド群で全文になります。道案内ボタンを押したときのみ正解、それ以外のボタンは無条件で間違いになるというだけなので比較的行数が少なくて済みました。
3-1-6.既知のバグおよび対応策
現在発覚している問題として挙げられるのは町人同士で列を作って並んでいる際にスタックしてしまうことがあるということです。町人の移動制御のところで一時停止機能について説明しましたが、これが悪さをしているんです。
↓再掲↓
#一時停止処理(前方に先客がいた場合)
tag @e[scores={customer=0..1}] remove stop
execute as @e[scores={customer=0..1}] at @s positioned ^^^1.2 if entity @e[scores={customer=0..1},r=1] run tag @s add stop
execute as @e[scores={customer=0..1},tag=stop] at @s run tp @s ~~~
町人の視点の前方に先客がいた場合、その場で止まるというものですが下記画像の青くハイライトしている部分のように二方向以上から町人が合流する場所でスタックするという問題があります。
テストプレイ時に町人同士の距離が近すぎるとこのバグが頻発したため、何度か試した末に視点の先1.2ブロックという微妙な距離間で前方に客がいるかどうかの検知するようにしました。
現在このバグが頻発することはないですが発生確率は低くはなく、何よりも致命的なのはこのバグが起こるとカウンター前の客種決定ゾーンに町人がアクセスできなくなりゲームが進行不能になってしまうことです。
移動制御のコマンドを改善して根本的になくす方法もあるのでしょうが、現段階ではバグが起きても進行できるような対処をしています。具体的には町人が店に入ってから一定時間動かないようであれば一連の処理が終わったとみなして強制的に退店させるというものです。
scoreboard players add @e[scores={customer=0}] time 1
scoreboard players set @e[scores={time=200..}] customer 2
内容は至極単純で、入店状態の町人にtimerというスコアを付与しつづけ、スコアが200になった時点、つまり10秒後にcustomerというスコアを強制的に対応済み状態を表す数値2に変えてしまうというものです。
3-1-7.配布ワールドへの移植
次回は配布ワールドへの移植について書いていきたいと思います。
ブログの更新頻度は執筆内容が現在の実際の進捗状況に追いつくまで時間がある限り毎日です。需要があるかはわかりませんが乞うご期待。
(思ったのですが毎日投稿するって大変ですね!)
次回に続く 最終更新(2023/11/8)
コメント
めっちゃ面白そうですね!
楽しみです!
更新されてない…いつかリリースされるのを楽しみに待っています!