ニコ生ゲームを作ろうと思ったらすぐコピペしよう その5


第5回のゲームは間違い探しです。

今回も使い回しを想定しています。
画像を用意して間違い箇所をプログラム上で指定する手間はありますが、頑張ればできます。

出来上がったものがこちらになります。アツマール公開版とは問題が違います。

※ 冒頭のランキング表示を追加しました。[2022/06/14]

 

絵はこちら

ゲームはこんな感じで画像が表示されます。アツマールはこちらです。
  




■目次

  • 画像準備
  • 差分箇所入力
  • デバッグモード
  • 出題数とダミー差分
  • 得点設定
  • ミラーモード設定
  • その他
  • レイヤーの切り替えで出題する





画像準備

画像の準備ですが、まず画像のサイズは 500×500 で固定です。

そして、一つの「元画像」に対して複数の「差分画像」を用意していきます。

元画像の例
animal0



元画像はゲーム画面の左右両方に表示されます。

差分画像の例
順にanimal1 , animal2 , animal3

   

差分画像も500×500で統一してください。名前は連番である必要はありません。

animal1の画像は元画像のanimal0をベースに7箇所の「差分箇所」があります。
差分箇所一つ一つを別々の画像にしても構いませんが、画像の管理とファイルサイズが大きくなるのがデメリットになると思います。
アツマールのファイルサイズ制限は結構ありそうですが、ニコ生ゲームは10MBまでです。

一つの画像の中のそれぞれの「差分箇所」は離れるように配置しています。
ゲーム内で差分箇所を切り取って表示する際に、別の箇所が映り込むのを防ぐためです。
離す距離については後述しますが、40以上を目安にしています。



 



ほぼ同じ場所に別のタイプの差分箇所を置きたい場合は、animal2のように別画像にします。
りんごの差分箇所とバナナの差分箇所はほぼ同じですが映り込むことはありません。
  animal2
また、animal2は差分箇所から離れたところは画像を消してしまっています。
逆に差分箇所から近いカラスとやハリネズミは残しています。

差分箇所を切り取ったときに、周囲が元画像と違う状況だと不自然になってしまうためです。
切り取る距離は差分の大きさと中央から40以上を目安にしてください。



差分画像に透過画像を使うこともできます
animal3は実は周囲が透過している画像になっています。
  animal3
上からかぶせるだけであれば、差分だけの透過画像を使うのがシンプルです。
使用している画像編集ソフトにレイヤー機能があればこの方法がやりやすいと思います。
ただし、元画像の後ろに配置するような重ね方はできません。


差分箇所入力

では差分を作成した箇所をプログラムに入力していきます。

入力例が 247行目から277行目 にあります。

  let quiz = [ // asset, x, y, size(片幅),
    [
      "animal0", //元画像
      [ "animal1", 52, 192, 50],
      [ "animal1", 248, 95, 80],
      [ "animal1", 414, 105, 50],
      [ "animal1", 403, 205, 40],
      [ "animal1", 110, 440, 100],
      [ "animal1", 299, 291, 40],
      [ "animal1", 433, 462, 50],
      [ "animal2", 134, 167, 40],
      [ "animal2", 410, 215, 40],
      [ "animal3", 133, 314, 70],
      [ "animal3", 453, 316, 40],
    ],
    [
      "zombie0", //元画像
      [ "zombie1", 60, 87, 40],
      [ "zombie1", 252, 190, 60],
      [ "zombie1", 0, 297, 70],
      [ "zombie1", 180, 424, 40],
      [ "zombie1", 457, 115, 50],
      [ "zombie1", 462, 391, 60],
      [ "zombie2", 7, 66, 90],
      [ "zombie2", 228, 241, 45],
      [ "zombie2", 131, 364, 40],
      [ "zombie2", 289, 33, 50],
      [ "zombie2", 446, 176, 70],
      [ "zombie2", 325, 399, 100],
    ],
  ];

まず全体が quiz という配列になっていてカッコで囲まれています。

その中に各元画像に関連する配列が2つあります。元画像が多ければいくらでも増やせます。

