投稿者「yuki」のアーカイブ

Touch the Wave for iPad v2.0リリース

Touch the Wave for iPadのv2.0が本日リリースされました。

主な変更点は以下になります。

・ プレイヤーが2つになりました
・ Multi Route Audio対応で、外部出力とは別にヘッドホンでのモニタリングが可能になりました
・ フラットっぽいデザインのUIになりました

既知の不具合として、Digital AV Adapterをつなげた場合に音がおかしくなってしまう事を確認しています。近いうちに修正する予定です。あと、iPad miniくらいの性能だとファイルの読み込み時に音が途切れる事があるかもしれません。

前のバージョンのアップデートのときにAudiobusに対応したいと書いていたと思いますが、あまりApple以外のFrameworkは入れたくないなぁと思っていた所にInter-App AudioなんていうのがiOS7に搭載されたり、でもMulti route Audioと同時には使えないっぽかったりといった流れで、別アプリとの連携的な機能は見送っています。

UIのデザインに関しては、まぁ、ほどほどにフラットって感じです。あまりiOS7のデザインの方向性は気にしないでやってます。プレイヤーが2つになってかなり詰め込んでいるので、あまり好きではない(というかあまり良い実装のアプリが無い)ロータリーエンコーダーを導入したのですが、そこそこ使いにくくはない感じになっているんじゃないかなと思っています。

値段も最近1,000円になっていたので、ちょっと前より安くして600円にしました。とくにセールという訳ではなくそのままでいきます。

Unityでストップウォッチを作る その10 シェーダーを変更する

今回は、見た目でちょっと気になる所を修正したいと思います。
背景を黒くしているとあまり分からないのですが、試しに背景を白くしてみると目盛りのフチがやや黒ずんでいるのが分かります。特に針は輪郭がはっきり出ています。
unitysw_10_1.png
このようにテクスチャ内の不透明な部分と透明な部分を補間した境目が黒ずんでしまうのですが、なぜ起きるのかについては以下のページで補間アルファの問題として丁寧に解説されていたので、気になる方は参考にしてみると良いと思います。

乗算済みアルファとは? その1:補間アルファの問題点

ちなみに、テクスチャの設定を「Billinear」から「Point」に変えて補間を効かなくすればなくなるのですが、それだときれいに描画されなくなってしまうので、補間の設定は切らずに、上記のページでも解説されている「乗算済みアルファ」という状態にしつつシェーダーのブレンドモードも変えて、解決しようと思います。
まず、テクスチャの方を乗算済みにして描画するようにします。BSWUtility.csのDrawRect()を以下の様に変更してください。

//
// BSWUtility.csの一部
//
static public void DrawRect(Texture2D tex, Rect rect, Color col) {
   // 追加ここから
   col.r *= col.a;
   col.g *= col.a;
   col.b *= col.a;
   //

上記のコードではアルファの値をRGBにかけています。コードを見るからに、アルファを効かせていなければ結果は変わりませんが、半透明の状態がテクスチャに含まれる場合は必要な処理です。
次に、シェーダーを作成します。ProjectウィンドウのCreateメニューから「Shader」を選択し、「Unlit-Alpha Premultiplied」と名前をつけて作成してください。また、一応「Shader」フォルダを作ってその中に移動しておいてください。
unitysw_10_2.png
作成出来たら以下のコードで上書きしてください。

//
// Unlit-Alpha Premultiplied.shader
//
Shader "Unlit/Transparent Premultiplied Alpha" { //

「Material」のシェーダーの項目の中に「Unlit/Transparent Premultiplied Alpha」が追加されていると思いますので、そちらに変更してください。
unitysw_10_3.png
このシェーダーコードは、Unity標準で入っている「Unlit/Transparent」のコードを2行ほど変更しているだけです。Unity標準搭載のシェーダーはUnity上ではコードが見れませんが、Web上に公開されていて、以下のページからダウンロードする事が出来ます。
http://download-cdn.unity3d.com/unity/download/archive
今回ベースにしているシェーダーはダウンロードしたフォルダ内の「DefaultResourcesExtra/Unlit/Unlit-Alpha.shader」にあります。
シェーダーの変更箇所は、シェーダーの名前と「Blend One OneMinusSrcAlpha」の部分です。左の値の「One」が元は「SrcAlpha」でした。「SrcAlpha」というのはこれから描画する色にアルファの値をかけるモードなのですが、それはテクスチャ側でやる事にしたので「One」に変更してアルファをかけずそのまま描画する事にしています。右の値は描画される背景側のモードなのでそのままにしておきます。

今回のWebPlayerビルド


今回は以上です。次回は最後の予定ですが、計測中のデータを保存してアプリを落としても計測が中断しないようにしたいと思います。
 
前へ | 次へ

Unityでストップウォッチを作る その9 リセットのアニメーション

今回は「RESET」の文字をアニメーションさせるのに加えて、画面全体の背景もタップしたときにアニメーションさせようと思います。そんなにこったものではなく、フェードさせたり回転させたりするくらいのものです。

「RESET」を表示するのに使っているTextMeshは、テキストにHTMLのようなタグを付けて1文字単位で色を変えたり装飾をしたりする事が出来ます。ですが、今回は文字列全体にしか色を指定しませんし、フェードさせたりもしたいので、文字列全体にタグをつける処理をするスクリプトを作ります。

新たに「ColoredText.cs」という名前でC#スクリプトをProjectウィンドウの「Script」フォルダに作成し、以下のコードをコピーしてください。

//
// ColoredText.cs
//
using UnityEngine;
using System.Collections;
[ExecuteInEditMode()]
[RequireComponent(typeof(TextMesh))]
[RequireComponent(typeof(MeshRenderer))]
public class ColoredText : MonoBehaviour {
 
 public string text = "empty";
 public Color color = Color.white;
 
 string prevText = null;
 Color prevColor = Color.clear;
 
 float duration = 0;
 bool animating = false;
 float fromAlpha;
 float toAlpha;
 float time = 0;
 
 TextMesh textMesh;
 
 // Use this for initialization
 void Start () {
   UpdateText();
 }
 
 // Update is called once per frame
 void Update () {
   
   bool needsUpdate = false;
   
   if (animating) {
     
     time += Time.deltaTime;
     float t = time / duration;
     
     if (t >= 1.0f) {
       t = 1.0f;
       animating = false;
     }
     
     float alpha = Mathf.Lerp(fromAlpha, toAlpha, t);
     color.a = alpha;
     
     needsUpdate = true;
     
   } else if (IsPropertyChanged()) {
     
     needsUpdate = true;
     
   }
     
   if (needsUpdate) {
     UpdateText();
   }
 }
 
 bool IsPropertyChanged() {
   if (text != prevText ||
     color != prevColor) {
     return true;
   }
   return false;
 }
 
 void UpdateText() {
   
   if (textMesh == null) {
     textMesh = GetComponent<textMesh>();
     textMesh.richText = true;
   }
   
   textMesh.text = HtmlColorText(text, color);
   
   prevText = text;
   prevColor = color;
 }
 
 public void SetAlpha(float alpha, float duration = 0.0f) {
   
   if (duration <= 0.0f) {
     
     animating = false;
     color.a = alpha;
     UpdateText();
     
   } else {
     
     fromAlpha = color.a;
     toAlpha = alpha;
     animating = true;
     time = 0;
     this.duration = duration;
   }
 }
 
 static private string HtmlColorText(string text, Color col) {
   
   int redValue = (int)(col.r * 255.0f);
   int greenValue = (int)(col.g * 255.0f);
   int blueValue = (int)(col.b * 255.0f);
   int alphaValue = (int)(col.a * 255.0f);
   string colorText = string.Format("#{0:x2}{1:x2}{2:x2}{3:x2}", redValue, greenValue, blueValue, alphaValue);
   return string.Format("<color={0}>{1}</color>", colorText, text);
   
 }
}

出来たら「ResetText」オブジェクトに追加してください。この「ColoredText」にTextMeshで表示したいテキストと色を渡す事で、わざわざタグを書かなくても色をつける事が出来ます。また、時間を指定してフェードイン・アウトをする事も出来ます。

unitysw_9_1.png

BigStopWatchでは「RESET」を表示・非表示するときにグルっと回転させています。これも実現したいので「ResetButtonRoot」オブジェクトのComponentに「AngleAnimation.cs」を追加してください。

unitysw_9_2.png

背景の色もタップしたら光るようなイメージで時間をかけて変えたりしたいと思います。新たに「BackgroundColor.cs」というスクリプトをProjectウィンドウの「Script」フォルダに作成して、以下のコードをコピーしてください。

//
// BackgoundColor.cs
//
using UnityEngine;
using System.Collections;
[ExecuteInEditMode]
public class BackgroundColor : MonoBehaviour {
 
 public Color[] colors;
 public Color offColor = Color.black;
 float flashDuration = 1.0f;
 
 Color prevOffColor = Color.clear;
 
 bool animating = false;
 float duration;
 int colorIndex;
 float time;
 
 // Use this for initialization
 void Start () {
   
   if (camera != null) {
     
     camera.backgroundColor = offColor;
     
   }
   
 }
 
 // Update is called once per frame
 void Update () {
   
   if (camera == null) return;
   
   if (animating) {
     
     time += Time.deltaTime;
     float t = time / flashDuration;
     
     if (t >= 1.0f) {
       t = 1.0f;
       animating = false;
     }
     
     camera.backgroundColor = Color.Lerp(colors[colorIndex], offColor, t);
     
   } else if (IsPropertyChanged()) {
     
     camera.backgroundColor = offColor;
     
     prevOffColor = offColor;
     
   }
 }
 
 public bool IsPropertyChanged() {
   
   if (offColor != prevOffColor) {
     
     return true;
   }
   
   return false;
   
 }
 
 public void Flash(int colorIndex) {
   
   if (colorIndex < 0 || colors.Length <= colorIndex) {
     return;
   }
   
   this.colorIndex = colorIndex;
   animating = true;
   time = 0.0f;
   
 }
}

作成したら、「Main Camera」オブジェクトへ追加してください。

unitysw_9_3.png

タップした場所によって光る背景の色を変えたいので、「BackgroundColor」では複数の色を持つようにしています。インスペクタにある「offColor」は待機状態の単独の色、「colors」はタップしたときに使う複数の色の配列となっています。

「BackgroundColor」の「Off Color」は黒、「Colors」の各色は以下のような感じにしてみてください。

unitysw_9_4.png

表示する側の準備はできたので、コントロールする側のコードを追加します。「Stopwatch.cs」を以下の様に修正してください。

//
// Stopwatch.cs
//
using UnityEngine;
using System;
using System.Collections;
public class Stopwatch : MonoBehaviour {
 
 enum StopwatchState {
   Zero,
   Play,
   Pause
 }
 
 public TextMesh timeText;
 public TimeCircleController timeCircleController;
 // ここから追加 ->
 public AngleAnimation resetButtonAngleAnimation;
 public ColoredText resetButtonText;
 public float buttonAnimDuration = 0.2f;
 
 public BackgroundColor bgColor;
 // <- ここまで追加
 StopwatchState state = StopwatchState.Zero;
 TimeSpan lastStopTimeSpan;
 DateTime startDateTime;
 
 // ここから追加 ->
 void Start () {
   float alpha = (state == StopwatchState.Pause) ? 1.0f : 0.0f;
   resetButtonText.SetAlpha(alpha, 0.0f);
 }
 // <- ここまで追加
 
 void Update () {
   
   bool circleAnim = false;
   
   if (Input.GetMouseButtonDown(0)) {
     ChangeState(ref circleAnim);
   }
   
   UpdateTime(circleAnim);
 }
 
 void ChangeState(ref bool circleAnim) {
   
   ButtonType buttonType = ButtonType.Background;
     
   Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
   RaycastHit hit;
   
   if (Physics.Raycast(ray, out hit, 2.0f)) {
     Button button = hit.collider.gameObject.GetComponent<Button>();
     if (button != null) {
       buttonType = button.type;
     }
   }
   
   if (buttonType == ButtonType.Reset && state == StopwatchState.Pause) {
     
     lastStopTimeSpan = new TimeSpan(0);
     startDateTime = DateTime.UtcNow;
     
     state = StopwatchState.Zero;
     
     circleAnim = true;
     
     // ここから追加 ->
     SetVisibleResetButton(false);
     FlashBackground(1);
     // ここまで追加
     
   } else if (state == StopwatchState.Play) {
     
     TimeSpan ts = DateTime.UtcNow - startDateTime;
     lastStopTimeSpan = ts + lastStopTimeSpan;
     
     state = StopwatchState.Pause;
     
     // ここから追加 ->
     SetVisibleResetButton(true);
     FlashBackground(0);
     // ここまで追加
     
   } else {
     
     startDateTime = DateTime.UtcNow;
     
     state = StopwatchState.Play;
     
     // ここから追加 ->
     SetVisibleResetButton(false);
     FlashBackground(0);
     // ここまで追加
     
   }
   
 }
 
 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);
     
   }
   
 }
 
 // ここから追加 ->
 void SetVisibleResetButton(bool visible) {
   
   if (resetButtonText != null) {
     
     float alpha = visible ? 1.0f : 0.0f;
     resetButtonText.SetAlpha(alpha, buttonAnimDuration);
     
   }
   
   if (resetButtonAngleAnimation != null) {
     
     float fromAngle = visible ? 360.0f : 0.0f;
     float toAngle = visible ? 0.0f : -360.0f;
     resetButtonAngleAnimation.SetAngle(fromAngle, toAngle, true, buttonAnimDuration, false);
     
   }
   
 }
 
 void FlashBackground(int colorIndex) {
   
   if (bgColor != null) {
     
     bgColor.Flash(colorIndex);
     
   }
 }
 // <- ここまで追加
 
 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));
   }
 }
}

