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

マウスをポチれよ、さすれば与えられん

進捗や技術情報などを外部公開しながら開発を進める「公開型開発」という取り組みがあります。


 
この開発の技術情報をリンク情報システム株式会社の「2021新春アドベントカレンダー TechConnect!」のリレー記事として、紹介したいと思います。
「TechConnect!」は勝手に始めるアドベントカレンダーとして、勝手に作った engineer.hanzomon というグループによって記事をリレーしていくイベントになります。

マウスを操作すれば教えてくれる

  • 仕事が早い人はPC操作も早いことが多いですよね。
  • そんな方はキーワードショートカットを多用していており、マウス操作は必要最小限で済ましています。
  • そんなショートカットを調べるのは億劫ですよね。
  • いつものマウス操作にショートカットがあるのを教えてもらえたら便利ですよね。

そんな悩みを救うアプリの技術情報です。
image.png

どうやってマウス操作を判別するか

ショートカットをレコメンドするためには、どのようなマウス操作が行われたかを判別する必要があります。
現在はExcelのみを対象にしてますが、Excel以外のアプリにも対応したいため、汎用的な処理方式が求められます。
つまり、操作イベントを得るために対象アプリのAPIを利用することや、対象アプリに処理を組み込むような方式は最終手段にする必要があります。

①機械学習

クリックされた周辺画像を機械学習の画像分類で判別を行う案です。
この方式は、精度が保証し辛いことと、判別したい操作ごとに多くの学習データを作成するのが大変という問題があります。
特に精度の面は重要で、このアプリにおいては少々筋が悪いです。

②OCR

OCR方式は、クリックされた周辺画像を切り抜き、その画像をOCRにかけて文字列を抽出、判別します。
この方式は、セル、シートのコンテキストメニューの「挿入(I)...」において、OCRで同一の文字列である「挿入(I)...」が得られるため、セルの挿入なのか、シートの挿入なのか、判別できないという問題がありました。
また、コンテキストメニューの選択肢などの文字列が表示されているものには対応できますが、リボン内のアイコンクリックに対応するのは困難です。

③テンプレートマッチング

テンプレートマッチングは、対象画像を走査してテンプレート画像に一致する部分を見つける手法です。
つまり、画面キャプチャで取得したコンテキストメニューの画像と、事前に用意した行、列、セル、シートのコンテキストメニュー画像をマッチングすることで、マウス操作の判別が出来ます。
OCR方式では困難であったアイコンクリックも判別可能です。

テンプレートマッチングはパラダイスの予感がします。

テンプレートマッチングの実装

OpenCVSharpを利用したテンプレートマッチングの例です。

  • 素の画像データ同士でマッチングを行った場合、アイコンクリックの判定精度が低かったです。
  • 二値化を行うことで精度を高めることが出来ました。
  • 識別具合を視覚的に確認し易くするため、マッチング箇所を赤枠で表示します。
  • このプログラムでは、入力を画像ファイルとしていますが、実際のアプリではクリック周辺のBitmapなどを入力として扱います。
OpeJudger.cs
using System;
using System.Collections.Generic;
using System.IO;
using OpenCvSharp;

namespace MyApp
{
    /// <summary>
    /// マウス操作の判定クラス
    /// </summary>
    class OpeJudger
    {
        private const double MatchThreshold = 0.9;

        /// <summary>
        /// どのような操作を行った時の画像か判別する
        /// </summary>
        /// <param name="imgFilePath">判別したい画像ファイルのパス</param>
        /// <returns>マッチしたテンプレファイル名</returns>
        public string Exec(string imgFilePath)
        {
            var targetMat = new Mat(imgFilePath);

            // テンプレート画像で回してマッチするものを探す
            string[] templateFiles = Directory.GetFiles(@".\MatchTemplate", "*", SearchOption.TopDirectoryOnly);
            foreach (var fileName in templateFiles)
            {
                // マッチング
                var templateMat = new Mat(fileName);
                var match = this.Matching(targetMat, templateMat, out var maxPoint);
                // マッチなし
                if (match < 0)
                {
                    continue;
                }

                Console.WriteLine($"マッチしたテンプレ画像={fileName}");

                // 確認用の画像表示
                // マッチした箇所を赤で囲む
                targetMat.Rectangle(maxPoint, new Point(maxPoint.X + templateMat.Width, maxPoint.Y + templateMat.Height), Scalar.Red, 2, LineTypes.AntiAlias, 0);
                // マッチ度を画面上部に表示
                targetMat.Rectangle(new Point(0, 0), new Point(800, 60), Scalar.White, -1, LineTypes.AntiAlias, 0);
                Cv2.PutText(targetMat, match.ToString("0.##"), new Point(0, 50), HersheyFonts.HersheyPlain, 2, Scalar.Black, 1, LineTypes.AntiAlias);
                Cv2.ImShow("確認用", targetMat);
                Cv2.WaitKey(0);

                return fileName;
            }

            return "NoMatch";
        }

