Aokashi Room

作った作品の紹介やレビュー、トラブルシューティングとか色々

「謎めいた機械を追い求めて 闇組織の争奪」の技術の話

この記事は WWA Advent Calendar 2024 の5日目です。

こんにちは。 WWA FanSquare の中の人・・・でもあるんですが、今回はコンテスト運営者ではなくコンテスト応募作品の制作者の身として、応募作品「謎めいた機械を追い求めて 闇組織の争奪」に関する話をブログ記事にしようかなと思います。

www.aokashi.net

話すことが多すぎるので、「技術の話」と「こだわりの話」に分けて展開します。今回ご紹介する「技術の話」は WWA Script とピクチャ機能によってできたことやシステムの仕組みなどを列挙し、 WWA ゲーム制作の可能性の高さを広めたらいいな~と思います。

はじめに

  • このゲームのネタバレ要素はなるべく取り除いた状態でお届けします
  • この記事の趣旨は「WWA Script やピクチャ機能で、こういうことができました!」を紹介することです
    • 紹介する仕組みの WWA Script やピクチャ機能のソースコード(メッセージテキスト)は全部掲載しておらず、そのままコピペしても単体で動ける状態にはなっていません
    • 要所要所だけ、関係ない処理を省いた形での掲載になるのでご了承ください
  • 本作を遊んでいて、かつ WWA Script やピクチャ機能の基本を理解している人をターゲットとしています。「この処理何しているんだ?」と思った場合は、本作を遊んでみると分かるかもしれません
  • 本作で使用している WWA Wing のバージョンは v4.1.0-unstable.based-on.3.12.11.p.28 です

WWA Script でこういうことができた!

まずこのゲームの基本システムは、2022年末に公開したWWAゲーム「ヌル岬の戦い」をベースとしています。

aokashi.hatenablog.jp

本作では

  • ザコ敵はオートバトル、ボスはコマンドバトル
  • 特徴の異なるプレイヤーの複数人行動
  • 技を習得してボスバトルに活用

と言った要素を取り入れて、自分流「WWARPG」を打ち出したと思ってますが、実はこれらの要素はヌル岬の戦いが先行して実装されたのでした。

なので、ここで記載しているほとんどのことは、 WWA Script がなくても、変数機能があれば実現できるんですが、 WWA Script があればさらに複雑なことがより分かりやすく実装したり、パーツ数を削減したりできるということで紹介します。

また、去年末に追加された WWA Wing の新機能「名前付きユーザー定義変数」は現段階の WWA Wing では WWA Script でないと使用することができません。なので、名前付きユーザー定義変数を活用している箇所についても触れたいと思います。

wwawing.notion.site

ボスバトル(コマンドバトル)

本作のシステム紹介でこれを語らないわけには行かないでしょう。複雑な四則演算、約20個以上の変数活用など・・・ WWA Script がなかったらあれほど機能の多いバトルは作れなかったと思います。

まずボスバトルの構造を知るところから始めましょう。左から「選択画面」「道具の選択画面」「バトルエフェクト用の画面」の3つのみで構成されています。意外とシンプル!? 「バトルエフェクト用の画面」では、 $move マクロでプレイヤーやモンスターを動かす必要があるため、バトルフィールドの上下の隣(青い四角のあるところ)に移動して演出をしています。

そしてボスバトルの流れは以下の通りに流れています。

上から「プレイヤーのターン」「モンスターのターン」「次のモンスターの交代判定」の順番で、それぞれのパーツの実行順番を図にしています。

プレイヤーのターンについて、上の図の各アイコンを左から順に説明します。

  1. プレイヤーが行動する内容の定義を変数に設定します。
  2. プレイヤーのターンについては最初にプレイヤーの状態(眠り状態になってないか、攻撃可能か)を判定します。
  3. 攻撃できればバトルエフェクト用の画面に移動、そこから各攻撃が定めるパーツを実行してプレイヤーを動かします。
    • 攻撃できなければこれらの処理はスキップされます。
  4. 以下の処理がまとめて実行されます。
    • プレイヤーが持つ技パワーや食事パワーの残り効果を消費します。
    • 1. によってモンスターの生命力がなくなったか確認。なくなったのであればバトル勝利扱いとし、以下2点を無視して 5. 以降に進みます。
    • プレイヤーに状態異常が抱えている場合は状態異常に応じてHPを減らします。
    • ヤツロウなどのパートナーキャラがいる場合は、パートナーキャラの攻撃を実行します。
  5. モンスターが負けた場合のメッセージです。
  6. プレイヤーの攻撃力や防御力が上がります。
  7. ボスバトルを終了し、元の座標に戻ります。
    • 開始時とは異なる座標にしたい場合は事前に変数を書き換えています。

