ダンスラっぽい創作譜面が作れるデスクトップアプリStyleStarを触ってみた

最近よくやってる音ゲーとしてコナミのDANCERUSH STARDOM(以下ダンスラ)と言うゲームがあります。
これはDDRの流れをくむアーケード用ダンスゲームで、迫りくるノーツに応じてステップを踏んだり、ジャンプしたり、出すと見せかけた足を出さなかったりするゲームです。

ところで、こういった音ゲーには時々ファンが作ったシミュレーターないしクローンゲームがあります。 BMSが有名ですが、DDRに対するStepMania、手前味噌ですがクロビに対するPxB Editorなどがそれです。
ダンスラにも存在し、それがStyleStarです。

気になった音ゲーについて自作の譜面(創作譜面)が作りたくなる質なので、こちらを導入して使ってみました。そして実際に譜面を作ることが出来たので、以下導入方法や使い方などを書いておきます。
ただし個人開発の野良アプリなのでどんなトラブルが発生するかわかりません。そこら辺は各々の責任でよろしくお願いします。

StyleStarのダウンロード

アプリのgithubページのreleasesから実行ファイルをダウンロードします。(執筆時点ではv0.1.0のみ。)

https://github.com/stylestar-game/StyleStar-Unity/releases

OSに応じてビルドファイルzipをDLします。

f:id:pentamania:20201004093321p:plain
実行ファイルのダウンロード

DL・解凍を終えたらその中のStyleStar.exeを実行すると起動します。(未認証アプリのため、Windows10では初回起動時に警告が出ます。その際は「詳細」から起動ボタンを探して押します)
インストール作業はありません。
起動は結構時間がかかるっぽいです。

起動時は環境設定ウィンドウが出るので適当にOKを押します。 さらに初回起動時、言語選択設定があるかと思いますが、Englishしかないと思うのでそれを選びます。

譜面制作

アプリは起動しましたが譜面データが一切無いため、No Songs的な表示がされて何もできないと思います。 ここから本題である譜面の作り方を見ていきます。

前準備:楽曲フォルダの作成

アプリケーションフォルダ(StyleStar Build)内にSongsというフォルダを作り、そこに適当な名前の楽曲フォルダを追加します。 そしてそこにogg(推奨)もしくはmp3形式の音源ファイルを追加します。

つまり以下のようなディレクトリ構造になります。

StyleStar Build
 |- Songs
   |- [楽曲フォルダ名]
     |- 音源.ogg

エディターの導入

譜面の編集にはChedSSFという、某チュウニ創作譜面用エディターChedをStyleStar向けに改造したものを使います。

執筆時点の最新版のDLリンクを以下に張ってしまいますが、アプリ本体・エディターともに最新版はdiscordサーバーの#releasesチャンネルで配布されるそうなので、気になったらそちらもチェックして見ましょう。

DLリンク: https://cdn.discordapp.com/attachments/535488235770609694/683405874248548388/Ched-SSF_2.6.3.1.zip

ChedSSFの使い方

Chedの基本的な使い方はChedのwikiを見たり、ググったりして下さい。(自分もよく分かりません…)

基本的には

  • 最初に「ファイル」>「譜面プロパティ」で音源やオフセット値の指定、「挿入」>「BPM」でBPMを設定
  • ノーツの配置はノーツアイコンを選ぶか鉛筆アイコンを選んだ状態でチャートクリック、消すときは消しゴムにしてノーツクリック
  • シークは選択範囲アイコンを選んだ状態でチャートクリック

という動作を覚えておけば良さげです。

ランニングマン配置など、StyleStar特有の配置方法はStyleStarのwikiに書かれています。 特にスライドの中継点で右クリックを使って設定することに気をつけます。

あと「編集」メニューのノーツ反転系の処理を覚えておくと非常に役立ちます。(位置・スライド方向はもちろん、LRの反転も可能)
また一つ注意点として、ChedSSF起動時のデフォルトで設定されている赤いノーツはStyleStar上では無視されるので、必ずどれかのノーツアイコンを選んでから配置します。

譜面のエクスポート

適当にノートを配置し終えたら、StyleStar向けの譜面データとしてエクスポートする必要があります。 「ファイル」 -> 「エクスポート」を選択し、以下のダイアログを開きます。

f:id:pentamania:20201004101348p:plain

基本的には全ての項目を埋めておきます。(空欄があるとうまく動作しないことがある)

  • サウンドファイル名を指定しないと音源が流れない(&ゲームが即終了になる)ので注意です
  • SONGIDについては今の所意味はないらしく、埋める必要はありません
  • ジャケットファイル名も必須ではありません

エクスポート時のファイル名は何でもいいようです。出力先は先ほど作成した楽曲フォルダを選びます。

エクスポートが完了すると拡張子が.ssfのファイルが生成されますが、これが譜面ファイルとなります。 これが楽曲フォルダ内にあれば楽曲が認識されるようになります。