そのさらに中には、最初に元画像の名前が入ります。
そして各差分箇所の配列が続きます。

各差分箇所の配列には4つはいっていて、

 [ 差分画像の名前 , X位置 , Y位置 , 大きさ ],

の順になっています。

全体的にカッコとカンマの位置によく気をつけてください。


X位置とY位置の入力は差分箇所の中心の座標を入力します。

MS標準のペイントでもマウスカーソルを持っていけば、下にその座標が出ると思います。


大きさは、差分画像を切り取る大きさです。
ただし、注意してほしいのは中心から端までの距離になっています。

大きさが40であれば、「切り取りエリア」の幅と高さは80になります。
横長や縦長には対応していません。長い方を採用してください。


大きさは基本的に40以上にしておいてください。小さいとタップするのが難しくなります。
スペースが許せば10ぐらい余裕を持って大きくすると、理不尽な誤タップ判定が減ります。
30台でも動きますので、スペース上仕方ないときは採用しても大丈夫です。


上限は明確にはありませんが、隣の差分箇所にまで到達すると映り込んでしまいます。

また、切り取りエリアが重なると同時には出題できなくなります。
250に近い値にすると、その差分が選ばれると1問しか出題できなくなったり、
右画像をクリックしたつもりが左画像のエリアをクリックしたことになったりします。
(その他の項目に補足説明あり。)

ちょっとややこしいのですが、同じ差分画像の中で、
切り取りエリア同士が重なることはOKです。同時でなければ出題できます
切り取りエリアが隣の差分箇所の描画に重なるのはNGです。映り込んでしまいます。



また、差分画像の切り取りには元画像を消し潰す目的もあることに注意してください。

下のカラスが移動している差分画像の場合、右のようなエリアで良いように感じますが、

     

元画像はもう少し左下にいて、元のカラスを白で消さなければならないので、
 
    

左下の空白も含めて中心座標と大きさを設定する必要があります。



デバッグモード

ここまでで最低限の動作はできるようになっていると思います。

しかし、登録した座標や切り取りの大きさがうまく行っているかを確認するには、
ランダムで出題される問題が全部出るまで正解し続ける必要があり、気が遠くなる長さです。

これを簡略化するためにデバッグモードを追加しています。
デバッグモードでは一つずつ順番に出題されるので、余計な映り込み等を確認しましょう。


デバッグモードは148行目の debugmode という値を true にすると使えます。

そして次の行の debugimage を元画像の番号にします。
元画像を quiz に格納したときの順番が番号になっていて、5枚なら 0~4です。

全部同時にはチェックできないので、1セットずつ確認してください。
また、チェック後は debugmode を false に戻してからリリースしてください。



出題数とダミー差分

ゲームの進行中に一度にいくつの間違い箇所を出題するかは、244行目で設定できます。
odainumという値を出題数にしてください。

公式のみんなで間違い探しだと4問で、マルチでないものは3~5問だったかと思います。
全体の問題数や大きさにもよりますが、同じぐらいでいいのではないでしょうか。


またダミー差分の数 dummynum も設定できます。

正解の差分画像だけを表示していると間違い探しゲームとして左右を比較する必要がなく、
差分画像を覚えてそれを探せば良いという問題があります。

ちゃんと違いを見る必要性を出すためにこの数だけ不正解の差分を両側に追加しています。

数はodainumと同じか、4、5個でいいんじゃないでしょうか。


得点設定

得点の数値は143行目から146行目にまとめました。

正解とハズレの得点と、一度に出題されたものを全て答えたときのクリア得点があります。

ハズレによって総得点がマイナスになるかどうかは、361行目にあります。
標準ではマイナス不可になっているので、許可したい場合は1行消してください。

クリア得点は最大値と最小値があり、クリア時間に応じて減少していきます。
クリア得点が減るスピードを変えたい方は467行目の最後の 5 を変更してください。


ミラーモード設定

実は間違い探しはとある方法をマスターすると超簡単になってしまいます。
それを防止するために動き等で邪魔することも考えたのですが…意地悪なのでやめました。
使える人と使えない人それぞれ別々で競えればいいなと思います。

と、思っていたのですが