モンスターのターンも同様の処理を行っていますが、モンスターの攻撃においては攻撃の演出が終わってからプレイヤーのHPを差し引いてます。演出の途中で差し引くと、負けた場合のリカバリが効かなくなるためです*1

プレイヤーの状態判定や、プレイヤーの攻撃処理、プレイヤー攻撃後の処理に関しては WWA Script のユーザー定義関数で一元管理しています。攻撃処理においては各モンスターの技によって攻撃が無力化されることもあるため、ユーザー定義関数にしておくと「こういう条件で攻撃が効かなくなるのか~」と分かるようになってて役立ちました。

/**
 * 自分から相手に攻撃します。
 * 
 * # 引数
 * - 101: 自分の攻撃量 (通常は AT - v[80] のままでOK)
 * - 102: 自分の攻撃種類 (0 で物理, 1 で特殊)
 * - 103: 自分の攻撃の頭脳限界値 (BPの値によって変わる)
 * - その他、 {@link startPlayerTurn} で充てた変数も使用されます
 */
function attackToEnemy() {
  v["battle_player_damage"] = 0;
  if (v[102] <= 0) {
    v["battle_player_attack_type"] = "physical";
  } else {
    v["battle_player_attack_type"] = "special";
  }
  if (v[140] == 2298 && v[181] > 0 && v["battle_player_attack_type"] == "physical") {
    // 実際はモンスターの名前で表示されますが、今回はネタバレを配慮してぼかしています
    MSG("特定のモンスターの技によって物理攻撃が無力化された!");
  } else if (v[140] == 2369 && v[181] > 0 && v["battle_player_attack_type"] == "special") {
    MSG("特定のモンスターの技によって特殊攻撃が無効化された!");
  } else {
    // バフデバフがある場合は、その分も計算
    v[201] = v[101] * (1 + (v[153] * 0.15));
    v[202] = v[144] * (1 + (v[156] * 0.15));
    v[203] = v[201] - v[202];
    if (v[203] < 0) {
      v[203] = 0;
    }
    if (v[102] <= 0) {
      v[203] = (v[203] * v[158]) / 100;
    } else {
      v[203] = (v[203] * v[159]) / 100;
    }
    if (v[103] > 0) {
      v["br_amount"] = v[203];
      v["br_cost"] = v["battle_player_bp"];
      v["br_limit"] = v[103];
      v[203] = getBrainPowerRate();
    }
    v["battle_player_damage"] = v[203];
    v[142] -= v[203];
    PICTURE(111, {
      pos: [360, 145],
      time: 1000,
      text: v[203] > 0 ? `${v[203]}` : "0",
      color: [255, 51, 51],
      bold: 1,
      textAlign: "center",
    });
  }
}

次のモンスターの交代判定については、バトルエフェクト用の画面で実行されます。交代すると交代画面、交代しないと選択画面に移動します。

参考程度に、ヌル岬の戦いでも攻撃処理を定義していましたが、ユーザー定義関数はないので、パーツをプレイヤーに置かせて実行する形式で共通化をしていました。

各変数の役割はここでは説明しないものの、 WWA Script がないと複雑な四則演算のために一時変数を使いまくるので分かりにくかったのです。

$set=(v[71] = v[51] * v[36])
$set=(v[71] /= 100)
$if=(v[2] == 1)
$set=(v[70] = HP * v[71])
$set=(v[70] /= HPMAX)
$set=(v[74] = 3 * v[71])
$set=(v[74] /= 2)
$set=(v[71] = v[74] - v[70])
$endif
$set=(v[77] = v[32] * v[42])
$set=(v[77] /= 100)
$if=(v[71] > v[77])
$set=(v[71] -= v[77])
$set=(v[30] -= v[71])
$endif
$var_map=50,+0,+0
<c>
50:次に実行するパーツ
51:自分の攻撃ダメージ

正直ボスバトルに関しては構造を色々と説明するとまた記事1つぐらい書ける分量になるので、ここで止めることにします。

フィールドの切り替え

たとえばプロローグ後にホウザン町を訪れるとふれあいプラザの建物が崩壊してしまいます。

崩壊状態にするには、壊れた背景パーツをその場所に、ひび割れの物体パーツをあの場所に・・・とするのはどこに何を置いたのかイメージができず、なかなか大変な作業です。

壊れた状態を別の場所に配置して、 WWA Script でコピーすると簡単に実現できます!

オレンジ枠の範囲を水色枠にコピーしたい

<script>
// 横幅21マス
for (i = 0; i < 21; i++) {
  // 縦幅11マス
  for (j = 0; j < 11; j++) {
    // フィールド上の物体パーツと背景パーツを、破壊後の状態からそれぞれコピー
    o[230 + i][290 + j] = o[220 + i][250 + j];
    m[230 + i][290 + j] = m[220 + i][250 + j];
  }
}