        /// <summary>
        /// テンプレートマッチングを実行する
        /// </summary>
        /// <param name="targetMat">探索対象とする画像</param>
        /// <param name="templateMat">テンプレート画像</param>
        /// <param name="matchPoint">探索対象画像内に存在するテンプレート画像の位置</param>
        /// <returns>マッチ度。マッチ無しの場合は負数を返す</returns>
        private double Matching(Mat targetMat, Mat templateMat, out Point matchPoint)
        {
            // 探索画像を二値化
            var targetBinMat = new Mat();
            Cv2.CvtColor(targetMat, targetBinMat, ColorConversionCodes.BGR2GRAY);
            Cv2.Threshold(targetBinMat, targetBinMat, 128, 255, ThresholdTypes.Binary);

            // テンプレ画像を二値化
            var templateBinMat = new Mat();
            Cv2.CvtColor(templateMat, templateBinMat, ColorConversionCodes.BGR2GRAY);
            Cv2.Threshold(templateBinMat, templateBinMat, 128, 255, ThresholdTypes.Binary);

            // マッチング
            var resultMat = new Mat();
            Cv2.MatchTemplate(targetBinMat, templateBinMat, resultMat, TemplateMatchModes.CCoeffNormed);

            // 一番マッチした箇所のマッチ具合(0~1)と、その位置を取得する(画像内でマッチした左上座標)
            Cv2.MinMaxLoc(resultMat, out _, out var maxVal, out _, out matchPoint);
            if (maxVal < MatchThreshold)
            {
                return -1.0;
            }

            // 閾値超えのマッチ箇所を強調させておく
            var binMat = new Mat();
            Cv2.Threshold(resultMat, binMat, MatchThreshold, 1.0, ThresholdTypes.Binary);

            return maxVal;
        }
    }
}

以下の形で利用します。

呼び出し側
var opeJudger = new OpeJudger();
var result = opeJudger.Exec(キャプチャした画像);
用意するテンプレート画像

以下の画像を MatchTemplate フォルダに格納します。
コンテキストメニューには「列の幅」を入れるなど、当該コンテキストメニューの特徴となる部分もテンプレ画像含めることがポイントになります。
コンテキストメニューだけでなく、アイコンクリックも判別出来ようにSave、Boldアイコンも用意しました。

image.png

実行結果

マウスクリックされた周辺画像を入力したところ、良好な結果を得ることができました。
きちんとテンプレート画像に合致する箇所が識別され、赤枠で囲まれています。


明日のリレー記事は @o-chang です!


リンク情報システム株式会社では一緒に働く仲間を随時募集しています!
また、お仕事のご依頼、ビジネスパートナー様も募集しております。お気軽にご連絡ください。
Facebook:https://ja-jp.facebook.com/lis.co.jp/
Twitter:https://twitter.com/liscojp/
公開型開発「PRODUCTICA」公式:https://twitter.com/Productica_lis
 
 

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

オブジェクトのテクスチャを送信して、画像として保存する

Unityでホワイトボードのようなもの作ってた時に使ってた。

大まかな流れ

1.対象のGameObjectを取得。
2.保存したいTextureを取得。
3.取得したTextureをバイト配列に変換。
4.サーバ上のphpに送信する。
5.phpで受信したバイト配列を画像に戻して保存する。
→受信側のphpは以前書いた記事そのまんまなので以下参照
pythonで画像を送信、phpで保存する
以上。

全体のソースコード

SendPic.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using System;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using UnityEngine.Networking;

public class SavePic : MonoBehaviour
{
    Texture2D texture;
    byte[] picData;