「Stopwatch」オブジェクトのインスペクタに「Reset Button Angle Animation」と「Reset Button Text」と「Bg Color」という項目が追加されていますのでそれぞれ以下のようにアサインしてください。

unitysw_9_5.png

これで「RESET」は計測のポーズ中だけ現れ、画面をタップしたときには画面全体が光るような感じになっていると思います。

今回のWebPlayerビルド

今回は以上です。次回はとりあえず放っておいていたシェーダーを差し替えたいと思います。

前へ | 次へ

Unityでストップウォッチを作る その8 リセットボタンを作る

今回はリセットボタンを作りたいと思います。ボタンと言ってもBigStopWatchではいかにも押せるような普通のボタンではないのですが、一応ボタンと表現しておきます。個人的にはリセットボタンの無い状態の方がスマートで好きなのですが、さすがにストップウォッチの機能として単純すぎるかなと思って機能を追加しています。ただ、そういう積算できないアナログのストップウォッチというのも存在しています。

オブジェクトの作成

リセットボタンを置くスペースを作りたいのでシーンの「Root」オブジェクトのPositionのXを「-60」にしてください。Gameウィンドウで見てこのくらいでしょうか。

unitysw_8_1.png

新たにゲームオブジェクトを作成して「ResetButtonRoot」と名前を変更してください。置き場所はそのまま一番上の階層で大丈夫です。「Position」を「X = 170 / Y = 110 / Z = 0」あたりにします。

unitysw_8_2.png

「ResetButtonRoot」の子に3DTextを作成して「ResetText」と名前を変更し、インスペクタのパラメータを以下の様にしてください。

・Text > RESET
・Character Size > 4
・Font Size > 50

unitysw_8_3.png

この「RESET」の文字あたりをタップしたことを知りたいのでコリジョンを作ります。四角い領域のBox Colliderとかでもいいのですが、四角より円にしたいと思います。Sphere Colliderでも円の判定は作れそうですが、ちょっとメッシュの量が無駄な気がしますので、オリジナルのメッシュでMesh Colliderを作ります。

新たに「Button.cs」と「CircleMesh.cs」というスクリプトをProjectウィンドウの「Script」フォルダに作成してください。コードは以下になります。

//
// Button.cs
//
using UnityEngine;
using System.Collections;
public enum ButtonType {
 Background,
 Reset
}
public class Button : MonoBehaviour {
 
 public ButtonType type = ButtonType.Reset;
 
}
//
// CircleMesh.cs
//
using UnityEngine;
using System.Collections;
[ExecuteInEditMode()]
[RequireComponent(typeof(MeshRenderer))]
[RequireComponent(typeof(MeshFilter))]
public class CircleMesh : MonoBehaviour {
 
 public float radius = 50.0f;
 public int triangleCount = 6;
 public int textureOffsetX = 40;
 public int textureOffsetY = 0;
 
 float prevRadius = 0.0f;
 int prevTriangleCount = 0;
 int prevTextureOffsetX = -1;
 int prevTextureOffsetY = -1;
 Vector2 uvPoint;
 
 MeshFilter meshFilter;
 
 // Use this for initialization
 void Start () {
   UpdateMesh();
 }
 
 // Update is called once per frame
 void Update () {
   if (IsPropertyChanged()) {
     UpdateMesh();
   }
 }
 
 bool IsPropertyChanged() {
   
   if (radius != prevRadius ||
     triangleCount != prevTriangleCount ||
     textureOffsetX != prevTextureOffsetX ||
     textureOffsetY != prevTextureOffsetY) {
     
     return true;
   }
   
   return false;
 }
 