預かり屋

買い替えで用なしになった武具たちを控える場所でもある

本作の預かり屋「エバーエクスプレス」では、アイテムを預けたり引き出したりできます。預けているアイテムは選択画面に配置されています。

任意の場所に任意のパーツを置く仕組みは WWA の初期からありましたし、 WWA のサンプルでも青い鍵を預ける預かり屋のシステムも昔から確立されていました。

しかしこれは預けられるアイテムが青い鍵固定。本作ではどんなアイテムでも預けられるようにしたい! のですが、従来の WWA ではできませんでした。

WWA Script であれば、手持ちのアイテムボックスを読み取って、そのアイテムを任意の場所に置けるようになれます*2

// v[101] と v[102] は預けているアイテム一覧で空き枠を選んだ時のプレイヤー座標 (それぞれ X, Y )
// v[103] は預けたいアイテムのアイテムボックス番号
if (v[103] > 0) {
  o[v[101]][v[102] - 2] = o[PX - 1][PY - 1];
  // 351 は「アイテムがあります。引き出しますか?」の二者択一パーツ
  o[v[101]][v[102] - 1] = 351;
  v["item_id"] = ITEM[v[103]];
  // 預けたアイテムは手元にないので消しておく
  if (isKeyItem()) {
    v["key_type"] = getKeyType();
    pickKey();
  } else {
    ITEM[v[103]] = 0;
  }
  SOUND(16);
}

ちなみにアイテムを引き出す場合は、置いてあるアイテムをそのまま拾えば済むので、細かい点を気にしなければ WWA Script に頼らなくても作れるかもです。

鍵を1つのアイテムにまとめて所持させる

ダンジョンで使用される鍵は複数所持される機会が多く、従来通りに実装するとアイテムボックスが鍵まみれで圧迫します。2個以上持っている場合は1つのアイテムに集約しています。これを実現するためには、以下の工夫をしています。

  • 配置する鍵パーツ
    • 「アイテム」ではなく「ステータス変化」パーツにする
    • 対応するアイテムパーツ(後述)があれば所持個数を加算する。なければアイテムパーツをアイテムボックスに加える
  • アイテムパーツ
    • アイテムの使用種類は「使用してもなくならない」とする
    • メッセージでは所持している個数が分かるようにする
  • 扉パーツ
    • 「扉」パーツのままとする
    • 扉の種類は「鍵なくならない」とする
    • 扉解放後のメッセージに鍵の所持数を減らす処理を実装する

複数のアイテムをまとめる仕組みについては他のダンジョンWWAでも同様の実装をしているので、誰かがいい感じに説明してくれると期待しながらこの程度に留めますが、この仕組みで厄介なのは、物語道中で発生する合流イベントです。

ジュニアーとマサトが合流する際、ジュニアーとマサトがお互い黄色い鍵だけを持っているならまだいいんですが、ジュニアーが回復アイテムで満杯で鍵を受け取る余地がない、マサトは鍵が大量にある・・・という場面だと困ります。エバーエクスプレス(預かり屋)に転送するにしても、鍵の所持個数の上限が理論上無限のため、転送処理を作るのはなかなか骨の折れる作業です。

そこでこれ以上鍵が持てない場合は、ニイズ駅の係員にお願いして、後で鍵を受け取れるようにしました。

// 合流する際または後で駅員から鍵を受け取りに行った際に実行。黄色い鍵の所持個数を加えます
if (HAS_ITEM(533)) {
  v["key_yellow_count"] += v["key_yellow_count_temp"];
  v["key_yellow_count_temp"] = 0;
} else if (v["key_yellow_count_temp"] > 0) {
  if (isItemFull()) {
    if (ID == 2753) {
      // 駅員から受け取った場合
      MSG("アイテムがいっぱいで黄色い鍵が持てませんね・・・、またよろしくお願いします!");
    } else {
      MSG("マサト\n「黄色い鍵が持てないじゃないか・・・ニイズ駅の係員にお願いしたから、後で受け取ろうか。」");
      o[392][37] = 2753;
    }
  } else {
    v["key_yellow_count"] = v["key_yellow_count_temp"];
    v["key_yellow_count_temp"] = 0;
    ITEM[0] = 533;
  }
}

アイテムボックスを使用しないで武器や防具を持たせる

本作はアイテムボックスの節約のために武器アイテムや防具アイテムを設けていません。従来の WWA であれば、アイテムに攻撃力や防御力を持たせることで武器や防具を実現していましたが、本作ではどうするか? そう、 WWA Script の出番です。