    public void SendPic()
    {
    //1.対象のGameObjectを取得。
    //Find()だったり、FindGameObjectWithTagだったり、インスペクターから直接指定したりなどなど
        GameObject _wb = GameObject.FindGameObjectWithTag("Whiteboard");

    //2.保存したいTextureをGameObjectから取得し、
    //3.取得したTextureをバイト配列に変換。
        texture = (Texture2D)_wb.GetComponent<Renderer>().material.mainTexture;

    //SendDataを呼び出す(コルーチンというらしい)
        StartCoroutine(SendData(picData));
    }
    IEnumerator SendData(byte[] postData)
    {
    //4.サーバ上のphpに送信する
        String url = "https://hagehoge/pic_save.php";//httpにandroid端末から送る場合はパーミッション関連で何か必要だったような・・・
        var request = new UnityWebRequest(url, "POST");
        request.uploadHandler = (UploadHandler)new UploadHandlerRaw(postData);
        request.downloadHandler = (DownloadHandler)new DownloadHandlerBuffer();
        request.SetRequestHeader("Content-Type", "image/png");

        yield return request.Send();
    }
}

実際やるとどんな感じなの

お絵かきして送信する。
image.png

WinSCP(FTPツール)で見てみるとこんな感じ。ちょっと細くなってるのは落書きに使ったPlaneは1.5倍に引き伸ばされてたため。
image.png

参考

・Photon Unity Networking 2 (PUN2) のRPCとRaiseEventを使ってテクスチャデータを送信する
https://nabla-tech-lab.hatenablog.com/entry/2019/05/15/180000

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

C# でゲームパッドの入力を処理しよう!

概要・背景

本記事では Vortice.XInput ライブラリを使ったゲームパッドの入力処理について解説します。

素の C# アプリケーションが対象となります。Unity 等のゲーム開発用フレームワーク上で動くプログラムの場合は、フレームワーク独自のやり方が用意されています。それらは本記事の対象ではないという意味です。

私自身のゲームパッド入力処理実装経験は今回が初めてです。
個人アプリでの利用経験を元に本記事を書いています。.NET Framework 4.8 のアプリとして実装した経験となります。

一般的なやり方から外れている部分があるよといったご指摘等もお待ちしております。

Vortice.XInput とは?

Vortice.Windows という DirectX 系 API の .NET 上での利用をサポートしてくれるライブラリの一部です。
同じ役割をしていた SharpDX というライブラリが有名だったようなのですが開発が滞り 2019 年に凍結された模様です。

Vortice.Windows はその後継として作られているライブラリです。SharpDX では使っていなかった新しい技術によって作られているみたいです。

Vortice.Windows のサイトには Unsafe や SharpGen.Runtime などの最新の技術を使っていると書いてあります。
これらの技術はアンマネージドコードをうまく使うためのものだと思います。(主にメンテナンスの面で)SharpDX よりスマートな実装になっていると想像できますね。

凍結されたとはいえ SharpDX をまだ使うという方法もありそうですが、個人アプリ開発目的ですので無茶ができますし、初のゲームパッド入力処理実装ということもありどうせゼロから覚えるので新しい Vortice.XInput の利用を試みることにしました。

使った感想は「めちゃめちゃ簡単だった」です。

Vortice.XInput でできることって?

以下のようなことができます。

  • ゲームパッドの機能取得(XInput.GetCapabilities)
  • ゲームパッドのバッテリー情報の取得(XInput.GetBatteryInformation)
  • ゲームパッドの状態の取得(XInput.GetState)
  • ゲームパッドのキーストロークの取得(XInput.GetKeystroke)
  • ゲームパッドのバイブレーション制御(XInput.SetVibration)

本記事では XInput.GetState と XInput.GetKeystroke について解説します。使ったのがこの2つだからです。

解説

共通事項

Vortice.XInput を使うには?

NuGet で Vortice.XInput をプロジェクトに追加してください。

ライブラリの名前空間は「Vortice.XInput」です。利用するプログラムには以下の一文を入れましょう。

using Vortice.XInput;

機能は Vortice.XInput.XInput クラスに集約されているようです。
static クラスですのでインスタンス化の必要もなく、直接メソッドを呼び出して使います。
機能を使うための事前処理は何もなく、いきなり呼び出して大丈夫です。

userIndex について

XInput の全てのメソッドは userIndex を受け取ります。userIndex はゲームパッドのインデックスで、0 ~ 3 の値を指定します。