 void UpdateMesh() {
   
   if (meshFilter == null) {
     meshFilter = GetComponent<meshFilter>();
   }
   
   Mesh mesh = meshFilter.sharedMesh;
   if (mesh == null) return;
   
   mesh.Clear();
   
   Material mat = renderer.sharedMaterial;
   if (mat == null) return;
   
   Texture tex = mat.mainTexture;
   if (tex == null) return;
   
   Texture2D tex2d = (Texture2D)tex;
   if (tex2d == null) return;
   
   // Draw Texture
   
   int texWidth = tex2d.width;
   int texHeight = tex2d.height;
   Vector2 size = Vector2.zero;
   int padding = 1;
   int border = 0;
   int scale = 1;
   
   uvPoint = new Vector2(
     (float)(textureOffsetX + padding) / (float)texWidth,
     (float)(textureOffsetY + padding) / (float)texHeight);
   
   Rect clearRect = BSWUtility.CreateRectForClear(textureOffsetX, textureOffsetY, size, padding, border, scale);
   BSWUtility.DrawRect(tex2d, clearRect, Color.clear);
   
   tex2d.Apply();
   
   // Create Mesh
   
   if (triangleCount < 1) {
     triangleCount = 1;
   }
   
   int verticesCount = triangleCount + 2;
   Vector3[] vertices = new Vector3[verticesCount];
   int[] triangles = new int[triangleCount * 3];
   Vector2[] uv = new Vector2[verticesCount];
   
   for (int i = 0; i < verticesCount; i++) {
     
     float phase = Mathf.PI * 2.0f / (float)verticesCount * (float)i;
     vertices[i] = new Vector3(Mathf.Sin(phase) * radius, Mathf.Cos(phase) * radius, 0.0f);
     uv[i] = uvPoint;
     
   }
   
   for (int i = 0; i < triangleCount; i++) {
     
     triangles[i * 3] = 0;
     triangles[i * 3 + 1] = i + 1;
     triangles[i * 3 + 2] = (i + 2) % verticesCount;
     
   }
   
   mesh.vertices = vertices;
   mesh.triangles = triangles;
   mesh.uv = uv;
   
   mesh.RecalculateNormals();
   mesh.RecalculateBounds();
   mesh.Optimize();
   
   prevRadius = radius;
   prevTriangleCount = triangleCount;
   prevTextureOffsetX = textureOffsetX;
   prevTextureOffsetY = textureOffsetY;
 }
}

ResetButtonRootオブジェクトの子に「ResetButton」という名前でゲームオブジェクトを作成してください。作成できたら「CircleMesh.cs」と「Button.cs」を「ResetButton」オブジェクトに追加します。

またここでも新たにメッシュを作ります。Projectウィンドウの「Create / Empty Mesh」を選択してメッシュを作成し、「Mesh」フォルダの中に「ResetButtonMesh」と名前をつけて入れてください。この「ResetButtonMesh」を先ほど作成した「ResetButton」オブジェクトのMeshFilterにアサインします。マテリアルもいままで使ってきたものと同じ「Material」をMeshRendererにアサインしてください。さらに同じく「ResetButton」オブジェクトに「Mesh Collider」を追加します。「ResetButton」オブジェクトを選択した状態で、メニューの「Component / Physics / Mesh Collider」を選択してください。

unitysw_8_4.png

「Stopwatch.cs」を以下のコードに差し替えてください。

//
// 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) {
   
   // 追加ここから ->
   ButtonType buttonType = ButtonType.Background;
     
   Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
   RaycastHit hit;
   
   if (Physics.Raycast(ray, out hit, 2.0f)) {
     Button button = hit.collider.gameObject.GetComponent<Button>();
     if (button != null) {
       buttonType = button.type;
     }
   }
   
   if (buttonType == ButtonType.Reset && 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));
   }
 }
}

これで、ストップウォッチを停止中にRESETの文字付近をクリックorタップしたときにはゼロに戻り、その他の領域をタップしたときには続きを再生する様になります。

解説

「Button.cs」クラスは特に機能がある訳ではなく、どの種類のボタンかのタグを付けているだけのものです。ボタンの種類はButtonTypeというenumでを作って表す様にしました。今回のチュートリアルのように単純な場合はUnity標準機能のタグを使ってもいいのですが、もしボタン以外にも何かタグを付けて判別をしたくなった場合に1種類のタグを使い回すのは良くないかなと思い、このように別のものを作っています。

「Stopwatch.cs」に追加したのは、クリックした位置がRESETに当たったかどうかを判定するコードです。ストップウォッチの停止中にRaycastがRESETのコリジョンに当たって、そのオブジェクトについている「Button」コンポーネントのButtonTypeが「Reset」である場合にリセットするというような処理です。

Input.mousePositionでクリックしたスクリーン上の位置を取得して、表示しているMain CameraでRaycastを飛ばしています。カメラの奥行きは-1〜1なのでRaycastの距離は2にしています。

今回のWebPlayerビルド

これで計測時間が積算できる様になりましたが、RESETが機能しないときも表示しっぱなしなので、次回はストップウォッチのステートの状態に合わせてRESETの表示・非表示を切り替える様にしたいと思います。

前へ | 次へ

Unityでストップウォッチを作る その7 「分」のサークルを作る

前回で「秒」を表示するサークルが出来上がりましたので、これをコピーして「分」のサークルも作ります。

オブジェクトの複製と設定

シーンの「SecTimeCircle」を選択した状態でコピー&ペーストをして複製します。新たに出来たオブジェクトの名前を「MinTimeCircle」と変更してください。

unitysw_7_1.png

複製したオブジェクトはほぼそのまま再利用しますが、メッシュだけは変更します。Projectウィンドウで新たに2つメッシュを作ってそれぞれ「MinTimeCircleMesh」「MinNeedleMesh」と名前を変更し、「MinTimeCircle」とその中にある「Needle」オブジェクトのメッシュを差し替えてください。

unitysw_7_2.png
unitysw_7_3.png

「MinTimeCircle」のインスペクタの値を以下の様に変更してください。

・Line Radius > 530
・Number Radius > 554
・Part Line Count > 6
・Texture Offset X > 20

unitysw_7_4.png

「MinTImeCircle」内の「Needle」のインスペクタの値を以下の様に変更してください。

・Position Y > 506
・Rotation Z > 180
・Texture Offset X > 20

unitysw_7_5.png

「TimeCircleController.cs」に「分」のサークルをコントロールするコードを追加します。「TimeCircleController.cs」を以下のコードに差し替えてください。

using UnityEngine;
using System;
using System.Collections;
public class TimeCircleController : MonoBehaviour {
 public AngleAnimation secCircleAngleAnimation;
 public AngleAnimation minCircleAngleAnimation; // <- 1行追加
 public float circleAnimDuration = 0.2f;
 
 public AngleAnimation secNeedleAngleAnimation;
 public AngleAnimation minNeedleAngleAnimation; // <- 1行追加
 public float needleAnimDuration = 0.1f;
 
 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);
   }
   
   if (secNeedleAngleAnimation) {
     secNeedleAngleAnimation.SetAngle(-secAngle, animate, needleAnimDuration);
   }
   
   // ここから追加 ->
   float minAngle = (float)((ts.TotalHours - Math.Truncate(ts.TotalHours)) * 360.0);
   
   if (minCircleAngleAnimation) {
     minCircleAngleAnimation.SetAngle(minAngle, animate, circleAnimDuration);
   }
   
   if (minNeedleAngleAnimation) {
     minNeedleAngleAnimation.SetAngle(-minAngle, animate, needleAnimDuration);
   }
   // <- ここまで追加
 }
}

「Circles」オブジェクトの「TimeCircleController」のインスペクタに新たに「Min Circle Angle Animation」と「Min Needle Angle Animation」という項目が追加されています。「Min Circle Angle Animation」に「MinTimeCircle」オブジェクトをアサインし、「Min Needle Angle Animation」には「MinTimeCircle」内の「NeedleHandle」オブジェクトをアサインしてください。

unitysw_7_6.png

これで「秒」と「分」のサークルがそれぞれ計測時間に応じてきちんと動く様になっていると思います。

解説

「MinTimeCircle」や「Needle」で変更したところは主に位置調整です。「Part Line Count」で1分の間の目盛りの間隔を10秒単位の6にしています。あと、「Texture Offset X」でテクスチャに描画する位置を「秒」のものと被らないようにしていますので、目盛りや針の色を変えることも出来る様になっています。

今回のWebPlayerビルド

ここまで出来るとだいぶBigStopWatchっぽくなってきましたが、今の状態だと積算できないストップウォッチのままなので、次回はリセットボタンを作りたいと思います。

前へ | 次へ

Unityでストップウォッチを作る その6 針を作る

今回はサークル上で計測時間を指し示す針を作っていきます。

スクリプトの追加と修正

まず、Projectウィンドウに「Needle.cs」という名前でC#スクリプトを作り、以下のコードをコピーしてください。

