- 投稿日:2020-01-10T23:33:16+09:00
[VisualStudio] Debug/Releaseビルドで、参照するdllを変えたい
もくじ
→https://qiita.com/tera1707/items/4fda73d86eded283ec4fやりたいこと
VisualStdio2019でC#アプリのプロジェクトを作り、そのアプリがなにかのdll(.netのdll)を参照するとする。
そのときに、Debugビルドのときと、Releaseビルドのときに、参照するdllを変えたい。※同じソリューション内にあるdllのプロジェクトであれば、「プロジェクトの参照」にすれば、使う側のビルドのConfigurationに自動で合わせてくれるが、外部の自分で作ってないdllだと「アセンブリの参照」を追加することになり、その際、Debugのとき、Releaseのとき、という設定ができないので困ったので、調べたことをメモする。
やり方
VisualStudioのプロジェクトファイル(.csproj)を直接開いて、参照の設定をいじることで実現する。
具体的には、csprojの参照の設定に、下記のように書き込む。
<Reference Include="参照するライブラリの名前"> <HintPath Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">..\Library\Debug\参照するライブラリの名前.dll</HintPath> <HintPath Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">..\Library\Release\参照するライブラリの名前.dll</HintPath> </Reference> ・ ・※上の例のパスは適当。
手順
- まずはビルドのConfigurationをDebugにする。
- C#のプロジェクトの「参照」から、とりあえずDebugビルド時に使用したいdllを、「アセンブリの追加」で追加する。
- 開いた参照マネージャダイアログの「参照」ボタンを押し、追加したいDebugビルド向けのdllを追加する。
- 追加したデバッグ用のdllが、参照の一覧に入る。そのプロパティを見ると、選んだパスが出ている。(今回は、Debugフォルダの中にDebug用を入れている。)
- 次に、そのプロジェクトの.csprojファイルを、なにかのエディタで開く。その中に、下記のような箇所ができているはず。(参照マネージャでdllを指定すると、相対パスでdllのパスが入るっぽい)
<ItemGroup> <Reference Include="DllTestCs"> <HintPath>dll\Debug\DllTestCs.dll</HintPath> </Reference> ・ ・このとき、参照に追加した直後にcsprojを開くと、まだdllの参照設定がファイルに反映されていない場合があるので、その場合はまず一度ビルドしておく。(そのときにcsprojに保存されるっぽい)
- ここを、冒頭の内容を参考に書き換えて、保存する。(このとき、一度VisualStudioを閉じておいた方がよいかも?)
<ItemGroup> <Reference Include="DllTestCs"> <HintPath Condition="'$(Configuration)|$(Platform)' == 'Debug|AnyCPU'">dll\Debug\DllTestCs.dll</HintPath> <HintPath Condition="'$(Configuration)|$(Platform)' == 'Release|AnyCPU'">dll\Release\DllTestCs.dll</HintPath> </Reference> ・ ・これで完了。下図のように、DebugとReleaseのConfigurationを選んだ時に別々の設定になっている。
※例では、プラットフォームの指定(ここでは
x64)をしているが、とくに指定しなくてもよい。
下記のようにしておけば、プラットフォームを何にしていても、DebugとReleaseによって使うものを分けてくれる。<ItemGroup> <Reference Include="DllTestCs"> <HintPath Condition="'$(Configuration)' == 'Debug'">dll\Debug\DllTestCs.dll</HintPath> <HintPath Condition="'$(Configuration)' == 'Release'">dll\Release\DllTestCs.dll</HintPath> </Reference> ・ ・
- 投稿日:2020-01-10T23:13:36+09:00
DXライブラリーで切断される
C#.Application.csusing System; using System.Diagnostics; using System.Threading; using DxLibDLL; namespace 通信テスト { class Application { const int TargetFPS = 60; // 目標のFPS(Frame Per Second, 1秒あたりのフレーム数) static readonly bool EnableFrameSkip = true; // 高負荷時にフレームスキップするか(falseの場合は処理落ち(スロー)) const double MaxAllowSkipTime = 0.2; // フレームスキップする最大間隔(秒)。これ以上の間隔が空いた場合はスキップせずに処理落ちにする。 const long IntervalTicks = (long)(1.0 / TargetFPS * 10000000); // フレーム間のTick数。1Tick = 100ナノ秒 = 1/10000000秒 const int MaxAllowSkipCount = (int)(TargetFPS * MaxAllowSkipTime); static long nextFrameTicks = IntervalTicks; // 次のフレームの目標時刻 static Stopwatch stopwatch = new Stopwatch(); // FPS制御のために時間を計るための高精度タイマー static int skipCount = 0; // 何回連続でフレームスキップしたか static long fpsTicks = 0; // FPS計測のためのTicks。 static int fpsFrameCount = 0; // FPS計測のためのフレームカウント。60回数えるごとに、要した時間からFPSを算出する。 /// <summary> /// 現在のFPS(Frame per Second) /// </summary> public static float CurrentFPS { get; private set; } static Game game; [STAThread] static void Main(string[] args) { Thread.CurrentThread.Priority = ThreadPriority.Highest; // スレッドの優先度を上げておく // 画面リフレッシュレートと目標フレームレートが等しい場合は垂直同期を有効に、等しくない場合は垂直同期を無効にする DX.SetWaitVSyncFlag(DX.GetRefreshRate() == TargetFPS ? DX.TRUE : DX.FALSE); DX.SetWindowText("通信テスト"); // ウィンドウのタイトル DX.SetGraphMode(932, 480, 32); // ウィンドウサイズ(画面解像度)の指定 DX.ChangeWindowMode(DX.TRUE); // ウィンドウモードにする(DX.FALSEを指定するとフルスクリーンになる) DX.SetAlwaysRunFlag(DX.TRUE); // ウィンドウが非アクティブでも動作させる DX.DxLib_Init(); // DXライブラリの初期化 DX.SetMouseDispFlag(DX.TRUE); // マウスを表示する(DX.FALSEを指定すると非表示になる) DX.SetDrawScreen(DX.DX_SCREEN_BACK); // 描画先を裏画面とする(ダブルバッファ) game = new Game(); game.Init(); DX.ScreenFlip(); stopwatch.Start(); while (DX.ProcessMessage() == 0) // ウィンドウが閉じられるまで繰り返す { // FPSの計測 fpsFrameCount++; if (fpsFrameCount >= 60) { long elapsedTicks = stopwatch.Elapsed.Ticks - fpsTicks; float elapsedSec = elapsedTicks / 10000000f; CurrentFPS = fpsFrameCount / elapsedSec; fpsFrameCount = 0; fpsTicks = stopwatch.Elapsed.Ticks; } game.Update(); if (DX.GetWaitVSyncFlag() == DX.TRUE) { if (EnableFrameSkip) { long waitTicks = nextFrameTicks - stopwatch.Elapsed.Ticks; // 余った時間 if (waitTicks < 0) // 目標時刻をオーバーしている { if (skipCount < MaxAllowSkipCount) // 連続フレームスキップ数が最大スキップ数を超えていなければ { skipCount++; // フレームスキップ(描画処理を飛ばす) } else { // 最大スキップ数を超えてるので、フレームスキップしないで描画 nextFrameTicks = stopwatch.Elapsed.Ticks; Draw(); } } else { Draw(); } nextFrameTicks += IntervalTicks; } else { Draw(); } } else { long waitTicks = nextFrameTicks - stopwatch.Elapsed.Ticks; // 余った時間(待機が必要な時間) if (EnableFrameSkip && waitTicks < 0) { if (skipCount < MaxAllowSkipCount) { skipCount++; // フレームスキップ(描画処理を飛ばす) } else { nextFrameTicks = stopwatch.Elapsed.Ticks; Draw(); } } else { if (waitTicks > 20000) // あと2ミリ秒以上待つ必要がある { // Sleep()は指定した時間でピッタリ戻ってくるわけではないので、 // 余裕を持って、「待たなければならない時間-2ミリ秒」Sleepする int waitMillsec = (int)(waitTicks / 10000) - 2; Thread.Sleep(waitMillsec); } // 時間が来るまで何もしないループを回して待機する while (stopwatch.Elapsed.Ticks < nextFrameTicks) { } Draw(); } nextFrameTicks += IntervalTicks; } } DX.DxLib_End(); // DXライブラリ終了処理 } static void Draw() { DX.ClearDrawScreen(); // 描画先の内容をクリアする game.Draw(); // ゲーム描画 DX.ScreenFlip(); // 裏画面と表画面を入れ替える skipCount = 0; // フレームスキップのカウントをリセット } } }C#.Game.csusing DxLibDLL; using MyLib; using System; using System.Collections.Generic; using System.Net; using System.Text; namespace 通信テスト { public class Game { string message = "待機中"; bool openNetWork = false; bool connectNetWork = false; int count = 0; string getData = null; int keyHandle = -1; public void Init() { Input.Init(); NetWork.Init(9850); } public void Update() { Input.Update(); if (Input.GetButtonDown(DX.PAD_INPUT_1)) { Reset(); NetWork.OpenNetWork(); openNetWork = true; message = "接続受付開始"; } else if (Input.GetButtonDown(DX.PAD_INPUT_2)) { Reset(); //StringBuilder sb = new StringBuilder(); ////DX.KeyInputString(0, 64, 8, sb, DX.TRUE); //int[] ip = GetConnectIPAddress(sb.ToString()); //if (NetWork.Connect(ip[0], ip[1], ip[2], ip[3])) //{ // connectNetWork = true; // message = "接続確認"; // NetWork.Send("データ送信:" + count); //} keyHandle = DX.MakeKeyInput(8, DX.TRUE, DX.TRUE, DX.FALSE); DX.SetActiveKeyInput(keyHandle); } else if (Input.GetButtonDown(DX.PAD_INPUT_3)) { count++; NetWork.Send("データ送信:" + count); } else if (keyHandle == -1 && Input.GetButtonDown(DX.PAD_INPUT_9)) { NetWork.End(); DX.DxLib_End(); } else if (Input.GetButtonDown(DX.PAD_INPUT_10)) { Reset(); } if (keyHandle != -1 && DX.CheckKeyInput(keyHandle) != 0) { StringBuilder sb = new StringBuilder(); DX.GetKeyInputString(sb, keyHandle); DX.DeleteKeyInput(keyHandle); keyHandle = -1; int[] ip = GetConnectIPAddress(sb.ToString()); if (NetWork.Connect(ip[0], ip[1], ip[2], ip[3])) { connectNetWork = true; message = "接続確認"; NetWork.Send("データ送信:" + count); } } if (openNetWork && NetWork.GetConnect()) { connectNetWork = true; message = "接続確認"; NetWork.closeNetWork(); NetWork.Send("データ送信:" + count); } if (connectNetWork) { if (NetWork.GetData() != getData) { getData = NetWork.GetData(); if (getData == "") { message = "メッセージ受信待ち"; } else { message = "メッセージを受信しました"; } } if (NetWork.LostNetWork()) { message = "ネットワークが切断されました"; connectNetWork = false; openNetWork = false; NetWork.End(); NetWork.Init(9850); getData = null; } } } public void Draw() { DX.SetFontSize(64); DX.DrawString(0, 0, message, DX.GetColor(255, 255, 255)); if(keyHandle != -1) { DX.DrawKeyInputString(0, 64, keyHandle); } if(getData != null) { DX.DrawString(0, 64, getData, DX.GetColor(255, 255, 255)); } NetWork.Draw(); if (openNetWork) { DX.DrawString(0, 128 + 64, GetConnectMessage().ToString(), DX.GetColor(255, 255, 255)); } } void Reset() { NetWork.Init(9850); connectNetWork = false; openNetWork = false; message = "待機中"; } /// <summary> /// IPアドレスの取得 /// </summary> /// <returns>IPアドレスのint型</returns> int[] GetIPAddress() { string hostName = Dns.GetHostName(); IPAddress[] address = Dns.GetHostAddresses(hostName); string[] splited = new string[4]; int[] result = new int[4]; string addressStr = address[address.Length - 1].ToString(); splited = addressStr.Split(new char[] { '.' }); for (int i = 0; i < splited.Length; i++) { result[i] = int.Parse(splited[i]); } return result; } /// <summary> /// 10進数から16進数に変換する /// </summary> /// <param name="x">変換したい10進数</param> /// <returns>変換された16進数</returns> string Change10to16(int x) { string result = ""; if(x == 0) { result = "0"; } while (x != 0) { string addStr; switch (x % 16) { case 10: addStr = "a"; break; case 11: addStr = "b"; break; case 12: addStr = "c"; break; case 13: addStr = "d"; break; case 14: addStr = "e"; break; case 15: addStr = "f"; break; default: addStr = (x % 16).ToString(); break; } result = String.Concat(addStr, result); x /= 16; } return result; } /// <summary> /// 16進数から10進数に変換する /// </summary> /// <param name="x">変換したい16進数</param> /// <returns>変換された10進数</returns> int Change16to10(string x) { int result = 0; int count = 1; while (x != "") { int addNum; switch (x[x.Length - 1]) { case 'a': addNum = 10; break; case 'b': addNum = 11; break; case 'c': addNum = 12; break; case 'd': addNum = 13; break; case 'e': addNum = 14; break; case 'f': addNum = 15; break; default: addNum = int.Parse(x[x.Length - 1].ToString()); break; } result += addNum * (int)Math.Pow(16, count - 1); count++; x = x.Remove(x.Length - 1, 1); } return result; } string GetConnectMessage() { int[] ip = GetIPAddress(); string[] result = new string[ip.Length]; for (int i = 0; i < result.Length; i++) { result[i] = Change10to16(ip[i]); } for(int i = 0; i < 4; i++) { if (result[i].Length == 1) { result[i] = String.Concat("0", result[i]); } } return result[0] + result[1] + result[2] + result[3]; } int[] GetConnectIPAddress(string message) { int[] result = new int[4]; List<string> list = new List<string>(); int length = (int)Math.Ceiling((double)message.Length / 2); for (int i = 0; i < length; i++) { int start = 2 * i; if (message.Length <= start) { break; } if (message.Length < start + 2) { list.Add(message.Substring(start)); } else { list.Add(message.Substring(start, 2)); } } for(int i = 0; i < list.Count; i++) { result[i] = Change16to10(list[i]); } return result; } } }C#.NetWork.csusing System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Runtime.InteropServices; using DxLibDLL; namespace 通信テスト { static class NetWork { static int port; static int netHandle; public static void Init(int port) { NetWork.port = port; netHandle = -1; } /// <summary> /// 接続する /// </summary> /// <param name="d1">IPの1個目</param> /// <param name="d2">IPの2個目</param> /// <param name="d3">IPの3個目</param> /// <param name="d4">IPの4個目</param> /// <returns>接続できたかどうか</returns> public static bool Connect(int d1, int d2, int d3, int d4) { DX.IPDATA ip; ip.d1 = (byte)d1; ip.d2 = (byte)d2; ip.d3 = (byte)d3; ip.d4 = (byte)d4; netHandle = DX.ConnectNetWork(ip, port); return netHandle != -1; } /// <summary> /// 接続を終了する /// </summary> public static void End() { DX.CloseNetWork(netHandle); } /// <summary> /// 接続を受け付ける /// </summary> public static void OpenNetWork() { DX.PreparationListenNetWork(port); } /// <summary> /// 接続受付を中止する /// </summary> public static void closeNetWork() { DX.StopListenNetWork(); } /// <summary> /// データを送信する /// </summary> /// <param name="str">送信するメッセージ</param> public static void Send(string str) { if (netHandle != -1) { IntPtr buffer = Marshal.StringToHGlobalAnsi(str); DX.NetWorkSend(netHandle, buffer, 1000); } } /// <summary> /// 接続があるか確かめる /// </summary> /// <returns>新たな接続があるかどうか</returns> public static bool GetConnect() { netHandle = DX.GetNewAcceptNetWork(); return netHandle != -1; } /// <summary> /// データが送信されているかどうか /// </summary> /// <returns>送信されたメッセージ</returns> public static string GetData() { if(DX.GetNetWorkDataLength(netHandle) > 0) { IntPtr strLength = new IntPtr(); IntPtr str = new IntPtr(); DX.NetWorkRecvToPeek(netHandle, strLength, 4); if(int.Parse(strLength.ToString()) + 4 <= DX.GetNetWorkDataLength(netHandle)) { DX.NetWorkRecv(netHandle, strLength, 4); DX.NetWorkRecv(netHandle, str, int.Parse(strLength.ToString())); } return str.ToString(); } else { return ""; } } /// <summary> /// 新しく接続が切られたかどうか /// </summary> /// <returns>新しく接続が切られたかどうか</returns> public static bool LostNetWork() { return DX.GetLostNetWork() != netHandle; } public static void Draw() { DX.DrawString(0, 128, netHandle.ToString(), DX.GetColor(255, 255, 255)); } } }C#.Input.csusing DxLibDLL; namespace MyLib { public enum Pad { One, Two, Three, Four, } static class Input { public const int MaxPadNum = 4; static int[] prevStates; // 1フレーム前の状態 static int[] currentStates; // 現在の状態 static int[] currentKey = new int[256]; static int[] prevKey = new int[256]; // 初期化。最初に1回だけ呼んでください。 public static void Init() { prevStates = new int[MaxPadNum]; currentStates = new int[MaxPadNum]; } static void UpdateKey() { currentKey.CopyTo(prevKey, 0); byte[] tmpKey = new byte[256]; DX.GetHitKeyStateAll(tmpKey); for (int i = 0; i < 256; i++) { if (tmpKey[i] != 0) { currentKey[i]++; } else { currentKey[i] = 0; } } } // 最新の入力状況に更新する処理。 // 毎フレームの最初に(ゲームの処理より先に)呼んでください。 public static void Update() { UpdateKey(); currentStates.CopyTo(prevStates, 0); currentStates[(int)Pad.One] = DX.GetJoypadInputState(DX.DX_INPUT_KEY_PAD1); currentStates[(int)Pad.Two] = DX.GetJoypadInputState(DX.DX_INPUT_PAD2); currentStates[(int)Pad.Three] = DX.GetJoypadInputState(DX.DX_INPUT_PAD3); currentStates[(int)Pad.Four] = DX.GetJoypadInputState(DX.DX_INPUT_PAD4); } // ボタンが押されているか? public static bool GetButton(Pad pad, int buttonId) { // 今ボタンが押されているかどうかを返却 return (currentStates[(int)pad] & buttonId) != 0; } public static bool GetButton(int buttonId) { return GetButton(Pad.One, buttonId); } //キーボードが押されているか public static bool GetHitKey(int buttonId) { return DX.CheckHitKey(buttonId) != 0; } // ボタンが押された瞬間か? public static bool GetButtonDown(Pad pad, int buttonId) { // 今は押されていて、かつ1フレーム前は押されていない場合はtrueを返却 return ((currentStates[(int)pad] & buttonId) & ~(prevStates[(int)pad] & buttonId)) != 0; } public static bool GetButtonDown(int buttonId) { return GetButtonDown(Pad.One, buttonId); } //キーボードが押された瞬間か public static bool GetHitKeyDown(int buttonId) { return currentKey[buttonId] == 1; } // ボタンが離された瞬間か? public static bool GetButtonUp(Pad pad, int buttonId) { // 1フレーム前は押されていて、かつ今は押されていない場合はtrueを返却 return ((prevStates[(int)pad] & buttonId) & ~(currentStates[(int)pad] & buttonId)) != 0; } public static bool GetButtonUp(int ButtonId) { return GetButtonUp(Pad.One, ButtonId); } //キーボードが離された瞬間か public static bool GetHitKeyUp(int buttonId) { return currentKey[buttonId] == 0 && prevKey[buttonId] > 0; } } }PC間でデータを送信しあうプログラムが作りたいです。
簡潔にやりたいことを伝えると、
IPアドレスを取得して、それを8桁の数字(16進数にして)入力させ、それをもとに戻して接続、接続が成功すると即座にデータを送信、その後は受け取り待ち状態。接続されたほうは接続があると即座にデータを送信。その後は受け取り待ち状態。Xキーを押すとちゃんと送れているかどうか判断できるようにカウントを添えて相手に送ります。
できないことは、
接続ができた後瞬時に接続が切れてしまうことです。Application.csは60FPSに制御するもので、人からもらったものなので間違いはありません。(過去に使用したことがあるため)
Game.csはApplication.csから呼ばれるもので、UpdateとDrawが呼ばれています。
Input.csももらったもので、間違いはありません。
NetWork.csが私が書いたもので、DXLibを使って通信するために自分が使いやすいようにしたものです。
Game.cs内のUpdate内のC#.Game.csif (connectNetWork) { if (NetWork.GetData() != getData) { getData = NetWork.GetData(); if (getData == "") { message = "メッセージ受信待ち"; } else { message = "メッセージを受信しました"; } } if (NetWork.LostNetWork()) { message = "ネットワークが切断されました"; connectNetWork = false; openNetWork = false; NetWork.End(); NetWork.Init(9850); getData = null; } }の部分で(おそらく)接続できたのにもかかわらず即切断されましたと表示されてしまいます。
Game.cs内のDraw内でmessageを常に表示し、データを受信したらgetDataに入れ、getDataに文字が入っているなら画面に表示する。という風にしています。
なぜ即切断されるかわからないので誰か教えてください。初の投稿なので、説明不足があるかもしれませんが、お優しい方お願いいたします。
環境は
visual studio 2017
windows10
です。
- 投稿日:2020-01-10T21:57:03+09:00
Unityでコードの速度比較をする時のベストプラクティスを模索してみた
速度比較をしたい!
UnityでC#のコードの最適化をしていて、「この書き方とこの書き方ではどっちが速いんや!」ってなることがよくあるので、速度比較をする時のベストプラクティスを模索してみました。
その1,検証用のコードはTest Runnerで書く
ちょっとテストコードを書きたいときに、空のゲームオブジェクト作ってスクリプトをアタッチして終わったら消すのって地味にめんどくさいです。
Test Runnerを使えば、上記の手順を踏むことなくコードを書いてすぐに実行できるので、爆速でトライ&エラーが出来ます。詳しい使い方はこちらの記事が分かりやすかったです↓
Unity使いは全員Unity Test Runnerを使え!爆速のトライ&エラー環境だぞ!その2,時間を計るだけならStopwatchクラス
時間を計りたいだけならStopwatchクラスが簡単で便利そうです。
現実のストップウォッチのような使い方で時間を計ることが出来ます。参考↓
より高い精度で時間を計測するその3,時間とGC Allocも図りたければPlofiler
時間やらGC Allowやらその他諸々を計測したい場合は、UnityのPlofilerを使いましょう。
速度比較に限らず、最適化には必須の機能なので絶対に使い方を覚えておいた方がいいです。詳しい使い方はこちらの記事が分かりやすかったです↓
【Unity】CPUプロファイラでパフォーマンスを改善する 前編その4,ILを確認する(上級者向け)
C#で書いたコードはコンパイルした際にILというものに変換され、そこから更に機械語に変換されて実行されます。
この変換の際にコンパイラがコードを良い感じに最適化してくれるので、実はC#のコードだけを見ても実際にどういう処理が行われるのかはよく分かりません。例えば、変数の宣言をループ外に書くか、ループ内に書くかの違いがある以下の2つのコードですが、ILを確認してみると全く同一の処理であることが分かります。
ループ外で宣言する場合public class C { public void M() { int sum = 0; int count = 0; int num; while(count < 100){ count++; num = count; sum += num; } } }ループ内で宣言する場合public class C { public void M() { int sum = 0; int count = 0; while(count < 100){ count++; int num = count; sum += num; } } }ILの確認にはSharpLabというWebサービスが便利です。試しに上のコードをコピペしてみましょう。
しかしSharpLabでは、標準ライブラリしか使えないようなのでUnity固有のライブラリを使う場合は、スクリプトをdll化し、逆アセンブルする必要がありそうです。やり方はこちらの記事が分かりやすかったです↓
UnityのスクリプトをDLL化する
C#で作られたプログラムをデコンパイルしてみようILを確認することで、そのコードが速度比較に適したものかどうかを確認したり、なぜその計測結果になるのかを調べる助けになると思います。
まとめ
正確な速度比較をしようと思うとかなり高度な知識が必要になってしまいますが(ILとか機械語とかワカラン)、Stopwatchで時間を計るコードを書いてTest Runnerで実行するくらいなら簡単にできそうですね。
ご意見ご感想、それは違うよ!等ございましたらコメントを頂けますと幸いです。
- 投稿日:2020-01-10T20:25:04+09:00
c#でatcoder その3
概要
c#でatcoderの練習問題やってみる。
練習問題
文字列Sが与えられ、始め空の文字列Tの末尾に "dream", "dreamer", "erase", "eraser"を追加する処理を行って、S=Tにできるか判定する問題です。
成果物
https://paiza.io/projects/aeec1g04HH4TH0CgP7A1FQ
以上。
- 投稿日:2020-01-10T20:22:47+09:00
c#でatcoder その2
概要
c#でatcoderの練習問題やってみる。
練習問題
時刻0のとき、平面上の(0, 0)にいます。平面上のN箇所の点を、ある時刻に移動できるかという問題です。
成果物
https://paiza.io/projects/UdsS3I8-KpPzBCWu_dxCmg
以上。
- 投稿日:2020-01-10T20:20:03+09:00
c#でatcoder
概要
c#でatcoderの練習問題やってみる。
参考にしたページ
https://qiita.com/NotFounds/items/7b166af69a6f52a332de
練習問題
3つの整数値と文字列を1つ受け取り、整数値の和と文字列を一行に出力する問題です。
成果物
https://paiza.io/projects/ln_uYwy4_73P-RMA-k8Ueg
以上。
- 投稿日:2020-01-10T16:22:33+09:00
三日間で作成したゲームの技術的な話
ゲーム開発の技術的な話
はじめに
Qiita初投稿です。
昨年度末に二人で、三日間かけてUnityの2Dにてスマホゲームを開発しました。
作成したゲームは
横スクロールゲームで、プレイヤーが落ちないようにタップして浮かせつつ、横からくる障害物(ブロック)をかわしつつアイテムをとり、体力ゲージを意識しながらプレイするものです。そのとき使った技術的な内容についてまとめてみます。
今回使用した技術・考え方としては以下の3つです。
- シングルトン(GameManager)
- ゲームマスタ(今回は操作説明シーンで使用)
- ブロック自動生成
これらについてまとめていこうと思います。
シングルトン(GameManagerの作成)
シングルトンとは
今回Unityで作成したゲームで最も開発がしやすかった手法として、シングルトンという方法(考え方?)が挙げられます。
シングルトンとは
https://qiita.com/shoheiyokoyama/items/c16fd547a77773c0ccc1
にも記載されている通り、
- 指定したクラスのインスタンスが1つしか存在しないことを保証する
- インスタンスが1個しか存在しないことをプログラム上で表現したい
という利点があります。
作成したアプリが100%この手法を使えているかといわれると自信はないのですが、基本的にはこの考え方に従って開発を進めました。Unityでシングルトンを利用する
Unityでシングルトンを実現するために、まずGameManagerを作成します。
GameManagerで変数を管理するため
ゲーム起動時にGameManagerでインスタンスを生成しておく必要があります。
ゲームを起動すると(基本的には)Startが最初に開かれるためStartSceneにGameManagerを設置します。StartSceneに空のオブジェクトを配置し、GameManagerのスクリプトをアタッチします。
GameManagerの作り方
GameManagerの構成は以下の4つの項目で作りました。
- Awake()でGameManagerのインスタンスを生成
- 使用する変数系(スコアやタイマー、プレイヤーの状態など)を宣言
- 変数を持ってくるためのgetter
- GameManagerにある変数に値をセットするためのsetter
GameManagerが完成すれば、あとは開発を進めていくうちに必要な変数は増えていくので、基本全てGameManagerで宣言、getter・setterを追記していきます。
必要になればGameManagerのインスタンスを参照しgetterを呼ぶ、値を変えたければsetterで値をセットする、をするだけで変数を管理できます。マスターデータの作成(操作説明用マスタの作成)
次は、操作説明に出力させる文言を管理する操作説明用マスターを作成しました。
開発当初はtxtファイルをResourcesフォルダからロードするだけでした。
しかし、txtファイルで読み込むと改行やページ番号の管理などが難しくなってしまったためにマスターを作ることにしました。(小規模のゲーム開発ならここまでしなくてもよかったかも)マスターとは
マスターとは、文字列で管理されたデータ群のことです。(違っているかも)
例えば、RPGなどで出てくるPlayerについて考えてみます。
ある人がいて、その人は剣を持っていて、HPは300で、MPは600で、髪の毛は黒で、レベルは30で……
とたくさんのデータが詰まっていると思います。
このような登場人物がたくさん出てくると管理が大変になってきます。
これらをうまく管理させるために表を作成します。
上記の例で行くと、以下のような表が作れそうです。Player
PlayerId HP Weapon Level 1 300 sord 30 2 500 - 50 3 100 stick 10 (長いので項目略)
このように、表で管理すると見やすく、管理がしやすくなります。
また、登場人物が増えても行を追加するだけで表現することができます。マスターの作り方・処理の流れ
今回Unityで作成たマスターは次のような流れに沿って作成しました。
- Excelでシートを作成
- Excelに必要項目を記載
- Excelのシートにボタンを設置
- マクロを組み、JSONデータを出力させる
- Unity側でMaster.csを作成
- Master.csでJSONデータを読み込む
- JSONデータに従ってUnityが出力する
上記の流れを図にすると、以下のようになります。
マスタを作成してしまえば、行を追加してJSONを出力させればUnity側ですぐ反映させることができます。
また、登場人物すべてのデータの管理が一括で行うことができます。今回のゲーム製作では、Playerなどの管理ではなく、操作説明で表示する文字列をマスタとして作成し
表示しているページ数や、表示するときの画像のパスなどをマスタで管理するようにしました。
(画像のパスはうまく使えていませんが)ブロックの自動生成
今回作成したゲームは、
「横スクロールゲーム」で、Playerが落ちないようにタップして宙に浮かせつつ、
横から流れてくるブロックに触れたり、かわしたりを楽しむものです。
加えて、障害物とPlayerに属性を持たせ、それぞれの属性の相性によって体力ゲージの増減が決定するという要素もあります。その時、障害物であるブロックを自動で、かつランダムで生成するアルゴリズムを考えたので以下にまとめてみます。
ブロックの生成
いきなりすべてのブロックを自動生成する方法を考えるのではなく、まずは1つのブロックをどのように生成するかを考えました。
まず、1×1の正方形のブロックを用意します。
このブロックを縦方向にランダムで伸ばしてあげることで、「壁」を表現できると考えました。
例えば、3×1と5×1と2×1のブロックをそれぞれ横においてみましょう。
すると、以下の図のようになると思います。
今はわかりやすくするために、外枠の線を黒にしていますが、ブロックと同一色にすれば、いい感じのブロックができそうです。
あとは「高さをランダムにして、1つ作ったら生成する場所を1つ右にずらす」をほしい長さの分だけループさせれば1まとまりのブロックが生成できそうです。
ただし、高さ0を含めてしまうと、最悪の場合、すべて高さ0となってしまう可能性があるので、ランダム関数を1~MaxHeightの中からランダムで数値を出す、という処理にすれば今回の問題は回避できそうです。
これをコードにしてみると、以下のようになりました。createBlock.cs// MAX_LENGTHはブロック群の横幅 // MAX_HEIGHTは各ブロックの最大の高さ for (int i = 0; i < MAX_LENGTH; i++) { GameObject block = (GameObject)Resources.Load("Block"); blockHeight = randam.Next(1, MAX_HEIGHT); block.transform.localScale = new Vector3(1, blockHeight, 1); // ランダムな高さを入れる Instantiate(block, new Vector2((float)i, 0.0f), Quaternion.identity); // 作ったブロックを配置 }アイテムの配置
ただ単にブロックが流れてくるだけでは面白くない、属性の変更ができないという意見から、Playerの色を変えるためのアイテムや体力回復アイテムの設置方法を追加で考える必要が出てきました。
純粋にブロックの上に置けばいいですが、ここもランダムにしました。
以下がアイテム設置のコードです。createBlock.cs// MAX_LENGTHはブロック群の横幅 // MAX_HEIGHTは各ブロックの最大の高さ // setItemPathには、アイテムが保存されているパスが格納されている for (int i = 0; i < MAX_LENGTH; i++) { GameObject block = (GameObject)Resources.Load("Block"); blockHeight = randam.Next(1, MAX_HEIGHT); block.transform.localScale = new Vector3(1, blockHeight, 1); // ランダムな高さを入れる var setColorRandomNum = randam.Next(0, 10); // 出現アイテムをランダムで設定 var setBlockAboveItem = (GameObject)Resources.Load(setItemPath[setColorRandomNum]); Instantiate(setBlockAboveItem , new Vector2((float)i, blockHeight + 1.0f), Quaternion.identity); // アイテムブロックの上に配置 Instantiate(block, new Vector2((float)i, 0.0f), Quaternion.identity); // 作ったブロックを配置 }ブロックを横にスクロール
ここまでできれば、あとはブロックを横に動かすだけです。
今回のゲームでは、右から左にブロックが流れるという仕様のため、時間とともに横に動かす処理を書きます。ただし、生成したブロックを各1つずつうごかしているととてもコードが長くなってしまう&冗長です。
そのため、空のparentBlockというオブジェクトを作り、作った各ブロック、アイテムなどを子にしてしまいます。
そして、この親オブジェクトを動かすことで、全体を動かすことができます。
イメージとしては以下の図のような状態です。
これを実際にコードにすると、このような形になります。createBlock.cs// MAX_LENGTHはブロック群の横幅 // MAX_HEIGHTは各ブロックの最大の高さ // setItemPathには、アイテムが保存されているパスが格納されている var parentBlock = (GameObject)Resources.Load("parentBlock"); GameObject parentBlockObj = Instantiate(parentBlock, new Vector2(0.0f, 0.0f), Quaternion.identity) as GameObject; for (int i = 0; i < MAX_LENGTH; i++) { GameObject block = (GameObject)Resources.Load("Block"); blockHeight = randam.Next(1, MAX_HEIGHT); block.transform.localScale = new Vector3(1, blockHeight, 1); // ランダムな高さを入れる var setColorRandomNum = randam.Next(0, 10); // 出現アイテムをランダムで設定 var setBlockAboveItem = (GameObject)Resources.Load(setItemPath[setColorRandomNum]); Instantiate(setBlockAboveItem , new Vector2((float)i, blockHeight + 1.0f), Quaternion.identity); // アイテムブロックの上に配置 var blockObj = Instantiate(block, new Vector2((float)i, 0.0f), Quaternion.identity) as GameObject; blockObj.transform.parent = parentBlockObj.transform; // 生成したブロックの親が誰かを教える }あとはこの親ブロックをTime.deltaTimeなどで右から左に動かす処理を書けばそれっぽい動きをしてくれました。
おわりに
今回まとめた内容はゲームを作るうえではとても基本的な内容だとは思います。
しかし、実際にプログラムを書き、それを自分の言葉でまとめることでより理解が深まったと思っています。
また、いろんな人に見てもらえる環境下で記事を書くことで自分の成長にもつながるかなと考えています。
三日間で成長できたと感じれたハッカソンでした。
- 投稿日:2020-01-10T16:02:20+09:00
c# メンバ変数に一括 代入
Base.csclass Document { public string Name1 { get; set; } public string Name2 { get; set; } public string Name3 { get; set; } public string Name4 { get; set; } public string Name5 { get; set; } public int Age1 { get; set; } public int Age2 { get; set; } public int Age3 { get; set; } public int Age4 { get; set; } public int Age5 { get; set; } } class Person { public string Name { get; set; } public int Age { get; set; } }InsertToDocument.csList&<Person> people = new List<Person>gt; { new Person { Name = "Taro", Age = 31 }, new Person { Name = "Hanako", Age = 33 }, new Person { Name = "Hitoshi", Age = 17 }, new Person { Name = "Yoshie", Age = 28 }, new Person { Name = "Kenta", Age = 21 } };Reflection.csDocument doc1 = new Document(); int index = 1; foreach (var item in people) { // Name[1-5]プロパティに動的にアクセスし、値を設定 var nameProperty = typeof(Document).GetProperty("Name" + index.ToString()); nameProperty.SetValue(doc1, item.Name); // Age[1-5]プロパティに動的にアクセスし、値を設定 var ageProperty = typeof(Document).GetProperty("Age" + index.ToString()); ageProperty.SetValue(doc1, item.Age); index++; }
- 投稿日:2020-01-10T12:00:14+09:00
【OpenCvSharp】画像の中から四角っぽいものの角をサブピクセル精度で取得する
これで良いのか?と言う不安もありますが、備忘録的な感じで。
最初はこう書いてみました。
using (var mat = image.ToMat()) using (var gray = mat.CvtColor(ColorConversionCodes.RGB2GRAY)) using (var bin = gray.Threshold(mythreshold, 255, ThresholdTypes.Binary)) { var contours = bin.FindContoursAsMat(RetrievalModes.List, ContourApproximationModes.ApproxSimple); var candidatre = contours .Select(c => { var outputMat = new MatOfPoint(); Cv2.ApproxPolyDP(c, outputMat, 0.01 * c.ArcLength(true), true); //MEMO : 角のPointコレクションと面積をペアで返します。 return new Tuple<Point[], double>(outputMat.ToArray(), Cv2.ContourArea(c.ToArray())); }) //MEMO : 面積で区切ってゴミを除去してます。 .Where(c => c.Item2 < myMaxArea && c.Item2 > myMinArea); return candidatre.Select(x => x.Item1).ToArray(); }これで
Mat.DrawContourすると若干ずれ(2ピクセル程度)が出てしまってました。
ApproxPolyDPで近似してるせいかな?と思いますが深く追ってません。取り合えず
outputMatをMatOfPoint2fにしたら勝手に精度上げてくれないかな?と期待したのですが、そうすると数値がおかしな値が返ってきてしまいました。
恐らく内部的に整数しか扱えないものと思われます。(これもソースは追ってません)そもそも位置がずれてしまっているので別の手段を使った方がよさそうです。
調べてみるとこんなページが有りました。
このリンク先ではサブピクセル精度のコーナー検出について触れられています。
・・・そういえば
CornerSubPixなんてありましたね・・・。と言うことで修正です。
サブピクセル精度using (var mat = image.ToMat()) using (var gray = mat.CvtColor(ColorConversionCodes.RGB2GRAY)) using (var bin = gray.Threshold(mythreshold, 255, ThresholdTypes.Binary)) { var contours = bin.FindContoursAsMat(RetrievalModes.List, ContourApproximationModes.ApproxSimple); var candidatre = contours .Select(c => { var outputMat = new MatOfPoint(); Cv2.ApproxPolyDP(c, outputMat, 0.01 * c.ArcLength(true), true); var criteria = new TermCriteria(CriteriaType.Eps | CriteriaType.MaxIter, 100, 0.001); var corners = Cv2.CornerSubPix(gray, outputMat.Select(x => new Point2f(x.X, x.Y)).ToArray(), new Size(5, 5), new Size(-1, -1), criteria); //MEMO : 角のPointコレクションと面積をペアで返します。 return new Tuple<Point2f[], double>(corners, Cv2.ContourArea(c.ToArray())); }) //MEMO : 面積で区切ってゴミを除去してます。 .Where(c => c.Item2 < myMaxArea && c.Item2 > myMinArea); return candidatre.Select(x => x.Item1).ToArray(); }今回のケースではこれで結構いい感じに追従できてます。
- 投稿日:2020-01-10T11:28:53+09:00
【C#】SQLのBulkInsertでファイルにアクセスできなかったのでC#で自作した話(初心者)
「ちょろいっすよ!」
私は入社2年目の駆け出し技術者です。
ある日上司がSQL初心者の私に頼み事をしてきました。
上司「ちょっと相談があるんやけど、、、」上司「今、DBにこんなテーブルがあるんだけど、」
TABLE1
ID 名称 サービスID 1 名前A 3 2 名前B 2 3 名前C 2 上司「本来このテーブルのサービスIDカラムに対応するサービス名称があるんだけど、
使わんから、このCSVの対応表だけ作って終わりにしたんや」Service.csv1,サービスA 2,サービスB 3,サービスC 4,サービスD 5,サービスE上司「でもやっぱりサービス名称これから使いそうやから、JOINで取れるようにCSVから対応表通りのテーブルを作ってくれない?」
上司「クライアント側にはCSVは定期的に変更してもいいって言っているから、テーブル作成するスクリプトやらなんやらをタスクスケジューラで定期実行してな」私「よくある話ですね。多分SQLのBulkInsert使えば一発なんでちょろいっすよwww」
ちょろいはずだった・・・
私「よーし、ちゃっちゃとSQL書いちゃおー」
私「まずはテーブル作成して、」CreateTable.sqlCREATE TABLE SERVICE_TABLE ( Service_ID int not null, Service_Name varchar(102) not null );私「あとはBulkInsertして終いや!」
私「定期実行するから頭でTRUNCATEしとこ。」BulkInsert.sqlTRUNCATE TABLE SERVICE_TABLE BULK INSERT SERVICE_TABLE FROM 'C:\hoge\Service.csv' WITH ( FIELDTERMINATOR = ',', ROWTERMINATOR = '\n' );私「あとは"C:\hoge"にService.csvを置いて、、、実行!」
私「、、、え!?」ファイル"C:\hoge\Service.csv"を開けなかったので、一括読み込みできません。
私「なんでや、、、」
悪戦苦闘
この後、ググりながらhogeフォルダのセキュリティ設定やDB側の設定をいじってみたのですが、1時間かけても結局できませんでした。
SQLサーバ認証を用いているとファイルのアクセス権がーとかBulkInsertで指定するパスはDBサーバ上のパスでーとかいろいろ書いてあり、もう何が本当かもわからなくなりました。
ちなみにこの作業は別のサーバ上で行いたかったのでDBサーバ上にcsvファイル置いてーはできません。もしかしたら、わかる人がやれば一瞬なのかもしれません。
設定でなんとかなるよっていう方はコメントでご教授お願いします。めんどくさくなった
私「めんどくさいなー。帰ってゲームしたいなー」
私「もうC#でBulkInsertと同じことするコンソールアプリ作ったほうが早くね?(やけくそ)」
私「そのアプリのexeをタスクスケジューラで定期実行したら同じじゃん!(天才)」私「まずはデータクラスを作って」
ServiceData.csclass ServiceData { /// <summary> /// サービスID /// </summary> public int ServiceID { get; set; } /// <summary> /// サービス名称 /// </summary> public string ServiceName { get; set; } }私「SQLServer操作クラスを作るか。今回の機能的にこれくらいあればいいかな」
MSSqlManager.csusing System; using System.Collections.Generic; using System.Data.SqlClient; namespace BulkInsertApp { public class MSSqlManager { private SqlConnection sqlConnection; private SqlTransaction sqlTransaction; /// <summary> /// 接続文字列生成 /// </summary> /// <returns>接続文字列</returns> private string GetConnectionString() { string connectionString = null; string userId = "<SQL認証のユーザ名>"; string password = "<SQL認証のパスワード>"; string dbname = "<DB名>"; string dbpath = "<DBサーバアドレス>"; connectionString = "Persist Security Info=False;" + "User ID = " + userId + "; Password = " + password + "; Initial Catalog = " + dbname + "; Data Source = " + dbpath; } return connectionString; } public MSSqlManager() { try { string connectString = GetConnectionString(); this.sqlConnection = new SqlConnection(connectString); this.sqlConnection.Open(); } catch (Exception ex) { Console.WriteLine(ex.Message); throw; } } public void Close() { try { this.sqlConnection.Close(); this.sqlConnection.Dispose(); } catch (Exception ex) { Console.WriteLine(ex.Message); throw; } } public void BeginTran() { try { this.sqlTransaction = this.sqlConnection.BeginTransaction(); } catch (Exception ex) { Console.WriteLine(ex.Message); throw; } } public void CommitTran() { try { if (this.sqlTransaction.Connection != null) { this.sqlTransaction.Commit(); this.sqlTransaction.Dispose(); } } catch (Exception ex) { Console.WriteLine(ex.Message); throw; } } public void RollBack() { try { if (this.sqlTransaction.Connection != null) { this.sqlTransaction.Rollback(); this.sqlTransaction.Dispose(); } } catch (Exception ex) { Console.WriteLine(ex.Message); throw; } } public void ExecuteInsert(string query, Dictionary<string, Object> paramDict) { SqlCommand sqlCom = new SqlCommand(); try { //クエリー送信先、トランザクションの指定 sqlCom.Connection = this.sqlConnection; sqlCom.Transaction = this.sqlTransaction; sqlCom.CommandText = query; foreach (KeyValuePair<string, Object> item in paramDict) { sqlCom.Parameters.Add(new SqlParameter(item.Key, item.Value)); } // SQLを実行 sqlCom.ExecuteNonQuery(); } catch (Exception ex) { Console.WriteLine(ex.Message); throw; } } public void ExecuteQuery(string query) { try { SqlCommand sqlCom = new SqlCommand(); sqlCom.Connection = this.sqlConnection; sqlCom.Transaction = this.sqlTransaction; sqlCom.CommandText = query; sqlCom.ExecuteNonQuery(); } catch (Exception ex) { Console.WriteLine(ex.Message); throw; } } } }私「あとはCSV読み込み→テーブルTruncate→CSVデータInsertすれば終いや!」
ServiceBulk.csusing System; using System.Collections.Generic; using System.Data.SqlClient; using System.IO; using System.Text; namespace BulkInsertApp { public class ServiceBulk { public void ServiceBulkManager() { List<ServiceData> ServiceDataList = CSVDataGet(); TruncateTable(); BulkInsert(ServiceDataList); } private List<ServiceData> CSVDataGet() { List<ServiceData> retList = new List<ServiceData>(); StreamReader sr = new StreamReader(@"C:\hoge\Service.csv", Encoding.GetEncoding("Shift_JIS")); try { while (!sr.EndOfStream) { ServiceData ServiceData = new ServiceData(); string line = sr.ReadLine(); string[] values = line.Split(','); List<string> items = new List<string>(); items.AddRange(values); if (items.Count == 2) { ServiceData.ServiceID = int.Parse(items[0]); ServiceData.ServiceName = items[1]; retList.Add(ServiceData); } } } catch (Exception e) { Console.WriteLine(e.Message); } finally { sr.Close(); } return retList; } private void TruncateTable() { MSSqlManager manager = new MSSqlManager(); try { manager.BeginTran(); string query = "TRUNCATE TABLE SERVICE_TABLE"; manager.ExecuteQuery(query); manager.CommitTran(); } catch (SqlException sqle) { string error = "Number: " + sqle.Number + " Message: " + sqle.Message; Console.WriteLine(error); manager.RollBack(); throw; } catch (Exception ex) { Console.WriteLine(ex.Message); throw; } finally { manager.Close(); } } private void BulkInsert(List<ServiceData> listData) { MSSqlManager manager = new MSSqlManager(); try { manager.BeginTran(); foreach (ServiceData data in listData) { string sqlstr = "INSERT INTO SERVICE_TABLE " + "(Service_ID, ServiceName)" + " values " + "(@Service_ID, @Service_Name)"; Dictionary<string, Object> paramDict = new Dictionary<string, object>(); paramDict.Add("@Service_ID", data.ServiceID); paramDict.Add("@Service_Name", data.ServiceName); manager.ExecuteInsert(sqlstr, paramDict); } manager.CommitTran(); } catch (SqlException sqle) { string error = "Number: " + sqle.Number + " Message: " + sqle.Message; Console.WriteLine(error); manager.RollBack(); throw; } catch (Exception ex) { Console.WriteLine(ex.Message); throw; } finally { manager.Close(); } } } }私「あとはこいつをメインから呼び出せばええんや」
Program.csusing System; using System.Collections.Generic; using System.Text; using System.Threading.Tasks; namespace BulkInsertApp { class Program { public static void Main(string[] args) { ServiceBulk sb = new ServiceBulk(); sb.ServiceBulkManager(); } } }私「よっしゃできたやで」
私「ビルドして実行や!よしテーブル見てみよう!」SERVICE_TABLE
Service_ID Service_Name 1 サービスA 2 サービスB 3 サービスC 4 サービスD 5 サービスE 私「よっしゃOKや!」
私「できましたよー。これを実行してくださいー!」
上司「ありがとうやで。(なんでこいつexe渡してきたんや?まぁできてるならいいか)」あとがき
最後まで読んでいただきありがとうございます。
業務内容に関わる箇所や処理等、都合により一部割愛しております。
まだまだ経験が浅いのでクソみたいなコードです。(StringBuilder使え)
コメントでコードレビューお願い致します
- 投稿日:2020-01-10T00:11:50+09:00
FFTでギターの演奏を正誤判定
概要
マイクで拾った演奏音をもとに、FFTで出力した周波数スペクトラムのピーク値の組み合わせから、演奏の正誤を判定する。
はじめに
ギターの基本的な演奏技術の一つに、複数の弦を同時に指で押さえて和音を奏でる『コード弾き』があるのは周知の事実である。本稿はそんなコード弾きを独自に練習する際に役立つと考える、コード弾き演奏音から正誤判定を行う手法を実現する。
具体的には、マイクで演奏した音を拾い、コンピュータで周波数成分を検出、検出結果をもとに正誤の判定を行う。
なお、本稿で想定するシステムユースケースは、コード弾きのお手本をこちらから提示し予め演奏されるであろう音が分かっているというシナリオであり、演奏者がランダムに演奏する音のコード認識をするというものではない。処理の流れ
1. フーリエ変換
フーリエ変換およびFFT(Fast Fourier Transform)についての詳細な説明は割愛するが、一言でいえば「周波数ごとの成分を数値的に見ることができるようにする変換」である。
今回開発フレームとして利用しているUnityには、
AudioSource.GetSpectrumDataというFFTスクリプトが既に用意されている。AudioSourceとして使用するマイクを指定し、FFT結果の周波数スペクトラムデータを受け取る2のべき乗サイズの配列を用意しておく。
以下図は実際に『Cメジャー』と呼ばれるコードを弾いた際の周波数スペクトラムである。2. ピーク値の探索
フーリエ変換により算出された周波数スペクトラムにおいて、ピークが立っている周波数がすなわち演奏された音と想定できる。したがって、ピーク値を探索し、検出されたピーク値の組み合わせから演奏の正誤を判定することができる。
一般的な(6弦)ギターは、最低周波数が82Hz(6弦開放)のE2、最高周波数が1245Hz(1弦23f)のD#6であり、この範囲内でピークを探索する。また、音階間の幅はE2とF2の間の5Hzが最小で、音階が高くなるほど大きくなっていくため、ピークを探索する幅も5Hzごととする。
ピーク値としては5Hz区間で探索する最大値をそのまま採用するのではなく、最大値間の最小値も同時に探索し、算出された最大値と最小値の差が閾値以上の場合にのみピーク値として採用する。これにより、ピークが立っていない、すなわちなだらかな区間での最大値を外すことができる。
3. 周波数を音階に変換
一般によく知られるドレミファソラシド、ギターをはじめとする国際スタンダードCDEFGAB。これらの音階は、1オクターブを分割した一つ一つ(という認識)であり、440Hz / ラ / Aを基音に、1オクターブ高くなると周波数が2倍になると定義されている。
ギターにおける音階は12音階であり、1オクターブの間にC、C#、D、D#、E、F、F#、G、G#、A、A#、Bが存在する。前述した通り、ギターの最低音はE2の82Hzであり、E3の164Hz、その間82Hzを12分割しているということである。話は長くなったが、本稿では便宜的にC2の65.5Hzを0、C3の131Hzを12とし、
scale = 12.0f * Mathf.Log(hertz / 65.5f) / Mathf.Log(2.0f)で周波数を数値的な音階に変換し、四捨五入を行ったのち変換した数値をもとに音階とオクターブをそれぞれ算出する。4. コードごとに正誤判定
あとは、変換した音階と演奏されるであろう音とを比較するだけである。
以下図は作例であるが、変換した音階を表示し、演奏されるであろう音と比較した結果に応じて、『人差指の位置が間違えています!』『GOOD!』などの判定を表示している。
コード例
Analyze.cs// FFT分解能(2の累乗) private static int FFT_RESOLUTION = 2048; // 採用する最低周波域 private static int FREQUENCY_RANGE = 450; // 極値を算出する幅 private static int EXTREMUM_RANGE = 5; // 極小値-極大値の閾値 private static float EXTREMUM_THRESHOLD = 0.0005f; // a ~ bでの最大値インデックスを返す public static int GetMaxNo(int a, int b, float[] c) { int maxNum = a; float max = c[a]; for (int i = a; i < a + b && i < c.Length; i++) { if (c[i] > max) { max = c[i]; maxNum = i; } } return maxNum; } // a ~ bでの最小値インデックスを返す public static int GetMinNo(int a, int b, float[] c) { int minNum = a; float min = c[a]; for (int i = a; i < b; i++) { if (min > c[i]) { min = c[i]; minNum = i; } } return minNum; } // FFT,ピーク値の算出・リターン public static float[] AnalyzeSound(AudioSource audio) { // FFT float[] spectrum = new float[FFT_RESOLUTION]; audio.GetSpectrumData(spectrum, 0, FFTWindow.BlackmanHarris); /* * 周波数スペクトル波形をSceneに描画【テスト用】 * * for (int i = 1; i < spectrum.Length - 1; ++i) * { * Debug.DrawLine( * new Vector3(Mathf.Log(i - 1), Mathf.Log(spectrum[i - 1]), 3), * new Vector3(Mathf.Log(i), Mathf.Log(spectrum[i]), 3), * Color.yellow); * } */ // 極大値インデックスの配列 int[] maxes = new int[spectrum.Length]; // 極小値インデックスの配列 int[] mins = new int[spectrum.Length]; // ピーク値インデックスの配列 int[] peaks = new int[spectrum.Length]; //極大値の探索 int count = 0; for (int i = 0; i < spectrum.Length - EXTREMUM_RANGE; i++) { if (GetMaxNo(i, EXTREMUM_RANGE, spectrum) == GetMaxNo(i + 1, EXTREMUM_RANGE, spectrum)) { int check = 0; for (int k = 1; k < EXTREMUM_RANGE; k++) { if (GetMaxNo(i, EXTREMUM_RANGE, spectrum) == GetMaxNo(i + k, EXTREMUM_RANGE, spectrum)) { check++; } } if (check == EXTREMUM_RANGE - 1) { maxes[count] = GetMaxNo(i, EXTREMUM_RANGE, spectrum); count++; } } } //極小値の探索 mins[0] = GetMinNo(0, maxes[0], spectrum); for (int i = 0; i < spectrum.Length; i++) { if (maxes[i + 1] == 0) break; mins[i + 1] = GetMinNo(maxes[i], maxes[i + 1], spectrum); } //差分の計算 int peakscnt = 0; for (int i = 0; i < spectrum.Length; i++) { if (spectrum[maxes[i]] - spectrum[mins[i]] >= EXTREMUM_THRESHOLD) { peaks[peakscnt] = maxes[i]; peakscnt++; } } // ピーク周波数を返す // ピーク周波数インデックスの配列 float[] freqs = new float[peakscnt]; // ピーク周波数 float[] pitches = new float[peakscnt]; // 各ピークの前後のスペクトルも考慮 for (int i = 0; i < peakscnt; i++) { freqs[i] = peaks[i]; if (peaks[i] > 0 && peaks[i] < spectrum.Length - 1) { float dL = spectrum[peaks[i] - 1] / spectrum[peaks[i]]; float dR = spectrum[peaks[i] + 1] / spectrum[peaks[i]]; freqs[i] += 0.5f * (dR * dR - dL * dL); } pitches[i] = freqs[i] * (AudioSettings.outputSampleRate / 2) / spectrum.Length; // 検出する周波数域を考慮 if (pitches[i] >= FREQUENCY_RANGE) pitches[i] = 0; } return pitches; } // 周波数から音階に変換 public static string ConvertHertzToScale(float hertz) { // 周波数を,C2を0,(中略),C3を12とする数値に変換 float scale = 12.0f * Mathf.Log(hertz / 65.5f) / Mathf.Log(2.0f); // 四捨五入 int s = (int)scale; if (scale - s >= 0.5) s += 1; int smod = s % 12; // 音階 int soct = s / 12; // オクターブ string value; // 音階 if (smod == 0) value = "C"; else if (smod == 1) value = "C#"; else if (smod == 2) value = "D"; else if (smod == 3) value = "D#"; else if (smod == 4) value = "E"; else if (smod == 5) value = "F"; else if (smod == 6) value = "F#"; else if (smod == 7) value = "G"; else if (smod == 8) value = "G#"; else if (smod == 9) value = "A"; else if (smod == 10) value = "A#"; else if (smod == 11) value = "B"; else value = "EXCEPTION"; value += soct + 2; return value; } void Start () { // マイク入力 Mic.clip = Microphone.Start(null, true, 999, 44100); while (!(Microphone.GetPosition(null) > 0)) { } Mic.Play(); } void Update () { // FFT,ピーク値の算出・リターン float[] hertz = AnalyzeSound(Mic); // 各ピーク周波数に対して for (int i = 0; i < hertz.Length; i++) { if (hertz[i] == 0) break; // 周波数から音階に変換 string scale = ConvertHertzToScale(hertz[i]); /* 演奏された音の周波数と音階を描画【テスト用】 * Debug.Log(hertz[i] + "Hz, Scale:" + scale); */ } }おわりに
今回は、FFTで出力した周波数スペクトラムのピーク値の組み合わせから、演奏の正誤を判定する手法を提示した。
正直なところ、判定性能は満足のいくものではない。マイクの設置する位置や指向性、性能に依存することが主な要因である。シンセサイザを噛ませたり、電気信号的に判定するといったハードウェアによる改善は比較的容易であるが、できればシステマチックに解決する手法を模索してみたい。












