20201223のC#に関する記事は8件です。

[C#/WPF] コンバーターの書き方

もくじ
https://qiita.com/tera1707/items/4fda73d86eded283ec4f

やりたいこと

WPFで画面を作るときにコンバーターをよく使うが、都度既存のコードからコピペしていて身についた感がなかったので、テンプレートとして持っておくために下記にメモしておく。

コンバーターの作り方

今回は、MSの公式サンプルをもとに、

  • DateTime型の値を、画面表示の際に日付の文字列に変換する
  • 日付の文字列を、プロパティget時にDateTime型の値に変換する

というコンバーターを作ってみる。

コンバーターのクラスを作る

クラス名はDateTimeConverterのように、〇〇Conveterというようにするのが慣例っぽい。
その名前で、IValueConverterインターフェースを実装した下記のようなクラスを作る。

  • Convertメソッドが「プロパティ→画面表示に反映時」の変換を記述するところ。
  • ConvertBackメソッドが「画面表示→プロパティに反映時」の変換記述するところ。
DateTimeConverter.cs
public class DateTimeConverter : IValueConverter
{
    // DateTimeを「yyyy/MM/dd HH:mm:ss.fff」形式の文字列に変換
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        DateTime date = (DateTime)value;
        return date.ToString("yyyy/MM/dd HH:mm:ss.fff");
    }
    // 文字列をDateTimeに変換
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        string strValue = value as string;
        DateTime resultDateTime;
        if (DateTime.TryParse(strValue, out resultDateTime))
        {
            return resultDateTime;
        }
        return DependencyProperty.UnsetValue;
    }
}

※個人的経験では、ほとんどの場合「プロパティ→画面表示に反映時」に変換することしかしないので、Converterメソッドだけ使ってConvertBackメソッドはほとんど使ったことがない。

そういう場合は、ConvertBackの中身は下記のようにしておけばいい。

public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
    throw new NotImplementedException();
}

画面にリソースとして登録し、それを変換したいところで使う

一番の親(今回はWindow)のリソースとして、下記のようにして登録する。

MainWIndowの一部.xaml
<Window.Resources>
    <converter:DateTimeConverter x:Key="DateTimeConverter"/>
</Window.Resources>

そいつのKeyを使って、変換を行いたいコントロールにバインドしたプロパティのコンバーターとして使用する。
(今回は、ConverterParameterは別に不要だが、パラメータ渡せるということを示すために渡すだけ渡す)

MainWIndowの一部.xaml
<TextBox Grid.Row="1" Grid.Column="2" Text="{Binding Dt, ElementName=root, Converter={StaticResource DateTimeConverter}, ConverterParameter=123}"/>

ViewModel等に、バインドするプロパティを作成して、get/setしてみる

下記のように、画面にバインドしたプロパティに

  • set(今回はDtに現在の日付をセットした)
  • get(今回はDtの値をゲットしてyyyyねんMMがつddにち形式で表示した)
MainWindow.xaml.cs
public DateTime Dt
{
    get { return _dt; }
    set { _dt = value; OnPropertyChanged(nameof(Dt)); }
}
// テキストボックス ← プロパティ
private void Button_Click(object sender, RoutedEventArgs e)
{
    // set
    Dt = DateTime.Now;
}
// テキストボックス → プロパティ
private void Button_Click_1(object sender, RoutedEventArgs e)
{
    // get
    AddLog(Dt.ToString("入力した日付は、yyyyねんMMがつddにち です"));
}

そうすると、コンバータを通って、下記のように画面に表示される。
image.png

以上で、一通りコンバーターが使えるようになった。

コード全部

DateTimeConverter.cs
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;

namespace WpfApp48.converter
{
    [ValueConversion(typeof(DateTime), typeof(string))]
    public class DateTimeConverter : IValueConverter
    {
        // DateTimeを「yyyy/MM/dd HH:mm:ss.fff」形式の文字列に変換
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            DateTime date = (DateTime)value;
            return date.ToString("yyyy/MM/dd HH:mm:ss.fff");
        }
        // 文字列をDateTimeに変換
        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            string strValue = value as string;
            DateTime resultDateTime;
            if (DateTime.TryParse(strValue, out resultDateTime))
            {
                return resultDateTime;
            }
            return DependencyProperty.UnsetValue;
        }
    }
}
MainWindow.xaml
<Window x:Class="WpfApp48.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp48"
        xmlns:converter="clr-namespace:WpfApp48.converter"
        mc:Ignorable="d"
        Title="MainWindow" Height="600" Width="800"
        Loaded="Window_Loaded"
        Name="root">
    <Window.Resources>
        <converter:DateTimeConverter x:Key="DateTimeConverter"/>
    </Window.Resources>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="5*"/>
            <RowDefinition Height="1*"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>

        <StackPanel Grid.Row="1" Grid.Column="0">
            <Button Content="1" Click="Button_Click"/>
            <Button Content="2" Click="Button_Click_1"/>
        </StackPanel>

        <ListBox Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" ItemsSource="{Binding Logs, ElementName=root}"/>

        <TextBox Grid.Row="1" Grid.Column="2" Text="{Binding Dt, ElementName=root, Converter={StaticResource DateTimeConverter}, ConverterParameter=123}"/>
    </Grid>
</Window>

MainWindow.xaml.cs
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows;

namespace WpfApp48
{
    public partial class MainWindow : Window, INotifyPropertyChanged
    {
        #region INotifyPropertyChanged
        public event PropertyChangedEventHandler PropertyChanged;
        protected void OnPropertyChanged(string propertyName)=> this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        #endregion

        #region LogFramework
        public ObservableCollection<string> Logs { get; set; } = new ObservableCollection<string>();

        public void AddLog(string log)
        {
            DateTime now = DateTime.Now;
            Logs.Add(now.ToString("hh:mm:ss.fff ") + log);
            OnPropertyChanged(nameof(Logs));
        }
        #endregion

        DateTime _dt = DateTime.Now;