//
// Needle.cs
//
using UnityEngine;
using System.Collections;
[ExecuteInEditMode()]
[RequireComponent(typeof(MeshRenderer))]
[RequireComponent(typeof(MeshFilter))]
public class Needle : MonoBehaviour {
 
 public Vector2 size = new Vector2(10.0f, 30.0f);
 public float bottomMargin = 4.0f;
 public Color color = Color.white;
 public int textureOffsetX = 0;
 public int textureOffsetY = 0;
 
 Vector2 prevSize = Vector2.zero;
 float prevBottomMargin = 0.0f;
 Color prevColor = Color.clear;
 int prevTextureOffsetX = -1;
 int prevTextureOffsetY = -1;
 
 MeshFilter meshFilter;
 
 void Start () {
   UpdateNeedle();
 }
 
 void Update () {
   
   if (IsNeedlePropertyChanged()) {
     UpdateNeedle();
   }
 }
 
 bool IsNeedlePropertyChanged()
 {
   if (size != prevSize ||
     color != prevColor ||
     bottomMargin != prevBottomMargin ||
     textureOffsetX != prevTextureOffsetX ||
     textureOffsetY != prevTextureOffsetY) {
     
     return true;
   }
   return false;
 }
 
 void UpdateNeedle()
 {
   if (meshFilter == null) {
     meshFilter = GetComponent<meshFilter>();
   }
   
   Mesh mesh = meshFilter.sharedMesh;
   if (mesh == null) return;
   
   Material mat = renderer.sharedMaterial;
   if (mat == null) return;
   
   Texture tex = mat.mainTexture;
   if (tex == null) return;
   
   Texture2D tex2d = (Texture2D)tex;
   if (tex2d == null) return;
   
   // Draw Texture
   
   int padding = 1;
   int border = 1;
   int scale = 1;
   
   float nHeight = size.y;
   float nHalfWidth = size.x * 0.5f;
   
   int drawHeight = (int)Mathf.Sqrt(nHeight * nHeight + nHalfWidth * nHalfWidth * 0.25f);
   float rad = Mathf.Atan2(nHalfWidth, nHeight);
   int drawWidth = Mathf.CeilToInt(nHeight * Mathf.Sin(rad));
   
   Vector2 drawSize = new Vector2(drawWidth, drawHeight);
   
   int texWidth = tex2d.width;
   int texHeight = tex2d.height;
   
   Rect clearRect = BSWUtility.CreateRectForClear(textureOffsetX, textureOffsetY, drawSize, padding, border, scale);
   BSWUtility.DrawRect(tex2d, clearRect, Color.clear);
   
   Rect drawRect = BSWUtility.CreateRectForDraw(textureOffsetX, textureOffsetY, drawSize, padding, border, scale);
   BSWUtility.DrawRect(tex2d, drawRect, color);
   
   float bottomRate = bottomMargin / size.y;
   Vector2[] needleUv = CreateUv(textureOffsetX, textureOffsetY, drawSize, texWidth, texHeight, padding, border, scale, bottomRate);
   
   tex2d.Apply();
   
   
   // Create Mesh
   
   float halfHeight = size.y * 0.5f;
   float halfWidth = size.x * 0.5f;
   float yBorder = Mathf.Sin(rad);
   float xBorder = Mathf.Cos(rad);
   float bottomY = - halfHeight + size.y * bottomRate;
   float bottomHalfWidth = halfWidth * bottomRate;
   
   Vector3[] vertices = new Vector3[] {
     
     // triangle 0 ~ 3
     new Vector3(0.0f, bottomY, 0.0f),
     new Vector3(- halfWidth, halfHeight, 0.0f),
     new Vector3(0.0f, halfHeight, 0.0f),
     new Vector3(halfWidth, halfHeight, 0.0f),
     
     // left border 4 ~ 5
     new Vector3(- bottomHalfWidth - xBorder, bottomY - yBorder, 0.0f),
     new Vector3(- xBorder - halfWidth, halfHeight - yBorder, 0.0f),
     
     // right border 6 ~ 7
     new Vector3(bottomHalfWidth + xBorder, bottomY - yBorder, 0.0f),
     new Vector3(xBorder + halfWidth, halfHeight - yBorder, 0.0f),
     
     // top border 8 ~ 10
     new Vector3(- halfWidth, halfHeight + border, 0.0f),
     new Vector3(0.0f, halfHeight + border, 0.0f),
     new Vector3(halfWidth, halfHeight + border, 0.0f),
     
     // bottom side 11 ~ 12
     new Vector3(- bottomHalfWidth, bottomY, 0.0f),
     new Vector3(bottomHalfWidth, bottomY, 0.0f),
     
     // bottom border 13 ~ 15
     new Vector3(- bottomHalfWidth, bottomY - border, 0.0f),
     new Vector3(bottomHalfWidth, bottomY - border, 0.0f),
     new Vector3(0.0f, bottomY - border, 0.0f)
   };
   
   int[] triangles = new int[] {
     // triangle
     11, 1, 2,
     11, 2, 0,
     0, 2, 3,
     0, 3, 12,
     // side
     4, 5, 11, 11, 5, 1,
     12, 3, 6, 6, 3, 7,
     // top
     1, 8, 2, 2, 8, 9,
     2, 9, 3, 3, 9, 10,
     // bottom
     13, 11, 0, 13, 0, 15,
     15, 0, 12, 15, 12, 14,
     // corner
     11, 13, 4,
     14, 12, 6,
     1, 5, 8,
     3, 10, 7
   };
   
   Vector2[] uv = new Vector2[] {
     // triangle
     needleUv[7],
     needleUv[1],
     needleUv[2],
     needleUv[1],
     // side
     needleUv[3],
     needleUv[4],
     needleUv[3],
     needleUv[4],
     // top
     needleUv[5],
     needleUv[6],
     needleUv[5],
     // bottom side
     needleUv[0],
     needleUv[0],
     // bottom border
     needleUv[8],
     needleUv[8],
     needleUv[9]
   };
   
   mesh.vertices = vertices;
   mesh.triangles = triangles;
   mesh.uv = uv;
   
   mesh.RecalculateNormals();
   mesh.RecalculateBounds();
   mesh.Optimize();
   
   // Keep properties
   
   prevSize = size;
   prevColor = color;
   prevTextureOffsetX = textureOffsetX;
   prevTextureOffsetY = textureOffsetY;
 }
 
 static public Vector2[] CreateUv(int originX, int originY, Vector2 squareSize, int texWidth, int texHeight, int padding, int border, int scale, float bottomRate) {
   
   float minX = (float)(originX + padding) / texWidth;
   float minY = (float)(originY + padding) / texHeight;
   float maxY = (float)(originY + padding + (border * 2 + squareSize.y) * scale) / (float)texHeight;
   
   float sqMinX = (float)(originX + padding + border * scale) / (float)texWidth;
   float sqMidX = (float)(originX + padding + (border + squareSize.x * bottomRate) * scale) / (float)texWidth;
   float sqMaxX = (float)(originX + padding + (border + squareSize.x - 1) * scale) / (float)texWidth;
   float sqMinY = (float)(originY + padding + border * scale) / (float)texHeight;
   float sqMaxY = (float)(originY + padding + (border + squareSize.y) * scale) / (float)texHeight;
   
   Vector2[] uv = new Vector2[] {
     new Vector2(sqMinX, sqMinY),
     new Vector2(sqMinX, sqMaxY),
     new Vector2(sqMaxX, sqMaxY),
     new Vector2(minX, sqMinY),
     new Vector2(minX, sqMaxY),
     new Vector2(sqMinX, maxY),
     new Vector2(sqMaxX, maxY),
     new Vector2(sqMidX, sqMinY),
     new Vector2(sqMinX, minY),
     new Vector2(sqMidX, minY),
   };
   
   return uv;
 }
}

また、既に作成している「TimeCircleController.cs」を以下の様に修正してください。

using UnityEngine;
using System;
using System.Collections;
public class TimeCircleController : MonoBehaviour {
 public AngleAnimation secCircleAngleAnimation;
 public float circleAnimDuration = 0.2f;
 
 // 追加ここから ->
 public AngleAnimation secNeedleAngleAnimation;
 public float needleAnimDuration = 0.1f;
 // <- 追加ここまで
 
 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);
   }
   
   // 追加ここから ->
   if (secNeedleAngleAnimation) {
     secNeedleAngleAnimation.SetAngle(-secAngle, animate, needleAnimDuration);
   }
   // <- 追加ここまで
 }
}

オブジェクトの追加と設定

「SecTimeCircle」の子に「SecTimeCircle / NeedleHandle / Needle」という感じでオブジェクトを作成して配置してください。「NeedleHandle」のPositionは「X=0 / Y=0 / Z=0」にしてください。また「NeedleHandle」オブジェクトに前回使ったスクリプト「AngleAnimation.cs」を追加してください。

