undefined

bokuweb.me

Mithril.jsでjubeatライクな音ゲーのプロトタイプを作る


※修正jubeats->jubeat

成果物

github.com

ここで遊べる(スマホもしくはタッチが有効な環境)
http://bokuweb-sandbox.github.io/mithril-game-prototype/

jubeatはこんなゲーム。

目的

Reactbmsできりゃテーマとか全部cssでカスタマイズできるし、cocos2d-JS(現在はcocos2d-x)のクソみたいなUI使わなくていいし最高じゃんって思ったんだけど、凶悪な譜面を思い浮かべると「あーやっぱ無理だろうな・・。」って、横道に逸れてこれ作った。 いつ飽きるかわからないけど。あと、結果はどうあれ、bmsの方もMithrilでひとまず簡単な譜面を再生するとこまでやってみたい。 本当はReactで作りたいけど描画のベンチマーク見せられちゃうとまずはMithrilだろうかって思ってる。

作り

譜面情報をこんな感じで持っとく。

m   = require 'mithril'
App = require './app'

app = m.component new App(), {
    audio : "./audio/Ouroboros.mp3"
    notes : [
        {row : 0, column : 0, time : 2.329}
        {row : 1, column : 1, time : 4.521}
        {row : 2, column : 2, time : 6.759}
        {row : 3, column : 2, time : 8.982}
        {row : 3, column : 3, time : 11.269}
        {row : 1, column : 1, time : 13.500}
        {row : 2, column : 2, time : 15.731}
        {row : 2, column : 2, time : 18.002}
        {row : 0, column : 0, time : 20.242}
        {row : 1, column : 1, time : 22.448}
        {row : 2, column : 2, time : 24.753}
        {row : 2, column : 2, time : 26.960}
        {row : 0, column : 0, time : 29.180}
        {row : 1, column : 1, time : 31.455}
        {row : 2, column : 2, time : 33.730}
        {row : 2, column : 2, time : 35.899}
        {row : 0, column : 0, time : 38.170}
        {row : 1, column : 1, time : 40.377}
        {row : 2, column : 2, time : 42.665}
        {row : 2, column : 2, time : 44.886}
        {row : 3, column : 3, time : 47.127}
        {row : 1, column : 1, time : 49.354}
        {row : 2, column : 2, time : 51.654}
        {row : 2, column : 2, time : 53.825}
        {row : 0, column : 3, time : 56.081}
        {row : 1, column : 1, time : 58.306}
        {row : 2, column : 2, time : 60.544}
        {row : 2, column : 2, time : 62.781}
        {row : 1, column : 1, time : 65.039}
        {row : 2, column : 2, time : 67.278}
        {row : 2, column : 2, time : 69.498}
        {row : 3, column : 2, time : 71.773}
        {row : 1, column : 1, time : 74.010}
        {row : 2, column : 2, time : 76.216}
        {row : 2, column : 2, time : 80.793}
        {row : 0, column : 3, time : 83.015}
        {row : 1, column : 3, time : 85.224}
        {row : 2, column : 2, time : 89.752}
        {row : 2, column : 2, time : 92.022}
        {row : 2, column : 2, time : 94.294}
      ]
    }

m.mount document.body, app

ゲーム開始前に各座標へノートを配置しておいて、経過時間に応じてノートのclassをnote-show->note-hide->note-deleteで変化させる。タッチされた場合はnote-hit->note-deleteで変化させる。audio.currentTimeの変化をViewに伝えるのをどうすべきかわかんなくて以下のように同期してる。スマートなやり方あったら教えてください。

do update = =>
  @time @audio.currentTime
  m.redraw()
  window.requestAnimationFrame update
m = require 'mithril'

class SixTeenNotes
  constructor : (score) ->
    return m.prop score

class SixTeenViewModel
  init : (score) =>
    @isPlaying = false
    @score = new SixTeenNotes score
    @judge = m.prop ""
    @audio = new Audio score.audio
    @time = m.prop @audio.currentTime
    do update = =>
      @time @audio.currentTime
      m.redraw()
      window.requestAnimationFrame update

   onTouchNote : (note, event) =>
     note.clearTime = @time()
     judge = if note.time - 0.1 < note.clearTime < note.time + 0.1
       "great"
     else if note.time - 0.2 < note.clearTime < note.time + 0.2
       "good"
     else "bad"
     @judge judge

   startGame : =>
     unless @isPlaying
       @isPlaying = true
       @audio.play()

class SixTeen
  constructor : ->
    @_vm = new SixTeenViewModel()
    return {
      controller : (score) => @_vm.init score
      view : @_view
    }

  _view : (ctrl) =>
    getNoteClass = (note) =>
      if note.clearTime?
        if note.clearTime + 0.2 < @_vm.time() then return "note-delete"
        else return "note-hit"
      if @_vm.time() >= note.time + 0.6 then return "note-delete"
      if @_vm.time() >= note.time + 0.4 then return "note-hide"
      if @_vm.time() >= note.time - 0.2 then return "note-show"

    addTouchNoteEvent = (note, element, initialized, context) ->
      unless initialized
        element.addEventListener 'touchstart', @_vm.onTouchNote.bind(this, note), false

    m "div#game", [
      m "button", {onclick: @_vm.startGame}, "start game"
      m "span#judge", @_vm.judge()
      m "div#notes", @_vm.score().notes.map (note) =>
        m "img.note.row-#{note.row}.column-#{note.column}", {
          src : "./image/dest.png"
          class : getNoteClass note
          config : addTouchNoteEvent.bind this, note
        }
    ]

module.exports = SixTeen

スタイルはstylusで以下のように設定。 rowNumcolumnNumでマスの数は自由に弄れる。 アニメーションはCSSアニメーションでやらしてるけど、どの程度の精度があるのか分かっていない。

width     = 300px
height    = 300px
rowNum    = 4
columnNum = 4

vendor(prop, args)
  -webkit-{prop} args
  -moz-{prop} args
  {prop} args

html, body
  margin 0
  padding 0
  height 100%
  width 100%
  overflow hidden

#game
  width 100%
  height 100%
  position relative
  background #f5f5f5

#notes
  width width
  height height
  background #ccc
  top 100%
  margin -(height + 20)px auto
  position relative

img.note
  width (width / rowNum)
  height (width / rowNum)
  position absolute
  vendor('transition', all 0.2s ease-out)
  vendor('transform', scale3d(0,0,0))

img.note-show
  vendor('transition', all 0.2s ease-out)
  vendor('transform', scale3d(1,1,1))

img.note-hit
  vendor('transition', all 0.2s ease-out)
  vendor('transform', scale3d(1.5,1.5,1))
  opacity 0

img.note-hide
  vendor('transition', all 0.2s ease-out)
  vendor('transform', scale3d(1,1,1))
  opacity 0

img.note-delete
  display none
  
for row in 0 ... rowNum
  img.row-{row}
    left : row  * (width / rowNum)

for column in 0 ... columnNum
  img.column-{column}
    top : column * (width / rowNum)

最後に

次はbmsでやってみる。