Unityでストップウォッチを作る その5 サークルを動かす

Pocket

今回は、前々回から作ってきた「TimeCircle」をストップウォッチの計測時間に合わせて動く様にしたいと思います。

スクリプトの追加と修正

新たに「AngleAnimation.cs」と「TimeCircleController」の2つのC#スクリプトを追加して以下のコードをコピーしてください。また、「BSWUtility.cs」と「Stopwatch.cs」も修正してください。修正と言っても各スクリプト内のコードは全部載せているのでマルっと上書きしてしまって大丈夫です。

//
// AngleAnimation.cs
//
using UnityEngine;
using System.Collections;
public class AngleAnimation : MonoBehaviour {
 
 float fromAngle;
 float toAngle;
 float duration;
 float time = 0.0f;
 bool animating = false;
 bool isLerpAngle = false;
 
 void Update () {
   
   if (animating) {
     
     time += Time.deltaTime;
     
     float t = time / duration;
     
     if (t >= 1.0f) {
       t = 1.0f;
       animating = false;
     }
     
     t = BSWUtility.EaseOutValue(t);
     
     float angle = (isLerpAngle) ? Mathf.LerpAngle(fromAngle, toAngle, t) : Mathf.Lerp(fromAngle, toAngle, t);
     
     transform.localEulerAngles = new Vector3(0, 0, angle);
     
   }
   
 }
 
 public void SetAngle(float fromAngle, float toAngle, bool animate = false, float duration = 0.5f, bool isLerpAngle = true) {
   
   if (animate) {
     
     this.duration = duration;
     this.toAngle = toAngle;
     this.fromAngle = fromAngle;
     time = 0.0f;
     animating = true;
     this.isLerpAngle = isLerpAngle;
     
   } if (animating) {
     
     this.toAngle = toAngle;
     
   } else {
     
     transform.localEulerAngles = new Vector3(0, 0, toAngle);
     
   }
 }
 
 public void SetAngle(float angle, bool animate = false, float duration = 0.5f) {
   
   SetAngle(transform.localEulerAngles.z, angle, animate, duration);
   
 }
}
//
// TimeCircleController.cs
//
using UnityEngine;
using System;
using System.Collections;
public class TimeCircleController : MonoBehaviour {
 public AngleAnimation secCircleAngleAnimation;
 public float circleAnimDuration = 0.2f;
 
 public void SetTime(TimeSpan ts, bool animate = false)
 {
   float secAngle = (float)((ts.TotalMinutes - Math.Truncate(ts.TotalMinutes)) * 360.0);
   
   if (secCircleAngleAnimation) {
     secCircleAngleAnimation.SetAngle(secAngle, animate, circleAnimDuration);
   }
 }
}
//
// BSWUtility.cs
//
using UnityEngine;
using System.Collections;
public class BSWUtility : MonoBehaviour {
 static public void DrawRect(Texture2D tex, Rect rect, Color col) {
   
   int minX = (int)rect.x;
   int minY = (int)rect.y;
   int maxX = (int)rect.xMax;
   int maxY = (int)rect.yMax;
   
   for (int x = minX; x < maxX; x++) {
     for (int y = minY; y < maxY; y++) {
       tex.SetPixel(x, y, col);
     }
   }
 }
 
 static public Rect CreateRectForClear(int originX, int originY, Vector2 lineSize, int padding, int border, int scale) {
   
   return new Rect(
     originX,
     originY,
     padding * 2 + (lineSize.x + border * 2) * scale,
     padding * 2 + (lineSize.y + border * 2) * scale);
 }
 
 static public Rect CreateRectForDraw(int originX, int originY, Vector2 lineSize, int padding, int border, int scale) {
   
   return new Rect(
     originX + padding + border * scale,
     originY + padding + border * scale,
     lineSize.x * scale,
     lineSize.y * scale);
 }
 
// 修正ここから ->
 static public float EaseOutValue(float t) {
   
   return Mathf.Sin(Mathf.PI * 0.5f * t);
   
 }
// <- 修正ここまで
}
//
// Stopwatch.cs
//
using UnityEngine;
using System;
using System.Collections;
public class Stopwatch : MonoBehaviour {
 
 enum StopwatchState {
   Zero,
   Play,
   Pause
 }
 
 public TextMesh timeText;
 // 修正ここから ->
 public TimeCircleController timeCircleController;
 // <- 修正ここまで
 StopwatchState state = StopwatchState.Zero;
 TimeSpan lastStopTimeSpan;
 DateTime startDateTime;
 
 // 修正ここから ->
 void Update () {
   
   bool circleAnim = false;
   
   if (Input.GetMouseButtonDown(0)) {
     ChangeState(ref circleAnim);
   }
   
   UpdateTime(circleAnim);
 }
 
 void ChangeState(ref bool circleAnim) {
   
   if (state == StopwatchState.Pause) {
     
     lastStopTimeSpan = new TimeSpan(0);
     startDateTime = DateTime.UtcNow;
     
     state = StopwatchState.Zero;
     
     circleAnim = true;
     
   } else if (state == StopwatchState.Play) {
     
     TimeSpan ts = DateTime.UtcNow - startDateTime;
     lastStopTimeSpan = ts + lastStopTimeSpan;
     
     state = StopwatchState.Pause;
     
   } else {
     
     startDateTime = DateTime.UtcNow;
     
     state = StopwatchState.Play;
     
   }
   
 }
 