unitysw_6_1.png

「Needle」オブジェクトに「Needle.cs」を追加してPositionを「X=0 / Y=664 / Z=0」にしてください。また、Projectウィンドウの「Create / Empty Mesh」で新たにメッシュを作成して「Needle」オブジェクトのMeshFilterにアサインしてください。名前は「SecNeedleMesh」しておきます。「Material」も同じく「Needle」オブジェクトに追加してください。

unitysw_6_2.png

「Circles」オブジェクトの「TimeCircleController」のインスペクタに「Sec Needle Angle Animation」という項目が追加されていますので、「NeedleHandle」オブジェクトをアサインします。

unitysw_6_3.png

これでサークル上に針が表示される様になっていると思います。

解説

針は、本当は外部の画像編集ソフトで作って単純な四角形のメッシュに表示するのが単純ではあるのですが、このチュートリアルの最初に外部アセットは使わないと宣言したので、ちょっと工夫してUnityだけできれいな三角形になるように作ってみました。UVやメッシュは以下のような感じで作っています。

unitysw_6_4.png

テクスチャには長方形を描画しておいて、オレンジの線の部分を中心に三角形の左側と右側を左右対象にして組み合わせています。さらに、その周囲の水色の部分のエッジの外側にマージンをつけて補間を効かせているという感じです。

「Needle.cs」でやっている事はテクスチャへの描画とメッシュの作成なので「TimeCircle」とそんなに違わないと思います。まだ説明はしていませんでしたが「Texture Offset X」と「Texture Offset Y」というパラメータが「TimeCircle.cs」と「Needle.cs」にあります。これはテクスチャに描画するときの左下の原点の位置です。「Needle」では「Texture Offset Y」の初期値を30にしているので最初から「TimeCircle」の描画領域と被らないようになっています。普通、テクスチャアトラスを作るソフトなどでは、組み込む画像を指定すると自動的に描画する座標を良い感じで決めてくれたりしますが、ここでそれをやるのはめんどくさいので、もしサイズを変更してかぶってしまう場合は手動で位置を調整してください。

「TimeCircleController」に追加した部分は針の回転位置をAngleAnimationに渡す処理を追加しています。サークル上の計測秒数の位置に動かしていますが、逆方向にサークルが回っているので常に針は固定した位置にいるという感じになっています。

今回のWebPlayerビルド

これでようやく秒のサークルが出来上がりました。1サークル分の素材が全部そろったので、次回はこれをそのまま活用して「分」のサークルを作ります。

前へ | 次へ

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

今回は、前々回から作ってきた「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ビルド

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

前へ | 次へ

Unityでストップウォッチを作る その4 秒数の表示

今回は秒数のサークルに数字を追加します。これの事です。

unitysw_4_1.png

数字のプレファブを作る

TextMeshを使って数字の表示をしますが、前回のようにまとめてメッシュを作るということまではせず、数字ひとつごとにゲームオブジェクトを作って置いていきます。

まずひとつの数字のプレファブをつくります。メニューから「GameObject / Create Other / 3DText」を選択し、シーンにオブジェクトを作成してください。

unitysw_4_0.png

出来たら「Number」と名前を変更して、TextMeshの設定を以下の様に変更してください

・Character Size > 4
・Alignment > center
・Anchor > middle center
・Font Size > 60

unitysw_4_2.png

Projectウィンドウに「Prefab」フォルダを作成し、その中に「Number」オブジェクトをドラッグ&ドロップしてプレファブを作成してください。プレファブが出来たらシーンからは削除してください。

unitysw_4_3.png

サークルに数字を表示する

「TimeCircle.cs」を以下のコードに差し替えてください。

using UnityEngine;
using System.Collections;
[ExecuteInEditMode()]
[RequireComponent(typeof(MeshRenderer))]
[RequireComponent(typeof(MeshFilter))]
public class TimeCircle : MonoBehaviour {
 
 public float lineRadius = 640.0f;
 public Vector2 largeLineSize = new Vector2(2.0f, 12.0f);
 public Vector2 smallLineSize = new Vector2(1.5f, 8.0f);
 public int largeLineCount = 60;
 public int partLineCount = 10;
 public Color largeLineColor = Color.white;
 public Color smallLineColor = Color.gray;
 public int textureOffsetX = 0;
 public int textureOffsetY = 0;
 
 float prevLineRadius = 0;
 Vector2 prevLargeLineSize = Vector2.zero;
 Vector2 prevSmallLineSize = Vector2.zero;
 int prevLargeLineCount = 0;
 int prevPartLineCount = 0;
 Color prevLargeLineColor = Color.clear;
 Color prevSmallLineColor = Color.clear;
 int prevTextureOffsetX = -1;
 int prevTextureOffsetY = -1;
 
 // 追加ここから ->
 public GameObject numberPrefab;
 public float numberRadius = 620.0f;
 
 float prevNumberRadius = 0;
 
 GameObject numberRoot = null;
 string numberRootName = "NumberRoot";
 // <- 追加ここまで
 
 MeshFilter meshFilter;
 
 void Start () {
   
   UpdateLines();
   // 追加ここから ->
   UpdateNumbers();
   // <- 追加ここまで
   
 }
 
 void Update () {
   
   if (IsLinePropertyChanged()) {
     
     UpdateLines();
   }
   
   if (IsNumberPropertyChanged()) {
     
     UpdateNumbers();
     
   }
 }
 
 bool IsLinePropertyChanged() {
   
   if (lineRadius != prevLineRadius ||
     largeLineSize != prevLargeLineSize ||
     smallLineSize != prevSmallLineSize ||
     largeLineCount != prevLargeLineCount ||
     partLineCount != prevPartLineCount ||
     largeLineColor != prevLargeLineColor ||
     smallLineColor != prevSmallLineColor ||
     textureOffsetX != prevTextureOffsetX ||
     textureOffsetY != prevTextureOffsetY) {
     
     return true;
   }
   
   return false;
 }
 
 bool IsNumberPropertyChanged() {
   
   if (numberRadius != prevNumberRadius) {
     
     return true;
   }
   
   return false;
 }
 
 void UpdateLines()
 {
   if (meshFilter == null) {
     meshFilter = GetComponent<meshFilter>();
   }
   
   Mesh mesh = meshFilter.sharedMesh;
   if (mesh == null) return;
   
   mesh.Clear();
   
   Material mat = renderer.sharedMaterial;
   if (mat == null) return;
   
   Texture tex = mat.mainTexture;
   if (tex == null) return;
   
   Texture2D tex2d = (Texture2D)tex;
   if (tex2d == null) return;
   
   // Draw Texture
   
   int padding = 1;
   int border = 1;
   int scale = 2;
   int originX = textureOffsetX;
   int originY = textureOffsetY;
   int texWidth = tex2d.width;
   int texHeight = tex2d.height;
   
   Rect largeClearRect = BSWUtility.CreateRectForClear(originX, originY, largeLineSize, padding, border, scale);
   BSWUtility.DrawRect(tex2d, largeClearRect, Color.clear);
   
   Rect largeDrawRect = BSWUtility.CreateRectForDraw(originX, originY, largeLineSize, padding, border, scale);
   BSWUtility.DrawRect(tex2d, largeDrawRect, largeLineColor);
   
   Vector2[] largeUv = CreateUv(originX, originY, largeLineSize, texWidth, texHeight, padding, border, scale);
   
   originX += (int)(padding * 2 + (largeLineSize.x + border * 2) * scale);
   
   Rect smallClearRect = BSWUtility.CreateRectForClear(originX, originY, smallLineSize, padding, border, scale);
   BSWUtility.DrawRect(tex2d, smallClearRect, Color.clear);
   
   Rect smallDrawRect = BSWUtility.CreateRectForDraw(originX, originY, smallLineSize, padding, border, scale);
   BSWUtility.DrawRect(tex2d, smallDrawRect, smallLineColor);
   
   Vector2[] smallUv = CreateUv(originX, originY, smallLineSize, texWidth, texHeight, padding, border, scale);
   
   tex2d.Apply();
   
   renderer.sharedMaterial.mainTexture = tex2d;
   
   // Create Mesh
   
   int lineCount = largeLineCount * partLineCount;
   int vertexCount = lineCount * 4;
   int triangleCount = lineCount * 6;
   
   Vector3[] vertices = new Vector3[vertexCount];
   int[] triangles = new int[triangleCount];
   Vector2[] uv = new Vector2[vertexCount];
   
   Vector3[] largeLinePos = CreateLinePositions(largeLineSize, border, lineRadius);
   Vector3[] smallLinePos = CreateLinePositions(smallLineSize, border, lineRadius);
   
   for (int i = 0; i < lineCount; i++) {
     
     int vTopIndex = i * 4;
     int tTopIndex = i * 6;
     
     float angle = - ((float)i / lineCount) * 360.0f;
     var rot = Quaternion.Euler(0, 0, angle);
           var m = Matrix4x4.TRS(Vector3.zero, rot, Vector3.one);
     
     Vector3[] currentPos;
     Vector2[] currentUv;
     
     if (i % partLineCount == 0) {
       
       currentPos = largeLinePos;
       currentUv = largeUv;
     
     } else {
       
       currentPos = smallLinePos;
       currentUv = smallUv;
       
     }
     
     for (int j = 0; j < 4; j++) {
       
       int index = vTopIndex + j;
       vertices[index] = m.MultiplyPoint3x4(currentPos[j]);
       uv[index] = currentUv[j];
       
     }
     
     triangles[tTopIndex] = vTopIndex;
     triangles[tTopIndex + 2] = triangles[tTopIndex + 3] = vTopIndex + 1;
     triangles[tTopIndex + 1] = triangles[tTopIndex + 4] = vTopIndex + 2;
     triangles[tTopIndex + 5] = vTopIndex + 3;
   }
   
   mesh.vertices = vertices;
   mesh.triangles = triangles;
   mesh.uv = uv;
   
   mesh.RecalculateNormals();
   mesh.RecalculateBounds();
   mesh.Optimize();
   
   // Keep Properties
   
   prevLineRadius = lineRadius;
   prevLargeLineSize = largeLineSize;
   prevSmallLineSize = smallLineSize;
   prevLargeLineCount = largeLineCount;
   prevPartLineCount = partLineCount;
   prevLargeLineColor = largeLineColor;
   prevSmallLineColor = smallLineColor;
   prevTextureOffsetX = textureOffsetX;
   prevTextureOffsetY = textureOffsetY;
 }
 