        public MainWindow() => InitializeComponent();
        private void Window_Loaded(object sender, RoutedEventArgs e) { }

        public DateTime Dt
        {
            get { return _dt; }
            set { _dt = value; OnPropertyChanged(nameof(Dt)); }
        }

        // テキストボックス ← プロパティ
        private void Button_Click(object sender, RoutedEventArgs e)
        {
            Dt = DateTime.Now;
        }

        // テキストボックス → プロパティ
        private void Button_Click_1(object sender, RoutedEventArgs e)
        {
            AddLog(Dt.ToString("入力した日付は、yyyyねんMMがつddにち です"));
        }
    }
}

参考

How to: Convert Bound Data
https://docs.microsoft.com/ja-jp/dotnet/desktop/wpf/data/how-to-convert-bound-data?view=netframeworkdesktop-4.8

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

アメリカ地質調査所(USGS)から世界中の地震情報を取得する

アメリカ地質調査所(United States Geological Survey, USGS)から世界中の地震情報を取得します。
フィードの種類は ATOM や GeoJSON など複数の形式が用意されていますが、今回は GeoJSON Summary 形式のフィードを C# で処理します。
サンプルコードはC# 9の機能を使用して記述しています。

過去1時間、過去24時間、過去1週間、過去30日間以内に発生した地震についてそれぞれ、顕著な地震、マグニチュード(以下M)4.5以上の地震、M2.5以上の地震、M1.0以上の地震、全ての地震情報のフィードが用意されています。取得したい内容に応じて適切なフィードを利用しましょう。
ここでは過去24時間以内のM4.5以上の地震について処理していきます。このフィードの URL は https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/4.5_day.geojson です。

フィードをC#で処理する

まずは、JSON を C# でデシリアライズするためのクラスを生成します。

Visual Studio を使っている場合は JSON を全て選択してクリップボードにコピーした後、Visual Studio のメニューから「編集」、「形式を選択して貼り付け」、「JSON をクラスとして貼り付ける」を実行してクラスを生成するとよいでしょう。自動生成されたクラスやプロパティ名、型は必要に応じて修正してください。ここでは Rootobject クラスの名前を UsgsJson に変え、全プロパティの名前をアッパーキャメルケースに、set アクセサーを init アクセサーに変更しました。

ちゃんと取得できたか確認するためにフィードのタイトルを取得してみました。ここまでのソースコードは次の通りです1

using System;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;

namespace UsgsSample
{
    class Program
    {
        const string Uri = "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/4.5_day.geojson";
        static readonly HttpClient HttpClient = new();

        static async Task Main()
        {
            await using var stream = await HttpClient.GetStreamAsync(Uri);
            var options = new JsonSerializerOptions
            {
                PropertyNamingPolicy = JsonNamingPolicy.CamelCase
            };
            var json = await JsonSerializer.DeserializeAsync<UsgsJson>(stream, options);
            Console.WriteLine(json.Metadata.Title);
        }
    }

    public class UsgsJson
    {
        public string Type { get; init; }

        public Metadata Metadata { get; init; }

        public Feature[] Features { get; init; }

        public float[] Bbox { get; init; }
    }

    public class Metadata
    {
        public long Generated { get; init; }

        public string Url { get; init; }

        public string Title { get; init; }

        public int Status { get; init; }

        public string Api { get; init; }

        public int Count { get; init; }
    }

    public class Feature
    {
        public string Type { get; init; }

        public Properties Properties { get; init; }

        public Geometry Geometry { get; init; }

        public string Id { get; init; }
    }

    public class Properties
    {
        public float Mag { get; init; }

        public string Place { get; init; }

        public long Time { get; init; }

        public long Updated { get; init; }

        public object Tz { get; init; }

        public string Url { get; init; }

        public string Detail { get; init; }

        public int? Felt { get; init; }

        public float? Cdi { get; init; }

        public int? Mmi { get; init; }

        public string Alert { get; init; }

        public string Status { get; init; }

        public int Tsunami { get; init; }

        public int Sig { get; init; }

        public string Net { get; init; }

        public string Code { get; init; }

        public string Ids { get; init; }

        public string Sources { get; init; }

        public string Types { get; init; }

        public object Nst { get; init; }

        public float Dmin { get; init; }

        public float Rms { get; init; }

        public int Gap { get; init; }

        public string MagType { get; init; }

        public string Type { get; init; }

        public string Title { get; init; }
    }

    public class Geometry
    {
        public string Type { get; init; }

        public float[] Coordinates { get; init; }
    }
}

「USGS Magnitude 4.5+ Earthquakes, Past Day」という文字列が出力されれば成功です。

続いて、地震情報を取り出していきます。
JSONデータを見ると features キーの中にそれぞれの地震情報が配列で格納されています。
さらに、その中に typepropertiesgeometryid というキーが存在します。今回使うキーは propertiesgeometry です。

まずは、properties のサブキーについてみていきます。
次の表は主な項目をピックアップしたものです。詳しいフォーマットについては USGS のページに記載されています。

キー名 値の説明
mag float マグニチュード
place string 震央位置
time long 地震発生時刻のエポックミリ秒(UNIX時間)
updated long 最終更新時刻のエポックミリ秒(UNIX時間)
url string USGSページへのリンク
mmi float(null許容) 推定最大計測震度
magtype string マグニチュードの種類
type string 地震の種類

この中でも特筆すべき項目について解説します。

place(震央位置)

日本の気象庁が発表する地震情報のように領域ごとに震央地名が設定されているのではなく、たいていは「○○の北 ○○km」のような表記です。最初の○○には代表的な都市名が入りますが、もう少し大雑把な地域でよい場合が多いです。そこで、海外の地震情報については Flinn-Engdahl Regions(F-E Regions) という境界の定義が使われることがあります。経緯度からF-E Regionsを取得し、日本語化する方法を後述します。

time(地震発生時刻) と updated(最終更新時刻)

ミリ秒の UNIX 時間から DateTimeOffset 型に変換するサンプルコードです。

