- 投稿日:2019-12-24T21:45:08+09:00
C# と SQL Server の DateTime を Assert.Equals する
C# と Database ではそれぞれミリ秒の保持の仕方が異なります。上手く Assert するには比較用のクラスを作って、 Assert の Parameter に渡してあげれば OK です。
使っているもの
- xUnit
- SQL Server
なぜ起きるのか
C# の DateTime ではミリ秒を正しく保持しています。
C#:
2013-05-01 23:59:59.991
しかし、これを SQL Server の datetime に入れるとこうなります。
SQL Server:
2013-05-01 23:59:59.990
これには理由があって、ミリ秒の精度が .000、.003、.007 であるためです。よって、このまま Assert.Equals すると Fail してしまいます。
解決方法
比較用のクラスをパラメーターで渡してあげて、多少の時間を許容するようにします。
Assert.Equal(expected, actual, new SqlServerDateTimeComparer());比較用のクラス
いくつか用意してあるので合ったものを使ってください。また、このクラスでは 10 秒までの差は許容しているので、この設定値は Constructor で受け取れるようにするなどすれば、別の比較 (e.g. DateTime が DI 出来なくて死ぬ Unit Test など) にも使えると思います。
public class SqlServerNullableDateTimeComparer : IEqualityComparer<DateTime?> { public bool Equals(DateTime? x, DateTime? y) { if (x == null && y == null) { return true; } if (x == null || y == null) { return false; } return (x.Value - y.Value).Duration() < TimeSpan.FromSeconds(10); } public int GetHashCode(DateTime? obj) { return obj.GetHashCode(); } } public class SqlServerDateTimeComparer : IEqualityComparer<DateTime> { public bool Equals(DateTime x, DateTime y) { return (x - y).Duration() < TimeSpan.FromSeconds(10); } public int GetHashCode(DateTime obj) { return obj.GetHashCode(); } } public class SqlServerNullableDateTimeOffsetComparer : IEqualityComparer<DateTimeOffset?> { public bool Equals(DateTimeOffset? x, DateTimeOffset? y) { if (x == null && y == null) { return true; } if (x == null || y == null) { return false; } return (x.Value - y.Value).Duration() < TimeSpan.FromSeconds(10); } public int GetHashCode(DateTimeOffset? obj) { return obj.GetHashCode(); } } public class SqlServerDateTimeOffsetComparer : IEqualityComparer<DateTimeOffset> { public bool Equals(DateTimeOffset x, DateTimeOffset y) { return (x - y).Duration() < TimeSpan.FromSeconds(10); } public int GetHashCode(DateTimeOffset obj) { return obj.GetHashCode(); } }Note
DATETIME データ型のミリ秒に関する注意事項 – Microsoft SQL Server Japan Support Team Blog
- 投稿日:2019-12-24T21:25:38+09:00
パスに沿ってなめらかに一定速度でオブジェクトを移動させる実装の解説
まえがき
問題
ゲームだと、パスに沿ってキャラを移動させたい時があります。
ただ、単純に実装すると速度がバラバラになってしまいます。目的
今回は曲線でもだいたい一定の速度で動かせるようにする実装を解説します。
UnityならDoTweenのDoPathやCinemachineのCinemachinePathを使用すれば実装しなくても可能ですが、ベジェ曲線のみで、他の曲線を使うことはできません。
自分で実装すれば、好きな曲線を使えます。
ただ、今回は解説のため、ベジェ曲線を使います。https://ja.wikipedia.org/wiki/ベジェ曲線
Vector3 Bezier3(float t, Vector3 p1, Vector3 p2, Vector3 p3, Vector3 p4) { var d = 1 - t; return d * d * d * p1 + 3 * d * d * t * p2 + 3 * d * t * t * p3 + t * t * t * p4; }参考
この記事はCinemachineのCinemachinePathBaseを参考に作っています。
github解説
単純な実装
これはベジェ曲線の引数tに$Time.t$を渡すだけの実装です。
明らかに速度がおかしいです。
0-0.1と0.5-0.6の移動距離が違うのと、
スタート位置から1つ目のパスまで1秒、そこからゴールまで1秒かけているのが原因です。void Update() { var t = Time.time; var indexA = Mathf.FloorToInt(Mathf.Min(Paths.Length - 1, t)); var indexB = Mathf.FloorToInt(Mathf.Min(Paths.Length - 1, indexA+1)); if (indexA == indexB) return; transform.position = CalcPos(t); } Vector3 CalcPos(float t) { var indexA = Mathf.FloorToInt(Mathf.Min(Paths.Length - 1, t)); var indexB = Mathf.FloorToInt(Mathf.Min(Paths.Length - 1, indexA+1)); return Bezier3( t-indexA, Paths[indexA].Pos, Paths[indexA].Pos + Paths[indexA].Tangent, Paths[indexB].Pos - Paths[indexB].Tangent, Paths[indexB].Pos); }改善した物
この問題はtを0-ベジェ曲線の長さで扱えれば速度をだいたい一定にすることができます。
やっていることは、tを少しずつ動かして、進んだ距離を測る。
進んだ距離からtを返すテーブルを作るという事です。以上です。
こうすると、距離からtに変換する関数を作成できるので、だいたい一定の速度で移動できるようになります。
コード
[SerializeField] int Segment; [SerializeField] PathContainer Paths; float PathLength; //セグメントの総数 int NumKeys; float[] DistanceToTArray; float DistanceStepSize; void Start() { Build(); } void Build() { PathLength = 0; NumKeys = (Paths.Length-1) * Segment+1; var tToDistance = CalcTToDistance(); DistanceToTArray = CalcDistanceToT(tToDistance); } void Update() { transform.position = CalcPos(DistanceToT(Time.time*PathLength/2)); } //距離からtに変換 float DistanceToT(float distance) { float d = distance / DistanceStepSize; int index = Mathf.FloorToInt(d); if(index>=DistanceToTArray.Length-1)return DistanceToTArray[DistanceToTArray.Length-1]; float t = d - index; return Mathf.Lerp(DistanceToTArray[index], DistanceToTArray[index+1], t); } //tをSegmentに分割して進んだ距離を配列に入れて返す float[] CalcTToDistance() { var tToDistance = new float[NumKeys]; var pp = Paths[0].Pos; float t = 0; for (int n = 1; n < NumKeys; n++) { t += 1f / Segment; Vector3 p = CalcPos(t); float d = Vector3.Distance(pp, p); PathLength += d; pp = p; tToDistance[n] = PathLength; } return tToDistance; } //距離をSegmentに分割してその位置のtを配列に入れて返す float[] CalcDistanceToT(float[] tToDistance) { var distanceToT = new float[NumKeys]; distanceToT[0] = 0; DistanceStepSize = PathLength/(NumKeys-1); float distance = 0; int tIndex=1; for (int i = 1; i < NumKeys; i++) { distance += DistanceStepSize; var d = tToDistance[tIndex]; while (d < distance && tIndex < NumKeys - 1) { tIndex++; d = tToDistance[tIndex]; } var prevD = tToDistance[tIndex - 1]; float delta = d - prevD; float t = (distance - prevD) / delta; distanceToT[i] = (1f/Segment)*(t + tIndex - 1); } return distanceToT; } Vector3 CalcPos(float t) { var indexA = Mathf.FloorToInt(Mathf.Min(Paths.Length - 1, t)); var indexB = Mathf.FloorToInt(Mathf.Min(Paths.Length - 1, indexA+1)); return Bezier3( t-indexA, Paths[indexA].Pos, Paths[indexA].Pos + Paths[indexA].Tangent, Paths[indexB].Pos - Paths[indexB].Tangent, Paths[indexB].Pos); }
あとがき
以上です。
動作するコードはこちらです。
https://github.com/nakajimakotaro/PathSmoothMove
- 投稿日:2019-12-24T19:57:23+09:00
【ポストコンパイル時定数計算】.NET系言語でconstexprを実現する研究【Mono.Cecil, AssemblyBuilder】
.NET用プログラミング言語に新地平を切り開きました。
MITライセンスでプロジェクト全体を配布中です。
要約
Abstract
背景
C++とD言語には
コンパイル時定数計算
という概念がある。
C++の文献:constexpr
D言語の文献:ctfe
コンパイル時定数計算
とはコンパイル時に明記された値のみに依存する副作用を持たないメソッド呼び出しを全てその計算結果に置換する仕様
である。この
コンパイル時定数計算
を用いることで自明な計算をあらかじめ行っておいて結果をテーブルに保持することが低コストで可能となる。
.NET言語で計算結果テーブルを作成する場合、外部ファイルからIOしてきて生成するか、あるいはstaticコンストラクタで計算をして静的変数に設定することとなる。
コンパイル時定数計算
を用いれば アプリケーションの起動速度を大幅に向上させる ことが可能になる。目的
普段筆者が使用するC#でも
コンパイル時定数計算
をおこないたい。方法
コンパイルの成果物(DLLやEXEファイル)をMono.Cecilで解析し、AssemblyBuilderを用いて抽象マシンを作成し、定数式に関数呼び出しを置換した。
結果
ポインタを除くプリミティブ型の配列やプリミティブ型を返すstaticメソッドの呼び出しを効率的な配列や定数式に置換できた。
ポストコンパイル時定数計算
を部分的に実現した。結論
ポストコンパイル時定数計算
の適用範囲の拡大について今後の研究が待たれる。
Introduction
「最高の最適化とは、そもそも計算しないことである」
従来C#やVB.NET、F#では関数呼び出しのインライン化までは実際行えた。
だが、関数呼び出し結果を定数に置換することはいずれの言語においてもサポートされていない。定数を引数に取る副作用のない関数の呼び出しをその結果に置換すれば、アプリケーションの実行速度は向上するはずである。
実際
ポストコンパイル時定数計算
がどれほど役に立つのかはC++erの成果であるコンパイル時レイトレーシングやコンパイル時Cコンパイラなどに示されている。本研究ではコンパイル後のDLLやILに対して処理を施すことにより擬似的な
コンパイル時定数計算
を行った。Environemtns
- .NET Core 3.1
- C#8.0
- Mono.Cecil
- version 0.11.1
- MicroBatchFramework
- version 1.6.1
- System.Runtime.CompilerServices.Unsafe
- version 4.7.0
Methods
.NET系言語において
ポストコンパイル時定数計算
を実現する際に、主にC++のconstexprを参考とした。
D言語のctfeは処理に掛かるコストが大きすぎるため参考としなかった。C++のconstexprでは関数にconstexpr修飾子を付けることにより、その関数が副作用を持たず、戻り値がリテラル型であることをコンパイラに検証させ、コンパイル時呼び出しを可能にさせる。
constexprに倣い、本研究ではConstExpr属性を関数に付与させることとした。全体構造
この研究では1つのソリューションの下に4つのプロジェクトを作成した。
- ConstExpr
- ConstExpr属性を定義したプロジェクト
- Nugetにて公開中 https://www.nuget.org/packages/ConstExpr-Core/
- post
ポストコンパイル時定数計算
を実現するコンソールプログラム- Target
- テスト用のプロジェクト
- ConsoleTest
- Targetを使用したプロジェクト
- postの処理によりTargetが壊れていないかを確認するためのもの
ConstExpr
ConstExprAttribute.csusing System; namespace MetaProgramming { [AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = true)] public sealed class ConstExprAttribute : Attribute { } }ConstExpr属性は次の型にのみ付与すべきである。
- unmanagedでBlittableなstruct
- static class
- staticメソッドにも付与できる
post
postはコンソールプログラムである。
DLLの集合を引数にとり、なんらかの処理を行う。CLIインターフェース
2つのコマンドが定義されている。
- call
- ConstExprな無引数のstaticメソッドを実行し、計算結果を表示する。
- 第1引数:ConstExpr属性が付与されたstaticクラスの名前空間付きの名前
- 第2引数:引数のないConstExpr属性が付与されたstaticメソッドの名前
- 第3引数:.dllを含むディレクトリパス @テキストファイルパスと記述するとそのテキストファイルに記述された複数のディレクトリパスから.dllを探す
- replace
- ConstExpr呼び出しを可能な限り定数に置換する。
- 第1引数:.dllを含むディレクトリ名 @テキストファイルパスと記述するとそのテキストファイルに記述された複数のディレクトリパスから.dllを探す
- 第2引数(省略可):処理したdllを出力する先のディレクトリパス
これらコマンドは次のように使用する。
カレントディレクトリはpostである。dotnet run call Target.Test D "../target\bin\Release\netstandard2.0" dotnet run release "../target\bin\Release\netstandard2.0" -o "../target\bin\Release"内部動作概説(Program.cs)
Program.cs全文
Program.csusing System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; using MicroBatchFramework; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Mono.Cecil; using post.ConstDynamicMethod; namespace post { class Program { static async Task Main(string[] args) { await new HostBuilder().RunBatchEngineAsync<ConstantExpressionInterpreter>(args); } } class ConstantExpressionInterpreter : BatchBase { private readonly ILogger<BatchEngine> logger; public ConstantExpressionInterpreter(ILogger<BatchEngine> logger) => this.logger = logger; [Command("call", "Call specific method")] public int ExecuteMethod ( [Option(0, "Type Name")]string typeName, [Option(1, "Method Name")]string methodName, [Option(2, "Directory that includes dll")]string directory ) { var directories = InterpretDirectory(directory); var builder = Build(directories); var type = builder.Type2dArray.SelectMany(x => x).FirstOrDefault(x => x.Item2.FullName == typeName).Item1; if (type is null) { Console.WriteLine(typeName + " not found."); return 1; } Console.WriteLine(type.GetMethod(methodName)?.Invoke(null, null)?.ToString()); return 0; } private static string[] InterpretDirectory(string directory) => directory.StartsWith('@') ? File.ReadAllLines(directory.Substring(1)) : new[] { directory }; [Command("replace", "Edit the DLLs")] public int ReplaceConstantExpression ( [Option(0)] string directory, [Option("include")] string? referenceOnlyDirectory = default, [Option("o", "output directory")] string? output = default ) { var directories = InterpretDirectory(directory); string[]? referenceOnlyDirectories = default; if (!(referenceOnlyDirectory is null)) { referenceOnlyDirectories = InterpretDirectory(referenceOnlyDirectory); } var builder = Build(referenceOnlyDirectories is null ? directories : directories.Concat(referenceOnlyDirectories).ToArray(), !(string.IsNullOrEmpty(output))); var replacer = new ConstExprReplacer(builder.ModuleArray, builder.Type2dArray); for (var moduleIndex = 0; moduleIndex < directories.Length; moduleIndex++) { replacer.ProcessModule(moduleIndex); var module = builder.ModuleArray[moduleIndex].Item2; if (output is null) { module.Write(); } else { module.Write(Path.Combine(output, Path.GetFileName(module.FileName))); } } return 0; } private static ConstExprBuilder Build(string[] directories, bool isReadWrite = false) { var moduleList = new List<ModuleDefinition>(directories.Length * 2); var readerParameters = new ReaderParameters() { AssemblyResolver = new DefaultAssemblyResolver(), ReadWrite = isReadWrite, }; foreach (var directory in directories) { foreach (var file in Directory.EnumerateFiles(directory, "*.dll", SearchOption.AllDirectories)) { var assemblyDefinition = AssemblyDefinition.ReadAssembly(file, readerParameters); moduleList.AddRange(assemblyDefinition.Modules); } } var builder = new ConstExprBuilder(moduleList.ToArray()); return builder; } } }MicroBatchFrameworkを使用してコマンドライン引数を解析し、コマンドを実行する。
callもreplaceもいずれもディレクトリパスを調べ、その直下に存在するDLL群をModuleDefinitionの配列に変換する。
そしてModuleDefinition[]を引数に与えてConstExprBuilderを構築する。
ConstExprBuilderはコンストラクタですべての処理を行う。
ConstExprBuilder解説
ConstExprBuilder.cs抜粋
ConstExprBuilder.cs/* using MethodAttributes = System.Reflection.MethodAttributes; using MA = Mono.Cecil.MethodAttributes; using MethodBody = Mono.Cecil.Cil.MethodBody; using OpCodes = System.Reflection.Emit.OpCodes; using MTuple = System.ValueTuple<System.Reflection.Emit.ModuleBuilder, Mono.Cecil.ModuleDefinition>; using TTuple = System.ValueTuple<System.Reflection.Emit.TypeBuilder, Mono.Cecil.TypeDefinition>; using TyTuple = System.ValueTuple<System.Type, Mono.Cecil.TypeDefinition>; using MdTuple = System.ValueTuple<System.Reflection.Emit.MethodBuilder, Mono.Cecil.MethodDefinition>; using CTuple = System.ValueTuple<System.Reflection.Emit.ConstructorBuilder, Mono.Cecil.MethodDefinition>; using FieldAttributes = System.Reflection.FieldAttributes; using FTuple = System.ValueTuple<System.Reflection.Emit.FieldBuilder, Mono.Cecil.FieldDefinition>; using GenericParameterAttributes = System.Reflection.GenericParameterAttributes; */ private readonly AssemblyBuilder[] assemblyBuilders; public readonly MTuple[] ModuleArray; private readonly TTuple[][] typePairArrays; private readonly MdTuple[][][] methodPairArray2ds; private readonly CTuple[][][] constructorPairArray2ds; private readonly FTuple[][][] fieldPairArray2ds; private readonly FTuple[][][] staticFieldPairArray2ds; public readonly TyTuple[][] Type2dArray; private readonly IConverterWithGenericParameter converter; public ConstExprBuilder(ModuleDefinition[] moduleDefinitions) { assemblyBuilders = new AssemblyBuilder[moduleDefinitions.Length]; for (var i = 0; i < assemblyBuilders.Length; i++) assemblyBuilders[i] = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("ConstExpr" + i), AssemblyBuilderAccess.Run); ModuleArray = new MTuple[moduleDefinitions.Length]; typePairArrays = new TTuple[ModuleArray.Length][]; methodPairArray2ds = new MdTuple[ModuleArray.Length][][]; constructorPairArray2ds = new CTuple[ModuleArray.Length][][]; fieldPairArray2ds = new FTuple[ModuleArray.Length][][]; staticFieldPairArray2ds = new FTuple[ModuleArray.Length][][]; Type2dArray = new TyTuple[ModuleArray.Length][]; ConstructTypeBuilders(moduleDefinitions); converter = new NotCreatedConverter(ModuleArray, typePairArrays, methodPairArray2ds, constructorPairArray2ds, fieldPairArray2ds, staticFieldPairArray2ds); ConstructFields(); ConstructMethodBuilderSignatures(); ConstructMethodBuilderBodies(); ConstructConstructorBuilderBodies(); Publish(); } private void Publish() { for (var moduleIndex = 0; moduleIndex < Type2dArray.Length; moduleIndex++) { ref TyTuple[] typeArray = ref Type2dArray[moduleIndex]; ref TTuple[] sourceArray = ref typePairArrays[moduleIndex]; typeArray = sourceArray.Length == 0 ? Array.Empty<TyTuple>() : new TyTuple[sourceArray.Length]; for (var typeIndex = 0; typeIndex < typeArray.Length; typeIndex++) { TTuple source = sourceArray[typeIndex]; TyTuple createType = source.Item1.CreateType(); if (createType is null) throw new NullReferenceException(source.Item2.FullName); typeArray[typeIndex] = (createType, source.Item2); } } }ConstExprBuilderでは読み込んだModuleDefinitionの数だけ新規にAssemblyBuilderとModuleBuilderを定義する。
そしてConstExpr属性が付与された型に対応するTypeBuilderをModuleBuilderを用いて定義する。
型を一周巡回した後、改めてフィールド情報やメソッドのシグネチャ情報を取得して定義する。
大凡の外形が定まった後、初めてMethodBuilderからILGeneratorを得、メソッドの中身を再構築する。そして全体を過不足なく再構成した後、Publish()内部で全てのTypeBuilderに対してCreateTypeメソッドを実行して再構築を完了し、実行可能なメソッドを得る。
Target
Target.cs全文
Target.csusing MetaProgramming; namespace Target { [ConstExpr] public static class Test { [ConstExpr] public static int Field; [ConstExpr, ConstantInitializer(nameof(Field))] public static int Initializer() { return 14; } public static int Accessor() => Field; [ConstExpr] public static sbyte D() { return new Q<sbyte>(114).value; //return FFF<int>.PPT(14); } [ConstExpr] public static int D2() { return Q<long>.P(32); //return FFF<int>.PPT(14); } [ConstExpr] public static int Z() => D() << 4; [ConstExpr] public static int Z2() => D2() - 4; [ConstExpr] public static int Z3<T>() where T : unmanaged => Y(1); [ConstExpr] public static int Z4() => Z3<char>(); [ConstExpr] public static int Y(int a) { var arr = Array(24); var arr2 = Array(12); var arr3 = Array(4); var arr4 = Array(1); var arr5 = Array(9); if (a == 1 && arr != null) return arr.Length; return Array(0).Length - 1; } [ConstExpr] public static double[] Array(int a) { var answer = new double[a]; for (int i = 0; i < answer.Length; i++) { answer[i] = i + 0.5; } return answer; } } [ConstExpr] interface T {} [ConstExpr] public struct Q<J> where J : unmanaged { public J value; [ConstExpr] public Q(J value) { this.value = value; } [ConstExpr] public static T P<T>(T v) where T : unmanaged => new Q<T>(v).value; } /*[ConstExpr] public static class FFF<T> where T : unmanaged { [ConstExpr] public static T PPT(T d) { return new Q<T>(d).value; } }*/ }ConsoleTest
ConsoleTest.cs全文
ConsoleTest.csusing System; using Target; namespace ConsoleTest { class Program { static void Main(string[] args) { Console.WriteLine(Test.Array(1)[0]); } } }Discussion
.NET系言語に
ポストコンパイル時
という実行環境を作り出した意義は大きい。
C#やF#の可能性が大きく広がったことは間違いない。以後の記述は現時点で何がこの研究において実現されていないかの補足であり、今後の課題である。
メソッドの仕様
現在のpostはConstExprメソッドの扱いがシビアであるので、そこを課題として取り扱いを向上させるべきではある。具体的には次のような制約がある。
- ref, in, outを許さない
- 引数は全てリテラル型である必要がある
- オーバーロードは定義可能
- ただし、引数の個数がConstExprのついた物同士では互いに異なっていなくてはならない
- Hoge(), Hoge(int i), Hoge(int i, int j)は定義可能
- Hoge(int i), Hoge(long i)はエラーとなる
ConstExprと属性が付けられていない副作用を持たない言語要素の使用
System.ValueTupleやSystem.MathなどのConstExprメソッドの内部で使用する分には問題のない構造体やstaticメソッド群を利用したい。
だが、現在の抽象マシンはConstExprとマークされた型とメソッドのみを元に構築される。
この制約は何らかの方法で突破せねばなるまい。文字列の使用
ILではSystem.String型も参照型でありながらリテラルとして使用可能である(ldstrやldnullなど)。
文字列も定数埋め込み可能となればさらに便利になるに違いない。感想・まとめ・こぼれ話
仮称「中3女子」として現在BOOTHで公開しています。
この「中3女子」という名前はC++のconstexprで有名なボレロ村上氏から来ています。
もっとおかたくて真面目でわかりやすく覚えやすい名前の案があれば「中3女子」から変更し、GitHubに公開するつもりです。C#でconstexprを再現することにどれほどの需要があるのか正直実現した自分にもわからないのです。
憧れは止められないので、これから用途を考えます。参考文献
1: constexpr
2: ctfe
3: Mono.Cecil
4: MicroBatchFramework
- 投稿日:2019-12-24T19:32:30+09:00
AnimationCurveを使わずにVRMをC#で走らせてみる【Unity】
これは Unity Advent Calendar 2019 の25日目の記事です。
昨日は @youri_ssk さんによる 2.5Dキャラクターアニメーション - Mirror Animation Playable でした。Animation Curveを使わずにC#だけでVRMを走らせてみる
Animation Curveを使えば、便利なGUIで3Dアニメーションを作ることができます!
そう!普通の人ならAnimation Curveを使いましょう!!(もちろん、BlenderとかUnity外のツールでもいいです)
しかし!Qiita読者の皆さんは プログラマー なんです!
プログラマーだったらプログラミングで3Dアニメーションを作りたいですよね!
というわけで、VRMアバターをC#で動かしてみます!
最終的にはこんな感じのアニメーションが作れました。
動作環境
以下の環境でやりました。
- Windows 10 64bit
- Unity 2019.2.11f1
- UniVRM 0.54.0
初期状態
とりあえずVRMファイルをシーンに読み込んでみます。
今回は 今里尚吾 くんにご協力いただきます!(VRoid Studioで作りました!)
Hierarchyを見てみたらこんな感じでした!
股関節を30度に曲げてみる
まずは、股関節を曲げてみましょう。
以下のスクリプトを作成します。
using System; using UnityEngine; public class PendulumRunning : MonoBehaviour { // transformを保管する変数 private Transform cHips; private Transform lUpperLeg; private Transform rUpperLeg; void Start() { // 腰のtransformを取得 cHips = transform.Find("Root") .Find("J_Bip_C_Hips"); // 股関節のtransformを取得 lUpperLeg = cHips.Find("J_Bip_L_UpperLeg"); rUpperLeg = cHips.Find("J_Bip_R_UpperLeg"); } void FixedUpdate() { // 脚を30°傾ける lUpperLeg.rotation = Quaternion.AngleAxis(-30.0f, Vector3.right); rUpperLeg.rotation = Quaternion.AngleAxis(30.0f, Vector3.right); } }作成できたら、シーン内のVRMにアタッチします。
脚が前後に開きました!
振り子のように足を揺らしてみる
せっかくなので、アニメーションさせたいですよね。
ということで、振り子のように揺らしてみます!
void FixedUpdate() { // 1秒周期の振り子を用意する float pendulum = (float)Math.Sin(Time.time * Math.PI); // 股関節を右軸(x軸)を中心に±60°幅で揺らす lUpperLeg.localRotation = Quaternion.AngleAxis(-60.0f * pendulum, Vector3.right); rUpperLeg.localRotation = Quaternion.AngleAxis(60.0f * pendulum, Vector3.right); }いいかんじ!
膝を曲げてみる
膝も曲げてみたくなったので、
LowerLeg
を追加してみました。あと係数とかを微調整したのがこちらです。public class PendulumRunning : MonoBehaviour { private Transform cHips; private Transform lUpperLeg; private Transform rUpperLeg; private Transform lLowerLeg; private Transform rLowerLeg; void Start() { cHips = transform.Find("Root") .Find("J_Bip_C_Hips"); lUpperLeg = cHips.Find("J_Bip_L_UpperLeg"); rUpperLeg = cHips.Find("J_Bip_R_UpperLeg"); // 膝のtransformを取得 lLowerLeg = lUpperLeg.Find("J_Bip_L_LowerLeg"); rLowerLeg = rUpperLeg.Find("J_Bip_R_LowerLeg"); } void FixedUpdate() { float pendulum = (float)Math.Sin(Time.time * Math.PI); // 股関節の動きを少し変更 lUpperLeg.localRotation = Quaternion.AngleAxis(-60.0f * pendulum - 20.0f, Vector3.right); rUpperLeg.localRotation = Quaternion.AngleAxis(60.0f * pendulum - 20.0f, Vector3.right); // 膝を揺らす lLowerLeg.localRotation = Quaternion.AngleAxis(-60.0f * pendulum + 60.0f, Vector3.right); rLowerLeg.localRotation = Quaternion.AngleAxis(60.0f * pendulum + 60.0f, Vector3.right); } }膝も曲がりました。
全身動かしてみる
同じ要領で、全身の関節の動かしてみます。色々調整したら、以下のようなコードになりました。
public class PendulumRunning : MonoBehaviour { private Transform cHips; private Transform lUpperLeg; private Transform lLowerLeg; private Transform rUpperLeg; private Transform rLowerLeg; private Transform cSpine; private Transform cChest; private Transform cUpperChest; private Transform lShoulder; private Transform lUpperArm; private Transform lLowerArm; private Transform rShoulder; private Transform rUpperArm; private Transform rLowerArm; void Start() { // 全身の関節のtransformを取得 cHips = transform.Find("Root") .Find("J_Bip_C_Hips"); lUpperLeg = cHips.Find("J_Bip_L_UpperLeg"); lLowerLeg = lUpperLeg.Find("J_Bip_L_LowerLeg"); rUpperLeg = cHips.Find("J_Bip_R_UpperLeg"); rLowerLeg = rUpperLeg.Find("J_Bip_R_LowerLeg"); cSpine = cHips.Find("J_Bip_C_Spine"); cChest = cSpine.Find("J_Bip_C_Chest"); cUpperChest = cChest.Find("J_Bip_C_UpperChest"); lShoulder = cUpperChest.Find("J_Bip_L_Shoulder"); lUpperArm = lShoulder.Find("J_Bip_L_UpperArm"); lLowerArm = lUpperArm.Find("J_Bip_L_LowerArm"); rShoulder = cUpperChest.Find("J_Bip_R_Shoulder"); rUpperArm = rShoulder.Find("J_Bip_R_UpperArm"); rLowerArm = rUpperArm.Find("J_Bip_R_LowerArm"); } void FixedUpdate() { // 速度を3倍に変更 float pendulum = (float)Math.Sin(Time.time * Math.PI * 3.0f); // 脚を揺らす lUpperLeg.localRotation = Quaternion.AngleAxis(-60.0f * pendulum - 20.0f, Vector3.right); rUpperLeg.localRotation = Quaternion.AngleAxis(60.0f * pendulum - 20.0f, Vector3.right); lLowerLeg.localRotation = Quaternion.AngleAxis(-30.0f * pendulum + 60.0f, Vector3.right); rLowerLeg.localRotation = Quaternion.AngleAxis(30.0f * pendulum + 60.0f, Vector3.right); // 腰にひねりを加える cHips.localRotation = Quaternion.AngleAxis(10.0f * pendulum, Vector3.up) * Quaternion.AngleAxis(10.0f, Vector3.right); // 胸は腰と反対にひねる cChest.localRotation = Quaternion.AngleAxis(-10.0f * pendulum, Vector3.up); cUpperChest.localRotation = Quaternion.AngleAxis(-20.0f * pendulum, Vector3.up); // 腕を揺らす lUpperArm.localRotation = Quaternion.AngleAxis(60.0f * pendulum + 30.0f, Vector3.right) * Quaternion.AngleAxis(70.0f, Vector3.forward); rUpperArm.localRotation = Quaternion.AngleAxis(-60.0f * pendulum + 30.0f, Vector3.right) * Quaternion.AngleAxis(-70.0f, Vector3.forward); lLowerArm.localRotation = Quaternion.AngleAxis(-60.0f * pendulum + 60.0f, Vector3.up); rLowerArm.localRotation = Quaternion.AngleAxis(-60.0f * pendulum - 60.0f, Vector3.up); } }だんだんそれっぽくなってきました。
ただ、重心が上下しないのはちょっと違和感がありますね。
重心を上下させてみた
重心も上下させてみます。
public class PendulumRunning : MonoBehaviour { private Transform cHips; private Transform lUpperLeg; private Transform lLowerLeg; private Transform rUpperLeg; private Transform rLowerLeg; private Transform cSpine; private Transform cChest; private Transform cUpperChest; private Transform lShoulder; private Transform lUpperArm; private Transform lLowerArm; private Transform rShoulder; private Transform rUpperArm; private Transform rLowerArm; // 腰の初期位置を保管する変数 private Vector3 firstHipsPosition; void Start() { cHips = transform.Find("Root") .Find("J_Bip_C_Hips"); lUpperLeg = cHips.Find("J_Bip_L_UpperLeg"); lLowerLeg = lUpperLeg.Find("J_Bip_L_LowerLeg"); rUpperLeg = cHips.Find("J_Bip_R_UpperLeg"); rLowerLeg = rUpperLeg.Find("J_Bip_R_LowerLeg"); cSpine = cHips.Find("J_Bip_C_Spine"); cChest = cSpine.Find("J_Bip_C_Chest"); cUpperChest = cChest.Find("J_Bip_C_UpperChest"); lShoulder = cUpperChest.Find("J_Bip_L_Shoulder"); lUpperArm = lShoulder.Find("J_Bip_L_UpperArm"); lLowerArm = lUpperArm.Find("J_Bip_L_LowerArm"); rShoulder = cUpperChest.Find("J_Bip_R_Shoulder"); rUpperArm = rShoulder.Find("J_Bip_R_UpperArm"); rLowerArm = rUpperArm.Find("J_Bip_R_LowerArm"); // 腰の初期値を取得する firstHipsPosition = cHips.localPosition; } void FixedUpdate() { float pendulum = (float)Math.Sin(Time.time * Math.PI * 3.0f); lUpperLeg.localRotation = Quaternion.AngleAxis(-60.0f * pendulum - 20.0f, Vector3.right); rUpperLeg.localRotation = Quaternion.AngleAxis(60.0f * pendulum - 20.0f, Vector3.right); lLowerLeg.localRotation = Quaternion.AngleAxis(-30.0f * pendulum + 60.0f, Vector3.right); rLowerLeg.localRotation = Quaternion.AngleAxis(30.0f * pendulum + 60.0f, Vector3.right); cHips.localRotation = Quaternion.AngleAxis(10.0f * pendulum, Vector3.up) * Quaternion.AngleAxis(10.0f, Vector3.right); cChest.localRotation = Quaternion.AngleAxis(-10.0f * pendulum, Vector3.up); cUpperChest.localRotation = Quaternion.AngleAxis(-20.0f * pendulum, Vector3.up); lUpperArm.localRotation = Quaternion.AngleAxis(60.0f * pendulum + 30.0f, Vector3.right) * Quaternion.AngleAxis(70.0f, Vector3.forward); rUpperArm.localRotation = Quaternion.AngleAxis(-60.0f * pendulum + 30.0f, Vector3.right) * Quaternion.AngleAxis(-70.0f, Vector3.forward); lLowerArm.localRotation = Quaternion.AngleAxis(-60.0f * pendulum + 60.0f, Vector3.up); rLowerArm.localRotation = Quaternion.AngleAxis(-60.0f * pendulum - 60.0f, Vector3.up); // 周期が半分の振り子を用意する float halfPendulum = (float)Math.Sin(Time.time * Math.PI * 3.0f * 2.0f); // 腰の位置を上下させる cHips.localPosition = firstHipsPosition + new Vector3(0.0f, 0.04f * halfPendulum, 0.0f); } }ちょ、ちょっとだけ、マシになったかな?
ゲシュタルト崩壊してきたので、このあたりで止めておきます。
一応、AnimationCurveは使わずにC#だけでアニメーションを作成することができました!
AnimationClipに保存してみる
せっかくだからAnimationClipに保存してみます。
Unityの GameObjectRecorder というAPIを使えば、シーン実行中のアニメーションをAnimationClipに保存できます。
以下のスクリプトを作成し、シーン内のVRMにアタッチしてください。
using UnityEngine; using UnityEditor.Animations; public class RecordTransformHierarchy : MonoBehaviour { public AnimationClip clip; private GameObjectRecorder m_Recorder; void Start() { m_Recorder = new GameObjectRecorder(gameObject); m_Recorder.BindComponentsOfType<Transform>(gameObject, true); } void LateUpdate() { if (clip == null){ return; } m_Recorder.TakeSnapshot(Time.deltaTime); } void OnDisable() { if (clip == null){ return; } if (m_Recorder.isRecording) { m_Recorder.SaveToClip(clip); } } }適当なフォルダーに空のAnimationClipファイルを作成します。
ここでは仮に
pendulum-running
というファイル名にします。これを
RecordTransformHierarchy
のClip
に割り当てます。これでシーンを実行すれば、AnimationClipに動きが保管されます!
FBX Exporter等を使えば、FBXに変換することもできますね!
さいごに
C#でもAnimationClipを作成することができました!
そのことに、果たして意味があるかはわかりませんが、個人的には
Quaternion
の勉強をできたのが収穫です。本記事作成にあたり、以下の記事を参考にさせていただきました。ありがとうございました。
- 投稿日:2019-12-24T14:54:50+09:00
Entity Framework Coreにおける行ロックと同時実行制御の挫折
やりたかったこと
Entity Framework CoreでAzureのSQL Databaseに接続し、
- 行の更新は競合させない
- 接続エラーは数回リトライする
を実現したかった
挫折した点
行ロックは相変わらずできない
EF Coreになり、行ロックに対応していないかと期待しましたが、まだ未対応。
System.ComponentModel.DataAnnotations.ConcurrencyCheckを使って楽観ロックをすることで、更新を競合させないことは可能。
https://docs.microsoft.com/ef/core/modeling/concurrencyConcurrencyCheckをいれたときのリトライがいまいち
公式のドキュメントでは、DbUpdateConcurrencyExceptionをキャッチして、問題のデータをアップデートするような例が提示されています。
ところが、現在のDBの値に応じて、保存したい値が異なる場合、次の(1)と(2)で同じような処理を入れる必要があります。
扱うエンティティも異なるので共通化しづらいし、リトライ機構も自分で実装する必要があり、どうもいまいち。using (var context = new PersonContext()) { // データを取得 // (1) 保存したいデータと現在のDBのデータを比較して保存する値を決める var saved = false; while (!saved) { try { // Attempt to save changes to the database context.SaveChanges(); saved = true; } catch (DbUpdateConcurrencyException ex) { foreach (var entry in ex.Entries) { if (entry.Entity is Person) { var proposedValues = entry.CurrentValues; var databaseValues = entry.GetDatabaseValues(); foreach (var property in proposedValues.Properties) { var proposedValue = proposedValues[property]; var databaseValue = databaseValues[property]; // (2) ここで(1)と同じ操作をしなくてはならない } // Refresh original values to bypass next concurrency check entry.OriginalValues.SetValues(databaseValues); } else { throw new NotSupportedException( "Don't know how to handle concurrency conflicts for " + entry.Metadata.Name); } } } } }参考
https://docs.microsoft.com/ef/core/saving/concurrencySqlServerRetryingExecutionStrategyはSqlExceptionしか扱わないらしい
Microsoft.EntityFrameworkCore.SqlServerRetryingExecutionStrategyのラッパークラスを作って、DbUpdateConcurrencyExceptionもSELECTからやり直すようにできればシンプルになると考え、作ってみました。
ただ、DbUpdateConcurrencyExceptionが発生しても、そもそもShouldRetryOnに入ってこないようで、挫折
まとめ
Entity Framework Coreになっても、まだいまいちなので他の方法を探したほうがよいな、という印象でした。
- 投稿日:2019-12-24T12:39:46+09:00
dotnet/runtimeのディレクトリツリー構造
始めに
2019/11頃、github上にdotnet/runtimeというリポジトリが誕生した。
これは、それまで分かれていたdotnetのランタイムとクラスライブラリを統合する目的で作られたものとなる。構成されるファイルについては、大半が既存のリポジトリからのインポートによるものとなるが、ここでどのようなディレクトリ構造になっているか、大雑把ではあるが書いておこうと思う。
ディレクトリ構造
おおまかに以下のようになる。
- docs
- クラスライブラリやランタイムに関するドキュメントをまとめたディレクトリ
- ソースそのものに興味がある場合を除いて、まずここから調べるのが良い
- 特にdocs/design/coreclr/botrはdotnetの動く仕組みについての解説等が載っているため、読み物として読むのも面白い
- 統合前のリポジトリにあったmdファイル等は大体この下
- eng
- dotnet/arcadeのファイル
- ビルド、パッケージング、インテグレーションのための共通ツール群
- dotnet/arcadeリポジトリから持ってきているものなので、通常見る必要は無い
- src
- ライブラリのソースコード等
- またここから分かれているが後述
- 移行に当たり、デフォルトで表示されるブランチはほぼ空となっているが、tagを参照することにより以前の状態を参照することは可能
- tools-local
- dotnet/runtimeリポジトリ限定で使うスクリプト群
その他、リポジトリ構成に関するイシューを見ると、aspnet/Extensionsやmonoの一部も取り込むようだが、現時点(2019/12)では確認できなかった。
src以下の構造
- src/coreclr
- 主にdotnet/coreclrにあったものをインポートしたもの
- JIT、GC、プラットフォーム別に実装が必要な部分等、ランタイム部分やネイティブ成分が多めだとこのディレクトリ以下にあると思っていい
- src/librariesに実装が見当たらない場合、大体 src/coreclr/src/System.Private.CoreLib 以下にあることが多い
- src/libraries
- 主にdotnet/corefxにあったものをインポートしたもの
- クラスライブラリの内、マネージドで済むものと、bait and switchに必要なリファレンス用ライブラリがここに含まれる
- netstandard時に参照されるライブラリも大体ここ
- src/installer
- 主にcore-setupにあったものをインポートしたもの
- dotnetコマンドや、publish時に生成される実行可能ファイルのベースとなるcorehostのソースや、一部パッケージングに関わる部分のソースがここに含まれている
終りに
来たるべき.NET 5に向けてリポジトリの統合をしたわけだが、一緒のリポジトリになることでこれまで散在していた記述等もまとめられることになったので、個人的には良かったと思う。特にdocs以下が一つにまとめられたのは良かったと思う。
また、これまでクラスライブラリレベルの修正は複数リポジトリにまたがって行われることが多かった(特にcorefxとcoreclr)が、この統合により、一つのPRに纏められるようになり、追跡もしやすくなる。
リポジトリ統合関連のイシューを見ると、まだ変更点はありそう(aspnet/AspNetCoreがdotnet/aspnetcoreとか、dotnet/sdkとか)だが、一番大きなdotnet/runtimeの統合を完了したので、今後更に何らかの動きがあるかもしれない。
- 投稿日:2019-12-24T00:17:01+09:00
【Unity(C#)】UniRxを使ってスキップ機能付きのテキストを1文字ずつ表示させる実装を試したけど微妙だった
UniRx
Unityで利用できるReactiveExtensionらしいです。
この説明だと私自身も意味がわからないので、参考リンク1のお言葉を頂戴して説明します。直訳すると「反応性拡張」で、イベントに対する反応を拡張するためのライブラリ
何かしらのイベント(ボタンを押した、プレーヤーが移動したなど何でも)に対する反応を
簡単に書くことができるっぽいです。私自身、ソースコードを見に行って、
"はいはいなるほどね"と言えるレベルまで理解できていません。Rxに採用されているデザインパターンで、Observerパターンというのがあるのですが、
それに関しても人に説明できるところまで理解でき次第、まとめようと思っています。今回は、使いながら覚えていきましょう
という意図で調べながら作ったらなかなか残念な仕上がりだったのでそれをメモに残します。テキストを1文字ずつ表示
既に何年も前に実装されている先駆者様がいらっしゃいました。
【参考リンク】: UniRXでuGUIのテキストをアニメーションさせる文字を1文字ずつ表示する機能自体は参考リンクで完結しているので、
今回はスキップ機能(文字をいっきに最後まで表示させる機能)をUniRxで実装してみます。コード
テキストコンポーネントを持つオブジェクトにアタッチusing UniRx; using System; using UnityEngine; using UniRx.Triggers; using UnityEngine.UI; public class TextPerOneWrite : MonoBehaviour { [SerializeField] float m_textInterval = 0.2f; [SerializeField] KeyCode m_keyCode = KeyCode.Space; Text m_windowText; IDisposable m_textDispose; IDisposable m_updateDispose; void Start() { m_windowText = this.gameObject.GetComponent<Text>(); m_windowText.text = ""; //実行サンプル 文字を1文字ずつ出す ShowPerOne("ウホウホバナナヨコセ"); ShowPerOne("ウホウホバナナヨコセ(早口)", 0.1f); } void ShowPerOne(string commentText) { m_windowText.text = ""; if (m_textDispose != null) { m_textDispose.Dispose(); m_updateDispose.Dispose(); } m_textDispose = Observable.Interval(TimeSpan.FromSeconds(m_textInterval)) .Take(commentText.Length) .Select(_ => 1) .Scan((accumulation, newValue) => accumulation + newValue) .DoOnCompleted(() => m_updateDispose.Dispose()) .SubscribeToText(m_windowText, length => commentText.Substring(0, length)) .AddTo(this); //特定のキー入力で文字を1文字ずつ出す機能を止める m_updateDispose = this.UpdateAsObservable() .FirstOrDefault(_ => Input.GetKeyDown(m_keyCode)) .Subscribe(_ => { m_textDispose.Dispose(); m_windowText.text = commentText; }); } void ShowPerOne(string commentText, double textInterval) { m_windowText.text = ""; if (m_textDispose != null) { m_textDispose.Dispose(); m_updateDispose.Dispose(); } m_textDispose = Observable.Interval(TimeSpan.FromSeconds(textInterval)) .Take(commentText.Length) .Select(_ => 1) .Scan((accumulation, newValue) => accumulation + newValue) .DoOnCompleted(()=>m_updateDispose.Dispose()) .SubscribeToText(m_windowText, length => commentText.Substring(0, length)) .AddTo(this); //特定のキー入力で文字を1文字ずつ出す機能を止める m_updateDispose = this.UpdateAsObservable() .FirstOrDefault(_ => Input.GetKeyDown(m_keyCode)) .Subscribe(_ => { m_textDispose.Dispose(); m_windowText.text = commentText; }); } }特定のキーを押すと1文字ずつ流れる文字を一気にスキップして表示することができます。
Observable.Interval
引数に時間を指定してあげるとその指定した間隔で値を流す(処理を実行)ことができます。
ちなみに
TimeSpan.FromSeconds
というのはどうやらC#の機能のようで
時間を指定するときによく使うみたいです。【参考リンク】:TimeSpan.FromSeconds(Double) メソッド
Scan
Scanというオペレーターは前回発行された値と今発行された値の畳み込みを行うことができます。
平たく言うと重ねて足し合わせるイメージです。.Scan((accumulation, newValue) => accumulation + newValue)
accumulation
は蓄積、累算などの意味を持つので、言葉の意味で覚えるとわかり易いです。
累算した値に、受け取った値(newValue)を足しているというわけですね。【参考リンク】:UniRxを使ってみる
ストリームの寿命管理
uniRxは非常に便利ですが、
使う上で気を付けることの1つにストリームの寿命管理があります。ストリームというのは
メッセージが伝達される経路、仕組み、機構のこと
らしいです。(またの名をObservableというらしい)
【引用元】:ObserverパターンからはじめるUniRx
このストリームというのが役目を終了した(もう必要でなくなった)段階で
購読を終了してあげる必要があります。そうしてあげないと、パフォーマンスが低下したり、
もう存在していないGameObjectを参照してエラーが起きたりします。【参考リンク】:UniRx入門 その2 - メッセージの種類/ストリームの寿命
Dispose
ストリームの購読終了を任意のタイミングで行うことが可能です。
一回変数に入れて、好きなタイミングで呼び出したらいいんじゃないでしょうか。
AddTo
AddTo
というメソッドを利用して、
先述したもう存在していないGameObjectの参照を未然に防ぐことができます。引数に与えたGameObjectが削除された際に、自動的にDisposeを呼び出してくれます。
OnCompleted
このメッセージが発行され
Subscribe
まで到達すると購読が終了するらしいです。今回どこにも
OnCompleted
を書いてませんが、
どうやらTake
によって指定回数分メッセージが通った際に発行され、
最後のSubscribeに到達しているようです。
(違うかもしれないんで、使ってておかしいと思ったらまた書き直します)ストリームいっぱいできちゃう問題
前回のストリームが実行中であっても次に作成したストリームを同じ変数に突っ込めば
前回のストリームを止めた上で次のストリームを実行できそう!ダメでした。なので、変数に既に何かしらが格納されているかチェックして
もし入っていたらストリームを止めてます。if (m_textDispose != null) { m_textDispose.Dispose(); m_updateDispose.Dispose(); }
m_textDispose = Observable.Interval
のように1つの変数に格納したからといって、
ストリームが1つしか作成されるわけではない、前回のストリームは止まらない...というのがわかりました。ストリームがストリームを監視するのはあまりよくない
強い人に見て頂いた際にご指摘頂きましたが、けっこうごちゃごちゃしてしまっています。
自分で作っていても感じたことなのですが、
・ストリームの処理を条件分岐したい
・ストリームの挙動を途中で動的に変えたいというような要望をUniRx使用時に盛り込むと、
”ストリームB”で”ストリームA”を監視するような状態になるので
条件分岐や変えたい挙動の数だけストリームが増えてしまいます。なので、今回のように
Observable.Interval
をUpdateAsObservable
から任意のタイミングで止めるというのは
本来便利なRxを使っているにもかかわらずややこしくなってしまっています。初心者あるあるらしいので、次回からはその辺りも意識してみようと思います。
参考リンク
UniRx入門シリーズ 目次
UniRx オペレータ逆引き
UniRxを紐解く「Take(1)とFirst()の違い」
【Unity】【UniRx】Observable.DoXxx()系のメソッドの挙動まとめ
【Unity】UniRx入門リンク集