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

[2022/12/20加筆]
以下の記事は、FPS60で音ゲーを作る内容です。
ただし、色々プレイ時の反応を見ていると音ズレがやはりあるようです。

開発者がテストプレイしたときの処理と比べて、プレイヤーのPCの処理が極端に速いか、プレイヤーのスマホ等の処理が遅いかするとズレが起きます。
タイミングの判定が甘くなりますがgame.jsonFPSを30にする設定も検討ください。

 


第7回のゲームはFPS60の音ゲーです。

楽曲ファイルを用意すれば譜面データを作って遊べると思います。

譜面データの作り方も解説します。

 

以下がサンプルです。ファイルのBGMとSEはアツマールとは違って自作です。

・シンプル音ゲー ファイル アツマール 



■目次

  • BGM差し替え
  • タイミング計測
  • 譜面作成
  • 制限時間設定
  • タイトル文字
  • SE差し替え
  • ラインモード設定
  • イメージモード設定
  • エフェクトの選択
  • タイミング判定の緩和
  • その他の設定




 

BGM差し替え

BGMはいろんなサイトで公開されているのでお借りしてきましょう。

私はよく https://dova-s.jp/ というサイトで検索します。ニコニコモンズもいいですね。

 

BGMが用意できたらoggファイルとaacファイルに変換し、audioフォルダに入れ、

akashic scan assetをやります。

 

ファイル名はbgmにしたほうが楽です。変える場合はmain.jsの中の"bgm"も書き換えましょう。

今回のファイルに入ってる曲は作曲ソフトのテンプレをつなげた自作のものですが、まあ使い回す人はいないと思います。

 


タイミング計測

曲のスピードはまちまちなので合わせていきます。

558行目ぐらいにある long と short という値を設定します。フレーム数で設定します。fps60だと1フレームが0.017秒です。

  • short 最短で押す可能性があるタイミング。20~30ぐらい。秒数で0.5秒ぐらい
  • long 曲の繰り返しに合わせた長めのタイミング。shortの8倍など。100~300。
       譜面を書きやすくするためのものなので16倍などでもいい

 

もう一つの値としてstartも設定します。shortの繰り返しが曲の再生開始からどれぐらいで開始するかを表します。曲のイントロ次第なのでマイナスになったり数百になることもありえます。今の所一桁の例が多いです。

 

 

これらの値を計測する方法ですが、ゲームのデバッグモードを利用します。

11行目にあるdebugmode を debugmode = true;に書き換えてakashic-sandboxでテスト起動します。

 

ゲームをテスト起動できたら、まずタップの対象になりそうな最短のタイミングを等間隔でタップしていきます。曲に合わせていい感じに譜面を作るのはまだ先です。

 

押すたびに画面に数値が出ますがこれはメモる必要はありません。下側にstartとshortの推奨値が出るのでこれをmain.jsに入力します。

startの値は画面に収まりきらなくなって曲が終わるぐらいまでは計測しましょう。精度が上がります。さらに5回ぐらいやり直して平均を取るのがベストです。

 

longの値も計測してもいいですが、shortの値の8倍などを計算して入れても大体合うと思います。

どの値も少数になっても大丈夫ですが、マイナスが許されるのはstartだけです。

 

当然ながらゲームをリリースするときは debugmode = false; に戻しましょう。

 

 

譜面作成

譜面データは564行目にあるような形で入力します。

 

        let table = [
            [  ,  ,  ,  , 0,  ,  ,  ],
            [ 0,  ,  ,  , 0,  ,  ,  ],
            [ 0,  ,  ,  , 0,  ,  ,  ],
            [ 0,  ,  ,  , 0,  ,  ,  ],

            [ 0,  ,  ,  , 0, 0,  ,  ],
            [ 0,  ,  ,  , 0, 0,  ,  ],
            [ 0,  ,  ,  , 0, 0,  ,  ],
            [ 0,  ,  ,  , 0, 0,  ,  ],

            [ 0, 0,  ,  , 0, 0,  ,  ],
            [ 0, 0,  ,  , 0, 0,  ,  ],
            [ 0, 0,  ,  , 0, 0,  ,  ],
            [ 0, 0,  ,  , 0, 0,  ,  ],

            [ 0,  , 0,  , 0,  , 0,  ],
            [ 0,  , 0,  , 0,  , 0,  ],
            [ 0,  , 0,  , 0,  , 0,  ],
            [ 0,  , 0,  , 0,  , 0,  ],

            [ 0,  , 0,  ,  , 0, 0, 0],
            [ 0,  , 0,  ,  , 0, 0, 0],
            [ 0,  , 0,  ,  , 0, 0, 0],
            [ 0,  , 0,  ,  , 0, 0, 0],

            [ 0,  ,  ,  ,  ,  ,  ,  ],
        ];

1行ごとに一つの配列がありますが、配列の長さはlong/shortです。

long = 192、short = 24なら配列の長さは8です。一つのlongの期間の中の8つのタイミングを表しています。

タップを要求するタイミングでは数字を入れ、要求しない場合は空白を入れます。空白には半角スペースを2つ入れていますが単に見た目を揃えるためです。

 

タイミングの数字は基本は0ですが、フレーム数を入れて微妙に遅らせることもできます。shortの半分の値を入れれば裏拍的なタイミングも可能です。

マイナスとshortより大きい値は不可です。処理上の問題でshortとの差が1より小さくても読み取られなくなります。

 

また、最初の数カ所はタイミング用の画像を出すのが間に合いません。step/2/shortにあたる2~3個のタイミングは数字を入れてもタイミング用画像が出てきません。

 

longの区切りがうまく行かずに一つずれてしまうこともあります。その場合はstartの値にshortをいくつか足したり引いたりして調整してください。

 

 

最終的にテストプレイしてみてタイミングがずれていると感じるなら計測値は無視してstartの値を1や2足したり引いたりするのが良いです。

 

 

制限時間設定

制限時間を設定しておくと終了の文字が出るようになります。

すべてのタイミング用画像がでて1,2秒たったあたりになるように数字を設定します。

 

制限時間はまずmain.jsの54行目のtimelimitに入力します。

 

もう一箇所ニコ生ゲーム用にゲームそのものを終了させる制限時間を設定します。game.jsonの方の最後にあるtotalTimeLimitに値を入力します。

こちらの値は説明を見る時間とロード時間を吸収するために15~20秒余裕を持って設定します。main.jsの制限時間が65秒だったので17秒加えて82秒としています。

 

 

タイトル文字

タイトル文字は471行目のtextを変えれば変わります

        let titlefont = new g.DynamicFont({
            game: g.game, fontFamily: "sans-serif", fontWeight: "bold",
            size: 96, fontColor: "hsl(180 , 100%, 50%)", strokeWidth: 2, strokeColor: "hsl(180 , 100%, 50%)",
        });
        new g.Label({// タイトル文字
            scene: scene, text: "シンプル音ゲー", parent: warmuplayer,
            font: titlefont, fontSize: 72,
            x: g.game.width/2, y: 115, anchorX: 0.5, anchorY: 0.5, opacity: 1,
        });

 

文字数が多くて収まらない場合はfontSizeを72から60などにします

文字色を変えるときは468行目のfontColorとstrokeColorを変えます。

最初の180という数字でシアンになっているので0なら赤、60なら黄色になります。

 

 

SE差し替え

タップした時に鳴るSEはドラムかなにかの音を作曲ソフトから自前で用意しています。

 

適当な効果音サイトから持ってくればBGMと同じように差し替えられます。

ファイル名はseにしておくと楽です。oggaacが必要です。

 

SEは冒頭の間を極限まで削ると、押した瞬間に音がなってる感触が得られます。0.1秒刻みぐらいで切り詰めるといいです。

 

 

ラインモード設定

タップするターゲットが出る位置を変更することができます。

12行目linemodeをfalseにするかtrueにするかで変わります。

  • linemode = false 画面内のランダムな場所に発生 花火太鼓と同じ ハード
  • linemode = true 画面下寄りのライン上に発生 花火太鼓2と同じ イージー

 

 

イメージモード設定

13行目imagemodeをtrueにすれば、タップするターゲットを画像に変更できます。

 

画像を変更する場合はimageという名前で用意します。画像のサイズが変わる場合はakashic scan assetをやってください。

 

画像のサイズがタップできるエリアに相当します。外周の空白が多い画像にすればタップの判定を甘くすることができます。標準で入れてある画像は200x200です。

 

 

エフェクトの選択

エフェクトは標準でいくつか用意しているので238行目辺りでどれを使うか選択してください。

複数のエフェクトを使うこともできますが重くなる恐れがあるのでおすすめしません。重くなって曲の進行に対してゲームの進行が遅れると、音ゲーの前提が崩れそうです。

 

エフェクトの発生数も変えられます。しかしこれも過剰にすると重くなりかねません。

 