対策として、左右の片方の画像を反転させる方法がある情報を得ました。
これなら、そこまでストレスではない上に割としっかり対策になります。

処理もそこまで難しくなかったので機能を追加しました。
246行目の mirrormode を true にすると使えます。


この対策について検索していると、ニコニコ大百科の記事が最初に出てきました。
しかも、「アツマールでゲームを作る人は注意」とピンポイントで書かれている有様。
灯台下暗しでした。


その他

差分のサイズを大きくする際の注意

左右の画面端に縦長の差分箇所がある場合、
隣のエリアまで差分領域が到達してしまうことがあります。

下の画像のケースでは赤い大型のキャラをうまく囲うため、中心を画像外においています。



縦長の切り取りエリアが出来上がっているように見えて、
左の画像の差分箇所にかぶさってしまっています。

大きさによっては完全に覆ってしまうことで、クリックできなくなる詰みの恐れがあります。


基本的な対処方法は画像内に中心を入れ、切り取りエリアも画像内に収める、です。

どうしても左右の画面端で縦長領域を作りたい場合は、下の設定を復活させてください。

  // a.touchable = false;
  // b.touchable = false;

452行目453行目にありますが、普段は無効になっています。
行頭の // を消すと設定が復活します。

この設定を復活させると、一度クリックして正解するとその領域が無効になって、
下の領域をクリックできるようになります。

逆にこの設定があると、間違ってダブルクリックすると2クリック目が不正解になります。


全体を覆うpaneを作っとけばよかったんですけどね。



チュートリアル画像について

チュートリアル画像はシンプルなものと、画像とタイトルを入れられるものを用意しました。


冒頭にある絵のリンクから持っていってもらってもいいですし、
1から作ってもらっても、透明画像に差し替えてもらっても構いません。

akashic scan asset さえやればサイズも不問です。




このゲームの記事は以上です。

アツマール版では、5枚の元画像にそれぞれ4枚の差分画像と50の差分箇所を作り、
250箇所の間違い箇所を用意しました。

画像編集と違いのネタ出しが結構大変だったので、20~30箇所を4枚とかでいいと思います。
しかも今回、BGMが2MBで画像が9MBあったので10MBをオーバーしました。
大変申し訳ないのですがBGMの尺を短くさせていただきました。
画像を圧縮する方法もあったのかも。

[追記] JPEGにすれば圧縮できました。画質?も特に問題なさそうです。



あと、間違い探し向けの画像というのがちょっと難しくて、
画面全体に物があって、あまり規則正しくないものという縛りがあります。

今回アツマール版はいらすとやさんからイラストをお借りしましたが、
結構苦労して途中から複数の絵を組み合わせる方向にシフトしました。
自分で絵を書ける人ならここはクリアできそうですね。




レイヤーの切り替えで出題する

ここからは間違い探しゲームの改造とは関係ありません
クイズ形式やパズル形式のゲーム全般で使える方法を解説します。

前回のクイズでも使っていましたが、出題に関する画像等をレイヤーに配置し
次の出題に移る際にレイヤーを破壊して、次のレイヤーを生成し直す方法があります。



レイヤー切り替えのメリット

メリットはレイヤーごと削除することでまとめて削除できるところです。

複数の画像を配置している場合、全て削除するには画像等の名前を覚えておき、
それぞれ、 sprite.destroy(); と書く必要があります。
ゲームの進行によって画像の枚数が違う場合は、更にめんどくさくなります。

全てレイヤーに乗せておけば、 layer.destory(); と書くだけですべて削除できます。


画像などを削除せずに使い回すことももちろんできますが、
リセットする際にこれまで変更したパラメータを全て把握して書き換える必要があります。

全て作り直せば、初期のパラメータでリセットできます。



レイヤー切り替えの流れ

流れはこのような形です。




レイヤーの構成

まず、162~164行目で表示順を制御するために全体で使うレイヤーを3つ配置しています。

  let backlayer = new g.E({ scene: scene, parent: scene });
  let uilayer = new g.E({ scene: scene, parent: scene });
  let buttonlayer = new g.E({ scene: scene, parent: scene });