var dateTime = DateTimeOffset.FromUnixTimeMilliseconds(feature.Properties.Time).ToLocalTime();

ToLocalTime() メソッドを使用して、プログラムが実行されているロケールのローカルタイムに変換しています。

mmi(推定最大計測震度)

最大加速度や最大速度をもとに推定された震度で、震度階級には改正メルカリ震度階級(ローマ数字表記)が使われます。この値は、日本国内の地震情報で用いられる気象庁震度階級とは異なります。この API では自動処理を行いやすいように実数で値が格納されていますが、ローマ数字のほうが人間は見やすいです。震度階級に変換するメソッドのサンプルです。

static string ToIntensityString(float? mmi) => mmi switch
{
    null => "",
    < 1.5f => "I",
    < 2.5f => "II",
    < 3.5f => "III",
    < 4.5f => "IV",
    < 5.5f => "V",
    < 6.5f => "VI",
    < 7.5f => "VII",
    < 8.5f => "VIII",
    < 9.5f => "IX",
    < 10.5f => "X",
    < 11.5f => "XI",
    _ => "XII"
};

ほとんどの地震は null が格納されており、null の場合の処理が必要です。

magtype(マグニチュードの種類)

日本国内の地震情報で地震の規模を表現する際には気象庁マグニチュード(Mj)が用いられます。しかし、USGS の地震情報で用いられるマグニチュードは1種類に統一されておらず、マグニチュードの値を参照する際はマグニチュードの種類も参照する必要があります。代表的な値には 「mww (モーメントマグニチュード)」、「mb(実体波マグニチュード)」があり、取り得る値については USGS のページにまとめられています。

type(地震の種類)

自然地震だけなく、発破や核実験による地震もあります。それらを識別するにはこの値を使用します。取り得る値のリストをUSGSのサイト内で見つけられませんでしたが、これまでに確認できた値をまとめておきます。

type
earthquake
nuclear explosion
quarry blast
volcanic eruption

続いて geometry のサブキーをみていきます。coordinates キーに震源要素が格納されています。coordinates キーは配列になっており、「震源の経度」、「震源の緯度」、「震源の深さ」の順で格納されています。

それでは実際に値を表示してみます。

foreach (var feature in json.Features)
{
    var originTime = DateTimeOffset.FromUnixTimeMilliseconds(feature.Properties.Time).ToLocalTime();
    Console.WriteLine($"地震発生時刻:{originTime}");
    Console.WriteLine($"震源地:{feature.Properties.Place}");
    Console.WriteLine($"震源の緯度:{feature.Geometry.Coordinates[1]}");
    Console.WriteLine($"震源の経度:{feature.Geometry.Coordinates[0]}");
    Console.WriteLine($"震源の深さ:{feature.Geometry.Coordinates[2]:F1}km");
    Console.WriteLine($"マグニチュード:{feature.Properties.Mag:F1}");
    Console.WriteLine($"マグニチュードの種類:{feature.Properties.MagType}");
    var intensity = ToIntensityString(feature.Properties.Mmi);
    if (intensity.Length != 0)
        Console.WriteLine($"改正メルカリ震度階級:{intensity}");
    Console.WriteLine(feature.Properties.Url);
    Console.WriteLine("---");
}

image1.png
このように表示されていれば成功です。

震央地名を日本語化する

先ほど震央位置を出力した際に「(都市名)の(方角) ○○km」のように表示されていました。今度は Flinn-Engdahl Regions(F-E Regions)という境界の定義に基づく地名を表示します。気象庁から発表される地震情報の一つに「遠地地震に関する情報」というものがありますが、実はこの情報の詳細震央地名は F-E Regions に基づいています。気象庁防災情報XMLのコード表に日本語の F-E Regions のリストがあるので、これを利用します。

まず、気象庁のサイトから個別コード表をダウンロードし展開します。その中の「地震火山関連コード表.xls」ファイルを開きます。シートが複数ありますが、「43(AreaEpicenterDetailコード表)」が今回使用するリストです。データ部だけをコンマ区切りの CSV 形式で保存します。
image2.png
保存したファイルをエディタで開いて、このように保存されていればOKです。4桁のコードが付番されていますが、千の位は不要なので削除して上書き保存します。ボックス選択に対応したエディタであれば Shift キーAlt キー を押しながら範囲を選択すると楽です。
image3.png

続いて、C# でコードを書いていきます。
実は USGS には経緯度を渡すと F-E Regions を返してくれる API があります。詳しくは USGS のページに記載されています。JSON 形式でデシリアライズをするためのクラスを定義します。

public class FeJson
{
    public Fe Fe { get; init; }
}

public class Fe
{
    public FeatureFe[] Features { get; init; }
}

public class FeatureFe
{
    public PropertiesFe Properties { get; init; }
}

public class PropertiesFe
{
    public int? Number { get; init; }
}

続いて、日本語の震央地名を取得するメソッドを定義します1

const string RegionsUri = "https://earthquake.usgs.gov/ws/geoserve/regions.json";
const string FeRegionsFile = "FeRegions.csv";

static async Task<string> GetJapaneseEpicenterNameAsync(float latitude, float longitude)
{
    var uri = $"{RegionsUri}?latitude={latitude}&longitude={longitude}&type=fe";
    await using var stream = await HttpClient.GetStreamAsync(uri);
    var options = new JsonSerializerOptions
    {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase
    };
    var json = await JsonSerializer.DeserializeAsync<FeJson>(stream, options);

    var lines = await File.ReadAllLinesAsync(FeRegionsFile);
    return lines.Select(line => line.Split(','))
                .Where(line => int.Parse(line[0]) == json.Fe.Features.First(feature => feature.Properties.Number is not null).Properties.Number)
                .Select(line => line[1]).FirstOrDefault();
}

JSON と CSV ファイルのコード番号同士を比較して、一致していれば CSV ファイルの日本語名を返します。

先ほど作成した地震情報を取得するコードに組み込んで実行してみます。

