LoginSignup
2
0

More than 5 years have passed since last update.

現在時刻に依存しないタイマーモデル

Posted at

要件

  • 残り時間のあるゲームを作る。
  • 成功すると +1秒, 失敗すると -1.5秒 残り時間に加算される。
  • 5 秒経過でゲームオーバー
  • 残り時間は5秒を越えない、また負の値にはならない
  • ポーズ機能欲しいな〜(最初無かったんだけどあとから友人に指摘されました)

仕様

残り時間(msec)を使って初期化

var timer = new Timer(5000)  // 5 sec

start(currentTime) で開始

timer.start(Date.now());

add(currentTime, msec) で秒数加算

timer.add(Date.now(),  1000); // 成功時
timer.add(Date.now(), -1500); // 失敗時

pause(currentTime) で一時停止, resume(currentTime) で再開

timer.pause(Date.now());
timer.resume(Date.now());

timeup(currentTime) で終了判定&(終了していたら)状態更新

timer.timeup(Date.now());

reset() でスタート前に戻る

timer.reset();

以上は内部状態によっては何も起こらないこともある(例えば Pausing 状態では timeup しない、など)

ステートマシン図

Timer2.png

実装

準備

javascript-state-machine という npm パッケージを使用しています。

npm install --save-dev javascript-state-machine

ソースコード

models/Timer.js
"use strict";

import StateMachine from "javascript-state-machine"

/*
 *  waiting    | running                      | finished
 * ------------|------------*-----------------|------------
 *             `startTime   `currentTime      `endTime
 */

export default class Timer {
  constructor(max) {
    this.max = max;
    var startTime, endTime, pauseTime;

    var setSpan = (n) => {
      startTime = n;
      endTime = n + max;
    }

    var fsm = StateMachine.create({
      initial: "Waiting",
      events: [
        {name: "start",  from: "Waiting",  to: "Running"},
        {name: "add",    from: "Running",  to: "Running"},
        {name: "pause",  from: "Running",  to: "Pausing"},
        {name: "resume", from: "Pausing",  to: "Running"},
        {name: "timeup", from: "Running",  to: "Finished"},
        {name: "reset",  from: "Finished", to: "Waiting"}
      ],
      callbacks: {
        onWaiting: () => { setSpan(Infinity); },
        onstart: (ev, f, t, now) => {
          setSpan(now);
        },
        onadd: (ev, f, t, now, msec) => {
          setSpan(Math.min(startTime + msec, now));
        },
        onbeforepause: (ev, f, t, now) => startTime <= now,
        onenterPausing: (ev, f, t, now) => {
          pauseTime = now;
        },
        onleavePausing: (ev, f, t, now) => {
          setSpan(startTime + now - pauseTime);
          pauseTime = null;
        },
        onbeforetimeup: (ev, f, t, now) => endTime <= now
      }
    });

    var elapsedTime = {
      Waiting: () => 0,
      Running: (now) => Math.min(max, Math.max(0, now - startTime)),
      Pausing: (now) => elapsedTime.Running(Math.min(now, pauseTime)),
      Finished: () => max
    }

    this.elapsedTime = (now) => elapsedTime[fsm.current](now);
    this.remain = (now) => max - this.elapsedTime(now);
    this.percent = (now) => Math.floor(100 * this.elapsedTime(now) / max);

    var run = (ev) => function() {
      return fsm.can(ev) && fsm[ev].apply(fsm, arguments);
    }

    this.start = run("start");
    this.add = (now, msec) => {
      this.timeup(now);
      run("add")(now, msec);
      this.timeup(now);
    }
    this.pause = (now) => {
      this.timeup(now);
      return run("pause")(now);
    }
    this.resume = run("resume");
    this.timeup = run("timeup");
    this.reset = run("reset");
    this.currentState = () => fsm.current;
    this.is = (s) => fsm.is(s);
    this.can = (e) => fsm.can(e);
  }
}

単に全てのメソッドで必ず現在時刻を渡しているだけ。 setTimeout などを使用していないので収まりが良くテストもしやすい(書いてないけど)。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0