私の環境では作り始めの際はとりあえず 0 を指定して動いたのでしばらくはそれで開発を進め、のちに XInput.GetState を使った userIndex の自動認識&選択UIの実装を行いました。

userIndex の自動認識については XInput.GetState にて解説します。

System.Timers.Timer と組み合わせて使う

ゲームパッドの入力を非同期で取得して使うために System.Timers.Timer を使います。
以下は 10 ミリ秒置きに処理する例です。

using System;
using System.Timers;

namespace ExampleApp
{
    public class GamepadTimer
    {
        private Timer Timer { get; set; }
        public int Interval { get; set; } = 10;  // 10 ミリ秒

        protected object Lock { get; } = new object();

        public void Start()
        {
            lock (Lock)
            {
                Stop();

                Timer = new Timer(Interval);
                Timer.Elapsed += Elapsed;
                Timer.Start();
            }
        }

        public void Stop()
        {
            lock (Lock)
            {
                if (Timer?.Enabled == true)
                {
                    Timer?.Stop();
                    Timer = null;
                }
            }
        }

        private void Elapsed(object sender, EventArgs e)
        {
            if (Monitor.TryEnter(Lock))
            {
                try
                {
                    // ここにゲームパッドの処理を書く
                }
                finally
                {
                    Monitor.Exit(Lock);
                }
            }
        }
    }
}

共通事項は以上になります。

ここからが本題となる具体的な機能の話です。

ゲームパッドの状態の取得(XInput.GetState)

XInput.GetState で現在のゲームパッドの状態を取得できます。

利用例は以下の通りです。

if (XInput.GetState(0, out var keystate))
{
  if (0 < (keystate.Gamepad.Buttons & GamepadButtons.A))
  {
    // A ボタンを押していたら
  }
}

XInput.GetState の第一引数は userIndex です。

以上の例は「0 のゲームパッドが有効かつ A ボタンを押していたら」という条件を実現する例です。

XInput.GetState メソッドは呼び出した時点で指定した userIndex のゲームパッドが有効なら true、無効なら false を返すという特性を持っており、有効ならその時点のボタンやスティックの状態を取得できます。

有効状態を返すという特性を利用して、ゲームパッドの自動認識に使えます。例えば以下のようなコードです。

public class GamepadDetector
    public int? GamepadIndex { get; set; }

    public State? GetKeystate()
    {
        // 認識済みの場合、認識済みのゲームパッドを使う
        if (GamepadIndex != null)
        {
            if (XInput.GetState(GamepadIndex.Value, out var keystate))
                return keystate;
            else
                // 認識済みのゲームパッドが無効になったとみなす
                GamepadIndex = null;
        }
        else
            // 未認識の場合、0 ~ 3 の順で有効なゲームパッドを探す
            for (var i = 0; i < 4; ++i)
                if (XInput.GetState(i, out var keystate))
                {
                    GamepadIndex = i;
                    return keystate;
                }

        return null;
    }
}

GamepadDetector.GamepadIndex に UI で選択した値を入れることもできます。

XInput.GetState はその時点の正確な状態を取得できます。
アクション/シューティングゲームの自機操作のような連続的な処理に向きます。
一方でロールプレイングゲームでの選択肢の変更のような断続的な処理には向きません。
その場合に使えるのが次に紹介する XInput.GetKeystroke です。

ゲームパッドのキーストロークの取得(XInput.GetKeystroke)

XInput.GetKeystroke でゲームパッドの「ボタンを押した」「ボタンを離した」というキーストロークイベントを取得できます。

利用例は以下の通りです。

if (XInput.GetKeystroke(0, out var keystroke))
{
  if (keystroke.Flags == KeyStrokeFlags.KeyDown && keystroke.VirtualKey == GamepadVirtualKey.A)
  {
    // A ボタンを押したら
  }
}

「ボタンを押した」を表す KeystrokeFlags.KeyDown と対になる「ボタンを離した」を表す KeystrokeFlags.KeyUp もあります。

また、ボタンを押しっぱなしにした場合を表す KeystrokeFlags.Repeat というものもあります。
KeystrokeFlags.Repeat は最初の KeystrokeFlags.KeyDown から少し間を開けてから始まり、そこから断続的な入力として受け取れます。
メモ帳とかでキーボードのボタンを押しっぱなしたときの入力と同じ感じです。