 void UpdateTime(bool circleAnim) {
   
   TimeSpan currentTs;
   
   if (state == StopwatchState.Play) {
     
     TimeSpan ts = DateTime.UtcNow - startDateTime;
     currentTs = ts + lastStopTimeSpan;
     
   } else {
     
     currentTs = lastStopTimeSpan;
     
   }
   
   if (timeText != null) {
     
     timeText.text = ConvertTimeSpanToString(currentTs);
     
   }
   
   if (timeCircleController != null) {
     
     timeCircleController.SetTime(currentTs, circleAnim);
     
   }
   
 }
 // <- 修正ここまで
 
 static public string ConvertTimeSpanToString(TimeSpan ts) {
   
   if (ts.Hours > 0 || ts.Days > 0) {
     return string.Format("{0}:{1:D2}:{2:D2}.{3}", ts.Days * 24 + ts.Hours, ts.Minutes, ts.Seconds, ts.Milliseconds.ToString("000").Substring(0, 2));
   } else {
     return string.Format("{0}:{1:D2}.{2}", ts.Minutes, ts.Seconds, ts.Milliseconds.ToString("000").Substring(0, 2));
   }
 }
}

オブジェクトの設定

スクリプトの作成と修正が済んだら、「AngleAnimation.cs」を「SecTimeCircle」オブジェクトのComponentに追加してください。

unitysw_5_1.png

「TimeCircleController.cs」はどこに置いても良かったりするんですが役割を考えて「Circles」オブジェクトに追加し、インスペクタ内の「Sec Time Angle Animation」に「SecTimeCircle」オブジェクトをアサインしてください。

unitysw_5_2.png

また、「Stopwatch」オブジェクトのインスペクタに「Time Circle Controller」が追加されているので「Circles」オブジェクトをアサインしてください。

unitysw_5_3.png

ここまで作業を終えればサークルがストップウォッチの表示時間に合わせて動くと思います。

解説

実際にサークルを回しているのは「AngleAnimation」で、「TimeCircleController」は「Stopwatch」から表示時間を受け取ってサークルの角度に変換する役割をしています。

表示タイムの角度でそのまま回転させてしまうと、再生中や停止するときは良いのですが、リセットしたときにいきなりゼロ秒にパッと戻ってしまっていまいちです。なので「AngleAnimation」というスクリプトを挟み込んで、リセットするときにはいきなり戻らずアニメーションをして戻るようにしています。

ちなみに「AngleAnimation」に常にアニメーションをtrueにして角度をセットすると常に動きがウォンウォン補間されて気持ちよいきもするのですが、そうすると計測秒数を表す位置がズレて分かりにくくなってしまうのでやらない方が良いと思います。必要なポイントだけちゃんとアニメーションして表示が飛んだりしないようにするくらいでとどめておくのがスマートではないでしょうか。

この「AngleAnimation」は後で使い回したいのでアニメーション位置の補間をLerpAngleでやるか普通にLerpでやるかを選べる様にしています。今回のサークルのアニメーションでは30秒を過ぎていれば60秒に向かって、過ぎていなければ0秒に向かってというように角度的に近い方戻したかったのでLerpAngleを使っています。

浮動小数点数の精度問題

このストップウォッチのサークルのように、大きい数値を元にオブジェクトを回転させる事になる場合は、必ず一旦0度〜360度(0〜2π)の中に収まるような値にしてから渡すようにします。値が浮動小数点数なので、オブジェクトのRotationや三角関数などへ大きな値を直接渡してしまうと、精度が足りずに動きがおかしくなってしまうからです。

たとえば、TimeCircleController.csのコードでは...

float secAngle = (float)((ts.TotalMinutes - Math.Truncate(ts.TotalMinutes)) * 360.0);

としていて、doubleで取得した「分」の値を、小数点以下の必要な部分だけにしてから角度に変換していますが、これを以下の様にして...

float secAngle = (float)((ts.TotalMinutes + 20000.0) * 360.0);

まったく切り捨てず、さらに約2週間ほど足した状態にしてみると、明らかにサークルの動きがガクガクしているのが分かると思います。このストップウォッチのサークルのように数値の上がり方が緩やかであれば1日くらいはなんとかスムーズさを保ってくれますが、1kHzのサイン波の音を鳴らす場合などでは、単純に加算していった数値をSin関数に渡してしまうとわずか数秒でノイズが含まれてしまい破綻します。

このあたりの精度問題は回転に限らずタイム表示の方でも起きるのですが、このチュートリアルのUnityバージョンでは値の取得にTimeSpanを使っているので気にしなくても大丈夫だと思います。

今回のWebPlayerビルド

今回は以上です。これでサークルが回る様になったのですが、このままだとどこが計測タイムの位置なのかわかりません。次回はタイムを指す針を追加したいと思います。

前へ | 次へ

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です