まずは武具を装備する処理を見てみましょう。以下は武具の装備で、今のプレイヤーがマサトだった場合にのみ起こる箇所だけを切り取ったソースコードです。

if (v[102] % 2 == 0) {
  // バッグメニュー画面にある装備アイコンを更新
  o[72 + v[101]][4] = v[103];
  o[62 + v[101]][1] = v[103];
  if (v[101] == 1) {
    // 手持ちのアイテムを引きずりおろす
    ITEM[0] = v["masato_armor_id"];
    v["masato_armor_id"] = ID;
    v["masato_armor_df"] = v[105];
  }
  updatePlayerStatus();
  SOUND(16);
  MSG(v["weapon_name"] + "を装備した!");
} else {
  MSG("マサト\n「このアイテムは自分には装備できないかな・・・」");
  ITEM[0] = ID;
}

名前付きユーザー定義変数 v["masato_armor_id"] に装備する武具のパーツ番号を、 v["masato_armor_df"] に武具の防御力を設定しています*3。その後の updatePlayerStatus(); 関数で今の装備状況を基に各プレイヤーのステータスを更新しています。

武具はお金をかけることで強化ができます。誰がどの武具を強化したかの管理は名前付きユーザー定義変数で管理しています。例えばマサトのダッシュシューズの強化状況は v["masato_164_lv"] で管理しています。

以下は前述で説明した updatePlayerStatus(); 関数でマサトの防御力を更新している箇所だけを切り取ったソースコードです。

// モンスターに勝った時の成長や武器の強化など、プレイヤーの攻撃力や防御力が変わった場合に実行される
// 装備レベルが設定されていない場合は 1 とする
if (v["masato_" + v["masato_armor_id"] + "_lv"] == "") {
  v["masato_" + v["masato_armor_id"] + "_lv"] = 1;
}
// v[23] はマサトの防御力。今いるプレイヤーがマサトであれば、 v[23] を防御力に割り当てる。
v[23] = ((v["masato_weapon_df"] + (v["masato_armor_df"] * v["masato_" + v["masato_armor_id"] + "_lv"])) / 2) + (v["masato_base_df"] / 100.0);

課題としては強化状況の表示と強化の可否の選択(この武器を 1234G で強化しますか? と表示されているところ)が別々に表示されていることでしょうか。これは WWA の「物を売る」や「物を買う」、「二者択一」パーツが名前付きユーザー定義変数から表示できないことが原因です。

  • Q. 最近になって追加されたオブジェクト変数や配列変数は使用しなかったのか?
      1. 使用しても良かったんですが、まだバグが多いことを見越して使用しませんでした。

状態異常

本作にはやけど状態や毒状態などの状態異常があり、先ほど挙げたやけど状態と毒状態には歩くとダメージを受けます。

WWA Script にはある行動を起こすたびに実行されるイベント関数を設定することが可能で、プレイヤーが1歩動くと実行される CALL_MOVE 関数で、一定歩数になるとダメージを受けるようにしています。

function CALL_MOVE() {
  // ボスバトルやメニュー選択の場合は除外
  if (v[1] == 0) {
    // 特定のイベント中など、この場でデスルーラを起こすとイベントが狂う場合は v["damage_on_walk"] を false に設定
    if (v["damage_on_walk"] == true) {
      for (i = 0; i < 3; i++) {
        if (STEP % 5 == 0) {
          v[111] = i;
          v[112] = 0;
          damageBySick();
        }
      }
    }
  }
}

総歩数が5で割った余りが0ならダメージを受けるという事なので、5歩に1回ダメージを受けることになります。 4歩に1回バッグメニューを開いてメニューの中で1回歩けば状態異常のダメージが回避できてしまいますが、そこについては触れてはいけない

以下は状態異常でダメージを受ける damageBySick(); 関数の中で、やけど状態だった場合に起こる箇所だけを切り取ったソースコードです。やけど状態には歩くたびにかかるダメージ量を変数 (v[37 + (20 * v[111])]) に設定していて、歩くたびに減って、 0 になったら収まるという設定です。

// v[111] は状態異常を受けているプレイヤーの番号
if (v[37 + (20 * v[111])] <= 0) {
  if (v[111] == v[2] && v[1] == 2) {
    MSG(v["my_name"] + "のやけどがおさまった!");
  }
  // 処理が類似しているので、完治処理ではあるが beSick を実行
  v[101] = 2;
  beSick();
} else {
  if (v[111] == v[2]) {
    if (v[1] == 2) {
      MSG(v["my_name"] + "はやけどのダメージを受けている。");
    }
    v[211] = v[37 + (20 * v[111])];
    HP -= v[211];
  } else {
    v[21 + (20 * v[111])] -= v[37 + (20 * v[111])];
    v[101] = v[111];
    // ここでプレイヤーのHPが残っているか確認。HPが0以下の場合は倒れる
    checkPlayerAlive();
  }
  v[37 + (20 * v[111])] -= 5;
}