var originTime = DateTimeOffset.FromUnixTimeMilliseconds(feature.Properties.Time).ToLocalTime();
Console.WriteLine($"地震発生時刻:{originTime}");
var latitude = feature.Geometry.Coordinates[1];
var longitude = feature.Geometry.Coordinates[0];
var epicenter = await GetJapaneseEpicenterNameAsync(latitude, longitude);
if (epicenter.Length != 0)
    Console.WriteLine($"震源地:{epicenter}");
Console.WriteLine($"震源の緯度:{latitude}");
Console.WriteLine($"震源の経度:{longitude}");
Console.WriteLine($"震源の深さ:{feature.Geometry.Coordinates[2]:F1}km");
Console.WriteLine($"マグニチュード:{feature.Properties.Mag:F1}");
Console.WriteLine($"マグニチュードの種類:{feature.Properties.MagType}");
var intensity = ToIntensityString(feature.Properties.Mmi);
if (intensity.Length != 0)
    Console.WriteLine($"改正メルカリ震度階級:{intensity}");
Console.WriteLine(feature.Properties.Url);
Console.WriteLine("---");

image4.png
このように表示されていれば成功です。


  1. 例外処理は省略しています。 

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

オブジェクトを追いかけるカメラをスクリプトで実現する方法

概要

Unity3Dにて、あるオブジェクトを追いかけるカメラをスクリプトで表現する方法を記します。

なお、本記事ではこのような追従するカメラを便宜的に「相対カメラ」と呼ぶことにします(オブジェクトとの相対的な位置を保っているから)。

Unityバージョン:Unity 2019.4.14f1
使用言語:C#

結論

本記事で扱う相対カメラの特徴:

  • オブジェクトの移動に連動する(一定の距離を保ちながら移動する)
  • オブジェクトとの相対的な距離は、再生時のお互いの位置で決める
  • オブジェクトの回転には連動しない

サンプルコードは以下の通りです。

RelativeCamera.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class RelativeCamera : MonoBehaviour
{
    public GameObject target; // カメラと連動するオブジェクト(インスペクターで取得)
    private Vector3 offset; // カメラとオブジェクトの相対的な距離

    void Start()
    {
        // offsetに再生時の相対的な距離を代入
        offset = transform.position - target.transform.position;
    }

    // Upddate()だと他のスクリプトの処理が終わる前に動く可能性がある
    void LateUpdate()
    {
        // 相対カメラの位置ベクトル = オブジェクトの位置ベクトル + offset位置ベクトル
        transform.position = target.transform.position + offset;
    }
}

本論

再生時のカメラとオブジェクトの距離を保つようなカメラの動きを表現しました。

そのため、Start()内で再生時のカメラとオブジェクトの距離をoffsetに代入し、
それを使ってLateUpdate()内で相対カメラの位置を更新し続けるようにしました。


中身は、単純なベクトルの足し算引き算を使っています。

Start()内では、

Sample1
// offset = 相対カメラの位置ベクトル - オブジェクトの位置ベクトル
offset = transform.position - target.transform.position;

としてoffsetを計算しています。

同様にして、LateUpdate()内では、

Sample2
// 相対カメラの位置ベクトル = オブジェクトの位置ベクトル + offset
transform.position = target.position + offset;

として相対カメラの位置ベクトルtransform.positionを計算しています。


また、カメラ移動のスクリプトをLateUpdate()内に書きました。

これは画面のカクつきを防ぐためです。

両者ともUpdate()内に書いてしまうと、カメラ移動がオブジェクト移動に先走ってしまい、
画面が変にカクついてしまう恐れがあります。

そのため、オブジェクト移動のスクリプトはUpdate()内に書くことを
想定して、
カメラ移動のスクリプトはLateUpdate()内に書きました。

参考文献

プレイヤーに追従するカメラ(カクつかない方法、滑らかに追従する方法) - ゆーじのUnity開発日記
【Unity C#】カメラの自動追従 | フタバゼミ

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

ゲームオブジェクトを追いかけるカメラをスクリプトで実現する方法

概要

Unity3Dにて、あるゲームオブジェクトを追いかけるカメラをスクリプトで表現する方法を記します。

なお、本記事ではこのような追従するカメラを便宜的に「相対カメラ」と呼ぶことにします(オブジェクトとの相対的な位置を保っているから)。

Unityバージョン:Unity 2019.4.14f1
使用言語:C#

結論

本記事で扱う相対カメラの特徴:

  • ゲームオブジェクトの移動に連動する(一定の距離を保ちながら移動する)
  • ゲームオブジェクトとの相対的な距離は、再生時のお互いの位置で決める
  • ゲームオブジェクトの回転には連動しない

サンプルコードは以下の通りです。

RelativeCamera.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class RelativeCamera : MonoBehaviour
{
    public GameObject target; // カメラと連動するゲームオブジェクト(インスペクターで設定)
    private Vector3 offset; // カメラとゲームオブジェクトの相対的な距離

    void Start()
    {
        // offsetに再生時の相対的な距離を代入
        offset = transform.position - target.transform.position;
    }

    // Upddate()だと他のスクリプトの処理が終わる前に動く可能性がある
    void LateUpdate()
    {
        // 相対カメラの位置ベクトル = ゲームオブジェクトの位置ベクトル + offset位置ベクトル
        transform.position = target.transform.position + offset;
    }
}

本論

再生時のカメラとゲームオブジェクトの距離を保つようなカメラの動きを表現しました。

そのため、Start()内で再生時のカメラとゲームオブジェクトの距離をoffsetに代入し、
それを使ってLateUpdate()内で相対カメラの位置を更新し続けるようにしました。


中身は、単純なベクトルの足し算引き算を使っています。

Start()内では、

Sample1
// offset = 相対カメラの位置ベクトル - ゲームオブジェクトの位置ベクトル
offset = transform.position - target.transform.position;

としてoffsetを計算しています。

同様にして、LateUpdate()内では、