利用例は以下の通りです。

if (XInput.GetKeystroke(0, out var keystroke))
{
  if (0 < (keystroke.Flags & KeyStrokeFlags.Repeat) && keystroke.VirtualKey == GamepadVirtualKey.A)
  {
    // A ボタンを押しっぱなしにしたら
  }
}

また、スティック操作も上下左右斜めの8方向のキー操作として受け取れるというのも特徴的です。

XInput.GetState と XInput.GetKeystroke を組み合わせたボタン同時押し判断の応用例

XInput.GetState と XInput.GetKeystroke を組み合わせてボタン同時押しの判断も簡単に作れました。

XInput.GetKeystroke のイベントをトリガーに XInput.GetState で同時押しを判断するといった感じです。

例は以下の通りです。

if (XInput.GetKeystroke(0, out var keystroke) && XInput.GetKeystroke(0, out var keystroke))
{
    if (keystroke.Flags == KeyStrokeFlags.KeyDown && keystroke.VirtualKey == GamepadVirtualKey.A
        && 0 < (keystate.Gamepad.Buttons & GamepadButtons.RightShoulder))
    {
        // RB を押しながら A ボタンを押したら
    }
}

まとめ

以上、Vortice.XInput を使ったゲームパッドの入力処理の方法について解説しました。

Vortice.XInput でゲームパッドを扱うプログラムがめちゃめちゃ簡単に作れます。

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

Unity 子オブジェクトのチェック(非アクティブ含む)・Component削除

全Prefabの子オブジェクトからTextMeshProを含むものを探し出す

// Resources内からPrefabを全検索
var allPrefabs = AssetDatabase.FindAssets("t:Prefab", new string[] { "Assets/Resources" });

foreach (var guid in allPrefabs)
{
    var path = AssetDatabase.GUIDToAssetPath(guid);

    GameObject currentObject = AssetDatabase.LoadAssetAtPath<UnityEngine.GameObject>(path);

    // trueを入れると非アクティブも含めて全て取得
    var textMeshProUGUIArray = currentObject.GetComponentsInChildren<TextMeshProUGUI>(true);

    if (textMeshProUGUIArray.Length > 0)
    {
            // ※0より大きければデータ存在する。
    }
}



指定したComponentが親オブジェクトにアタッチされている場合、削除する。

// Resources内からPrefabを全検索
var allPrefabs = AssetDatabase.FindAssets("t:Prefab", new string[] { "Assets/Resources" });


foreach (var guid in allPrefabs)
{
    var path = AssetDatabase.GUIDToAssetPath(guid);

    GameObject currentObject = AssetDatabase.LoadAssetAtPath<GameObject>(path);

    // アタッチされているかをチェック
    if (currentObject.GetComponent<アタッチ済みクラス名>() != null)
    {
        // Trueを入れると削除可能になる。
        DestroyImmediate(currentObject.GetComponent<アタッチ済みクラス名>(),true);
    }
}

// 変更を保存する
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();


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

DataGridでAutoGenerateColumnsが有効なままで、フォーマットをカスタムする

概要

DataGridでデフォルトのフォーマット以外にしたいことがあります。しかしフォーマットを変えるためだけに、AutoGenerateColumnsを無効にして全ての列を自分で定義するのは面倒です。
1つの解決方法はDataGridのAutoGeneratingColumnイベントでフォーマットを指定することです。
しかしできればコードビハインドを書きたくないし、複数のDataGridで流用したい、そういう時は添付プロパティが使えます。

image.png

添付プロパティ

DateTime用のDateTimeFormatAutoGenerateTimeSpan用のTimeSpanFormatAutoGenerateの2つのフォーマットを指定できる添付プロパティが定義されています。

class DataGridOperation
{
    public static string GetDateTimeFormatAutoGenerate(DependencyObject obj) => (string)obj.GetValue(DateTimeFormatAutoGenerateProperty);
    public static void SetDateTimeFormatAutoGenerate(DependencyObject obj, string value) => obj.SetValue(DateTimeFormatAutoGenerateProperty, value);
    public static readonly DependencyProperty DateTimeFormatAutoGenerateProperty =
        DependencyProperty.RegisterAttached("DateTimeFormatAutoGenerate", typeof(string), typeof(DataGridOperation),
            new PropertyMetadata(null, (d, e) => AddEventHandlerOnGenerating<DateTime>(d, e)));