画像やアニメーション画像を使うタイプもあります。エフェクト用のアニメーション画像素材もウェブ上で公開されている事があります。

 

用意されている画像は適当に作ったやつなので差し替えてもらうのもいいと思います。使ってもらっても構いませんが。

 

タイミング画像の残留時間

タイミング画像はタップしたあとも一定時間残るようになっています。

画像のズレから早すぎたのかおそすぎたのか判断するためです。

 

しかし次の画像が見づらいというフィードバックが来ました。ごもっともです。

 

時間を短くする場合は次の214行目の最後の数字を変えてください。

        scene.setTimeout*1 target.destroy();
        }, 1000);

 

ミリ秒で指定するので200 にすれば0.2秒になります。

数字を0にするのはやったことがありませんがうまく処理されないかもしれません。

 

 

タイミング判定の緩和

タップしたタイミングは1フレーム単位で判定することができます。

しかし、そもそも曲と譜面のタイミングのズレが完璧とは限らないのに、プレイヤーの1フレームのズレをとがめるのは少々納得感にかけます。

 

そこで220行目と221行目判定が近いとすべて満点にする処理を追加しています。

    point += 0; //タイミング判定の緩和 0~3推奨
    if (point > 100) point = 100;

 

point += 2; のようにすると、ズレが2フレームまでは100点になります。

花火太鼓2は0フレームにしてみました。

花火太鼓の方は2フレームにしていましたが、総合で満点を出された方は一人でした。

 

ちょっと反省点なのが画像の重なりでタイミングのズレを見れるようにしたせいで、満点にしても画像はズレてしまいます。

判定を厳しくして理不尽な感じさせるのとどちらがいいかというところですね。

 

緩和は入れつつ動かす方の四角のサイズを少し小さくすれば良かったかもしれません。

190行目のwidthとheightの指定に -10 等を書き加えれば小さくできます。

 

 

その他の設定

タップするターゲットの四角は140行目辺りで調整できます。

  • size 四角の大きさ 大きくするとタップしやすい 標準150
  • edge 四角の枠の太さ 標準10
  • swing 四角が移動する距離 小さくすると遅くなる 標準1000
  • step 四角が発生するフレーム数 大きくすると遅くなる 標準120 整数のみ

 

 


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