ちなみに譜面を選択したときに譜面(ssfファイル)が都度読み込まれるようなので、再エクスポート後も、StyleStarの再起動は必要ありません。 (また一度設定を終えていればChedのエクスポートアイコンをクリックするだけで再エクスポート可能です)

f:id:pentamania:20201004094240p:plain
エクスポートアイコン

ただし一部設定の反映にはStyleStar再起動が必要っぽいです。(アーティスト名など)

譜面の再生

正しく楽曲フォルダ・ファイルを配置していれば、StyleStar起動後にソートカテゴリが表示され、enterキーで進めると楽曲リストが表示されます。

ソートカテゴリ->楽曲->難易度を選択すれば再生されます。(難易度選択は上下ではなく右キーを使います。左キーを押すと本家みたいにソートカテゴリにもどります。)

f:id:pentamania:20201004102635p:plain
楽曲選択画面(下に素材アセットっぽいものが見切れてるのは何でだろう…)

再生画面では上下キーでスクロールスピードを変えることができます。 さらにESCキーで途中終了できます。

f:id:pentamania:20201004103108p:plain
再生画面

作ったもの

習作として以下を上げています。

作ってみての雑感

  • ランニングマンからのTステップ譜面を入れるとそれっぽさがぐっと出て感動。しかしはっきりとしたイメージの無いつなぎの部分をどうするかが迷って難しいところ
  • 作っていくうちにだんだん理解が進むが、そうすると最初のほうで作った部分がちょっと違うな~みたいな感じでちょくちょく修正するためなかなか完成しなかったり…
  • 譜面を作るにあたって本家の譜面も当然参考にしていますが、個人的にはATRAXふつう譜面が色々なパターンを網羅しており、かつ良譜面なので模範例として優れているのではと思いました。

その他

  • 日本語訳された公式wikiがあるのでそちらも合わせてチェック
  • より詳しい事が知りたければDiscordに入って質問するといいかも。(基本は英語ベースですが、日本語OKなチャンネルもあるようです。過疎ってますが…)
  • Unity製でオープンソース(ライセンスはMIT)なので自分で改造してビルドすることも可能です。個人的にはSEがほしいので、なんか機会があったらいじってみるかも…

参考

pixi.jsで背景を(半)透明にする

pixi.jsが描画するcanvasをオーバーレイしたかったので背景色を半透明にしようとしてました。

f:id:pentamania:20200504141038p:plain
イメージ図

これは一見簡単そう(&地味)ですが、実現するのに若干躓いたのでメモしておきます。

前提

pixi.jsはv5.2.0を使用。主にgoogle chromeで検証してます。(モダンブラウザなら大きな違いはない・・・はず)

以下のような雛形HTMLをベースにしています。
描画結果をわかりやすくするため、canvas背後に確認用divボックスをおいてます。
またpixi側には仮のスプライトを配置します。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <meta http-equiv="x-ua-compatible" content="IE=Edge">
  <meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
  <title>pixi starter</title>
  <style type="text/css">
    .container {
      position: relative;
    }
    canvas {
      border: 1px solid black;
    }
    .box {
      position: absolute;
      top: 100px;
      left: 100px;
      background: #FADC8D;
      width: 200px;
      height: 200px;
      z-index: -1;
    }
  </style>
</head>
<body>

<div class="container">
  <div class="box">このボックスは確認用</div>
  <canvas id="pixi-view"></canvas>
</div>
<script src='https://cdnjs.cloudflare.com/ajax/libs/pixi.js/5.2.0/pixi.min.js'></script>
<script>
  /* 以降に表記するスクリプトはここに書いたもの */
  const SCREEN_SIZE = 256
  const app = new PIXI.Application({
    width: SCREEN_SIZE,
    height: SCREEN_SIZE,
    view: document.getElementById("pixi-view"),
  });

  // テスト用の星スプライト
  const star = new PIXI.Graphics();
  star.lineStyle(2, 0xFFFFFF);
  star.beginFill(0x35CC5A, 1);
  star.drawStar(0, 0, 5, 50);
  star.endFill();
  star.position.set(app.view.width/2, app.view.height/2)
  app.stage.addChild(star);
  
  // ループ処理
  app.ticker.add((delta)=> {
    star.rotation -= 0.01 * delta;
  });
</script>

以降のコードはところどころ前後省略してますので、そのつもりで適宜補完してください。

背景を完全に透明にするには

はじめに思いつくのが、App(renderer)の生成時のオプションとしてbackgroundColorにアルファ値を渡すことですが、backgroundColorには6桁の16進数でRGB値までしか渡せません(*)。

var app = new PIXI.Application({
  width: SCREEN_SIZE,
  height: SCREEN_SIZE,
  view: document.getElementById("pixi-view"),

  // アルファチャンネルは指定できない
  backgroundColor: 0xff00ff,
  // backgroundColor: 0xff00ff00, // これはうまくいかない
});