なお、現在の WWA Wing (不安定版) では CALL_MOVE 時にゲームオーバーによる移動を起こすと、映しているマップ画面と実際のプレイヤー位置が一致しないバグが発生します。こうした事象を解消するために、ゲームオーバーになった場合はピクチャ機能を使用してゲームオーバー座標への移動を再度遅延実行させています。 今後のバージョンアップで解消される場合がありますが、同じような仕組みを作る方はご注意ください。

function CALL_GAMEOVER() {
  PICTURE(-1, {
    timeFrame: 1,
    script: "afterGameOverJump"
  });
  // 一瞬プレイヤーが表示されてしまうので隠す
  DEL_PLAYER(1);
}

function afterGameOverJump() {
  JUMPGATE(GET_GAMEOVER_POS_X(), GET_GAMEOVER_POS_Y());
  // ゲームオーバー時に実行する処理を実装する
}

一部フィールド技

マサトのフィールド技「ピッキング」は、任意の鍵をBPでこじ開ける技となっています。

公開前情報に掲載した画像をそのまま使用

今フィールド上にいる座標から上下左右のパーツを読み取って、対応している扉であれば消費BPを提示。承諾すれば実行となっています。実行自体はその扉を消すだけです。

こうしたフィールド上を特殊な技で攻略させる手法はアルクスさんの「スキルファイター」などありますが、こうした作品は背景パーツがシンプルなら比較的自由に技の種類を増やせると思います。

plicy.net

しかしながら本作は背景パーツのバリエーションが恐ろしいほど多く、技の内容次第では街や国の破壊につながってしまうため、その辺は慎重でした。

同じフィールド技としてスモークがあります。あまり詳細に話しませんが、ピッキング以上に WWA Script を活用していると思います。

ピクチャ機能でこういうことができた!

現時点でピクチャ機能を最大限に活用しているのは本作が唯一ではないでしょうか。これから説明する事例を参考に、 WWA 制作の可能性を広げてもらえればと思います。

wwawing.notion.site

ピクチャ機能については WWA Wing に付属しているピクチャ機能サンプルマップを一通り見てからこの記事を読むと、少しは分かるかもしれません。

wwawing.com

HPやBPのゲージ表示

バッグ画面やボスバトル画面では、プレイヤーのHPとBP、ボスモンスターの生命力がゲージで表示されます。

今まで WWA においてゲージ表示とはできなくは無かったんですが、きめ細かく表示することはできませんでした。

ヒラリラーさんのゲージ画像素材。画像の用意はできても WWA 本体が・・・

hirarira.net

ピクチャ機能が来たことで、ピクチャのサイズをピクセル単位で指定できるようになりました。という事はHPとHP上限とで割合を求めれば、ゲージの表示ができる。数値で表示するよりも分かりやすく、作らない理由はない! という事で作りました。

画像素材は新しく作り、伸ばしても縮んでも違和感がないようにしました。

docs.aokashi.net

例えばボスバトルのプレイヤーのHPゲージは以下のコードで表示されています。

// HPゲージの外枠は背景パーツで配置してください。
PICTURE(103, {
  // 4 はゲージの外枠部分
  pos: [320 + 4, v[201]],
  // 72 = 80 - (4 * 2)
  // このまま割り算をすると切り捨てで 100% か 0% かどっちかしか算出しないため、一度倍にして計算する
  size: [(72 * ((v[142] * 72) / v[141]) / 72), 40],
  img: [3, 172],
});

街やフィールドの名前

街を出て道に入った場合や、逆に街に入った場合に街や道の名前が数秒間表示されます。ピクチャ機能ができて最初に作りたかったのはこれでした。

開発者用資料の一部を抜粋。洲入りの洞窟は今でいうサカの洞窟です。

行っていることは単純で、枠の左と中、右のピクチャと、街の名前のピクチャを表示させています。再度同じ名前を表示させないために、現在と前で街やフィールドの名前を管理しています。

function showAreaNamePicture() {
  v[201] = (PY % 10);
  // プレイヤーが画面端にいる場合は、どの画面にいるのか分からないため、進行するプレイヤーの向きに応じて適切な位置に表示する
  v[202] = v[201] > 5 || (v[201] == 0 && PDIR == 8) ? 80 : 320;
  if (v["current_area"] != v["prev_current_area"]) {
    PICTURE(71, {
      pos: [120, v[202]],
      img: [7, 134],
      time: 2000,
    });
    PICTURE(72, {
      pos: [160, v[202]],
      img: [8, 134],
      time: 2000,
      repeat: [3, 1]
    });
    PICTURE(73, {
      pos: [280, v[202]],
      img: [9, 134],
      time: 2000,
    });
    PICTURE(74, {
      pos: [220, v[202] + 12],
      time: 2000,
      text: v["current_area"],
      textAlign: "center",
      color: [0, 0, 153],
      bold: 1,
    });
    v["prev_current_area"] = v["current_area"];
  }
}