*1:) => {
            if (!target.destroyed(

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

物理回とマルチ回が終わったのでノーマルな「コピペしよう」シリーズを再開します。

第6回はシンプルな動きのゲーム集です。

使いまわせそうなシンプルなゲームデザインで複数のサンプルを用意しました。

 

ファイルはこちら。 絵はこちら

※2022/7/16 13:28 スコア処理を追加しファイルを更新

ゲーム画面はまちまちですが、画像はシンプルにし音声ファイルはakashic公式のものを使っています。アツマールはこちらです。

[2022/12/20加筆]
以下の記事は、アツマールがサービス提供中の時点の内容です。
アツマールがサービス終了したあとの手順はこの一連の記事では説明していません。
別途、公式チュートリアルや解説記事を探してください。

 

 


■目次

  • 改造準備のおさらい
  • 画像ファイルの変更
  • 音声ファイルの変更
  • 制限時間の変更
  • ゲームの公開
  • サンプルゲームの特徴と注意点

改造準備のおさらい

改造に必要な流れを簡単に説明し、それぞれの説明があるリンクをまとめます。

新しくプログラムについて書くことが特にないのでおさらいです。

 

改造ができるようになるまでには以下の2つが必要です。

  • Akashic Engineのインストール
  • 起動テスト akashic-sandbox

 

公式の記事だとこちらに該当します。こちらこちらも参考になります。

 

記事の中で akashic init ~ のようなコマンドの説明もありますが、今回は不要です。

 

画像ファイルの変更

画像ファイルを変えると簡単に雰囲気を変えられます。以下の手順が必要です。

  • 画像ファイルをimageフォルダに保存
  • akashic scan asset を実行する(自動でgame.jsonに記載する)
  • main.jsのassetIdsにファイル名を追記
  • 各g.Spriteのsrcの項目にファイル名を記入

 

公式だとこちらこちらに記載があります。こちらも参考になります。

 

画像ファイルとその名前は、上記の4つの位置に正しく配置します。

 imageフォルダ ≧ game.json ≧ main.jsのassetIds ≧ 各g.Spriteのsrc

のように左のほうが多くなっていれば使わないものがあっても大丈夫です。

左に含まれていないと、エラーメッセージが出たりロードが途中で止まったりします。

 

画像ファイルを削除したときもakashic scan assetでgame.jsonの登録を削除できます。

画像のサイズを変更したときもakashic scan assetをやります。ファイル名が同じでもgame.jsonにサイズ情報が含まれているためです。

 

アニメーション画像のFrameSpriteが使われてる場合は以下も必要になると思います。

  • 画像の結合
      私はこちらのサイトで背景を透過させて使っています。

  • g.FrameSprireの画像サイズ情報を設定
     
     width, height, srcWidth, srcHeight の4つを書き換えます。

音声ファイルの変更

音声ファイルも基本は同じです。

  • 音声ファイルをaudioフォルダに保存
  • akashic scan asset を実行する(自動でgame.jsonに記載する)
  • main.jsのassetIdsにファイル名を追記
  • 音声ファイルを再生する処理にファイル名を記入

音声ファイルはaacファイルとoggファイルの両方が必要です。

aaciOSで使います。iOSだけロードが終わらない不具合が出た場合はだいたいaacファイルが抜け落ちているか、aacファイルが壊れています。

 

BGMを変えて再生時間が変わったときも、akashic scan assetをやる必要があります。

 

制限時間の変更

今回のサンプルで制限時間を変える場合、2箇所編集する必要があります

  • main.jsの50行目ぐらいにある let timelimit
  • game.jsonの最後にあるtotalTimeLimit

 

1つ目はゲーム内で競技を開始してから得点が入らなくなるまでの時間です。

2つ目はゲームそのものが起動してからゲームが終了するまでの時間です。

 

これに加えてゲーム起動から競技開始までの時間をmain.jsの9行目ぐらいにlet warmupとして記載しています。warmupは基本的に7秒にしています。

 

競技時間timelimitを60秒にした場合、67秒がゲームに必要な時間です。

更にロード時間として余裕を持って10秒を加え、77秒をtotalTimeLimitとしています。

 

ゲームの投稿

最後にゲームを投稿します。

  • akashic export htmlを実行する
     akashic export html -f --output file.zip --atsumaru --inject ./injectdata.txt
     とコマンドを入力します。
    アツマールに投稿する
     
    アイコンも作っておきます
  • ニコ生ゲームとして登録申請する
     
    アツマールで公開設定にしていないと申請できません

akashic exportの最後のinjectdata.txtというのはアツマールでの背景色指定です。
現在は中にblackと書いてあります。不要ならファイルとコマンドを削りましょう。

 

公式だとこちらです。こちらも参考になります。

 

サンプルゲームの特徴と注意点

冒頭の今回のファイルに入っているゲーム一覧です。

基本的な動きなどに焦点を当てているので、ゲームとしては作り込まれてません。

画像変えるだけでも一応ゲームにはなりますが、こだわる場合は他のサンプル等を参考にして要素を追加してください。

ボタン系

  • button
     ボタンをタップするだけ もぐらたたきに近い
  • buttontile
     敷き詰められたタイルをタップして操作する クロスカウント風
  • buttonupgrade
     アップグレードボタンで生産をインフレさせる おにぎり量産体制風
     設定で連打用ボタンや、1度しか使えないボタンも可能

 

ジャンプ系

  • jump
     強制スクロールで流れてくるアイテムをジャンプで取る カエル跳び競走風
  • jumpdouble
     2段ジャンプ
  • jumphop
     細かくジャンプして高さを保つ フラッピーバード風
  • jumplow
     タップの長さでジャンプを調整可能
  • jumpplat
     足場の上に乗れる
  • jumpset
     足場と障害物にパターンが有る

 

移動系

  • move
     ドラッグして移動 流れてくるアイテムを取る 高速に移動できる
  • movedir
     ドラッグして移動する方向を指定
  • movejump
     ドラッグで移動方向を指定し、はなすとジャンプ 餃子の皮サバイバル風
  • movepoint
     タップで移動先を指定 はなしても到達するまで移動する
  • moveroll
     ドラッグで移動方向を指定し、はなすと無敵のローリング
  • moveslide
     ドラッグで加速方向を指定 慣性があり滑る

 

スクロール系

  • scrolljump
     上の方向にジャンプしていく ぴょんクラ・ニンジャン風
  • scrollmove
     moveslideのスクロール版 見下ろしレースゲームのコーナリング
  • scrollshot
     shotautoのスクロール版 世間で人気の吸血鬼サバイバル風

スクロールがある場合は起動テストに akashic serve -s nicolive を使います。

エラーも出ますが気にせず左上のリセットボタンを押しましょう。

 

射撃系

  • shot
     右方向のみに射撃 ドラッグで移動 横スクロールSTG
  • shotaimrun
     周辺をタップして射撃 キャラをドラッグで移動 まどゾン風
  • shotauto
     近くの敵を自動的に射撃
  • shotdir
     ドラッグした場所に射撃 キャラ固定 この戦いが終わったら風
     武器アップグレードも作ってあるのでこれだけそのままリリース
  • shotdirrun
     ドラッグ方向に射撃と移動
  • shottrig
     タップして射撃 キャラ固定 偏差射撃 縁日射的風

 

タイミング系

  • timing
     タップしたときの精度で得点計算 FPS60 スピード伐採風
     ゲージとしてパワーバー、仰角決定、図形一致、の3種類を用意
     ゲージの再編成、演出の強化が必要

 

 

以上です。

 

これらのサンプル利用時のクレジット表記について、基本的には表記は不要としているのですが…

今回はほとんどこちらではリリースしていません。万一複数の方が利用された場合にどっちがどっちのパクリ、のような話になると厄介です。軽く触れていただくかgm26499をコンテンツリー親登録をやっていただくと回避できるかもしれません。

 

今回のファイルは画像はすべて自作、音声はすべてAkashic公式です。

画像はせっかくなんで別のものに変えてみてほしいですが、足場とか弾なんかはめんどくさかったら使ってもらって構いません。

Akashic公式の音声ファイルの利用表記はそちらの指示に従ってください。ニコニコモンズにある公式の素材なんかも使うといいと思います。

 

 

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

ニコ生ゲームを作ろうと思ったときのリンク集

ニコ生ゲーム関連の情報は様々な場所にあります。

出会うのが大変なのでここにまとめておきます。

 

■目次

  • ニコ生ゲーム
  • Akashic Engine
  • アツマール
  • 公式の情報発信・窓口
  • ユーザー解説記事
  • ユーザーソースコード
  • ニコ生ゲームとは関係ない情報

 

ニコ生ゲーム

入門編

ニコ生ゲームの基礎
  最低限のインストールと泥棒バスターの改造記事がある

 ニコ生ゲームを作ろう・改造編

 ニコ生ゲームを作ろう・ランキング対応ゲーム編
  制限時間などについて

 ニコ生ゲームを投稿しよう

 

ニコニコ生放送で遊べるゲームの作成
 
 上と内容がかぶるが若干詳細な内容

 ニコ生ゲーム

 ranking モードのゲーム作成
   制限時間・共通乱数などについて

 投稿と申請

 

今日からはじめるニコ生ゲーム制作
 公式生放送 半分ぐらいゲームで遊んでいますが貴重な動画資料

 

生ゲームプレイ中
 ニコ生ゲームをプレイ中の放送を探せるページ

 

中級編

公式配布の画像・音声素材
 営利利用不可ですがクリ奨は「ニコニ・コモンズにおける営利利用」ではないとのこと

ニコ生ゲーム関連の仕様

つまづきポイントと対策集
 マルチでアツマールのみの処理をする場合の記載

デザインガイドライン

公式ニコ生ゲームのソースコード

 

上級編

ニコ生ゲームを作ろう・マルチプレイゲーム編

ユーザーIDの取得 マルチプレイツールの作成

ユーザー名の利用 概要

ユーザー名の利用 詳細

multi_admission モードのゲーム作成

 

Akashic Engien公式

ニコ生ゲームを1から作ったり要素を追加するにはAkashic Engineの習得が必要です。

入門編

Akashic Engine入門
 Akashic Engineで一通りできるようになりたい人向け

Akashic Engine 逆引きリファレンス
 やりたいことからコードの書き方を調べられる

Akashic Engineサンプル集1
 ページ内メニューもしくは一番下にリンクあり

Akashic Engineサンプル集2

 

中級編

Akashic Engine リファレンス
 Akashic Engineの詳細  右上のメニューや検索ボックスから色々見れます

物理エンジンライブラリAkashic-box2d

拡張ライブラリ・周辺ツール
 ページ内の一番下にリンクあり

素材の推奨仕様

よくある落とし穴

 

上級編

v2 からの移行
 古いサンプルをv3にするとき用

高速化TIPS

Akashic-box2dのボディ関係のパラメータ

公式のビットマップフォントを作るツール1

公式のビットマップフォントを作るツール2

Akashic Engineのコンセプト紹介

 

アツマール

ニコ生ゲームとは直接関係ありませんが、アツマール向けの機能も追加すればアツマールのプレイヤーにより楽しんでもらえます

 

ニコ生ゲームとは関わりの薄いものが多いですが、以下の機能は活用できそうです

スコアボード概要

スコアボード詳細

おれいポップアップ詳細

 

アツマールのマルチモード

 

公式の情報発信・窓口

ニコ生自作ゲーム窓口 Twitter
 Akashic Engine も ニコ生ゲーム関連の質問もこちら

ニコ生ネタ情報局
 ニコ生ゲーム関連のイベントなどの情報

 

Akashic Engineの更新情報
 機能が更新されたときに差分が掲載される

 

アツマールのブロマガ

アツマール Twitter

 

ユーザー解説記事

初級編

お茶漬けびよりさんの作例記事 typescript

 

魔物さんの作り方まとめ記事

 

ドワンゴ所属の方の環境構築からゲーム投稿までの記事 javascript 2019年

 

そのいちさんの環境構築からゲーム投稿までの記事 javascript 2018年

 

筆者の記事 長い javascript

 

中級編

フェルミウム湾さんのRPGツクールMZからニコ生ゲームへの移植記事

ツクールで出力されるjavascriptをコピペするという期待の新手法

 

上級編

やっしーさんのAkashic Engineマルチゲームの特徴についての解説

 

やっしーさんのマルチにおけるアニメーション同期の記事

 

ニコ生ゲームとWEBカメラを連携させる拡張プラグインの記事

Akashic Engine運営の方?

 

ユーザーソースコード

伝説の開発者5.0 ★★★★★さんのソースコード githubで公開 typescript

 

みんなでジグソーパスルの開発者むじゅりんさんのソースコード

githubで公開 typescript

 

筆者のソースコード zipで配布 javascript

 

ニコ生ゲームとは関係ない情報

Box2dの英語の説明

QiitaにあるBox2dの解説

Box2Dで力とかを加える方法

Box2Dジョイントの解説

 

ffmpeg で音のボリュームを調整する

FFMPEG で指定時間でカットする

 

以上です。

あまりにも古い記事や個人的にはあまり使わない拡張機能の記事は省いています。

 

記事アイコン用画像

 

ニコ生ゲームを作ろうと思った時のコピペ用置き場


Akashic EngineV3以降に作ったゲームを公開していきます。

すでに記事で紹介したものもまとめています。

「ファイル」と書いてあるところがソースコードのリンクです。

 

画像を変えただけとか制限時間を変えただけとかでも歓迎ですので、

色々改変してみてください。

 

 

動かすには最低限こちらこちらこちらの内容を理解する必要があります。

 

 

 

 ・ファイル 解説記事

 ・アカシックのサンプルコードから改造しているのであまり整理されていない

 

 

 ・ファイル 解説記事

 ・画像差し替えで改変可能。多数実績あり

 

 

 ・ファイル 解説記事

 ・画像アニメーション有無がややこしかった。後悔の残ったサンプル

 

 

 ・ファイル 解説記事

 ・カクつきの報告あり

 ・正直誰かにfps60のタイミングゲーをこれを使わずに1から作って欲しい 需要はあるはず

 

 

 ・ファイル 解説記事

 ・画像はいらすとやから差し替え済み

 

 

 ・ファイル 解説記事

 ・画像はいらすとやから差し替え済み

 

 

 ・ファイル 解説記事

 ・マルチ

 ・ゲームではない

 

 

 ・ファイル 解説記事

 ・マルチ

 ・ユーザー名前利用周りでバグを多く出してしまった 修正したがまれにバグる

 ・色変えシステムがあるせいで画像を用意するのがめんどくさい

 

 

 ・ファイル 解説記事

 ・マルチ

 ・名前利用バグがまれに有る、色変えシステムが有りめんどくさい

 

 

 ・ファイル 解説記事

 ・マルチ

 ・名前利用バグがまれに有る、色変えシステムが有りめんどくさい

 

 

 

 ・ファイル

 

 

 

 ・ファイル

 

 

 ・ファイル

 ・名前利用バグがまれに有る、色変えシステムが有りめんどくさい

 ・モードが多すぎる どうなってるのかわからない

 

 

 ・ファイル

 ・名前利用バグがまれに有る、色変えシステムが有りめんどくさい

 ・人数に応じてマップサイズを変えたりしている ややこしい

 

 

 ・ファイル

 

 

 ・ファイル

 ・スクロールあり akashic serve -s nicolive で動作確認する必要あり

 

 

 ・ファイル

 ・スクロールあり akashic serve -s nicolive で動作確認する必要あり

 

 

 ・ファイル

 ・タイミングゲーだがfps30

 

 

 ・ファイル

 ・スクロールあり akashic serve -s nicolive で動作確認する必要あり

 

 

 ・ファイル  解説記事

 ・物理

 

 

 ・ファイル 解説記事

 ・物理

 

 

 ・ファイル 解説記事

 ・物理

 

 

  ・ファイル 解説記事

 ・物理

 

 

 ・ファイル

 

 

 ・ファイル

 ・画像はいらすとやから差し替え済み

 

 

 ・ファイル 解説記事

 ・物理

 

 

 ・ファイル

 ・物理

 ・スクロールあり akashic serve -s nicolive で動作確認する必要あり

 

 

 ・ファイル

 ・スクロールあり akashic serve -s nicolive で動作確認する必要あり

 ・得点計算処理に不具合あり 修正済みだが参考にするのはおすすめできない

 

 

 ・ファイル

 ・物理

 ・スクロールあり akashic serve -s nicolive で動作確認する必要あり

 

 

 ・ファイル 解説記事

 ・物理

 ・スクロールあり akashic serve -s nicolive で動作確認する必要あり

 ・ゲームモードに放送者用の"single"が含まれています

  不要であればgame.jsonを編集して削除してください

 

 

 ・ファイル

 

 

 ・ファイル

 ・物理

 

 

 ・ファイル 解説記事

 ・物理

 

 

 ・ファイル 解説記事

 ・物理

 

 

 ・ファイル

 ・物理

 

 

 ・ファイル 紹介記事

 ・シンプルな動きのゲーム詰め合わせ 記事に説明あり

 

 

 ・ファイル

 ・物理 distancejoint

 

 

 ・ファイル

 ・物理

 ・画像はいらすとやから差し替え済み

 ・ランキングスコア送信にディレイあり

 

 


こちらに記事のリンクがまとまっていますので御覧ください。

 

記事アイコン用画像

 

ニコ生ゲームを作ろうと思ったら物理的に考える その7


物理回の最後は物理マルチゲームについてです。

 

結論を先にいうと、厄介な不具合が発生する恐れがあります。

その際の注意点を紹介していきます。

 

今回の記事用に作ったゲームはありませんが、以下の過去作が物理マルチです。

エアリアルサッカー アツマール

ドアラッシュ アツマール

 

残念ながらソースコードは公開していません。

昔の書き方がメチャクチャでお見せできないためです。

 

■目次

  • 物理マルチで発生する不具合
  • 展開ズレの事例
  • 物体配置時の注意点
  • 公式提供の修正パッチ

 

物理マルチで発生する不具合

物理マルチ特有の不具合として、プレイヤーごとにゲームの展開が異なってくるというものがあります。

よくあるのが最後のスコア結果がプレイヤー毎に違っていて発覚するケースです。

 

呼び方としてはパラレルワールドとか違う世界線などと呼ばれることもありますが、個人的には展開ズレと呼んでいます。

 

一般的には同期ズレという言葉がありますが、Akashic Engine のマルチは同期はさせていません。やろうと思えば同期もできるはずなんですが技術が足りずやっていません。

冷静に考えたら同期の処理はしていませんが、一致している状態を「同期している」と呼んでいい気がしてきました。同期ズレでいいと思います。

 

この展開ズレはプレイしている環境のブラウザ(chrome,firefox,ニコ生アプリ等)が異なると発生しやすいイメージがあります。しかしプレイヤーの報告を聞いていると同じブラウザでも発生していそうです。

 

展開ズレの事例

1つ目はエアリアルサッカーでの事例で、こちらの画像になります。

これは左がChromeでプレイした映像で、右がFireFoxでプレイしています。

下にある135度の角に向かって四角い物体で体当たりをしているのですが、左右で跳ね返る方向が大きく変わっています。

 

2つ目はドアラッシュでの事例ですが、こちらの動画になります。

左右のブラウザはChrome,FireFoxで同じです。

四角い角をぎりぎり通り抜けようとしてあたったことになるかどうかで、大きく結果が変わってしまっています。

 

物体配置の注意点

1つ目の事例は、実はこの135度の角に2つの物体がありました。

下のようなイメージです。

このせいでどちらの物体に衝突したことになるかで、大きく反射角度が変わったのではないかと思っています。

 

これは三角形を作るのがめんどくさかっただけなのですが、不具合を回避するため領域が重ならないように三角形を配置しました。

この対策はうまく行ったようで、たまに発生していたレベルのものが全く発生しなくなりました。

結論として物体配置の注意点としては「衝突が起こりうる角には物体を重ねない」になります。

 

そもそもの理想としては一つの物体で作ってしまうことです。

ただ、このときは坂と坂の上で摩擦の設定を変えたかったので、苦肉の策で別の物体として配置していました。

 

公式提供の修正パッチ

2つ目の事例は物体の重なりは無く、単純な四角形と単純な円形の衝突でした。

これは公式にも質問してみたのですが、三角関数の誤差がブラウザによって異なることがあるようで、それが原因だったのではと思っています。

小数点以下10桁というレベルですが、計算の過程で増幅して結果が変わってしまう事があるそうです。

 

対策としてこちらpatchBox2DMathの項目にあるパッチを適用しました。

 

導入方法ですが、パッチのファイル自体はakashic-box2dをインストールしたときについでに導入されています。

ゲーム内で使用できるようにするのに以下の2つが必要です。

  1. game.jsonへの登録

    追加した追加機能はgame.jsonに登録する必要があります。
    akashic-box2dはインストール時に登録されますが、patchは手動で追記します。

    追記内容は moduleMainScriptsに1行globalScriptsに3行あります。

        "moduleMainScripts": {
            "@akashic-extension/akashic-box2d": "node_modules/@akashic-extension/akashic-box2d/lib/index.js",
            "@akashic-extension/akashic-box2d/patch": "node_modules/@akashic-extension/akashic-box2d/patch/index.js",
            "box2dweb": "node_modules/box2dweb/box2d.js"
        },

        "globalScripts": [
            "node_modules/@akashic-extension/akashic-box2d/lib/Box2D.js",
            "node_modules/@akashic-extension/akashic-box2d/lib/ContactManager.js",
            "node_modules/@akashic-extension/akashic-box2d/lib/index.js",
            "node_modules/@akashic-extension/akashic-box2d/lib/parateters.js",
            "node_modules/@akashic-extension/akashic-box2d/patch/box2d.js",
            "node_modules/@akashic-extension/akashic-box2d/patch/math.js",
            "node_modules/@akashic-extension/akashic-box2d/patch/index.js",
            "node_modules/box2dweb/box2d.js"
        ]

  2. main.jsへの登録

    akashic-box2dと同じように以下の文字列を追加します。

        let patch = require("@akashic-extension/akashic-box2d/patch");
        patch.patchBox2DMath(box2d, { tableSize: 8192 });

    ただし、記載場所はbox2dの後ろである必要があります。
    let box2d = new b2.Box2D(world); のような行があればその後ろです。

導入できているかどうかを知る方法はわかりませんが、エラーはなくなると思います。

 

ドアラッシュに関してはちょくちょく起きていた不具合を抑制する効果がありました。

ただし、展開ズレを完全に無くせたわけではありませんでした。

 

こちらもざっくりとした対策として、物体を回転させたり曲線の動きで三角関数を多用するのは避けたほうがいいかもしれません。
ただし、この指針の効果は確認できていません。

 

 

 

Akashic Engine関連の書き方紹介はおそらく今回で終わりです。

 

特にプログラミングに詳しくもないのに偉そうな記事を書くのは抵抗がありましたが、Akashic Engine関連で調べた結果を記録していけばいつかうっすらと参考にされる機会があるんではないかと思い続けてきました。

 

一方で記事に紹介できるような書き方を優先して、ゲームの面白さの優先順位を下げてゲームを作ってきてしまった感があります。

 

更にまた一方ではモチベーションを保つためサンプルをニコ生ゲームとしてリリースしつつゲームに盛り込む要素もこだわってしまい、参考にしやすいシンプルな形にはなっていなかったとも思います。

 

色々反省は多かったですが、得た代償として最後まで続けられたと考えればまあいいかというところです。

 

 

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

 

ニコ生ゲームを作ろうと思ったら物理的に考える その6


今回は新たに3つのジョイントを紹介していきます。

 

以下がサンプルです。音は抜いています。

・天秤クイズ ファイル アツマール 

・関節ラン ファイル アツマール

・物理もぐらたたき ファイル アツマール

 

 

■目次

  • 回転ジョイントの基本 .Joints.b2RevoluteJointDef()
  • 背景にピン止めする
  • ジョイントを回転させる
  • 回転の可動範囲を設定する
  • 特定の角度に制御する
  • 複数の関節を組み合わせる
  • SetFixedRotationとの組み合わせ
  • ピストンジョイントの基本 .Joints.b2PrismaticJointDef()
  • ピストンを動かす
  • ピストンの可動範囲を設定する
  • ピストンの現在位置を読み取る
  • バネの動きをさせる
  • 固定するジョイントの基本 .Joints.b2WeldJointDef()

 

回転ジョイントの基本

回転ジョイントは角度の制御や滑車や車輪を作ることができます。

回転させるだけならb2Body.SetangularVelocityでもできますが、中心以外を軸にして回転させるのは面倒です。

また、他の物体との干渉で回るようにする場合も回転ジョイントが適しています。

 

回転ジョイントを作るには、b2.Box2DWeb.Dynamics.Joints.b2RevoluteJointDef()を使って「回転ジョイントの設定」を作ります。

 let def = new b2.Box2DWeb.Dynamics.Joints.b2RevoluteJointDef();

 

 

そしてInitializeを実行して物体と回転の中心を指定します。

   def.Initialize(body1.b2Body, body2.b2Body, box2d.vec2(x, y));

 

順に、固定する物体回転する物体回転の中心座標です。

このコマンドはマウスジョイントにはありませんでした。

 

物体はb2Bodyで指定します。body1とbody2の名前は適宜変わります。

回転の中心座標はゲーム全体の座標で指定します。固定する物体の座標からの位置を計算する必要はありません。

また、中心座標がそれぞれの物体から外れた位置にあっても構いません。

 

最後に回転ジョイントの設定でジョイントを作ります。

        box2d.world.CreateJoint(def);

 

 

背景にピン止めする

物体と物体を固定せずに背景にピン止めすることもできます。

 

initializeの1つ目box2d.world.GetGroundBody()を指定します。

            let def = new b2.Box2DWeb.Dynamics.Joints.b2RevoluteJointDef();
            def.Initialize(box2d.world.GetGroundBody(), body.b2Body, box2d.vec2(x, y));
     ︙
            box2d.world.CreateJoint(def);

 

円形の物体の中心を背景に固定すれば滑車やコンベアのようなものも作れます。

 

 

ジョイントを回転させる

ジョイントを回転させる場合以下の3つのパラメータを設定します。

  • maxMotorTorque
    回転に加えられる最大の回転力
    回転を阻むものがあっても必要な力がこの値以下なら無理やり回転する
    この値以上が必要なら回転が止まる
    値の基準は1000などだが、10~100000のように大きく変えて探るのがおすすめ

  • motorSpeed
    回転のスピード
    0.1程度あれば回転しているのがわかる
    maxMortoTorqueを大きくしてもこのスピード以上では回転しない
    プラスで時計回り マイナスで反時計回り

  • enableMotor
    モーターを有効にするかどうか
    maxMotorTorqueとmotorSpeedの設定でモーターを有効にするかどうか
    trueかfalseを指定
    無効にすると重力や他の物からの干渉で自由に回転する
    mortorSpeedを0にしてenableMotorをtrueにしていると回転しない

 

ジョイントを作る際に設定するには以下のように記載してからCreateJointをやります。

            let def = new b2.Box2DWeb.Dynamics.Joints.b2RevoluteJointDef();

            def.Initialize(body1.b2Body, body2.b2Body, box2d.vec2(x, y));
            def.maxMotorTorque = 1000;
            def.motorSpeed = 1;
            def.enableMotor = true;

            box2d.world.CreateJoint(def);

 

画面をタッチした時のように後でモーターの動きを操作する場合は、CreateJoint時に名前をつけておいて以下のように記載します。

            let joint = box2d.world.CreateJoint(def);

     ︙

            joint.SetMotorTorque(2000);
            joint.SetMotorSpeed(2);
            joint.EnableMotor(false);

回転の可動範囲を設定する

膝の関節のように可動範囲を設定することもできます。

以下の3つのパラメータを設定します。

  • lowerAngle
    可動範囲の最小値
    回転する角度を制限する
    モーターの力や他の物体から受ける力でも基本的にこれ以下に回転しない
    ラジアン角度で指定するのでMath.PIで180度
    6*Math.PIのように360度以上を指定すると、0度ではなく3回転分になる
    マイナスも可

  • upperAngle
    可動範囲の最大値
    基本的にこれ以上に回転しない

  • enableLimit
    可動範囲を有効にするかどうか
    lowerAngleとupperAngleの設定で回転を制限するかどうかを設定
    trueかfalseを指定

 

Motorの設定と合わせると以下のように記載します。

            let def = new b2.Box2DWeb.Dynamics.Joints.b2RevoluteJointDef();

            def.Initialize(body1.b2Body, body2.b2Body, box2d.vec2(x, y));
            def.maxMotorTorque = 1000;
            def.motorSpeed = 1;
            def.enableMotor = true;
            def.lowerAngle = Math.PI*0;
            def.upperAngle = Math.PI*1;
            def.enableLimit = true;

            box2d.world.CreateJoint(def);

 

画面をタッチした時のように後でモーターの動きを設定する場合は、CreateJoint時に名前をつけておいて以下のように記載します。

            joint.SetLimits(Math.PI*0, Math.PI*2);

            joint.EnableLimit(false);

SetLimitはカッコの中で2つの値をlower、upperの順に指定します。

 

特定の角度に制御する

関節ランではクリック位置に応じて腰と膝の角度を制御していました。

回転ジョイントで「90度に保つ」のように特定の角度に制御することは可能です。

 

ただしスマートな方法ではなく、upperlimitとlowerlimitを制御したい角度に指定していました。

        let pointx = 0;
        let controlx = 0;
        touch.onPointDown.add(function(ev) {
            pointx = ev.point.x;
            controlx = (pointx - g.game.width/2) / 300;
            if (controlx > 1) controlx = 1;
            if (controlx < -1) controlx = -1;
            setcontrol();
        });

        touch.onPointMove.add(function(ev) {
            pointx += ev.prevDelta.x;
            controlx = (pointx - g.game.width/2) / 300;
            if (controlx > 1) controlx = 1;
            if (controlx < -1) controlx = -1;
            setcontrol();
        });

        function setcontrol() {
            joint.SetLimits(Math.PI * (0.1 * controlx), Math.PI * (0.1 * controlx));
        }

 

このやり方だと角度を変更した瞬間は現在の角度が範囲外になってしまい、あまりいい感じがしません。

次の章の複数の関節では明らかに問題が発生しました。

 

複数の関節を組み合わせる

関節ランでは人間の関節を再現するため複数の関節を組み合わせていました。

ジョイントの初期設定は簡単で、胴体ー右もも右ももー右ふくらはぎ右ふくらはぎー右足の3つを作るだけです。

 

ただ一つ痛々しい問題がありました。膝が逆に曲がってしまうことです。

 

複数の関節を強制的に制御しているので、膝の可動範囲の設定が正しくても腰より膝の優先順位が下がると容赦なく逆に曲がります。

 

これを防ぐために2つ対策を実施しました。

 

1つ目は角度の上下限に現在のジョイントの角度を含める方法です。

現在の角度は joint.GetJointAngle() で取得できます。これと目標の角度の2つをSetLimits()に入力します。

現在の角度の目標の角度大小を判定して、どちらをlowerにするかも判定します。回転方向も変わってくるのでSetMotorSpeed()の値を切り替えます。

この辺は関節ランの807行目あたりにあります。

 

2つ目はジョイントに登録する物体の順序を変える方法です。

複数のジョイントを作る順番ではなく、ジョイントのInitializeに登録する2つの物体の順序です。

 

最初は胴体に足のパーツを追加していくイメージで、

  腰(胴体, もも)、膝(もも, ふくらはぎ)、足首(ふくらはぎ, くつ)

の順番でジョイントを作っていました。

 

これだと優先順位が腰>膝>足首になる気がしたので腰と膝の順序を逆にしました。

  腰(もも,胴体)、膝(ふくらはぎ,もも)、足首(ふくらはぎ、くつ)

 

色々試してたどり着いただけなので、なぜこれがいいのか本当にこれがいいのかはわかりません。順序が混在している方がいいような気がします。

また、順序を逆にすると回転角度の正負も逆にする必要があります。

 

色々やっている感じでは今回のコードには両方とも必要な対策だったと思います。

 

SetFixedRotationとの組み合わせ

回転ありの物体の一部の角度を固定すると、理想とする動きを再現しやすくなります。

天秤ゲームでは左右の皿が常に垂直になるように、関節ランでは胴体を前傾姿勢で固定するようにしました。

 

これらは一部の物体のみSetFixedRotationをtrueにして実現しています。

しかし、物体が細かく振動するという割と深刻な問題がありました。

 

たぶんジョイントで加わる力と回転固定で加わる力がぶつかり合って安定しないのだと思いますが、根本的な解決策は見つけられませんでした。

 

関節ランの方は動きが面白くなったことにして放置しました。

しかし、天秤ゲームの方は皿の上のものが滑って落下していました。振動によって常に浮いた状態だったので摩擦を上げても効果はなく、皿の端に壁を作って対応しました。

 

ピストンジョイントの基本

ピストンジョイントを使うと一方向のみに動きを限定することができます。

 

動きのイメージはレールのほうが近いかもしれません。

モーターを使えばピストン運動をさせたりバネのように反発させることもできますが、モーターが無効ならレール上を自由に動くカーテンのようになります。

 

ピストンジョイントを作るには、b2.Box2DWeb.Dynamics.Joints.b2PrismaticJointDef()で設定を作ります。

 let def = new b2.Box2DWeb.Dynamics.Joints.b2PrismaticJointDef();

 


そして回転ジョイントと同じくInitializeを実行しますが、先に軸の設定をします。

どちらの方向にピストンが動くかはb2Vec2というベクトルを使います。

ベクトルはいつもよく使っているbox2d.vec2(x, y)ではうまくいきません。

 

記載方法は、

右向き(xがプラスになる) let axis = new b2.Box2DWeb.Common.Math.b2Vec2(1, 0);

下向き(yがマイナスになる) let axis = new b2.Box2DWeb.Common.Math.b2Vec2(0, -1);

のようにしてaxisという名前のベクトルを作ります。

 

斜め方向の場合は、角度をdirとして以下のようにします。

  let axis = new b2.Box2DWeb.Common.Math.b2Vec2(Math.cos(dir/180*Math.PI), Math.sin(dir/180*Math.PI));

 

(1, 0)のところを(2, 0)のように大きくすると動きが変わってきますが、どうなっているのかよくわかりません。あまり実用性はないと思います。

 

 

軸が決まったら以下のようにInitializeします。

def.Initialize(body1.b2Body, body2.b2Body, body2.b2Body.GetPosition(), axis);

 

順に、固定する物体移動する物体アンカーの位置、です。

 

3つ目のアンカーの位置となっていますが、回転ジョイントの回転軸と違って特に場所は指定しなくても動きが決まるはずなので、正直意味不明です。

離れた場所に指定してもその場所に動くわけではありません。しかしピストンの動きがおかしくなるので、上の例のように動かす物体の場所body2.b2Body.GetPosition()が良いと思います。

 

最後に設定でジョイントを作ります。

        box2d.world.CreateJoint(def);

 

ピストンを動かす

ピストンに動力を与えるには以下の3つのパラメータを設定します。

回転ジョイントと似ているところは詳細を省略します。回転のときは力がTorqueでしたがこちらはForceになります。

  • maxMotorForce
    移動に加えられる最大の力
    値の基準は1000などだが、10~100000のように大きく変えて探るのがおすすめ

  • motorSpeed
    移動のスピード
    1程度あれば移動しているのがわかる
    プラスで軸の方向 マイナスで軸の逆の方向

  • enableMotor
    モーターを有効にするかどうか
    trueかfalseを指定

 

ジョイントを作る際に設定するには以下のように記載してからCreateJointをやります。

            let def = new b2.Box2DWeb.Dynamics.Joints.b2PrismaticJointDef();
            let axis = new b2.Box2DWeb.Common.Math.b2Vec2(0, -1);

            def.Initialize(body1.b2Body, body2.b2Body, body2.b2Body.GetPosition(), axis);
            def.maxMotorForce = 1000;
            def.motorSpeed = 1;
            def.enableMotor = true;

            box2d.world.CreateJoint(def);

 

あとから操作する場合は、以下のように記載します。

            let joint = box2d.world.CreateJoint(def);

     ︙

            joint.SetMotorForce(2000);
            joint.SetMotorSpeed(2);
            joint.EnableMotor(false);

 

ピストンの可動範囲を設定する

可動範囲の設定にはTranslationの上下限を設定します。

Translationと聞くと翻訳しか思いつきませんが、移動という意味もあるようです。

軸の方向にどれだけ進んだかを示します。

  • lowerTranslation
    可動範囲の最小値
    通常の座標の単位ではなく物理世界の単位で指定
    通常の単位で50の場合、 50 / world.scale のようにして変換する
    マイナスにすると軸とは逆の方向まで進める

  • upperTranslation
    可動範囲の最大値

  • enableLimit
    可動範囲を有効にするかどうか

 

Motorの設定と合わせると以下のように記載します。

            let def = new b2.Box2DWeb.Dynamics.Joints.b2PrismaticJointDef();
            let axis = new b2.Box2DWeb.Common.Math.b2Vec2(0, -1);

            def.Initialize(body1.b2Body, body2.b2Body, body2.b2Body.GetPosition(), axis);
            def.maxMotorForce = 1000;
            def.motorSpeed = 1;
            def.enableMotor = true;
            def.lowerTranslation = -10 / world.scale;
            def.upperTranslation = 50 / world.scale;
            def.enableLimit = true;

            box2d.world.CreateJoint(def);

 

あとから操作する場合は、回転ジョイントと同じように以下のように記載します。

            joint.SetLimits(-20 / world.scale, -100 / world.scale);

            joint.EnableLimit(false);

 

ピストンの現在位置を読み取る

ピストンの移動量によってイベントを起こす場合、GetJointTranslationを使います。

スイッチのように押されたときに処理を実施する場合以下のような条件を使います。

            scene.onUpdate.add(function () {
                if (joint.GetJointTranslation()*world.scale < 10) {
                    ︙
                }
            });

バネの動きをさせる

motorForceとmotorSpeedが固定だと弾力のある感触は表現できません。

ピストンの変化量とMotorSpeedを連動させるとバネっぽくなります。

 

            let def = new b2.Box2DWeb.Dynamics.Joints.b2PrismaticJointDef();
            let axis = new b2.Box2DWeb.Common.Math.b2Vec2(0, -1);

            def.Initialize(body1.b2Body, body2.b2Body,  body2.b2Body.GetPosition(), axis);
            def.maxMotorForce = 1000;
            def.motorSpeed = 1;
            def.enableMotor = true;
            def.lowerTranslation = -100 / world.scale;
            def.upperTranslation = 100 / world.scale;
            def.enableLimit = true;

            let joint = box2d.world.CreateJoint(def);

            scene.onUpdate.add(function () {
                joint.SetMotorSpeed(-0.2 * joint.GetJointTranslation()*world.scale);
            });

 

world.scaleでの変換は必須ではありませんが、変化量のイメージがしやすくなります。

 

この辺のやり方は公式だとこちらにもあります。

書き方は割と違いますが、ブラウザで動かせるサンプルがあるのがわかりやすいです。

 

 

固定するジョイントの基本

サンプルコードは用意していませんがウェルドジョイントもシンプルで使えそうです。

物体と物体を溶接のように固定してくっつけるだけです。

複数の物体をくっつけて複雑な形状を実現する使い方もありますが、ゲーム中に接触したら付着するという動的な使い方ができます。

 

以下ようにInitializeだけで固定することができます。

            let def = new b2.Box2DWeb.Dynamics.Joints.b2WeldJointDef();
            def.Initialize(body1.b2Body, body2.b2Body,  body2.b2Body.GetPosition());
            let joint = box2d.world.CreateJoint(def);

 

また、追加でStiffnessという値も設定することができます。

たぶん、 def.stiffness = 1 や SetStiffness(1)で使えると思います。

使ったことがないので値の基準はわかりません。

Stiffnessの値以上の力が加わったら溶接が外れるので、破壊表現等にも使えそうです。

Stiffnessの説明ですが間違っていた可能性があります。説明を見ると柔らかさ寄りの話で、壊れるものではなさそうです。どこかにそう書いていた記憶があるんですが確認できませんでした。実際に動かして試そうとしたのですが、これもうまくいきませんでした。厳密に言うと、あっているのか間違っているのかもわかりません。

 

単純に外すときはマウスジョイントと同じようにDestroyJointでいけると思います。

            box2d.world.DestroyJoint(joint);

 

 

ジョイントの紹介も今回で終わりです。

まだ紹介していない中では滑車のプーリーがありますが、まああまり使うことはないかと思います。

 

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

 

ニコ生ゲームを作ろうと思ったら物理的に考える その5


今回は接触する物体の情報を利用する方法と物体をつかむ方法を紹介していきます。

 

以下がサンプルです。音は抜いています。

・ニコニコ煮込み鍋 ファイル アツマール


■目次

  • 物体に力を加える
  • 物体に衝撃を加える
  • 接触イベントで物体の情報を利用する .GetFixtureA().GetBody()
  • b2BodyからEbodyとentityを特定する .getEBodyFromb2Body()
  • 接触した物体を操作する
  • 物体をつかんで操作する
  • ジョイントの種類
  • マウスジョイントを作る
  • ローカル座標のグローバル座標を取得する localToGlobal
  • マウスジョイントの目的地を変える
  • マウスジョイントを解除する
  • 物体の角度を固定する body.b2Body.SetFixedRotation(true)
  • 物体をすり抜けさせない bullet

 

衝突物体に力を加える

今回のゲームはなべに具材を浮かせて、ヘラでかき混ぜるゲームです。

液体の中で浮かばせるために、重力とは別に浮力を加えます

 

力と衝撃を加えるのは基本中の基本ですが紹介が遅れてしまいました。

力が衝撃と違うのは継続するか一瞬かです。力は重力のように継続して加わります。

         body.b2Body.ApplyForce(box2d.vec2(0, -1000), body.b2Body.GetPosition());

 

このような形でApplyForceを使います。カッコの中には2つの座標を入れます。

1つ目のbox2d.vec2(0, -1000)は力の大きさと向きです。Yの値は適当ですがマイナスなら上向きに力が加わります。

2つ目のbody.b2Body.GetPosition()力を加える場所です。今回は物体の中心に力を加えるためにGetPositon()を使って物体の中心座標を入れています。
box2d.vec2(entity.x, entity.y)のようにbox2d.vec2で指定してもいけます。

 

力を加える場所を中心以外にすると回転してしまいます。あと、重心が物体の中心にないような場合もちょっとややこしくなりそうです。

 

 

しかし、今回このApplyForceを使ったかというと、使いませんでした

 

今回は鍋から出ると浮力がなくなってほしいので、力のOFFが必要です。

この方法を調べたところ、ClearForcesというものがありそうでしたが使い方がわからず。結局、次の章にあるApplyImpulseで継続的に衝撃を加えて代用しました。


物体に衝撃を加える

ApplyImpulseも基本中の基本で公式のサンプルコードでもよく出てきます。

         body.b2Body.ApplyImpluse(box2d.vec2(0, -1000), body.b2Body.GetPosition());

という形でApplyForceに似ています。

1つ目は衝撃の大きさ。2つ目は衝撃が加わる場所で、box2d.vec2でも指定可です。

 

衝撃の大きさの目安は物理の計算が面倒です。数字を適当に入れて確認すると楽です。値が小さいと動いていないように見えるので1000000とか大きな数字を試しましょう。

 

ApplyImpluseがLinearVelocityより優れているのは、慣性を残せるところです。
LinearVelocityだとそれまでの動きを無視して動きを決めますが、ApplyImpulseだと勢いと衝撃を組み合わせるためより複雑な動きができます。

 

ApplyImpluseのデメリットは、衝撃が加わる場所を指定する必要があるところです。
1つ目と2つ目の値のどっちが場所の指定なのかも忘れがちですし、場所の座標の算出にも気を使います。

入力する座標ゲーム全体のグローバルな座標なので、物体を回転させる場合は回転に合わせて座標(と方向も?)を調整する必要があります。回転させたい場合はジョイントというものもあるのでそちらがいいかもしれません。

 

そして、今回は浮力の代用にするため、onUpdateの常に処理するイベントの中でApplyImpluseを使いました。607行目にあります。

  entity.onUpdate.add(function () {
      body.b2Body.SetLinearDamping(0.2);
      if (Math.abs(entity.x - g.game.width/2) < 400) { //鍋の中の処理
          if (entity.y > 350) {
              body.b2Body.ApplyImpulse(box2d.vec2(0, -18 * body.b2Body.GetMass()), body.b2Body.GetPosition());
              body.b2Body.SetLinearDamping(2);
          }
      }

               ︙
  });

座標で鍋の中にあるかどうか判定し、ギリギリ上に動く大きさの衝撃を加えています。ついでに水の中はDampingを大きくして抵抗がある感じに指定しています。

 


接触イベントで物体の情報を利用する

接触イベントはcontactListener.BeginContactの中に記載します。前々回やりました。

今回は接触した物体の情報をもとに処理を変えます

 

取得する情報はfixturecontact.GetFixtureA()でまず取得します。

そして続けて.b2BodyGetBody()で取得します。以下のように書きます。

    contactListener.BeginContact = function (contact) {
        let a = contact.GetFixtureA().GetBody();
        let b = contact.GetFixtureB().GetBody();
    ︙
    }

これでaとbが接触している2つの物体のb2Bodyになります。

aとbは勝手に名前をつけていますが、FixtureAとFixtureBは決められた名前です。

 

b2BodyがわかればUserDataも取得できます。a.GetUserData() もしくは最初から書くとcontact.GetFixtureA().GetBody().GetUserData() になります。

 

今回のゲームでは同じ具材が重なると消えるルールなので、2つのUserDataが同じかどうかを判定して処理を進めています。

            if (a.GetUserData() == b.GetUserData()){
                ︙
            }

 

b2Bodyだけでなくentityの情報も取得することが可能です。次の章に続きます

 

 

b2BodyからEbodyとentityを特定する

物体には Ebody と b2Body と entity の3つがあります。

Ebodyの下に残り2つがぶら下がっている形なので、b2Bodyからentityを特定するにはまず.getEBodyFromb2Body()を使ってEbodyを特定します。

        let a = contact.GetFixtureA().GetBody();
        let b = contact.GetFixtureB().GetBody();
        let ea = box2d.getEBodyFromb2Body(a);
        let eb = box2d.getEBodyFromb2Body(b);

 

これでeaとebがそれぞれのEbodyになります。

entityを特定するにはEbodyに.entityをつけるだけです。ea.entity と eb.entity ですね。

 

色々と特定が完了したら操作もしていきたいですが、contactListener.の中では制約があります。次の章に続きます

 


接触した物体を操作する

今回は同じ種類の物体が接触すると消えるルールなので、removeBodyをやりたいところですがcontactListener.の中で以下のように記載するとエラーになります。

        contactListener.BeginContact = function (contact) {
            let a = contact.GetFixtureA().GetBody();
            let b = contact.GetFixtureB().GetBody();
            let ea = box2d.getEBodyFromb2Body(a);
            let eb = box2d.getEBodyFromb2Body(b);
            if (a.GetUserData() == b.GetUserData()){
                box2d.removeBody(ea);
                ea.entity.destroy();
                box2d.removeBody(eb);
                eb.entity.destroy();
           }
        }

 

同じようにApplyImpulseなどもエラーが出ます

詳細はわかりませんが、b2Bodyに関わるところは操作できないと思われます。

一方で、それ以外の変数の操作・entity.tagの編集・entity.modified()などはできます

scene.setTimeoutで発動するタイミングをずらせばb2Bodyも操作できるみたいです。

 

これを利用して今回はentity.tagをfalseからtrueに変更しておいて、別途用意したonUpdateのイベントでentity.tagを見て削除しました。

 

485行目に以下のように記載しています。

    contactListener.BeginContact = function (contact) {
        let a = contact.GetFixtureA().GetBody();
        let b = contact.GetFixtureB().GetBody();
        let ea = box2d.getEBodyFromb2Body(a);
        let eb = box2d.getEBodyFromb2Body(b);
        if (a.GetUserData() != "wall" && b.GetUserData() != "wall" && touch.opacity == 0){
            if (a.GetUserData() == b.GetUserData()){
                if (!ea.entity.tag && !eb.entity.tag) {
                    ea.entity.tag = true;
                    eb.entity.tag = true;
                    addscore(a.GetUserData());
                }
            }
        }
    }

処理が長いのでaddscore()という関数にまとめていますが、その中でもEbodyとb2Bodyの操作はしていません。

 

onUpdateの方は615行目にあります。

entity.onUpdate.add(function () {
      ︙
     if (entity.tag || entity.y > (g.game.height + d)) { //同種接触または画面下に行った処理
        scene.assets["se_gu"].stop();
        if (soundstate) scene.assets["se_gu"].play().changeVolume(0.3);
        addlabel(entity.frames[0], entity.x, entity.y);
        addgu++;
        box2d.removeBody(body);
        entity.destroy();
    }
});

 

 

物体をつかんで操作する

今回のゲームでは物体としてのヘラをつかんでドラッグで操作します。

 

物体をタッチしている位置に追随させるには、マウスジョイントを使います。

タッチ位置と物体の位置から計算してApplyImpluseを調整することもできますが、マウスジョイントなら簡単にできます。

公式のサンプルコードでもこちらで使われています。

 

 

ジョイントの種類

マウスジョイントはジョイントの一種です。

ジョイントは何かの物体と物体をつなげて特定の動きをさせるもので、よく使われそうな動きが複数用意されています。

 

私もまだ使ったことがないものばかりなので、ざっくり想像で機能を書きます。

  • b2DistanceJointDef
    物体と物体の距離を制御する。ひも。振り子
    使用例はこちら

  • b2FrictionJointDef
    摩擦? 見下ろし視点の摩擦 カーリングの停止のようなイメージか?

  • b2GearJointDef
    歯車。物体の回転が連動する?

  • b2MouseJointDef
    マウスで物を動かす。特定の地点への引力にも使えそう
    使用例はこちら

  • b2PrismaticJointDef
    直線上を動く。ピストンやバネの動き。ピンボールの発射とか
    なぜPrismaticという名前なのかは不明
    使用例はこちら

  • b2PulleyJointDef
    プーリー。滑車。井戸のようなものに使える?

  • b2RevoluteJointDef
    物体と物体をピンで止める。自由に回転させたり、回転する力を加えたり
    関節やタイヤに使えそう

  • b2WeldJointDef
    溶接。おそらく物体同士をつなげて固定する
    ゲーム中に別々の物体をくっつける場合に使えそう

 

あとb2MotorJointDef 、b2WheelJointDef というのも一般的なBox2dにはあるようなのですが、エディターの候補に出ないのでAkashicEngineでは使えないのかもしれません。

 

 

マウスジョイントを作る

マウスジョイントを作るには、b2.Box2DWeb.Dynamics.Joints.b2MouseJointDef()を使って「マウスジョイントの設定」を作ります。

 let jointdef = new b2.Box2DWeb.Dynamics.Joints.b2MouseJointDef();

 

そしてその後に基本のパラメーターを5つ設定します

  • bodyA
    つなげる1つ目の物体
    マウスジョイントの場合は、ワールド全体を一つの物体とみなす値としてbox2d.world.GetGroundBody()を指定します

  • bodyB
    つなげる2つ目の物体
    マウスで操作したい物体をb2Bodyで指定します

  • collideConnected
    bodyAとbodyBが衝突するかどうか
    Web上の記事だとマウスジョイントの場合trueにするように書かれているんですが、falseでも特に問題ない気がします。よくわかりません

  • maxForce
    引き寄せる力
    大きければぴったり追従し、小さければ遅れたり引っかかって止まります

  • target
    引き寄せる場所の座標
    通常の座標ではなくBox2D用の座標にするため、box2d.vec2()を使います

 

最後にマウスジョイント」に「マウスジョイントの設定」を登録します。

マウスジョイントは適当に変数を作っておきます。

  let mousejoint = null;

設定の登録はbox2d.world.CreateJoint()を使います。

        mousejoint = box2d.world.CreateJoint(jointdef);

 

まとめると以下の形です。

  let mousejoint = null;
     ︙
       let jointdef = new b2.Box2DWeb.Dynamics.Joints.b2MouseJointDef();
        jointdef.bodyA = box2d.world.GetGroundBody();
        jointdef.bodyB = hera.b2Body;
        jointdef.collideConnected = false;
        jointdef.maxForce = 1000;
        jointdef.target = box2d.vec2(point.x, point.y);
        mousejoint = box2d.world.CreateJoint(jointdef);

 

mousejointとjointdefの名前は勝手につけているので別名でも大丈夫です。

 

ジョイントの設定は完了ですが、実際にマウスで操作するまで3章ほど続きます。

 


ローカル座標のグローバル座標を取得する

今回はtouchという四角いエンティティをヘラの物体の上に配置し、touchをクリックすることでヘラをつかんでいる感触にします。

 

マウスジョイントはtouchのonPointDownイベントで生成し、touchをクリックした場所を登録します。

 

クリックした場所はonPointDownのイベントの中ではev.point.xとev.point.yで取得できますが、これはtouchというエンティティの中での座標です。

 

グローバルな座標にするには、 touch.x + ev.point.x のように補正が必要です。
さらにanchorXが0.5になっている場合は、touch.x - touch.width/2 + ev.point.x になり、
回転があるとさらにややこしくなります。

この面倒な計算を省略するために .localToGlobal() を使います。

961行目にあります。

        let point = { x: 0, y: 0 };
    ︙
        point = touch.localToGlobal({ x: ev.point.x, y: ev.point.y });

 

上のような使い方をしますが、エンティティの名前につづけて.localToGlobalと記載し、カッコの中にxとyを持つ連想配列を入れます。

結果としてxとyを持つ連想配列ができるのでpoint.xやpoint.yのようにして使います。

 

これはbox2dとは関係ないので、box2dを入れていなくても使えます。
逆の関数としてGlobalToLocalもあります。

 


マウスジョイントの目的地を変える

マウスをドラッグさせるとその場所へ引き寄せられるようにします。

引き寄せる場所の更新SetTargetを使います。

        if (mousejoint != null) mousejoint.SetTarget(box2d.vec2(point.x, point.y));

 

mousejointのあとに .SetTargetをつけてカッコの中に新しい座標を入れます。

もしmousejointがうまく設定されていないとエラーになるので、if (mousejoint != null) をつけています。

 

 

目的地の座標はtouch.onPointMoveの中で更新します。

 

onPointDownと似たようなやり方で指定すると、

    touch.localToGlobal({ x: ev.point.x + ev.startDelta.x, y: ev.point.y + ev.startDelta.y });

となりますがこれはうまくいきません

おそらくtouchエンティティ自体が移動するので計算がずれていると思います。

 

代わりに、 ev.prevDelta.xev.prevDelta.y を使うとうまくいきます

        touch.onPointMove.add(function(ev) {
            if(!startstate) return;
            if(finishstate) return;
            point.x += ev.prevDelta.x;
            point.y += ev.prevDelta.y;
            if (mousejoint != null) mousejoint.SetTarget(box2d.vec2(point.x, point.y));
        });

なぜうまくいくのかはわかりませんが、とりあえずうまく行っています。

 


マウスジョイント解除する

クリックやタップを離したときは、マウスジョイントを解除して物体が自由に動くようにします。

onPointUpイベントの中でマウスジョイントを破壊し、次にonPointDownがあったときにまたマウスジョイントを生成します。

マウスジョイントの破壊にはDestroyJointを使います。

        touch.onPointUp.add(function(ev) {
            if(!startstate) return;
            if(finishstate) return;
            if (mousejoint != null) box2d.world.DestroyJoint(mousejoint);
            mousejoint = null;
        });

最後のmousejoint = nullは不要そうですが、公式のコードに有ったので残しています。

 


物体の角度を固定する

細かい設定で個人的によく使うのがSetFixedRotationです。

これをtrueにしておくと物体の角度が固定されます。

 

今回のヘラは常に立てた状態で使いたかったので551行目で設定しています。

            let body = box2d.createBody(entity, bodydef, fixturedef);
            body.b2Body.SetPosition(box2d.vec2(x, y));
            body.b2Body.SetAngle(box2d.radian(angle));
            body.b2Body.SetFixedRotation(true);

 

これがないとヘラがくるくる回って力が伝わりません。

 

 

これ以外で使える用途が、アクションゲームの操作キャラクターです。

 

エアリアルサッカーでは操作キャラクターがヘディングしているシーンをイメージしていたので.SetFixedRotationをtrueにしています。

ドアラッシュの方もキャラクター顔が回転するのが嫌だったので同じ設定です。

 

 

物体をすり抜けさせない

今回のヘラは見た目より薄くしているのですが、高速で動かしたときに具材をすり抜けるという問題が発生していました。

 

これを解決するために使用したのがBodyDefのbullet設定です。

これをtrueにしておくとすり抜けないようになります

 

もともとは高速で発射する弾丸に使う設定のようです。

どうせなら常にすり抜けないようにしたいところですが、これをtrueにすると処理が重くなるそうです。最低限の使用にしましょう。

 

535行目付近に以下の記載があります。

        function makehera(x, y, width, height, angle, color) {
      ︙

           let bodydef = box2d.createBodyDef({
                type: b2.BodyType.Dynamic, // 静止:Static 運動:Kinematic 動的:Dynamic
                linearDamping: 0, // 速度の減衰率 0~3ぐらい
                angularDamping: 0, // 回転速度の減衰率 0~3ぐらい
                bullet: true,
                userData: "wall",//識別用
            });
      ︙
        }

 

今回はここまでです。

 

今回の記事でBox2dの使いそうな機能はほぼ終わったと思います。

あとはまだ自分が使ったことのないジョイントの記事と、Box2dでマルチゲームをやるときの注意点の記事を作るかもしれません。

 

 

この記事を書いているとちょうどニコ生ゲーム開発のイベントが始まりました。

ch.nicovideo.jp

なかなかニコ生ゲーム開発者が増えてこない中で、裾野の広がりに期待したくなるありがたい企画ですね。

私もそちらのゲームを作っていこうと思います。

 

 

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