 static public Vector3[] CreateLinePositions(Vector2 lineSize, int border, float radius) {
   
   float minX = - lineSize.x * 0.5f - border;
   float maxX = lineSize.x * 0.5f + border;
   float minY = - lineSize.y * 0.5f - border + radius;
   float maxY = lineSize.y * 0.5f + border + radius;
   
   return new Vector3[] {
     new Vector3(minX, minY, 0.0f),
     new Vector3(maxX, minY, 0.0f),
     new Vector3(minX, maxY, 0.0f),
     new Vector3(maxX, maxY, 0.0f)
   };
   
 }
 
 // 追加ここから ->
 void UpdateNumbers()
 {
   if (numberPrefab == null)
     return;
   
   foreach (Transform child in transform) {
     if (child.gameObject.name == numberRootName) {
       DestroyImmediate(child.gameObject);
     }
   }
   
   numberRoot = new GameObject(numberRootName);
   numberRoot.transform.parent = transform;
   numberRoot.transform.localScale = Vector3.one;
   numberRoot.transform.localPosition = Vector3.zero;
   numberRoot.transform.localRotation = Quaternion.identity;
   
   for (int i = 0; i < largeLineCount; i++) {
     
     GameObject handleObj = new GameObject("NumberHandle_"+i);
     handleObj.transform.parent = numberRoot.transform;
     handleObj.transform.localRotation = Quaternion.Euler(0, 0, - (float)i / largeLineCount * 360.0f);
     handleObj.transform.localScale = Vector3.one;
     handleObj.transform.localPosition = Vector3.zero;
     
     GameObject numObj = Instantiate(numberPrefab) as GameObject;
     numObj.transform.parent = handleObj.transform;
     numObj.transform.localScale = Vector3.one;
     numObj.transform.localRotation = Quaternion.identity;
     
     TextMesh textMesh = numObj.GetComponent<textMesh>();
     
     if (textMesh != null) {
       
       textMesh.text = i.ToString();
       numObj.name = "Number_" + textMesh.text;
       textMesh.transform.localPosition = new Vector3(0, numberRadius, 0);
       
     }
   }
   
   prevNumberRadius = numberRadius;
 }
 // <- 追加ここまで
 
 static public Vector2[] CreateUv(int originX, int originY, Vector2 lineSize, int texWidth, int texHeight, int padding, int border, int scale) {
   
   float minX = (float)(originX + padding) / texWidth;
   float maxX = (float)(originX + padding + (border * 2 + lineSize.x) * scale) / (float)texWidth;
   float minY = (float)(originY + padding) / texHeight;
   float maxY = (float)(originY + padding + (border * 2 + lineSize.y) * scale) / (float)texHeight;
   
   Vector2[] uv = new Vector2[] {
     new Vector2(minX, minY),
     new Vector2(maxX, minY),
     new Vector2(minX, maxY),
     new Vector2(maxX, maxY)
   };
   
   return uv;
 }
}

「TimeCircle」のインスペクタに「Number Prefab」の項目が追加されていますので、先ほど作成した「Number」プレファブをアサインしてください。サークルの目盛りの内側に数字が表示される様になっていると思います。

unitysw_4_4.png

スクリプトで「Number」プレファブを秒の数だけ作って回転させた位置においています。前回の様にメッシュを自力でまとめたりはしていませんが、この段階でDrawcallを見てもおそらく2しか使ってないと思います。おそらく「TimeCircle」で1、「TextMesh」で1使っているだけでうまいことDynamic Batchが効いているんじゃないでしょうか。

今回のWebPlayerビルド

今回はここで終わって、次回はサークルを動かしたいと思います。

前へ | 次へ

Unityでストップウォッチを作る その3 目盛りのメッシュを作る

今回は秒数目盛りのメッシュを表示するオブジェクトを作りたいと思います。こんな感じの1分で1周するアナログのストップウォッチ的な目盛りです。

unitysw_3_1.png

このように画面よりもかなり大きい目盛りの円(以後サークルと呼びます)を作りたいので、一枚の大きな画像を表示するのではなく、目盛りひとつの画像をたくさん並べて表示するという作り方をします。

スクリプトの作成

Projectウィンドウの「Script」フォルダの中に「BSWUtililty.cs」と「TimeCircle.cs」の2つのC#スクリプトを追加して、以下のコードをコピーしてください。

//
// 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);
  }
}
//
// TimeCircle.cs
//
using UnityEngine;
using System.Collections;
[ExecuteInEditMode()]
[RequireComponent(typeof(MeshRenderer))]
[RequireComponent(typeof(MeshFilter))]
public class TimeCircle : MonoBehaviour {
 
 public float lineRadius = 640.0f;
 public Vector2 largeLineSize = new Vector2(2.0f, 12.0f);
 public Vector2 smallLineSize = new Vector2(1.5f, 8.0f);
 public int largeLineCount = 60;
 public int partLineCount = 10;
 public Color largeLineColor = Color.white;
 public Color smallLineColor = Color.gray;
 public int textureOffsetX = 0;
 public int textureOffsetY = 0;
 
 float prevLineRadius = 0;
 Vector2 prevLargeLineSize = Vector2.zero;
 Vector2 prevSmallLineSize = Vector2.zero;
 int prevLargeLineCount = 0;
 int prevPartLineCount = 0;
 Color prevLargeLineColor = Color.clear;
 Color prevSmallLineColor = Color.clear;
 int prevTextureOffsetX = -1;
 int prevTextureOffsetY = -1;
 
 MeshFilter meshFilter;
 
 void Start () {
   
   UpdateLines();
   
 }
 
 void Update () {
   
   if (IsLinePropertyChanged()) {
     
     UpdateLines();
   }
 }
 
 bool IsLinePropertyChanged() {
   
   if (lineRadius != prevLineRadius ||
     largeLineSize != prevLargeLineSize ||
     smallLineSize != prevSmallLineSize ||
     largeLineCount != prevLargeLineCount ||
     partLineCount != prevPartLineCount ||
     largeLineColor != prevLargeLineColor ||
     smallLineColor != prevSmallLineColor ||
     textureOffsetX != prevTextureOffsetX ||
     textureOffsetY != prevTextureOffsetY) {
     
     return true;
   }
   
   return false;
 }
 