イベント演出

本作の魅力要素の一つ。ゲーム序盤でミサイルで吹き飛ぶ二人や、マップが下から上にスクロールして現れるタイトルロゴも、ピクチャ機能なしでは実現できなかったと思います。

両者併せて以下のパーツで構成されています。

  1. 吹き飛ぶ二人のピクチャ(後述)を呼び出す。ピクチャで描画が代用されるので、今まであった二人の物体パーツは削除する。
  2. ケダキを表示するピクチャ。左上に飛ばして、下方向に加速を付けている。一定時間経つと次の次のパーツをプレイヤー座標に配置する。
  3. マサトを表示するピクチャ。右上に飛ばして、下方向に加速を付けている。
  4. メッセージを表示
  5. しばらく待ち時間を設けて、マップをスクロールするピクチャを呼び出す。
  6. 画面がスクロールするピクチャ。縦31マス、横11マスのマップの下 1/3 だけを表示させて、上に動かす。

2. のピクチャの定義は以下の通りとなっています。左上に飛びつつも、加速運動によって下方向に引っ張られる動きが定義で分かると思います。

{
  pos: [160, 200],
  timeFrame: 100,
  move: [-5, -10],
  accel: [0, 0.5],
}

マップをスクロールするピクチャの定義は・・・そうだった、WWA Script で共通化しているんでした。大きいマップの一部だけを表示させ、ピクチャそのものを動かすことでスクロールの動きを実現しています。

<script>
v[101] = 0;
v[102] = -20;
v[103] = 30;
v[104] = 60;
v[105] = 2;
scrollScreen();
PICTURE(1, {
  timeFrame: v[103] + v[208],
  waitFrame: v[103] + v[208],
  map: [1551, PX, PY, 1]
});
/**
 * 今の画面をスクロールさせます。
 *
 * # 引数
 * - 101: スクロールしたいマス (X座標)
 * - 102: スクロールしたいマス (Y座標)
 * - 103: スクロール開始までの待機時間
 * - 104: スクロール後、終了までの待機時間
 * - 105: スクロール速度
 */
function scrollScreen() {
  PICTURE(-1);
  v[201] = ABS(v[101]);
  v[202] = ABS(v[102]);
  // 描画基準となる座標
  v[203] = v[101] >= 0 ? 0 : v[101];
  v[204] = v[102] >= 0 ? 0 : v[102];
  v[205] = PX - (PX % 10) + v[203];
  v[206] = PY - (PY % 10) + v[204];
  // 描画距離
  if (v[201] > v[202]) {
    v[207] = v[201];
  } else {
    v[207] = v[202];
  }
  // 移動にかかるフレーム時間
  v[208] = (v[207] / v[105]) * 40;
  PICTURE(-1, {
    pos: [v[203] * 40, v[204] * 40],
    timeFrame: [v[103] + v[208] + v[104], 0],
    animTimeFrame: [v[103], v[103] + v[208]],
    imgMap: [v[205], v[206], 1],
    crop: [11 + v[201], 11 + v[202]],
    move: [(v[105] * v[101] * -1) / v[207], (v[105] * v[102] * -1) / v[207]],
  });
  PICTURE(-1, {
    pos: [v[203] * 40, v[204] * 40],
    timeFrame: [v[103] + v[208] + v[104], 0],
    animTimeFrame: [v[103], v[103] + v[208]],
    imgMap: [v[205], v[206], 0],
    crop: [11 + v[201], 11 + v[202]],
    move: [(v[105] * v[101] * -1) / v[207], (v[105] * v[102] * -1) / v[207]],
  });
}

ソースコード長い)

ボスバトルのバトル演出

これも本作の魅力要素の一つ。ボスバトルでの技の種類は74種類ありますが、ピクチャ機能などを使用して演出を使用している技は64種類。ピクチャ機能を使用した演出だけに限るとさらに数は絞られますが、よくここまで演出作ったなぁ・・・と振り返って思いました。

連続イベントで二者択一が発生する都合や、背景パーツを踏んだらすぐに行動が実行できる運用のしやすさから、ボスバトルのピクチャの演出はすべて物体パーツで記載しています。おかげで物体パーツ数は 2800 をオーバー。その4分の1程度がこのボスバトルの演出に使われてると思います。

