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

第2回のゲームは見下ろし型のアクションゲームです。

マウスやタッチパネルを使って操作性の良いアクションゲームを作るのは結構難しいです。
その中で可能なやり方の一つが、タッチ箇所へキャラクターが向かう操作方法です。

公式のゲームでいうと逃げゲーと同じです。
ボスネコラッシュもすこし近いですね。ニコニコスネークは違う操作方式です。


とりあえず出来上がったものがこちらになります。


これはアツマール用にエクスポートしたzipではなく、ファイルをまとめただけです。
音ファイルはありません。main.jsの音の記載も無効にしてあるので、追加した場合はplay()で検索して有効にし、assetIDsの登録scan assetをやってください。

絵はこちら

ゲームはこんな感じで真ん中のアヒルが画面内を動きます。アツマールはこちらです。

  


■目次

  • 画像入れ替え
  • プレイヤーパラメータ変更
  • 敵キャラパラメータ変更
  • 敵出現パターン
  • アイテムパラメータ変更
  • タッチ操作
  • フレームアニメーション
  • プレイヤーと敵キャラの状態
  • プレイヤーの見た目変化
  • ヒット判定


プレイヤーと敵を改造すれば、別ゲーにしやすいように意識したつもりです。

敵の動きはいくつかパターンを用意しました。

画像の差し替えと速度などのパラメータを変更し、出てくる数も変えればゲームの印象を変えられるんじゃないかと思います。





画像入れ替え

今回も画像入れ替えで雰囲気が変えられるようにします

変えるとよい画像はこちら

  • tutorial.png     タイトルと操作説明
  • player.png      メインのプレイヤーキャラ
  • ene1 ~ ene8.png  敵キャラ 登場させたい数だけ用意する
  • item.png      得点になるアイテム 登場させたい数だけ


キャラクターはアニメーションさせる場合は複数の画像が結合されたものを使いますが、
アニメーションさせない場合は1枚のものでも大丈夫です。

変えるとよいが、用意しなくてもいい画像はこちら。
ただし、画像自体が無いと起動できないので透明画像に差し替えるか、画像のopacityを0にするなどの処理が必要です。

  • background.png   背景画像 なしまたは透過四角でいい
  • playerhit.png     メインキャラのやられ表示
  • fire.png       メインキャラのパワーアップ表示


また、playerhitだけは、消すとプレイヤーが表示されない瞬間が出てくるので、
後述するplayerhittimeを0にするか、playerと同じ画像をplayerhitに名前を変えて
置いてください。

それぞれimageフォルダに上書きして、assetIDsに登録して、scan assetをやります。


プレイヤーパラメータ変更

プレイヤーのパラメーターは変更しやすいように142行目あたりにまとめています。

let playerspeed = 4.5; //プレイヤーのスピード
let playersize = 50; //プレイヤーの当たり判定の大きさ 直径 ピクセル 
let playercenter = 30; //プレイヤー中央の停止判定エリアの大きさ
let playerscale = 1.0; //プレイヤー画像の倍率 当たり判定は別
let playerimagenum = 4; //プレイヤー画像の分割枚数
let playerimagew = 80; //プレイヤー画像の分割 横幅
let playerimageh = 80; //プレイヤー画像の分割 縦高さ
let playerdash = 4; //プレイヤーダッシュ時のスピード倍率
let playerdashtime = 0.4; //プレイヤーダッシュの持続秒数
let playerdashcooltime = 1.0; //プレイヤーダッシュのクールタイム秒数
let playerhittime = 1.5; //プレイヤーがダメージで硬直する秒数
let playermutekitime = 4.0; //プレイヤーがダメージ後に無敵になる秒数
let playerpowerupnum = 10; //プレイヤーのパワーアップに必要なアイテムの敵
let playerpoweruptime = 5.0; //プレイヤーパワーアップの秒数


数が多いですがコメントも入れていますので、色々変更してみてください。