    public static string GetTimeSpanFormatAutoGenerate(DependencyObject obj) => (string)obj.GetValue(TimeSpanFormatAutoGenerateProperty);
    public static void SetTimeSpanFormatAutoGenerate(DependencyObject obj, string value) => obj.SetValue(TimeSpanFormatAutoGenerateProperty, value);
    public static readonly DependencyProperty TimeSpanFormatAutoGenerateProperty =
        DependencyProperty.RegisterAttached("TimeSpanFormatAutoGenerate", typeof(string), typeof(DataGridOperation),
            new PropertyMetadata(null, (d, e) => AddEventHandlerOnGenerating<TimeSpan>(d, e)));

    private static void AddEventHandlerOnGenerating<T>(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (!(d is DataGrid dGrid))
            return;

        if ((e.NewValue is string format))
            dGrid.AutoGeneratingColumn += (o, e) => AddFormat_OnGenerating<T>(e, format);
    }

    private static void AddFormat_OnGenerating<T>(DataGridAutoGeneratingColumnEventArgs e, string format)
    {
        if (e.PropertyType == typeof(T))
            (e.Column as DataGridTextColumn).Binding.StringFormat = format;
    }
}

使用方法

XAML上でフォーマットを指定します。
DataGridOperation.DateTimeFormatAutoGenerate="yy年MM月dd日"

コードビハインドは使用していません。

MainWindow.xaml
<Window
   x:Class="DataGridAutogenerateCustom.MainWindow"
   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   xmlns:local="clr-namespace:DataGridAutogenerateCustom"
   Width="400"
   Height="250">
   <Window.DataContext>
      <local:MainWindowViewModel />
   </Window.DataContext>
   <StackPanel>
      <TextBlock Text="DEFAULT FORMAT" />
      <DataGrid ItemsSource="{Binding Dates}" />
      <TextBlock Margin="0,30,0,0" Text="CUSTOM FORMAT" />
      <DataGrid
         local:DataGridOperation.DateTimeFormatAutoGenerate="yy年MM月dd日"
         local:DataGridOperation.TimeSpanFormatAutoGenerate="dd\日hh\時mm\分ss\秒"
         ItemsSource="{Binding Dates}" />
   </StackPanel>
</Window>

ViewModelは以下のようになります。

MainWindowViewModel.cs
public class MainWindowViewModel
{
    public DatePairs[] Dates { get; } = new DatePairs[]
    {
        new (){StartDate= new (2011,1,1), EndDate= new (2011,2,1) },
        new (){StartDate= new (2020,1,1), EndDate= new (2021,1,1) },
    };
}

public class DatePairs
{
    public DateTime StartDate { get; set; }
    public DateTime EndDate { get; set; }
    public TimeSpan Span => EndDate - StartDate;
}

環境

VisualStudio 2019 Version 16.8.4
.NET 5
C#9

参考

https://stackoverflow.com/questions/7978249/date-formatting-in-wpf-datagrid

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