Sample2
// 相対カメラの位置ベクトル = ゲームオブジェクトの位置ベクトル + offset
transform.position = target.position + offset;

として相対カメラの位置ベクトルtransform.positionを計算しています。


また、カメラ移動のスクリプトをLateUpdate()内に書きました。

これは画面のカクつきを防ぐためです。

両者ともUpdate()内に書いてしまうと、カメラ移動がゲームオブジェクト移動に先走ってしまい、
画面が変にカクついてしまう恐れがあります。

そのため、ゲームオブジェクト移動のスクリプトはUpdate()内に書くことを
想定して、
カメラ移動のスクリプトはLateUpdate()内に書きました。

補足(カメラをゲームオブジェクトの子オブジェクトにする)

カメラをゲームオブジェクトの子オブジェクトにする方法もありますが、これは以下のようなメリットとデメリットがあります。

メリット:楽ちん
デメリット:ゲームオブジェクトの回転にも連動してしまう

例えば、ゲームオブジェクトが回転しながら移動するボールの場合、カメラもグルグルと回転してしまうわけです。
もしこれでも問題がないのなら、この方法を採用してもいいかもしれません。

参考文献

プレイヤーに追従するカメラ(カクつかない方法、滑らかに追従する方法) - ゆーじのUnity開発日記
【Unity C#】カメラの自動追従 | フタバゼミ

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

IAM認証のAWS API GatewayにEC2インスタンスからSigV4署名してアクセスするには

IAM認証を使っているAWSのAPI Gatewayは、APIリクエスト時にSigV4署名が必要です。

以前に同様の記事を書きましたが、EC2のインスタンスプロファイルからIAMロールにスイッチしてからリクエスト署名する処理になっていました。

スイッチせずにインスタンスプロファイルで直接署名すればいいことがわかりましたので、そのコードをここに残しておきます。

前提

IAMロールのアタッチされているEC2インスタンスでC#のコードを実行します。 ~/.aws/config は不要です。

※前回の記事では、EC2にIAMロールがアタッチされているだけではなく、インスタンスプロファイルからIAMロールにスイッチする権限が必要でした。このような権限が必要なケースが前回の記事のコード以外の場面であるのかよくわからず、おそらく前回の記事はミスリードでした。

API GatewayのリソースポリシーにはこのIAMロールからのAPIアクセスを許可してあるものとします。

動作確認した環境はUbuntu 20.04です。

C#の環境は以下の通り。

$ dotnet --version
3.1.404

本記事でのライブラリ等は2020/12/21時点のものです。

サンプルコードダウンロード

SigV4署名するC#のサンプルコードはAWS公式サイトにありますので、それをダウンロードし、必要なディレクトリのみ残します。

この手順の詳細は前々回の記事を参照。

$ mkdir sample
$ cd sample
$ mkdir tmp
$ cd tmp
$ wget https://docs.aws.amazon.com/AmazonS3/latest/API/samples/AmazonS3SigV4_Samples_CSharp.zip
$ unzip AmazonS3SigV4_Samples_CSharp.zip
$ cd ..
$ mv tmp/AWSSignatureV4-S3-Sample/Signers ./
$ mv tmp/AWSSignatureV4-S3-Sample/Util ./
$ rm -r tmp
$ grep -rl AWSSignatureV4_S3_Sample Signers | xargs sed -i 's/AWSSignatureV4_S3_Sample/Sample/g'
$ grep -rl AWSSignatureV4_S3_Sample Util | xargs sed -i 's/AWSSignatureV4_S3_Sample/Sample/g'

C#のプロジェクト作成

dotnetコマンドでプロジェクトを作成します。

$ dotnet new console

以下のようなディレクトリ構成になります。

$ tree
.
├── obj
│   ├── project.assets.json
│   ├── project.nuget.cache
│   ├── sample.csproj.nuget.dgspec.json
│   ├── sample.csproj.nuget.g.props
│   └── sample.csproj.nuget.g.targets
├── Program.cs
├── sample.csproj
├── Signers
│   ├── AWS4SignerBase.cs
│   ├── AWS4SignerForAuthorizationHeader.cs
│   ├── AWS4SignerForChunkedUpload.cs
│   ├── AWS4SignerForPOST.cs
│   └── AWS4SignerForQueryParameterAuth.cs
└── Util
    └── HttpHelpers.cs

3 directories, 13 files

sample.csprojに以下のように RootNamespace の項目を追加します。サンプルダウンロード後に全置換したnamespaceを指定します。

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <RootNamespace>Sample</RootNamespace>
  </PropertyGroup>

</Project>

必要なパッケージをダウンロードします。

$ dotnet add package AWSSDK.SecurityToken

C#のソースコード

Program.cs は以下です。

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

using Amazon.Runtime;
using Amazon.Runtime.CredentialManagement;
using Amazon.SecurityToken;
using Amazon.SecurityToken.Model;

using Sample.Signers;
using Sample.Util;

namespace Sample
{
    class Program
    {
        private static async Task Run()
        {
            InstanceProfileAWSCredentials instanceCredentials = new InstanceProfileAWSCredentials();
            var credentials = await instanceCredentials.GetCredentialsAsync();

            var uri = new Uri("https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/Prod/hello");

            // 署名するためのソースとなるヘッダ情報
            var headers = new Dictionary<string, string>
            {
                {AWS4SignerBase.X_Amz_Content_SHA256, AWS4SignerBase.EMPTY_BODY_SHA256},
                {"content-type", "text/plain"},
                {"x-amz-security-token", credentials.Token}, // IAMロールではこれが必要
            };

            // 署名を作成
            var signer = new AWS4SignerForAuthorizationHeader
            {
                EndpointUri = uri,
                HttpMethod = "GET",
                Service = "execute-api",
                Region = "ap-northeast-1"
            };
            var authorization = signer.ComputeSignature(headers,
                                                        "",   // no query parameters
                                                        AWS4SignerBase.EMPTY_BODY_SHA256,
                                                        credentials.AccessKey,
                                                        credentials.SecretKey);

            // リクエストヘッダに署名を追加
            headers.Add("Authorization", authorization);

            // リクエスト実行
            // HttpHelpers はUtilで定義
            HttpHelpers.InvokeHttpRequest(uri, "GET", headers, null);
        }