そして、切り替え用のレイヤーtouchlayer308行目に書いてあります。

  let touchlayer = new g.E({ scene: scene, parent: backlayer});

このとき、 parent を backlayer にしてbacklayerの上にtouchlayerを配置します。
スコア等を表示するuilayerと音量ボタン用のbuttonlayerの後ろに配置するためです。


レイヤーの構成を図で表すとこのような形です。



ちなみに、「レイヤー」という呼び方は私が勝手に言っているだけです。
正式には「入れ子」「Eオブジェクト」「親エンティティ」「g.E」などです。うーん



レイヤーを最初に配置するタイミング


touchlayerは307行目makequiz関数というまとめた処理の中で配置します。

function makequiz() {
  let touchlayer = new g.E({ scene: scene, parent: backlayer});

   ︙

}

そしてこのmakequiz関数は179~188行目のように、
ウォームアップ時間がなくなった瞬間に1度だけ処理されます

  scene.onUpdate.add(function () {//時間経過
    if (warmup >= 0) warmup -= 1 / g.game.fps;
    if (warmup < 0) {
      if (!startstate) {
        makequiz();
      }
      startstate = true;
      gametime += 1 / g.game.fps;
    }
  });

onUpdateを使っているのでこんな書き方ですが、setTimeoutでもいいです。



画像を配置する関数へのレイヤー引き継ぎ

makequizの処理を進めていくと、画像の配置は似たような処理が出てきます。
makeimage、makesabun、makedummyの3つの関数にまとめ、都度再利用しています。

その中で配置する画像等はparent を touchlayerにしますが、そのままでは使えません

makequizの中で作ったtouchlayerの名前はmakequizの中でしか通用しません


このため、313行目のようにmakeimageを使う際に、
touchlayerをカッコの中に入れて引き継ぎします。

  makeimage(odai[0], -1, touchlayer);

他にも画像の名前と配置する位置情報も引き継いでいます。