 void UpdateLines()
 {
   if (meshFilter == null) {
     meshFilter = GetComponent<meshFilter>();
   }
   
   Mesh mesh = meshFilter.sharedMesh;
   if (mesh == null) return;
   
   mesh.Clear();
   
   Material mat = renderer.sharedMaterial;
   if (mat == null) return;
   
   Texture tex = mat.mainTexture;
   if (tex == null) return;
   
   Texture2D tex2d = (Texture2D)tex;
   if (tex2d == null) return;
   
   // Draw Texture
   
   int padding = 1;
   int border = 1;
   int scale = 2;
   int originX = textureOffsetX;
   int originY = textureOffsetY;
   int texWidth = tex2d.width;
   int texHeight = tex2d.height;
   
   Rect largeClearRect = BSWUtility.CreateRectForClear(originX, originY, largeLineSize, padding, border, scale);
   BSWUtility.DrawRect(tex2d, largeClearRect, Color.clear);
   
   Rect largeDrawRect = BSWUtility.CreateRectForDraw(originX, originY, largeLineSize, padding, border, scale);
   BSWUtility.DrawRect(tex2d, largeDrawRect, largeLineColor);
   
   Vector2[] largeUv = CreateUv(originX, originY, largeLineSize, texWidth, texHeight, padding, border, scale);
   
   originX += (int)(padding * 2 + (largeLineSize.x + border * 2) * scale);
   
   Rect smallClearRect = BSWUtility.CreateRectForClear(originX, originY, smallLineSize, padding, border, scale);
   BSWUtility.DrawRect(tex2d, smallClearRect, Color.clear);
   
   Rect smallDrawRect = BSWUtility.CreateRectForDraw(originX, originY, smallLineSize, padding, border, scale);
   BSWUtility.DrawRect(tex2d, smallDrawRect, smallLineColor);
   
   Vector2[] smallUv = CreateUv(originX, originY, smallLineSize, texWidth, texHeight, padding, border, scale);
   
   tex2d.Apply();
   
   renderer.sharedMaterial.mainTexture = tex2d;
   
   // Create Mesh
   
   int lineCount = largeLineCount * partLineCount;
   int vertexCount = lineCount * 4;
   int triangleCount = lineCount * 6;
   
   Vector3[] vertices = new Vector3[vertexCount];
   int[] triangles = new int[triangleCount];
   Vector2[] uv = new Vector2[vertexCount];
   
   Vector3[] largeLinePos = CreateLinePositions(largeLineSize, border, lineRadius);
   Vector3[] smallLinePos = CreateLinePositions(smallLineSize, border, lineRadius);
   
   for (int i = 0; i < lineCount; i++) {
     
     int vTopIndex = i * 4;
     int tTopIndex = i * 6;
     
     float angle = - ((float)i / lineCount) * 360.0f;
     var rot = Quaternion.Euler(0, 0, angle);
           var m = Matrix4x4.TRS(Vector3.zero, rot, Vector3.one);
     
     Vector3[] currentPos;
     Vector2[] currentUv;
     
     if (i % partLineCount == 0) {
       
       currentPos = largeLinePos;
       currentUv = largeUv;
     
     } else {
       
       currentPos = smallLinePos;
       currentUv = smallUv;
       
     }
     
     for (int j = 0; j < 4; j++) {
       
       int index = vTopIndex + j;
       vertices[index] = m.MultiplyPoint3x4(currentPos[j]);
       uv[index] = currentUv[j];
       
     }
     
     triangles[tTopIndex] = vTopIndex;
     triangles[tTopIndex + 2] = triangles[tTopIndex + 3] = vTopIndex + 1;
     triangles[tTopIndex + 1] = triangles[tTopIndex + 4] = vTopIndex + 2;
     triangles[tTopIndex + 5] = vTopIndex + 3;
   }
   
   mesh.vertices = vertices;
   mesh.triangles = triangles;
   mesh.uv = uv;
   
   mesh.RecalculateNormals();
   mesh.RecalculateBounds();
   mesh.Optimize();
   
   // Keep Properties
   
   prevLineRadius = lineRadius;
   prevLargeLineSize = largeLineSize;
   prevSmallLineSize = smallLineSize;
   prevLargeLineCount = largeLineCount;
   prevPartLineCount = partLineCount;
   prevLargeLineColor = largeLineColor;
   prevSmallLineColor = smallLineColor;
   prevTextureOffsetX = textureOffsetX;
   prevTextureOffsetY = textureOffsetY;
 }
 
 static public Vector3[] CreateLinePositions(Vector2 lineSize, int border, float radius) {
   
   float minX = - lineSize.x * 0.5f - border;
   float maxX = lineSize.x * 0.5f + border;
   float minY = - lineSize.y * 0.5f - border + radius;
   float maxY = lineSize.y * 0.5f + border + radius;
   
   return new Vector3[] {
     new Vector3(minX, minY, 0.0f),
     new Vector3(maxX, minY, 0.0f),
     new Vector3(minX, maxY, 0.0f),
     new Vector3(maxX, maxY, 0.0f)
   };
   
 }
 
 static public Vector2[] CreateUv(int originX, int originY, Vector2 lineSize, int texWidth, int texHeight, int padding, int border, int scale) {
   
   float minX = (float)(originX + padding) / texWidth;
   float maxX = (float)(originX + padding + (border * 2 + lineSize.x) * scale) / (float)texWidth;
   float minY = (float)(originY + padding) / texHeight;
   float maxY = (float)(originY + padding + (border * 2 + lineSize.y) * scale) / (float)texHeight;
   
   Vector2[] uv = new Vector2[] {
     new Vector2(minX, minY),
     new Vector2(maxX, minY),
     new Vector2(minX, maxY),
     new Vector2(maxX, maxY)
   };
   
   return uv;
 }
}

オブジェクトの作成と設定

スクリプトが作成できたら、シーンの中の「Root」オブジェクトの子に「Root/Circles/SecTimeCircle」という配置でゲームオブジェクトを2つ作成してください。それぞれのPositionを「Circles」は「X=0 / Y=-640 / Z=0」に、「SecTimeCircle」は「X=0 / Y=0 / Z=0」にしてください。

「SecTimeCircle」オブジェクトのComponentに「TimeCircle.cs」を追加します。

このままでは何も表示するメッシュやテクスチャがありませんので、前回のスクリプトを使って作成します。

ProjectウィンドウのCreateメニューから「Empty Mesh」を選択してメッシュを作成し、「SecTimeCircleMesh」と名前を変更してください。今後いくつかメッシュを作る事になるので「Mesh」というフォルダを作成してその中に移動しておいてください。

同じくProjectウィンドウのCreateメニューから、今度は「Texture...」を選択し、開いたウィンドウのなかの「Width」と「Height」が両方とも「128」、「Texture Format」が「ARGB32」になっている事を確認したら「Create」ボタンをクリックしてテクスチャを作成してください。テクスチャは今後もこの一枚しか使わないので適当に「Texture/Texture」な感じに置いておいてください。テクスチャ設定の「Filter Mode」は「Bilinear」にしておきます。

このテクスチャを使うのにはMaterialが必要なので作っておきます。Projectウィンドウの「Create/Material」で作成し、これも今後一つしか使わないので適当な名前ですが「Material/Material」のように置いておいてください。「Texture」をこのマテリアルにアサインし、shaderは「Unlit/Transparent」を選択します。このシェーダはラインティングの影響を受けず、テクスチャの透明部分を透過するシェーダーです。実はこのシェーダーを使うとちょっと問題があるのですが、その修正はチュートリアルの最後の方でやりますので、今の所はこれで我慢します。

先ほどシーン上に作った「SecTimeCircle」オブジェクトに「Material」と「SecTimeCircleMesh」をアサインします

この時点で画面上に秒数目盛りのラインが表示されていると思います。あと、タイムのテキストがサークルと被ってしまっているので、TimeTextのYを100くらいに移動しておいてください。

ここまで作業を終えると、プロジェクトの状態は以下のような感じになっていると思います。もし表示されていなかったら一旦Unityを再生してみると表示されるかもしれません。

unitysw_3_2.png

解説

「TimeCircle」のインスペクタのパラメータは以下のような感じです。

・lineRadius > サークルの半径
・largeLineSize > 長いラインのサイズ
・smallLineSize > 短いラインのサイズ
・largeLineCount > 長いラインの数(サークル全体が1分なら60で1秒)
・partLineCount > 長いラインの間を短いラインで分ける数(1秒につき0.1秒ごとなら10)
・largeLineColor > 長いラインの色
・smallLineColor > 短いラインの色
・textureOffsetX > テクスチャに描画する左の位置
・textureOffsetY > テクスチャに描画する下の位置

とりあえず良い感じに表示するようにパラメータの初期値を入れていますが、これらのパラメータを変更するとUnityを再生していなくてもリアルタイムで表示が変更されていると思います。これはスクリプトに「[ExecuteInEditMode]」と記述しているからで、これによって再生していないエディット中でもStart()やUpdate()が呼ばれます。ただ、毎アップデートごとにデータを更新するのはさすがに処理が重いので、データを更新したら「prev〜」という変数に保存して余計なアップデートをしない様にしています。実際にアプリをリリースするときにはStart()だけでやればいい処理なので、Updateはリリースビルド時に削除するようなマクロを仕込んでおいた方がいいかもしれません。