【Unity(C#)】お互いに正面で顔を合わせているのを内積(Vector3.Dot)で判定を行う

この記事は、こちらの記事を参考にお互いが正面向き合ってる状態で、判定する内容になります。
内積、Vector3.Dot、normarizedについての説明は、参考記事内にあるため省略します。
【Unity(C#)】Rayではなく内積(Vector3.Dot)で視線判定を行う

記述が増えてしまう為、お互いの頭のゲームオブジェクトは今回Inspecterからアタッチしてます。
概ね頭のある場所は、キャラクターオブジェクト>Root>Hips>Spine_1>Spine_2>Neck>Headと言った感じの階層です。
取り敢えず、どんなもんか気になる人向けに先にコードを出します。

コード

LookMatch.cs
using UnityEngine;
public class LookMatch : MonoBehaviour
{
    [SerializeField]private GameObject playerHead; //プレイヤーの頭
    [SerializeField]private GameObject targetHead; //対象となる相手の頭
    [SerializeField]private float distance;        //判定に必要な距離
    // Start is called before the first frame update
    void Start()
    {

    }
    // Update is called once per frame
    void Update()
    {
        //互いの距離が一定距離内だったら判定する。
        float mag = (targetHead.transform.position - playerHead.transform.position).magnitude;
        if (distance >= mag)
        {
            //ターゲットからこちらの方向へ正規化したベクトルを作成
            Vector3 targetToCharaDirection = (playerHead.transform.position - targetHead.transform.position).normalized;
            if (Vector3.Dot(targetToCharaDirection, playerHead.transform.forward.normalized) < -0.9      //正規化したベクトルの内積がプレイヤーの頭から一定以下
                && Vector3.Dot(targetToCharaDirection, targetHead.transform.forward.normalized) > 0.9    //正規化したベクトルの内積が相手の頭から一定以上 
                && targetHead.transform.InverseTransformPoint(playerHead.transform.position).z >= 0)    //対象にとって正面側にいるかどうか
            { 
                //顔が向き合っている時の処理内容を記述
            }else{
                //顔が向き合ってない時の処理内容を記述
            }

        }else{
            //距離が離れている時の処理を記述
        }
    }
}

デモ ~顔が合うと?マークを表示する~

顔が合う.gif
・一定距離にいて
・顔も向き合っている

以上の状態であると?マークが表示されるようになります。

InverseTransformPoint

Transform.InverseTransformPoint
ワールド空間からローカル空間へ position を変換します。

これにより、プレイヤーの頭が対象の頭にとってローカル空間にpositionが変換され
前方側(z >= 0 )にいる状態になります。
Vector3.Dot(targetToCharaDirection, targetHead.transform.forward.normalized) > 0.9  //正規化したベクトルの内積が相手の頭から一定以上
によりあまりいらない気もします。

magnitude

Vector3.magnitude
ベクトルの長さ(読み取り専用)
ベクトルの (x x+y y+z* z) の平方根の長さを返します。

今回の使い方としては、対象の頭の位置とプレイヤーの頭の位置を差し引いた時の距離の値が
distanceに設定した値より小さければ距離の判定を満たす形になっています。
そうしないと、数百m離れても判定する事に・・・

顔が向き合っていれば胴はどうでもいい

顔.PNG

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

Unityを使い始めてからのメモ(随時更新)

[SerializeField]って?

  • privateメンバをInspectorに表示させたい時に変数の前につける
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWSのDescribeAvailablePatchesを使ってみた

【執筆中】コードとかきれいにしてないけど現時点のメモ書きを載せておく
とりあえずセキュリティのパッチを取得してみる

        public static async Task<string> SayHelloAsync([ActivityTrigger] string name, ILogger log)
        {
            //log.LogInformation($"Saying hello to {name}.");
            var ssmClient = AwsSsmService.GetClient();


            // Windows Server 2019
            var request = new DescribeAvailablePatchesRequest();
            var productFilter = new PatchOrchestratorFilter();
            productFilter.Key = "PRODUCT";
            productFilter.Values.Add("WindowsServer2019");
            request.Filters.Add(productFilter);

            var classificationFilter = new PatchOrchestratorFilter();
            classificationFilter.Key = "CLASSIFICATION";
            classificationFilter.Values.Add("SecurityUpdates");

            request.Filters.Add(classificationFilter);

            try
            {
                var response = await ssmClient.DescribeAvailablePatchesAsync(request);
                string nextToken = null;
                var patches = new List<Patch>();
                int productCount = 0;
                int repositoryCount = 0;
                int savedProductCount = 0;
                int savedRepositoryCount = 0;

                if (response.HttpStatusCode == HttpStatusCode.OK)
                {
                    do
                    {
                        Thread.Sleep(500);

                        nextToken = response.NextToken;
                        patches.AddRange(response.Patches);

                        var platformList = patches.Select(r => r.Product).Distinct();
                        productCount = platformList.Count();

                        var repositoryList = patches.Select(r => r.Repository).Distinct();
                        repositoryCount = repositoryList.Count();

                    } while (nextToken != null);
                }
                else
                {
                    // Error
                    var code = response.HttpStatusCode;
                }

                // 2021/01のパッチだけ取得
                var date = new DateTime(2021, 1, 1, 0, 0, 0, 0);

                var currentPatches = patches.Where(r => r.ReleaseDate > date).ToList();

                var temp = string.Empty;

                return $"Hello {name}!";
            }
            catch (Exception ex)
            {   
                var message = ex.Message;
                throw;
            }

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