比較的序盤のところで見ることのできるマシキテ兄弟(兄)のつかみ投げは9パーツで動いています。

  1. つかみ投げをするプレイヤーのパーツ番号を記録する
  2. 怒った状態のマシキテ兄を 0.5 秒間出現、その後 3. に続ける
  3. マシキテ兄を下に2マス分動かす。速度はゲームの歩行速度と同じ
  4. マシキテ兄はそのまま
  5. 記録したプレイヤーのパーツ番号のイメージ画像からピクチャを作成
  6. 5. の定義そのまま
  7. 縦だけの円運動を行い、プレイヤーのピクチャを投げつける動作をする
  8. プレイヤーのピクチャを攻撃エフェクトに差し替える
  9. 移動したマシキテ兄のピクチャを消して、元に戻す。そしてダメージ計算してプレイヤーのHPを減らす

6. のピクチャの定義はこのようになっています。あらかじめ circle プロパティで円運動を起こすことで、次の投げつける動作をズレなく円滑に進めることができます。

{
  pos: [200, 200],
  time: 500,
  wait: 500,
  img: [GET_IMG_POS_X(v[201]), GET_IMG_POS_Y(v[201])],
  circle: [0, 60, 90, 0],
  next: [951, 0, 1],
}

・・・何でプレイヤーを投げつけるために円運動を起こしているんだ? と思いますが、 circle プロパティによる円運動は、どちらか一方の軸を 0 にすることで、縦方向や横方向の往復移動ができます。そんな感じのデモンストレーションをしてみたかっただけです。

図にするとこんな感じ

7. については 6. のピクチャの定義を継承し、アニメーションで動きを加えています。

{
  time: 0,
  wait: 0,
  // 18 = 180 (真下から真上に運動) / 10 (circle プロパティの運動速度)
  timeFrame: 18,
  waitFrame: 18,
  circle: [0, 60, 90, 10],
  next: [952, 0, 1],
}

こんな難しい定義書くの面倒そう~と思いますが、あらかじめ WWA Script が即時実行できるテキストボックスを活用すると、結果を見ながら編集できます。

ピクチャのイメージについては img プロパティにイメージ画像の座標を探す必要がありますが、結構手間がかかるのでこの時点では img: [3, 3] (WWA でモンスターと戦闘する時のエフェクト画像) で代用しています。出来上がったらちゃんと座標算出して差し替えたり、パーツメッセージに移して img プロパティを消したりしています。

一方で最初にボスバトルで挑むブレーキについては、まだピクチャ機能が開発途中だった時代に作られたボスモンスターだったため、ピクチャ機能を活用した技はありませんでした。

マップ表示

バッグにあるマップや家の地図から、現在地と次のタスクの目的地が表示されます。

メッセージと同時に表示されるため、別に $face マクロ (WWA Script なら FACE 関数) でも良くね? と思いますが、このマップだけで約30マス〜50マスぐらいイメージ画像を圧迫します*4

もう限界じゃぁー😇

ピクチャ機能は外部画像ファイルを読み込んで表示することが可能で、表示時間を1フレームに設定すれば、メッセージを閉じたときにすぐに消えるようになります。メッセージ送り毎に設定できない点を除けば、$face マクロと同じような感覚で扱うことができるんです。

// FACE 表示時は常にメッセージが下に表示されるため、空のFACEを表示させる
FACE(0, 0, 1, 0, 1, 1);
PICTURE(201, {
  pos: [v[201], v[202]],
  timeFrame: 1,
  imgFile: "map-suna_region",
});

WWA Script の話になりますが、現在地の算出方法については WWA Script のイベント関数で、歩行時に現在座標を変数に設けることで実現しています。

function CALL_MOVE() {
  // ボスバトルやメニュー選択の場合は除外
  if (v[1] == 0) {
    // 建物の屋内やダンジョンにいる場合は除外
    if (v[5] == 0) {
      v[6] = PX;
      v[7] = PY;
    }
  }
}

同じような理由で新南周辺の世界地図もピクチャ機能で表示しています。

引っ張り魚釣りゲーム

引っ張り魚釣りゲームは、一定時間以内に魚を上下キー連打で釣り上げないと逃げられてしまいます。

ピクチャ関係なくね? と思いますが、ピクチャ機能では、「一定時間経つとピクチャが消える」機能 ( time プロパティ) と「ピクチャが消えると任意のパーツを配置する」機能 ( map プロパティ) が備わっています。そして、ピクチャで何かを表示させる必要はありません。

という事で、タイマー用ピクチャを設けることで制限時間以内に魚を釣らないと行けない、引っ張り魚釣りゲームが出来上がりました。

/**
 * 引っ張り魚釣りゲームをはじめます。
 * 
 * # 引数
 * - 101: 魚の生命力
 * - 102: 所要時間
 * - 103: 得られた魚のアイテムパーツ番号
 */