        static void Main(string[] args)
        {
            Run().Wait();
        }
    }
}

uriはAPI GatewayのAPIのURLを入れます。

実行

以下のコマンドで実行できます。

$ dotnet run

ダウンロードしたサンプルコードのSignersUtilにデバッグ用出力があるので、いろいろ表示されますが、最後にAPI Gatewayからのレスポンスが表示されます。

前回の記事との違い

前回InstanceProfileAWSCredentials からassumeRoleしていたのが、今回は InstanceProfileAWSCredentials をそのまま使っている点です。

diffを見たほうが早いか。

@@ -16,21 +16,8 @@
     {
         private static async Task Run()
         {
-            // ~/.aws/credentials からRoleArnを読み取る
-            SharedCredentialsFile sharedFile = new SharedCredentialsFile();
-            sharedFile.TryGetProfile("default", out CredentialProfile credentialProfile);
-            string roleArn = credentialProfile.Options.RoleArn;
-
-            // IAMロールにassumeする
             InstanceProfileAWSCredentials instanceCredentials = new InstanceProfileAWSCredentials();
-            AmazonSecurityTokenServiceClient stsClient = new AmazonSecurityTokenServiceClient(instanceCredentials);
-            AssumeRoleRequest assumeRoleRequest = new AssumeRoleRequest
-            {
-                RoleArn = roleArn,
-                RoleSessionName = "test_session",
-            };
-            var assumeRoleResponse = await stsClient.AssumeRoleAsync(assumeRoleRequest);
-            var credentials = assumeRoleResponse.Credentials;
+            var credentials = await instanceCredentials.GetCredentialsAsync();

             var uri = new Uri("https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/Prod/hello");

@@ -39,7 +26,7 @@
             {
                 {AWS4SignerBase.X_Amz_Content_SHA256, AWS4SignerBase.EMPTY_BODY_SHA256},
                 {"content-type", "text/plain"},
-                {"x-amz-security-token", credentials.SessionToken}, // IAMロールではこれが必要
+                {"x-amz-security-token", credentials.Token}, // IAMロールではこれが必要
             };

             // 署名を作成
@@ -53,8 +40,8 @@
             var authorization = signer.ComputeSignature(headers,
                                                         "",   // no query parameters
                                                         AWS4SignerBase.EMPTY_BODY_SHA256,
-                                                        credentials.AccessKeyId,
-                                                        credentials.SecretAccessKey);
+                                                        credentials.AccessKey,
+                                                        credentials.SecretKey);

             // リクエストヘッダに署名を追加
             headers.Add("Authorization", authorization);

関連記事

SigV4署名に関する私の記事

最近API Gatewayの記事ばかり続いています。

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

作り続けるために大事なこと

この内容は 個人アプリ/サービス開発の進め方と運用、得た学び 【PR】 Lenovo Advent Calendar 2020の23日目の記事です。

自分のサービス紹介

僕は趣味で3年半程前からマンガログサービスマンガ読んだ!!を作り続けています。作り始めたきっかけは息子が1歳の誕生日を迎えたことで、それまではほぼ全部の時間を息子に使っていたので、少しずつでも自分の時間を取って何か作れないかなと思って始めました。このサービスは公開してからも既に2年半以上経っていますが、ユーザー数もPVもかなり少なくとてもメジャーと言えるサービスではありません。

作り続けているという価値

多分Qittaでバズりやすいのは短期間で多くのPVやユーザーを獲得した話だと思います。もちろん僕もそういうのは憧れるし有益な情報だと思います。長く続けてかつ流行っていないサービスの話など誰も興味ないかもしれません。それでも3年半以上、殆ど毎日家で趣味でコツコツと作り続けているのは多少なりとも価値があると思っています。今回、作り続けることについて記事(ポエム?)を書きました。

自分の好きなものを作る

僕が考える、作り続けるために大事なことは、自分の好きなものを作ることです。僕はマンガが好きです。それはもう本当に誰よりも好きです。子供の時からおこづかいの殆どはマンガに使っており自宅にはマンガが2000冊以上あったし、高校生の時から今日現在に至るまで1日1時間ぐらいはコンビニで立ち読みするのが日課だし、結婚するまでは週末はほぼマンガ喫茶に行っていたし、今でも毎日マンガアプリを読みまわっているし、大切なことは全てマンガから教わりました。マンガログサービスを作っている理由の一つは、将来的には色んなマンガ読んだランキングを作って、僕が全ての1位を独占したいというのもあります。自分の好きなものそして欲しいもの作るのは作り続けるためにはとても大事です。

自分の好きな道具で作る

次に作り続けるために大事なことは、自分の好きな道具を使うことです。毎日作り続けるのであれば、毎日作るための道具をさわることになります。僕にとって作るための道具はC#です。僕は兎に角C#が好きです。初めてC#をさわった時に一目惚れをして、それ以来ずっとC#を使っています。C#で人生が変わったと言っても大げさではなくて、C#がきっかけで勉強会を開催することになって、C#がきっかけでMicrosoftMVPに選ばれて、C#がきっかけで書籍を執筆することになって、C#がきっかけで専門学校の講師になることになって、C#がきっかけで転職することになりました。ここ10年以上自分の人生の節目には必ずC#が出てきます。C#でプログラムを書くのがただただ楽しいです。毎日使っている道具が好きな道具であることも作り続けるためにはとても大事です。

辞めないためには鈍感力も必要

好きなものを、好きな道具で作っても、作ることを辞めてしまうことはあると思います。例えば他人からの批判です。もちろん「このサービス最悪だな」というような直接的な批判は僕も経験ありません。そうではなくて、自分で批判と思ってしまうようなケースです。ようやく出来た自分のサービスを大々的にSNSで発表して、そこで一切レスがつかなかったりするだけでも、辞めてしまいたい気持ちになることはあると思います。また、他人の成功体験でメンタルが削れることもあります。つまり他の人は3ヶ月で圧倒的な成果を出しているのに、自分は1年かけてもそれ以下の成果しか出していないという事実を突きつけられるとやはり凹みます。極端に言えば悪意のないアドバイスですらそれが刃として刺さることもあると思います。冷静になって考えれば、書いたことは全て被害妄想ですが、他人の評価や成功は気にしないと思える能力、ある種の鈍感力は作り続けるためには意外と大事だと思います。

作り続けるための明確な目標をもつ

さて、作り続けることでやることは増えていきます。実際僕も最初の目標である、サービスを公開してからの方がやりたいことはずっと増えました。ある程度作り続けた時に、さらに続けるために大事なことは明確な目標を持つことです。僕も作り出した時は考えていなかった大きな目標を決めました。現在僕のサービスには20万冊以上のマンガのページがあります。これはマンガサービスの中でも多い方だと思います。しかし世の中にはまだまだ沢山のマンガがあります。例えばマンガ出版社であっても、その会社が出したマンガの全情報は載っていません。出版社にも依りますが、絶版したマンガはHPからも外してしまうケースが殆どです。紀伊国屋書店やAmazonなどは絶版になったマンガも乗っていますが、それでも全部ではありません。全てのマンガを載せているサイトは現状どこにもないと思います。僕の目標は「世の中の全てのマンガの情報が載っているサービスを目指す」です。

作り続けることで得られるものは沢山ある

使う人が少なくても、世の中で話題になっていなくても作り続けることで得られるものは沢山ありました。僕はこのサービスを作るまでWebの実務経験がほぼありませんでしたが、今ではクライアント、サーバ、クラウドまで一通りのことは分かります。Qittaで軽くバズったこともあります。最近ではモブプロでの定期ミーティングみたいなこともやっていてアドバイスをくれる友人も増えました。その内PVやユーザー数のアップにも注力して、またそういう記事も書ければと思います。作り続けていれば必ず目的を叶えられるとは言いませんが、少なくとも続けることは目的を叶えるための最も確実な方法だと思います。

まとめ

最後にここまで書いてきて何ですが、個人開発において作り続けることが一番大事なことではないと思います。大事なことは自由であることだと思います。いつ始めても、またいつ辞めても良い。作り続けても良い。その自由こそが個人開発において一番大事なことだと思います。だからこそ、もし作り続けるのであれば、出来れば自分の意思以外の要因で辞めない方が幸せだと思います。もし今回の記事が誰かにとって作り続ける一つの要因になったのであれば嬉しいなと思います。

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

VContainerを組み込んだゲームサンプル

PONOS Advent Calendar 2020 24日目の記事です。
昨日は@ackylaさんGoogleCloudShellのteachmeコマンドが便利でした。

はじめに

VContainerについての記事は以前に書いているので、そもそもVContainerってなんだろうと思った方はこちらを読んでみてください。VContainerとはどういうものなのかについて触れています。

サンプルについて

j4qf0-vk3x1.gif
今回はこのように表示されている文字を小文字から大文字へと変換するサンプルになっています。MonoBehaviourを継承した一つのクラスでも問題はないのですが、サンプルとしての有用性を高めるためにMVRPとして作成しています。またイベントのやりとりについてはUniRxを使用しています。

コード

GameSampleLifetimeScope

GameSampleLifetimeScope.cs
using VContainer;
using VContainer.Unity;
using UnityEngine;

namespace GameSample
{
    public class GameSampleLifetimeScope : LifetimeScope
    {
        [SerializeField] View view;
        [SerializeField] MessageData data;

        protected override void Configure(IContainerBuilder builder)
        {
            builder.RegisterComponent<IView>(view);
            builder.Register<ToUpperModel>(Lifetime.Scoped).WithParameter<MessageData>(data).As<IModel>();
            builder.RegisterEntryPoint<Presenter>(Lifetime.Scoped);
        }
    }
}

LifetimeScopeはこのようになっています。
ViewとModelに関してはインターフェースとして登録しています。
また、PresenterについてはEntryPointに登録をしています。

ModelについてはWithParameterを使用してLifetimeScopeに登録されているMessageDataをコンストラクタの時に渡してあげるようにしています。

MVP

ToUpperModel.cs
using UniRx;

namespace GameSample
{
    public class ToUpperModel : IModel
    {
        ReactiveProperty<string> message = new ReactiveProperty<string>();
        public ReadOnlyReactiveProperty<string> Message => message.ToReadOnlyReactiveProperty();

        public ToUpperModel(MessageData msg)
        {
            message.Value = msg.Message;
        }

        public void Modify(string msg)
        {
            message.Value = msg.ToUpper();
        }
    }
}
View.cs
using UnityEngine;
using UnityEngine.UI;
using System;
using UniRx;

namespace GameSample
{
    public class View : MonoBehaviour, IView
    {
        [SerializeField] Text text;
        [SerializeField] Button modify;

        Subject<string> modifySubject = new Subject<string>();
        public IObservable<string> OnClickModify => modifySubject;

        private void Start()
        {
            modify.OnClickAsObservable().Subscribe(_ => modifySubject.OnNext(text.text)).AddTo(this);
        }

        public void RefreshMessage(string msg)
        {
            text.text = msg;
        }
    }
}
Presenter.cs
using VContainer.Unity;
using System;
using UniRx;

namespace GameSample
{
    public interface IView
    {
        IObservable<string> OnClickModify { get; }
        void RefreshMessage(string msg);
    }

    public interface IModel
    {
        ReadOnlyReactiveProperty<string> Message { get; }
        void Modify(string msg);
    }

    public class Presenter: IDisposable, IInitializable
    {
        CompositeDisposable disposables;

        readonly IView view;
        readonly IModel model;

        public Presenter(IView view, IModel model)
        {
            this.view = view;
            this.model = model;
            disposables = new CompositeDisposable();
        }

        public void Initialize()
        {
            view.OnClickModify.Subscribe(model.Modify).AddTo(disposables);
            model.Message.Subscribe(view.RefreshMessage).AddTo(disposables);
        }

        public void Dispose()
        {
            disposables.Dispose();
        }
    }
}

Model、View、Presenterについてはこのようになっています。
ModelのコンストラクタにMessageDataが引数で渡されていますが、こちらは先ほど述べたようにLifetimeScopeにて登録されていたデータが渡されます。このコンストラクタは依存解決時に自動的に呼び出されます。

今回の肝になっているのはPresenterになっています。
PresenterもModel同様にコンストラクタは依存解決時に自動的に呼び出されてIViewIModelが引数に渡されます。Presenterが呼び出されている箇所はもちろんMVRPなのでありませんが、PresenterにIInitializableを設定し、EntryPointに登録することによって自動的に呼び出され、その時に依存解決されるのです。これにより紐づる式にModelも生成されます。
またIDisposableも継承していますが、こちらもインスタンス破棄のタイミングで自動的に呼ばれる仕組みになっています。
DIの使い方としてインターフェースを渡すことにより、修正コストを少なくして別の処理に置き換えることができます。
ToUpperModelをToLowerModelという小文字に変換するModelへと置き換えた場合の修正コストは以下になります。

ToLowerModel

ToLowerModel.cs
using UniRx;

namespace GameSample
{
    public class ToLowerModel : IModel
    {
        ReactiveProperty<string> message = new ReactiveProperty<string>();
        public ReadOnlyReactiveProperty<string> Message => message.ToReadOnlyReactiveProperty();

        public ToLowerModel(MessageData msg)
        {
            message.Value = msg.Message;
        }

        public void Modify(string msg)
        {
            message.Value = msg.ToLower();
        }
    }
}

既存コードの修正箇所

GameSampleLifetimeScope.cs(修正箇所のみ)
builder.Register<ToLowerModel>(Lifetime.Scoped).WithParameter<MessageData>(data).As<IModel>();

デバッグ用の処理と、本番用の処理の切替が楽にすみますね!
DIの利点はこの修正コストの少なさだと私は思っています。

MessageData

MessageData.cs
using UnityEngine;

[CreateAssetMenu(menuName = "Create/Create Message")]
public class MessageData : ScriptableObject
{
    public string Message;
}

渡しているMessageDataの中身はこのようになっています。

EntryPoint

先ほどのPresenterの登録についてEntryPointで行いました。
EntryPointとはPlayerLoopおよびMonoBehaviourで自動的に行われる処理と同じようなタイミングで走る処理の総称と思って間違いなと思っています。
詳しくはこちらをご確認いただければと思います。

まとめ

前回も書きましたがZenjectとコードの書き方が似ているので移行するにしてもそこまで難しくないかなと考えています。
まだまだプロジェクトへの導入はありませんがこれから増えていって欲しいなと思います。そしてどんどん記事も増えて情報も増えていって欲しいですね。
最後に公式のサイトがオープンしたらしいのでこちらで色々と使い方をみてみてください。

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

Xamarin Community Toolkit カタログ(MVVM編)

この記事は、Xamarin Advent Calendar 2020の22日目の記事です。

はじめに

以下の記事の続きです。

ソースコード
XamarinCommunityToolkitCatalog

MVVM

MVVMのビューモデルで使うような機能です。

AsyncCommand

Taskの非同期処理を扱えるコマンドです。allowsMultipleExecutionsfalseにすれば、Taskの実行中は、CanExecutefalseを返すようになり、ボタンの2度押しを防げます。デフォルトではtrueです。onExceptionで例外発生時の処理を、continueOnCapturedContextで、Task実行時に設定するConfigureAwaitの引数を指定できます。

private IAsyncCommand _asyncCommand;
public IAsyncCommand AsyncCommand => _asyncCommand ??=
    new AsyncCommand(async () =>
    {
        await Task.Delay(1000);
    }, allowsMultipleExecutions: false);

AsyncValueCommand

ValueTaskの非同期処理を扱えるコマンドです。機能的には、AsyncCommandと同じです。

private IAsyncValueCommand<bool> _asyncValueCommand;
public IAsyncValueCommand<bool> AsyncValueCommand => _asyncValueCommand ??=
    new AsyncValueCommand<bool>(async shouldDelay =>
    {
        if (shouldDelay)
        {
            await Task.Delay(1000);
        }
    }, allowsMultipleExecutions: false);

ObservableObject

INotifyPropertyChangedを実装したクラスです。SetPropertyPropertyChangedの発火を行えます。PropertyChangedのハンドラーは、WeakReferenceで管理されます。

public class ObservableObjectPageViewModel : ObservableObject
{
    private int _count;
    public int Count
    {
        get => _count;
        set => SetProperty(ref _count, value);
    }

    private ICommand _countCommand;
    public ICommand CountCommand => _countCommand ??=
        new DelegateCommand(() => Count++);
}

ObservableRangeCollection

複数データの追加、削除、置き換えができるObservableCollectionです。

public class ObservableRangeCollectionPageViewModel
{
    public ObservableRangeCollection<int> Items { get; } =
        new ObservableRangeCollection<int>();

    private ICommand _addRangeCommand;
    public ICommand AddRangeCommand => _addRangeCommand ??=
        new DelegateCommand(() =>
        {
            Items.AddRange(Enumerable.Range(Items.Count, 5));
        });

    private ICommand _removeRangeCommand;
    public ICommand RemoveRangeCommand => _removeRangeCommand ??=
        new DelegateCommand(() =>
        {
            Items.RemoveRange(Items.TakeLast(5));
        });

    private ICommand _replaceRangeCommand;
    public ICommand ReplaceRangeCommand => _replaceRangeCommand ??=
        new DelegateCommand(() =>
        {
            Items.ReplaceRange(Enumerable.Range(0, 5));
        });
}

おわりに

とりあえずこれで最後です。

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