(* 厳密には渡せますが、期待したような結果にはなりません)

背景を透明にするにはbackgroundColorではなくtransparentというオプションを指定します。

const app = new PIXI.Application({
  width: 256,
  height: 256,
  view: document.getElementById("pixi-view"),

  transparent: true,
});

ただし気をつけたいのがtransparentと同時にbackgroundColorを指定していると、背後にあるDOMがその設定の影響を(なぜか)受けます。
例えばbackgroundColorがピンクだったら、ピンク色のフィルターがかかったようになります。

const app = new PIXI.Application({
  width: SCREEN_SIZE,
  height: SCREEN_SIZE,
  view: document.getElementById("pixi-view"),

  backgroundColor: 0xff00ff,
  transparent: true,
});

f:id:pentamania:20200504141654p:plain

これはこれで面白いと思いますが、意図してない場合はbackgroundColorは無指定にします。

背景を半透明にするには

では半透明にするにはどうすればいいのでしょうか。
canvas全体の不透明度を変えるだけならCSSでopacityを変更すると言ったこともできますが、スプライト等はそのまま・背景だけを変えたい場合はその方法では駄目です。

これは今の所、スクリーン全体を覆うGraphicsを用意して、それを背景代わりに使う方法しか思いつきませんでした。
(ググっても出てこない)

const backgroundAlpha = 0.8; // 透明度設定

const app = new PIXI.Application({
  width: SCREEN_SIZE,
  height: SCREEN_SIZE,
  view: document.getElementById("pixi-view"),

  // 本来の背景は透明にする
  transparent: true,
});

// 背景代わりのGraphicsを設定
const backgroundVeil = new PIXI.Graphics();
backgroundVeil.beginFill(0x000000, backgroundAlpha);
backgroundVeil.drawRect(0, 0, app.view.width, app.view.height);
backgroundVeil.endFill();
app.stage.addChild(backgroundVeil);

// テスト用の星スプライト
const star = new PIXI.Graphics();
star.lineStyle(2, 0xFFFFFF);
star.beginFill(0x35CC5A, 1);
star.drawStar(0, 0, 5, 50);
star.endFill();

これで冒頭のイメージ図を再現できます。

👇出力サンプル👇

runstant.com

とりあえずの点 vs 多角形の当たり判定

所用で点vs多角形の当たり判定が必要になったのでメモしておきます。

基本的な考え方は以下を参考にしています。
http://poltergeist.web.fc2.com/hit_test.html

関数と使い方

/**
 * 点 vs 多角形の当たり判定
 * 
 * @param  {Vector2}    point 対象点
 * @param  {...Vector2} vertices 多角形頂点
 * @return {Boolean} 範囲内かどうか
 */
function hitTestPolygon(point, ...vertices) {
  const peripheryVecs = []; // 多角形外周ベクトル群
  const vertToPointVecs = []; // 各頂点~対象点までのベクトル群
  vertices.forEach((v, i)=> {
    // 外周ベクトル生成
    const dest = (vertices[i+1] != null) ? vertices[i+1] : vertices[0];
    peripheryVecs.push({
      x: dest.x - v.x,
      y: dest.y - v.y,
    })
    // 各頂点から点へのベクトル生成
    vertToPointVecs.push({
      x: point.x - v.x,
      y: point.y - v.y,
    })
  })

  // 上記二種のベクトルの外積Z軸成分を計算
  // 全て正もしくは負になれば範囲内という扱い
  let signFlag;
  const isOutsideRange = peripheryVecs.some((vec, i)=> {
    const pVec = vertToPointVecs[i];
    const crossZ = (vec.x * pVec.y) - (vec.y * pVec.x);
    if (signFlag != null) {
      if (signFlag !== (crossZ > 0)) {
        // 符号が変わった場合、領域外になるのでループ抜ける
        return true;
      }
    } else {
      signFlag = (crossZ > 0);
    }
  })

  return !isOutsideRange;
}

見ての通り第2引数以降はrestパラメータ(可変長引数)になっており、五角形なら頂点オブジェクトを5つ、八角形なら8つ渡すみたいな感じで使います。
(ただし頂点同士のなす角度が180以上だとNG)

point, verticesは共にプロパティにxyをもったオブジェクトであれば何でもOKです。

const mouse = {
  x: 20,
  y: 200
}

if (hitTestPolygon(mouse, 
  {x:20, y: 20},
  {x:20, y: 30},
  {x:40, y: 30})) {
  console.log('hit!')
}

使い方サンプル

まだ最適化しきれてないような気がしますが、とりあえずということで…

おまけ

Mapオブジェクトを渡したいとき。(es2017以降?)

const vertexMap = new Map([
  ["bl", new Vector2()],
  ["br", new Vector2()],
  ["tr", new Vector2()],
  ["tl", new Vector2()],
])

hitTestPolygon(p, ...vertexMap.values())