function startFishingMode() {;
  // 釣りたい魚のアイコンをゲーム画面に表示
  o[15][432] = v[103];
  // 魚釣りゲーム画面に移動
  JUMPGATE(15, 435);
  PICTURE(1, {
    time: v[102],
    // PX, PY と指定してピクチャを表示すると、表示したときの座標にパーツが配置されてしまうが、今回プレイヤーは動かないのでこれで十分
    map: [771, PX, PY, 1],
  });
}

釣り上げた場合は、 PICTURE(1); とタイマー用ピクチャと同じレイヤー番号のピクチャを削除することで、タイマーを消すことができます。

開発当初は WWA Script の機能だけで実現していましたが、ピクチャ機能で実現したほうが分かりやすく、動作も安定しています。

技実行時のプレイヤーのグラフィック

公開前情報に掲載した画像をそのまま使用

ピッキングや集中など、フィールド上で使用できる技を使用すると、一時的にプレイヤーの姿が変わります。

これ普通に物体パーツ置けばよくね? と思いますが、結構難しいことなんです。

  1. 技実行時のプレイヤーの物体パーツを配置することになるので、プレイヤーの姿は透明、物体パーツにイベントを置くことはできない(イベントメッセージを書くとその物体パーツの姿が消えるため)
  2. なので背景パーツでイベントメッセージを書かないと行けない
  3. 物体パーツ配置時、その背景パーツのグラフィックが変わってしまうので、各床に応じた背景パーツを用意・・・(多分10種類以上はある)

ピクチャ機能を用いれば、こういう形で実現できます。

  1. 技実行状態のプレイヤーをピクチャ機能で定義
  2. 事前にプレイヤーの姿は透明にする
  3. イベントメッセージは物体パーツで書く。プレイヤーの座標に対して 1. のピクチャを表示
  4. ピクチャの表示が終わったら各技の効能を実行

プレイヤーごとにこのピクチャを表示させている

ホウテン塔

公開前情報に掲載した画像をそのまま使用

首都サイカにある観光施設のホウテン塔は、サイカの街を縮小した状態で見ることができます。

ピクチャ機能には、マップの一部をピクチャにして表示する機能があり、 "imgMap" プロパティを活用することで実現できます。

// v[101] と v[102] は一望したい範囲の左上座標 (それぞれ X, Y )
PICTURE(1, {
  pos: [0, 0],
  size: [20, 20],
  imgMap: [v[101], v[102], 1],
  crop: [22, 22],
});
// 物体パーツの分も忘れずに (毎回忘れる)
PICTURE(2, {
  pos: [0, 0],
  size: [20, 20],
  imgMap: [v[101], v[102], 0],
  crop: [22, 22],
});

ルーレット(ゲームランド)

イタミ市にはゲームランドが存在し、スロットゲームやルーレットを遊ぶことができます。中でもルーレットはピクチャ機能を活用しています。

最初右から玉が2周し、後はゆっくり移動して落ちる場所で止まります。落ちる場所は玉を転がす前にランダムで決定済み。狙った場所と落ちる場所が一致していればランドコインいただきとなります。

この機能は前作にもありましたが、約30パーツも消費していました。ピクチャ機能を活用することで約10パーツにまで節約に成功しています。

技術の話は以上

ここまで長々と見ていただきありがとうございます。見ているだけじゃ分からない、コピペしても動かせないという感じの記事になりましたが、 WWA Script やピクチャ機能がある程度分かるようになると、多少役立つのではないかと思います。

今回を機に WWA Script を活用してみたい! と思ったはいいけど、使い方が分からない方は入門記事を探してみましょう。幸いにも、同じ Advent Calendar で WWA Script 入門の記事が投稿される予定です。 WWA の制作の可能性を広げてみてはいかがでしょうか?

次の「こだわりの話」は、このゲームの制作段階にまつわる話や影響を受けた要素、隠し要素などを紹介するつもりです。

↓ 今年の Advent Calendar はこんな感じ ↓

adventar.org

明日の記事はひろちょびさんの『「四つの村と封印の洞窟」制作裏エピソード』です! WWA Contest 2024 ゲーム部門で7位を獲得したマルチエンディングのゲームです。

*1:実はプレイヤー側も整合性を保つために同じだったりします

*2:WWA Script でなくても、変数機能があれば多分実現可能です

*3:マサトは武器がエクステンドレイヤー固定のため、武具の攻撃力は含まれていませんが、他のプレイヤーはこれに攻撃力の分が加わります

*4:本作のイメージ画像ファイルは画像編集ソフトの EDGE2 で扱える限界の縦 8000 ピクセルに迫っているので結構カツカツです