makeimageの本体の方でも、受け入れる変数を記載しています。

  function makeimage(asset, pos, touchlayer) {
    let image = new g.Sprite({
      scene: scene, src: scene.assets[asset], parent: touchlayer,
      x: g.game.width * (0.5 + pos * 0.22), y: g.game.height * 0.5,
      scaleX: pos > 0 && mirrormode ? -1 : 1, scaleY: 1,
      anchorX: 0.5, anchorY: 0.5, opacity: 1, touchable: true,
      tag : { stage: stage },
    });
      ︙

こうすることでmakeimageの中でもtouchlayerが使えるようになります。

受け入れの時の名前は別の名前で構いませんが、makeimageの中では統一して使います。
layerにしておいて、状況に応じて引き継ぐレイヤーを変えたりすることもできます。

「引き継ぎ」は正確には「引数」と呼ぶらしいです。
「いんすう」ではなく「ひきすう」らしいです。引き算は関係ないと思います。



レイヤーの破壊と生成

レイヤーの破壊は426行目onPointDownでクリックするタイミングから始まっていきます。

a.onPointDown.add(function(ev) {
 if (startstate && !finishstate && !clearstate && !missstate) {
  if (a.tag.stage == stage && !a.tag.clear && remain > 0) {
   let score = seikaipoint; //正解一つの得点
    g.game.vars.gameState.score += score;
    let marua = new g.FrameSprite({
     scene: scene, src: scene.assets["batumaru"], parent: touchlayer,
     x: a.x, y: a.y, scaleX: 0.5, scaleY: 0.5,
     width: 200, height: 200, srcWidth: 200, srcHeight: 200, frames: [1],
     anchorX: 0.5, anchorY: 0.5, opacity: 0.6, touchable: false,
    });
    let marub = new g.FrameSprite({
     scene: scene, src: scene.assets["batumaru"], parent: touchlayer,
     x: b.x, y: b.y, scaleX: 0.5, scaleY: 0.5,
     width: 200, height: 200, srcWidth: 200, srcHeight: 200, frames: [1],
     anchorX: 0.5, anchorY: 0.5, opacity: 0.6, touchable: false,
    });
    let label = new g.Label({
     scene: scene, text: "+" + score, parent: uilayer,
     font: font, fontSize: 48,
     x: g.game.width/2+40, y: scorelabel.y,
     anchorX: 0.5, anchorY: 0.5, opacity: 1,
    });
    a.tag.clear = true;
    b.tag.clear = true;
    // a.touchable = false;
    // b.touchable = false;
    remain --;
    remainlabel.text = "あと" + remain + "つ";
    remainlabel.invalidate();
    scene.setTimeout(function() {
     label.destroy();
    }, 1250);
    if (remain <= 0){
     if (soundstate) scene.assets["se_clear"].play().changeVolume(0.4);
     stage++;
     remain = 0;
     clearstate = true;
     remainlabel.text = "クリア!";
     remainlabel.invalidate();
     let clearscore = Math.floor(clearmaxpoint - (g.game.age - startframe) / g.game.fps * 5);
     if (clearscore < clearminpoint) clearscore = clearminpoint; //1クリア時の最低限のボーナス
     g.game.vars.gameState.score += clearscore;
     let clearlabel = new g.Label({
      scene: scene, text: "+" + clearscore, parent: uilayer,
      font: font, fontSize: 48,
      x: g.game.width/2-160, y: scorelabel.y,
      anchorX: 0.5, anchorY: 0.5, opacity: 1,
     });
     scene.setTimeout(function() {
      clearlabel.destroy();
     }, 1250);
     scene.setTimeout(function() {
      if (!finishstate){
       touchlayer.destroy();
       makequiz();
      }
     }, 1250 - 1000 * debugmode);
    } else {
     if (soundstate) scene.assets["se_seikai"].play().changeVolume(0.4);
    }
   }
  }
 });

今回のゲームは出題箇所が0になったら次に進むので、460行目残りの数を判定しています。
これがスライドパズルのように盤面全体が一致しているかを判定する場合は、
全パネルをチェックする for (let i = 0; i < paneltable.length; i++) を入れたりします。

そして、正解であることを表示する時間を479行目タイマーで開始し、
touchlayer.destoy();で破壊、そのままmakequiz();で次の出題を生成します。


makequizがある意味無限ループですが、クリックしないと進まないので大丈夫です。



関数の中で常に監視する処理

491行目にonUpdateがありますが、sceneではなくtouchlayerのonUpdateにしています。

これをscene.onUpdateにしてしまうとsceneはtouchlayer.destory();で破壊されません
このため、ループが進んで繰り返されると処理が重複します
100問目になると100処理たまることになります。

touchlayer.onUpdateにすれば、処理も毎回削除されて常に一つしかないようになります。


また、scene.onUpdateの中に、touchlayerの画像等に関する処理があると、
破壊した後に処理が行われるとエラーになります。

同じ名前の画像等を作ったとしても別物になってしまうので、
そういう場合は関数の外に破壊しない変数を作っておいて
生成するごとにその変数に画像等を格納する方法があります。






次回からは、テンプレゲームはお休みしてマルチゲームと物理の解説をやろうと思います。
アツマールの方の流れでマルチゲームの需要が出てきそうです。


また、テンプレゲームの方針が迷子になってきているのもあります。
残っているネタとして音ゲースワップパズル、画像からのさがし物、はあるのですが
再利用したいと思えるほど魅力的なゲームデザインとなるとなかなか見つかりません。

もともとはACTや横STG 弾幕STGも考えていたんですが、割と改造が大変だったり、
ゲーム性が変わりにくかったりでテンプレとしては使いづらそうな見込みです。
テンプレとしての再利用は忘れて作ったものを公開することはあるかもしれません。


もしテンプレートとして良さそうなネタが有りましたら教えて下さい。

ただし、あくまで再利用に向いていて多くの方が使えるジャンルのアイデア募集であって、
作って欲しいゲームのリクエストを受け付けるわけではありません。
欲しいものがあったら基本的に自分で作りましょうという記事でもあります。


この記事以外にも、コピペできそうなコードや解説を公開しています。
こちらに記事のリンクがまとまっていますので御覧ください。
そろそろブロマガから移行する準備もしないとだめですね。