とりあえずスピードを異常に早くしたりすれば雰囲気が変わるんじゃないでしょうか。

画像を差し替えたときに、画像のサイズや枚数が変わった場合は2~6個目を変えてください。

画像サイズを変えた場合は、playerimagewを変えるだけではなく、当たり判定も変える必要があるのでplayersizeを同じぐらいの値か、6割ぐらいに変えます。
画像の外周に余白が多くある場合はそれも考慮して、当たり判定を小さくします。

このゲームではキャラクターの中心をポイントしていれば、動きが止まります。
その中心の大きさをplayercenterとしてplayersizeの半分ぐらいにします。
0でもいいんですが、キャラクターがブルブル動いてしまうかもしれません。

作った画像が意外と小さかった場合は、playerscaleを1.5とか2とか3にしてください。

アニメーションさせるつもりで複数の画像を結合している場合は、playerimagenumにその枚数を入れてください。
アニメーションさせない場合はplayerimagenum = 1と入れてください。

それ以外のパラメータも興味があったら色々変えてみてください。
ダッシュは、残念ながら直進しかできません。


敵キャラパラメータ変更

敵キャラは8タイプ用意しています。

function makeene1(inix, iniy) {

}


のようにくくったところが敵を作る処理です。makeene1~makeene8まであります。

最初にパラメータを並べてあります。タイプによって種類が異なります。

let speed = 6; //敵のスピード
let size = 70; //敵の当たり判定の大きさ 直径
let scale = 1.0; //敵画像の倍率 当たり判定は別
let imagenum = 1; //敵画像の分割枚数
let imagew = 120; //敵画像の分割 横幅
let imageh = 120; //敵画像の分割 縦高さ
let lifetime = 15; //敵の生存秒数 即消滅と画面外へ出ていく場合あり
let dashtime = 0.8; //敵ダッシュの持続秒数
let dashleng = 300; //敵ダッシュの距離

size、scale、imagenum、imagew、imageh はプレイヤーと同じですね。
画像のサイズや枚数を変えた場合は調整しましょう。


lifetimeというのは、敵が画面内に居座り続ける秒数です。
ゲームに多数発生させる画像等は、用済みになれば.destroy()で消すのが望ましいです。

消さないと数が溜まってきて、処理が重くなります。数百個なら大丈夫だったりしますが。数千、数万は怪しいですね。PCだと大丈夫だけどスマホだと重い、ということもありえます。

画面外に出ていって戻ってくる予定がないなら消すのが一般的です。
今回はいろいろな動きをするので、寿命lifetimeを決めてそれが終われば外へ出ていくようにしています。


パラメータが少し違う似た敵を出したければmakeene9を作ってください。
名前はmakesnakeとかでも大丈夫です。

動きを組み合わせたものや、1から作り直したものを作ってもらっても構いません。


敵出現パターン

敵出現パターンは429行目から547行目にあります。

scene.setTimeout(function() {
  makestage();
}, warmup * 1000);
function makestage() {
  switch (stage) {
    case 1:
    switch (g.game.random.get(1, 3)) {
      case 1:
      makeene1(g.game.random.get(0, 1), g.game.random.get(2, 5)/10);
      makeene4(g.game.random.get(0, 1), g.game.random.get(2, 8)/10);
      break;
      case 2:
      makeene3(g.game.random.get(0, 1), g.game.random.get(2, 8)/10);
      makeene7(g.game.random.get(0, 1), g.game.random.get(2, 8)/10);
      break;
      case 3:
      makeene5(g.game.random.get(0, 1), g.game.random.get(2, 8)/10);
      makeene6(g.game.random.get(0, 1), g.game.random.get(2, 8)/10);
      break;
    } break;
    case 2:
      makerandom(); break;

    ~ 中略 ~

    default:
    break;
  }
  scene.setTimeout(function() {
    stage ++;
    if (!finishstate) makestage();
  }, 10000);
}

 

まず最初の3行で、ウォームアップの時間を待って開始するようにしています。