「UpdateLines()」というメソッドの中ではテクスチャへの描画とメッシュの作成をしています。やっていることは、2種類の目盛りの画像をテクスチャに描画するのと、その目盛りをたくさん並べた大きなサークルをひとつのメッシュとして作成する事です。Unityでテクスチャへ描画するのはドットを打つくらいしか出来ないのですが、BigStopWatchでは単純な四角くらいしか必要としないので十分です。

UnityのメッシュはOpenGLでいうところのGL_TRIANGLESで描画されます。頂点配列をmesh.verticesに、UV配列をmesh.uvに同じ要素数になるように作り、mesh.trianglesでその頂点番号の配列を作ってメッシュへ設定しています。

頂点配列(vertices)は必要な分だけ作れば良いのですが、頂点番号の配列(triangles)は、三角形ごとの頂点単位、つまり3つ単位でないといけません。たとえば四角形をひとつ作るなら、三角形を二つ組み合わせるけど2つの頂点は共通で使えるので、verticesとuvの配列の要素数は4、trianglesは6という事になります。

「UpdateLines()」の中の処理で、後々共通で使いたいものは「BSWUtility」の方に記述しています。四角をテクスチャに描画したり座標を取得するところあたりです。

目盛りはこんな感じで、四角に少しマージンを持たせて描画しています。

unitysw_3_3.png

四角いメッシュを単純に塗りつぶせばいいんじゃないかと思うかもしれませんが、やってみるとジャギーでひどい事になってしまいます。最近のiOSではマルチサンプリングが使えたりして多少ごまけたりもしますが、補間をテクスチャに任せたほうが遥かにきれいな線が表示できます。ただ、それは回転させるからの話で、回転させない四角は、ぴったりとピクセルが合う様に配置して補間させない方がきれいです。

メッシュは「lineRadius」の値だけY軸方向に移動させた位置に目盛りひとつ分の座標を「CreateLinePositions()」でつくって、それを必要な数だけMatrixで回転させてメッシュ全体を作っている感じです。

わざわざ1個のメッシュにしないで、目盛り1個のゲームオブジェクトを複製して作っても見た目は同じでコードも分かりやすいと思うのですが、やはり処理の負荷を考えると最初からまとめられるメッシュはまとめておいた方が良いと思います。UnityではDynamicBatchという機能があって勝手にメッシュをまとめてくれたりもしますが、一度にまとめられる頂点数に制限があったりとかあまり当てにしすぎると裏切られたりするので、自分でやっておくにこしたことはないです。

今回のWebPlayerビルド

以上で今回は終わりです。次回はこのサークルに数字を追加します。

前へ | 次へ

Unityでストップウォッチを作る その2 メッシュとテクスチャの作成

前回はタイム表示をしたので、次は目盛りのラインなどを描画したいと思う訳ですが、Unityで描画をするにはまずなによりメッシュが必要になります。しかし、Unityのプロジェクトを作った段階では、CubeやらSphereなどの3D用のものが数種類と、無駄に細かいメッシュのPlaneしか作れません。BigStopWatchのラインひとつひとつを標準のPlaneで置いていくのはちょっと無駄な感じです。

さらにBigStopWatchではきれいなラインを引くために、テクスチャにラインを描画して少しマージンを付けたものをメッシュに割り当てて描画しています。メッシュを単純に塗りつぶすとメッシュのエッジ部分がガタガタになってしまいます。なのでそのテクスチャも作りたいのですが、Unityのメニューのどこを見渡してもテクスチャを作成するような項目は見つかりません。

しかし、なければ自分で作ってしまえるというのもUnityの良い所です。今回はエディタ拡張をちょっとだけ使ってメッシュとテクスチャを作れるようにしたいと思います。

メッシュとテクスチャをスクリプトで作る

Projectウィンドウ内のメニューから「Create/Folder」を選択して「Editor」というフォルダを作成します。このフォルダ名は重要です。以後作っていくフォルダ名は適当で良いですが、これはちゃんと「Editor」としてください。

その「Editor」フォルダ内に
「BSWEditorUtility.cs」
「CreateEmptyMesh.cs」
「CreateTexture.cs」
という3つのC#スクリプトを作成してください。

unitysw_2_1.png

それぞれのスクリプトは以下になります。

//
// BSWEditorUtility
//
using UnityEngine;
using UnityEditor;
using System.IO;
using System.Collections;
public class BSWEditorUtility : MonoBehaviour {
  static public string GetSelectedDirectoryPath() {
     
    string path = null;
     
    foreach (UnityEngine.Object obj in Selection.objects) {
         
      path = AssetDatabase.GetAssetPath(obj);
         
      if (!string.IsNullOrEmpty(path) && !Directory.Exists(path)) {
        path = Path.GetDirectoryName(path);
      }
         
      break;
    }
     
    if (string.IsNullOrEmpty(path)) {
     return "Assets";
    }
     
    return path;
  }
}
//
// CreateTexture.cs
//
using UnityEngine;
using UnityEditor;
using System.Collections;
public class CreateTexture : ScriptableWizard {
 
  public string textureName;
  public int width = 256;
  public int height = 256;
  public TextureFormat textureFormat = TextureFormat.ARGB32;
  
  [MenuItem ("Assets/Create/Texture...")]
  static void CreateWizard()
  {
    ScriptableWizard.DisplayWizard("Create Texture", typeof(CreateTexture));
  }
  
  void OnWizardCreate() {
    
    if (string.IsNullOrEmpty(textureName)) {
      textureName = "New Texture " + width + "x" + height;
    }
     
    string path = BSWEditorUtility.GetSelectedDirectoryPath() + "/" + textureName + ".asset";
    path = AssetDatabase.GenerateUniqueAssetPath(path);
    
    Texture2D tex = new Texture2D(width, height, textureFormat, false);
    tex.name = textureName;
     
    ClearTexture(tex);
    
    AssetDatabase.CreateAsset(tex, path);
  }
   
  static public void ClearTexture(Texture2D tex) {
     
    int texWidth = tex.width;
    int texHeight = tex.height;
     
    for (int x = 0; x < texWidth; x++) {
      for (int y = 0; y < texHeight; y++) {
        tex.SetPixel(x, y, Color.clear);
      }
    }
  }
}
//
// CreateEmptyMesh.cs
//
using UnityEngine;
using UnityEditor;
using System.Collections;
public class CreateEmptyMesh : MonoBehaviour {
  [MenuItem ("Assets/Create/Empty Mesh")]
  static void Create() {
   
    string path = BSWEditorUtility.GetSelectedDirectoryPath() + "/New Empty Mesh.asset";
    path = AssetDatabase.GenerateUniqueAssetPath(path);
   
    Mesh mesh = new Mesh();
   
    AssetDatabase.CreateAsset(mesh, path);
  }
}

これらのコードを追加する事でメニューの「Assets/Create/」の中に新たにメニュー項目が追加されます。「Empty Mesh」を選択すれば空のメッシュが作成でき、「Texture...」を選択すればサイズを指定してテクスチャが作成できるようになります。

ちょっと解説

今回のスクリプトには全て「using UnityEditor」という宣言が追加されています。この名前空間にはエディタとしての機能を利用できるクラス、例えば今回だとAssetDatabaseというプロジェクトウィンドウ内のアセットにアクセスできるクラスなどがあります。UnityEditorの名前空間を使うスクリプトは「Editor」フォルダに入れておかないといけないようです。と言っても、エディター上で実行しているだけなら「Editor」フォルダに入れていなくても問題なく使えてしまいます。ただ、ビルドするときにはエラーが出てしまうので、何かの理由でEditorフォルダに入れたくないということがあるようでしたら、以下のページに解決法が書いてありましたので、参考にすると良いと思います。
【Unity Tips】using UnityEditorを使うけどEditorに入れたくない

メニューに項目を追加して実行できるようにしているのは「[MenuItem ("〜")]」という一行です。これによってメニューに項目が追加され、このあとに記述されたメソッドがメニューを選択すると実行されます。ちなみに、「Asset/Create/」というメニューのパスで指定されたものは、画面上のメニューのAssetsに表示されるだけでなく、ProjectウィンドウのCreateメニューにも表示されます。

unitysw_2_3.png

「BSWEditorUtility」はメッシュとテクスチャの両方共通の処理があったので作りました。GetSelectedDirectoryPath()というのがそれで、Projectウィンドウで選択されているフォルダを取得するものです。Selectionクラスで選択されているフォルダorファイルが取得できるので、選択されていればその階層のフォルダを返し、選択されていない場合は最上位階層の「Assets」を返すようにしています。

「CreateEmptyMesh」と「CreateTexture」は、それぞれ新たにメッシュやテクスチャを作成してアセットとして保存するということをやっています。ただ単に「new Mesh()」とするだけではいずれどこかに消えてなくなりますが、AssetDatabase.CreateAsset()でアセットとして保存して残せます。

今回は以上です。次回は実際にメッシュの中身を作成します。

前へ | 次へ