20191224のC#に関する記事は7件です。

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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

パスに沿ってなめらかに一定速度でオブジェクトを移動させる実装の解説

my.gif

まえがき

問題

ゲームだと、パスに沿ってキャラを移動させたい時があります。
ただ、単純に実装すると速度がバラバラになってしまいます。

目的

今回は曲線でもだいたい一定の速度で動かせるようにする実装を解説します。
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

解説

単純な実装

first.gif

これはベジェ曲線の引数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);
}

改善した物

my.gif

この問題は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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【ポストコンパイル時定数計算】.NET系言語でconstexprを実現する研究【Mono.Cecil, AssemblyBuilder】

.NET用プログラミング言語に新地平を切り開きました。

MITライセンスでプロジェクト全体を配布中です。

dotnet-constexpr.zipを解凍した後の導入方法

おそらく「dotnet-constexpr」フォルダの下に「post」「ConsoleTest」「ConstExpr」「target」の4フォルダがあると思われます。
dotnet-constexprをカレントディレクトリとして、PowerShellやbashなどのターミナルで dotnet restore と実行してください。
4プロジェクト全てがrestoreされます。

要約

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
  • post
    • ポストコンパイル時定数計算を実現するコンソールプログラム
  • Target
    • テスト用のプロジェクト
  • ConsoleTest
    • Targetを使用したプロジェクト
    • postの処理によりTargetが壊れていないかを確認するためのもの

ConstExpr

ConstExprAttribute.cs
using 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.cs
using 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の数だけ新規にAssemblyBuilderModuleBuilderを定義する。
そしてConstExpr属性が付与された型に対応するTypeBuilderModuleBuilderを用いて定義する
型を一周巡回した後、改めてフィールド情報やメソッドのシグネチャ情報を取得して定義する。
大凡の外形が定まった後、初めてMethodBuilderからILGeneratorを得、メソッドの中身を再構築する。

そして全体を過不足なく再構成した後、Publish()内部で全てのTypeBuilderに対してCreateTypeメソッドを実行して再構築を完了し、実行可能なメソッドを得る。

Target

Target.cs全文
Target.cs
using 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.cs
using 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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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#で動かしてみます!

最終的にはこんな感じのアニメーションが作れました。

201912241856a.gif

動作環境

以下の環境でやりました。

  • Windows 10 64bit
  • Unity 2019.2.11f1
  • UniVRM 0.54.0

初期状態

とりあえずVRMファイルをシーンに読み込んでみます。

今回は 今里尚吾 くんにご協力いただきます!(VRoid Studioで作りました!)

image.png

Hierarchyを見てみたらこんな感じでした!

image.png

股関節を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にアタッチします。

image.png

脚が前後に開きました!

image.png

image.png

振り子のように足を揺らしてみる

せっかくなので、アニメーションさせたいですよね。

ということで、振り子のように揺らしてみます!

    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);
    }

201912241743a.gif

いいかんじ!

膝を曲げてみる

膝も曲げてみたくなったので、 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);
    }
}

201912241818a.gif

膝も曲がりました。

全身動かしてみる

同じ要領で、全身の関節の動かしてみます。色々調整したら、以下のようなコードになりました。

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);
    }
}

201912241847a.gif

だんだんそれっぽくなってきました。

ただ、重心が上下しないのはちょっと違和感がありますね。

重心を上下させてみた

重心も上下させてみます。

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);
    }
}

201912241856a.gif

ちょ、ちょっとだけ、マシになったかな?

ゲシュタルト崩壊してきたので、このあたりで止めておきます。

一応、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ファイルを作成します。

image.png

ここでは仮に pendulum-running というファイル名にします。

image.png

これを RecordTransformHierarchyClip に割り当てます。

image.png

これでシーンを実行すれば、AnimationClipに動きが保管されます!

FBX Exporter等を使えば、FBXに変換することもできますね!

さいごに

C#でもAnimationClipを作成することができました!

そのことに、果たして意味があるかはわかりませんが、個人的には Quaternion の勉強をできたのが収穫です。

本記事作成にあたり、以下の記事を参考にさせていただきました。ありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Entity Framework Coreにおける行ロックと同時実行制御の挫折

やりたかったこと

Entity Framework CoreでAzureのSQL Databaseに接続し、

  1. 行の更新は競合させない
  2. 接続エラーは数回リトライする

を実現したかった

挫折した点

行ロックは相変わらずできない

EF Coreになり、行ロックに対応していないかと期待しましたが、まだ未対応。

System.ComponentModel.DataAnnotations.ConcurrencyCheckを使って楽観ロックをすることで、更新を競合させないことは可能。
https://docs.microsoft.com/ef/core/modeling/concurrency

ConcurrencyCheckをいれたときのリトライがいまいち

公式のドキュメントでは、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/concurrency

SqlServerRetryingExecutionStrategyはSqlExceptionしか扱わないらしい

Microsoft.EntityFrameworkCore.SqlServerRetryingExecutionStrategyのラッパークラスを作って、DbUpdateConcurrencyExceptionもSELECTからやり直すようにできればシンプルになると考え、作ってみました。

ただ、DbUpdateConcurrencyExceptionが発生しても、そもそもShouldRetryOnに入ってこないようで、挫折

参考
https://docs.microsoft.com/dotnet/api/microsoft.entityframeworkcore.sqlserverretryingexecutionstrategy

まとめ

Entity Framework Coreになっても、まだいまいちなので他の方法を探したほうがよいな、という印象でした。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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/Extensionsmonoの一部も取り込むようだが、現時点(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の統合を完了したので、今後更に何らかの動きがあるかもしれない。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity(C#)】UniRxを使ってスキップ機能付きのテキストを1文字ずつ表示させる実装を試したけど微妙だった

UniRx

Unityで利用できるReactiveExtensionらしいです。
この説明だと私自身も意味がわからないので、参考リンク1のお言葉を頂戴して説明します。

直訳すると「反応性拡張」で、イベントに対する反応を拡張するためのライブラリ

何かしらのイベント(ボタンを押した、プレーヤーが移動したなど何でも)に対する反応を
簡単に書くことができるっぽいです。

私自身、ソースコードを見に行って、
"はいはいなるほどね"と言えるレベルまで理解できていません。

Rxに採用されているデザインパターンで、Observerパターンというのがあるのですが、
それに関しても人に説明できるところまで理解でき次第、まとめようと思っています。

今回は、使いながら覚えていきましょう 
という意図で調べながら作ったらなかなか残念な仕上がりだったのでそれをメモに残します。

テキストを1文字ずつ表示

GIFで見たまんまの意味です。
RxText.gif

既に何年も前に実装されている先駆者様がいらっしゃいました。
【参考リンク】: 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.IntervalUpdateAsObservableから任意のタイミングで止めるというのは
本来便利なRxを使っているにもかかわらずややこしくなってしまっています。

初心者あるあるらしいので、次回からはその辺りも意識してみようと思います。

参考リンク

UniRx入門シリーズ 目次
UniRx オペレータ逆引き
UniRxを紐解く「Take(1)とFirst()の違い」
【Unity】【UniRx】Observable.DoXxx()系のメソッドの挙動まとめ
【Unity】UniRx入門リンク集

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む