その後 function makestage() { の中で、配置を設定しています。
このmakestage()最後の5行で、10秒ごとに繰り返されるようになっています。

その際にstageという数字を増やしており、毎ステージ違う敵が出てきます。



各ステージの配置は固定配置ランダム配置交互に来ます
固定配置は3種類の固定配置からランダムで一つ選ぶようになっています。

面倒だったら以下のように全部ランダム配置にしてください。

function makestage() {
  makerandom();
  scene.setTimeout(function() {
    stage ++;
    if (!finishstate) makestage();
  }, 10000);
}


ステージ数ごとの切り替えと、ランダムな切り替えはswitchを使います。
switchはif文と同じように場合分けができます。

switch (処理番号) {
  case 1:
  処理内容1
  break;
  case 2:
  処理内容2
  break;
  case 3:
  処理内容3
  break;
}


処理番号が1の場合、case 1: と break; で囲まれた処理内容を実行する形ですね。
処理番号g.game.random.get(1, 3)にすれば、ランダムで処理内容が選択されます。

厳密に言うとこれはカッコではありません。また、処理番号は処理文字列でもいけますが、
そのへんの説明は省略します。流石に怒られそうですね。
コロン : とセミコロン ; の違いに気をつけてください。ていうかコピペしてください

ステージ1の固定パターンの1個めは以下のようになっています。

case 1:
  makeene1(g.game.random.get(0, 1), g.game.random.get(2, 5)/10);
  makeene4(g.game.random.get(0, 1), g.game.random.get(2, 8)/10);
break;


makeene1makeene4で2つの敵を配置します。
それぞれのカッコの中には配置する場所の数値が入っています。横位置inixと縦位置iniyです。

ランダムを使わずにシンプルに書くと、
makeene1(0, 0); であれば画面左上に、
makeene1(0.5, 0.5); であれば画面中央に敵1を配置します。

今回の配置では、左端か右端に配置するために、横位置inixを0か1のランダムにし、
上端か下端から少し離して配置するために、縦位置iniyを0.2~0.8にしています。
さらに、敵1に関しては特性上、上半分に限って0.2~0.5にしています。


ランダム配置の方は、549行目に処理が書いてあります。

function makerandom() {
  for (let i = 0; i < stage/3 + 1; i++) {
    switch (g.game.random.get(1, 8)) {
      case 1: makeene1(g.game.random.get(0, 1), g.game.random.get(2, 5)/10); break;
      case 2: makeene2(g.game.random.get(0, 1), g.game.random.get(2, 8)/10); break;
      case 3: makeene3(g.game.random.get(0, 1), g.game.random.get(2, 8)/10); break;
      case 4: makeene4(g.game.random.get(0, 1), g.game.random.get(2, 8)/10); break;
      case 5: makeene5(g.game.random.get(0, 1), g.game.random.get(2, 8)/10); break;
      case 6: makeene6(g.game.random.get(0, 1), g.game.random.get(2, 8)/10); break;
      case 7: makeene7(g.game.random.get(0, 1), g.game.random.get(2, 8)/10); break;
      case 8: makeene8(g.game.random.get(0, 1), g.game.random.get(2, 8)/10); break;
      default:
    }
  }
}


まずforの繰り返しで配置する数を決めています。

数はstage/3 + 1としていて、ステージが進むにつれて数は増えます。
固定の数字でもいいんですが、この場合ステージ10までで2→5に上がっていきます。

もっと出す数を増やしたかったらstageだけでもいいですね。
stage*100でも動きましたが、ちょっと重かったのでやっぱり1000体近いときついですね。


完全にランダムにするのは楽ですが、同じ場所に配置してしまったり、難易度が不安定になってしまったり、敵の組み合わせによるシナジーを表現できません。
このため、固定の配置とランダム配置が交互に来るようにしています。


アイテムパラメータ変更

アイテムの配置も基本同じですが、敵と違って一つの画像に様々な種類をまとめています。

1192行から1239行のところがアイテム配置の処理です。
let imagenum = 4; //アイテム画像の分割枚数のところでアイテム種類数を指定しています。


let itemtable = [ 0, 0, 0, 0, 1, 1, 1, 2, 2, 3];

let scoretable = [20, 30, 50, 100];
scene.setTimeout(function() {
  makeitems();
}, warmup * 1000);
function makeitems() { //アイテム設置
  for (let i = 0; i < Math.floor(stage/3)+2; i++) {
  makeitem(0.1 + 0.8*g.game.random.generate(), 0.15 + 0.7*g.game.random.generate(), itemtable[g.game.random.get(0, itemtable.length-1)]);
  }
  scene.setTimeout(function() {
    if (!finishstate) makeitems();
  }, 3000);
}
function makeitem(x, y, type) { //アイテム設置
  let size = 40; //アイテムの当たり判定の大きさ 直径
  let scale = 1.25; //アイテム画像の倍率 当たり判定は別
  let imagenum = 4; //アイテム画像の分割枚数
  let imagew = 50; //アイテム画像の分割 横幅
  let imageh = 50; //アイテム画像の分割 縦高さ
  let yokokutime = 12; //アイテム消滅の予告点滅が始まる秒数
  let lifetime = 15; //アイテムの生存秒数

 ~ 中略 ~

}


用意した画像を分割して左から順番に番号が振られます。
4分割なので0~3の番号がアイテムの種類と対応します。

  

各アイテムの出現確率を変えるため、let itemtable = [ 0, 0, 0, 0, 1, 1, 1, 2, 2, 3];
というテーブルを作り、この中からランダムで選んで出現確率を変えます。
0.5%とかに設定したい場合は、かなり書き方が変わるので紹介はまたの機会に。

それぞれのアイテムから得られる得点として、let scoretable = [20, 30, 50, 100];
というテーブルで管理します。
scoretable[1]とすれば、1番のスコアである30を引用することができます。

アイテムが登場するタイミングは3秒毎になっています。

残念ながら、アイテムをアニメーションさせたい場合は全体的に作り変える必要があります。





まだ半分ですが、ここまでを色々変更すればゲームを変えられると思います。

ここからは、今回使っている処理の解説がメインになります。


タッチ操作

タッチ操作が今回のメインですね。

233行目辺りから以下のような処理があります。

let touch = new g.FilledRect({// タッチ画面生成
  scene: scene, cssColor: "black", parent: touchlayer,
  x: g.game.width/2, y: g.game.height/2,
  width: g.game.width, height: g.game.height,
  anchorX: 0.5, anchorY: 0.5, opacity: 0, touchable: true,
});
let arrow = new g.Sprite({
  scene: scene, src: scene.assets["arrow"], parent: uilayer,
  x: 0, y: 2000, scaleX: 0.4, scaleY: 0.6,
  anchorX: 0.5, anchorY: 0.5, opacity: 0, touchable: false,
});
let pointx;
let pointy;
touch.onPointDown.add(function(ev) {//操作押し込み
  if (touchstate == false && startstate) {
    touchstate = true;
    if (player.tag.state != "dash") {
      pointx = ev.point.x;
      pointy = ev.point.y;
    }
  }
});
touch.onPointMove.add(function(ev) {//操作移動
  if (touchstate == true) {
    if (player.tag.state != "dash") {
      pointx = ev.point.x + ev.startDelta.x;
      pointy = ev.point.y + ev.startDelta.y;
    }
  }
});
touch.onPointUp.add(function(ev) {//操作リリース
  if (touchstate == true){
    touchstate = false;
    if (player.tag.state != "hit" && player.tag.state != "dash") {
      if (charge == 1) {
        chargeframe = 0;
        player.tag.state = "dash";
        player.tag.frame = 0;
        player.interval = 30;
        player.start();
        // if(soundstate) scene.assets["se_dash"].play().changeVolume(1);
      } else {
        player.tag.state = "stop";
        player.stop();
        player.frameNumber = 0;
      }
    }
  }
});
touch.onUpdate.add(function() {//矢印表示
  if (touchstate && startstate){
    if (player.tag.state == "stop" || player.tag.state == "walk") {
      let controlx = pointx - player.x;
      let controly = pointy - player.y;
      let leng = Math.sqrt(controlx**2+controly**2);
      let angle = 0;
      if (leng < playercenter){
        arrow.opacity = 0;
        player.tag.state = "stop";
        player.stop();
        player.frameNumber = 0;
      } else {
        arrow.opacity = 0.7;
        angle = -Math.atan2(controlx, controly)*180/Math.PI+90;
        arrow.angle = angle;
        player.tag.dir = angle;
        player.interval = 150;
        if (player.tag.state != "walk") player.start();
          player.tag.state = "walk";
        }
        arrow.x = pointx;
        arrow.y = pointy;
      }
    } else {
      arrow.opacity = 0;
    }
  arrow.modified();
});

まず最初にtouchという画面サイズと同じ大きさの四角を配置しています。
透明ですが、touchableをtrueにしているので基本的にこのtouchを触ることになります。

音用のボタンなどはレイヤーでtouchより手前にしてあるのでポイントできます。



そして3つのタッチ関係の処理を使っています。touch.onPointDown.add
touch.onPointMove.addtouch.onPointUp.addの3つです。

これらの処理の中では、functionにfunction(ev)と入れておくと以下の数値を使えます。

 ev.point.x     最初にポイントした場所のX座標
 ev.startDelta.x  最初にポイントした場所から動かした総量(PointDownでは使えない)
 ev.prevDelta.x  その瞬間に動かした量 (PointDownでは使えない)

厳密な定義はちょっと違うので、興味あればこちらを確認ください。
それぞれ.yもあります。


また、touch.onPointMove.addは押したまま動かない場合は処理されません。
押したままの状態か、既にはなした状態かで処理が変わる場合を考慮し、touchstateという値を用意して、touch.onUpdate.addで常に監視することにしています。

これが最善かどうかわかりませんが、マルチタッチの複雑な状況も排除できている・・・かもしれません。



プレイヤー操作の処理の流れはこの様になっています。

  1. onPointDownとonPointMoveでpointxとpointyという現在座標を出す
  2. onUpdateで、プレイヤーの進むべき方向angleを計算する
  3. ポイントしている場所がプレイヤーと近ければ止まらせる
  4. player.tag.dirという値に角度angleをいれる
  5. あとは、327行目のplayer.onUpdate.addに任せる

onPointUpダッシュを開始するかどうかの処理にのみ使っています。


angleの計算Math.atan2というものを使えばシンプルにできました。

Math.acosを使っていますが、これは角度は0度と180度のときに使えないので場合分けが複雑になってしまっています。
誰か簡単に計算する方法を知っていたら教えてください。

コピペして使えるようなら無視してそのまま使ってください。


フレームアニメーション

すでに何度か使っていますがFrameSpriteではアニメーションをさせることができます。

let enemy = new g.FrameSprite({
  scene: scene, src: scene.assets["ene2"], parent: enemylayer,
  x: g.game.width*inix, y: g.game.height*iniy, scaleX: inix<0.5 ? scale : -scale, scaleY: scale,
  width: imagew, height: imageh, srcWidth: imagew, srcHeight: imageh, frames: frames, interval: 150, loop: true,
  anchorX: 0.5, anchorY: 0.5, opacity: 1, touchable: false,
  tag: {state: "walk", vx: inix<0.5 ? 1 : -1, vy: 0, frame: -1, lifeframe: 0, targetx: 0, targety: 0},
});
enemy.start();


FrameSpriteの場合はwidth、height、srcWidth、srcHeight、framesが必要です。
ただのSpriteの場合はこの行はなくていいです。srcWidth、srcHeightの2つの値で、画像を切り取って番号をつけていきます。
画像の並べ方は横一列や縦一列だけでなく5x5などでも大丈夫です。

framesのところに[0, 1, 2, 3]のようなアニメーションの順番が書かれた配列を入れます。
[0] や [3, 1, 1, 1]のような変則的なものでも大丈夫です。

アニメーションさせない場合は切替可能な画像のセットのような使い方もできます。
enemy.frameNumber = 1; enemy.modified();とすると画像を切り替えられます。

この場合、配列の中の2番めの画像に変わります。

  


アニメーションさせる場合は追加でloopとintervalも指定できます。

loopはtrueにしていればアニメーションが途切れずにループします。基本trueですね。

interval画像の切り替えの時間ですね。100なら100ミリ秒です。
指定しない場合はg.game.fpsに合わせてくれます。33ミリ秒ぐらいですね。

アニメーションはenemy.start()と書かれた処理が行われるまで動きません。
ただし、常にこの処理が行われるようなところに置くと、毎回最初の画像だけ表示されてしまうので、1回ずつでしか処理されないところにおいてください。
enemy.stop()とすると止まります。


プレイヤーと敵キャラの状態

プレイヤーと敵キャラの処理は状態.tag.stateで切り替えるようにしました。

敵キャラの場合はタイプが様々ですが、基本的に.tag.state
"stop" → "walk" → "dash" → "stop" の順番で切り替わります。


プレイヤーが敵にあたった場合は、 "hit"になったあとに一定時間"stop"に戻ります。
この制限時間の処理は以下のように.tag.frameのカウントで制御し、
ある値を超えたら次の状態に移るようになっています。


if (player.tag.frame >= 0) player.tag.frame ++;

 ~ 中略 ~

if (player.tag.state == "hit"){
  if (player.tag.frame > playerhittime * g.game.fps) {
    player.tag.state = "stop";
    player.stop();
    player.frameNumber = 0;
    player.opacity = 1;
  } else {
    player.opacity = 0;
  }
}

.tag.frameは状態変化に応じて適宜0にし、カウンターをリセットしています。
また、初期の状態では値を-1にしてあって、その状態では制限時間の判定が無効になります。

無敵時間の方は状態.tag.stateとは絡めずに独立のカウンターで管理し、
無敵時間中でもダッシュや停止ができるようになっています。



敵キャラクターにも"hit"の状態があり、プレイヤーがパワーアップ中のダッシュに敵が当たると"hit"状態になります。

こうなると、他の処理を無視して斜め上方向に飛んでいきます

if (enemy.tag.state == "hit"){
  enemy.x += inix<0.5 ? -enemyhitspeed : enemyhitspeed;
  enemy.y -= enemyhitspeed;
} else {

 ~ 他の処理 ~

}



プレイヤーの見た目変化

プレイヤーは敵にあたったときと、アイテムが揃ったときに見た目の画像が変わります。

今回はプレイヤーにアニメーションをつけてしまったので、状態変化による見た目の画像変化は別途画像を用意しました。

playerhitfireの画像は普段はopacityが0ですが、常にplayerの位置についていきます。
必要なタイミングでopacityを1にし、役目が終わればまた0に戻ります。

fire.onUpdate.add(function () {
  fire.x = player.x;
  fire.y = player.y-12;
  if (fire.tag.frame >= 0) {
    fire.tag.frame ++;
    if (fire.tag.frame < playerpoweruptime * g.game.fps) {
      fire.opacity = 1;
    } else {
      fire.tag.frame = -1;
      fire.opacity = 0;
      power = 0;
    }
  }
  fire.modified();
});



ヒット判定

敵キャラがプレイヤーにあたったかどうかはhitcircleで判定しています。

今回は敵もアイテムも、円形の当たり判定で統一しました。
縦長などの敵の場合は、判定式を作り直す必要があります。


function hitcircle(enemy, size) { //円状の敵とプレイヤーのヒット判定処理
  if (player.tag.state != "hit") {
    if (((enemy.x-player.x)**2 + (enemy.y-player.y)**2) < (size/2 + playersize/2)**2){
      if (fire.tag.frame < 0) {
        if (player.tag.mutekiframe < 0){
          player.tag.state = "hit";
          // if (soundstate) scene.assets["se_hit"].play().changeVolume(1);
          player.tag.frame = 0;
          player.tag.mutekiframe = 0;
          player.tag.mutekitime = playermutekitime;
          playerhit.tag.frame = 0;
          if (!finishstate) g.game.vars.gameState.score -= 100;
          makelabel(scorelabel.x - 50, scorelabel.y + 50, "" + (-100), uilayer);
        }
      } else {
        if (player.tag.state == "dash"){
          if (!finishstate) g.game.vars.gameState.score += 100;
          makelabel(scorelabel.x - 50, scorelabel.y + 50, "+" + 100, uilayer);
          enemy.tag.state = "hit";
          // if (soundstate) scene.assets["se_attack"].play().changeVolume(1);
        }
      }
    }
  }
}


また、この処理の中ではenemy.xのようにenemyの値を使用します。

この関数はmakeene1などの中で呼び出されますが、カッコの中にenemyをいれてhitcircle(enemy, size);のようにして呼び出します。

これによってmakeene1の中で配置された各enemyの情報をもとに判定処理がなされます。




また長くなってしまいましたが、これで終わりです。

ある程度使い回せるものを用意しようとすると、多くなってしまいますね。
次は使い回しというより参考にするための単純な例で短く作りたいところです。

今回のファイルも記事冒頭においてあります。

画像含め使いまわしてもらって構いません。



[リリース後追記]

リリースしたダックダッシュですが、

割と評判が悪いです。


まあちょっと思いあたる節もあるので、反省点をまとめていきます。

時間が長い

今回は複数の敵がいるので、回避方法が構築される時間があればと思い、
長めの100秒にしていましたが、どうも長いようです。

やはり70秒以下がいいですね。

攻略方法を構築したくなるようなゲームジャンルであれば長くてもいいのかもしれませんが、
このアクションだと敵に受動的になるのでネガティブ印象で固定されるのかもしれません。


敵が多い

テストしていると無駄にプレイスキルが上がり簡単に対処できるようになります。

難易度が上昇するタイプのアクションは、最終局面のみ自分がやりごたえを感じ、
序盤は退屈になる、ぐらいで初見の人にも優しい難易度になるのかもしれません。

たくさんいる敵をまとめて倒したりできればいいなとも思ってたのですが、
少なくとも終盤以外は半分ぐらいで良かったかもしれません。

stage/3+1という比例する形よりも、
1+1*(stage>2)+1*(stage>5)+1*(stage>6)とか、
let enemytable = [0, 1, 1, 2, 2, 2, 3, 4]; を作っておいて enemytable[stage] とか、
にしてステージ7までにする方法もあったかと思います。


敵が機敏すぎる

攻略方法も含めて自分で考えているので、つい簡単に避けられるようになりますが、
ニコ生ゲームは人によっては一生で一度しかプレイしないかもしれません。

攻略法を知らなくても避けれるレベルには、回避のしやすさや予備動作による予告などが必要でしたね。犬、蛇、カニ あたりは初見だときつかったかもしれません。

いっそのことダッシュ中無敵にして、ローリング回避風にしたほうが良かったかも。
これは1165行目を少し長くして下のようにすれば実現できると思います。

  変更前 if (player.tag.state != "dash"){

  変更後 if (player.tag.mutekiframe < 0 && player.tag.state != "dash"){



再利用したいという方がいれば、参考にしていただければと思います…

黒歴史化しそうだし、誰かこの作品が忘れられるレベルの奴に作り直してくれないかなあ。


この記事以外にも、コピペできそうなコードや解説を公開しています。
こちらに記事のリンクがまとまっていますので御覧ください。