- 投稿日:2019-12-22T23:59:26+09:00
クソゲーづくりで学んだこと ~ミスしてもいいから動いて学ぼう~
はじめに
この記事はNitKit コンピュータ研究部 Advent Calendar 2019 22日目の記事です。
Cpawの記事に隠れてひたすらこの記事を書いてました。
今回は私がゲームを始めて作ったときのメイキング・軌跡みたいなものを思い出しながら、時々コードやスクショも載せながらゆるく解説していきます。それと共に私がこのゲーム作りで学んだことを少し書いてみたいと思います。よろしくお願いいたします。早速ですが
私はゲームが本当に好きです。今現在も就活をしているくらいにはゲーム企業での就業を夢みて日々ESを出して
は落とされています。
でも、実際に学校生活の中でゲーム開発を行ってきてはおらず、いざ就活を始めるときにふとこう思いました。「ゲーム作ったことなくったって学校のプログラミング経験でどうにかなるもんなのかな?」
これはまごうことなく偽です。プログラミング学習で学べるのはあくまでプログラミングのやり方であり、ゲームの作り方ではないからです。
この至極まっとうなことに気づくのがあまりにも遅すぎたのは本当に今でも悔やんでいるのですがこの事実に気づいた私はその時こう思いました。「よし、それじゃあ実際に簡単なゲームを作って会社側に意欲を見せてみよう!」
そこから私の初めてのゲーム作りが始まりました。
ずぶの素人のゲーム作り
~その1:仕様書づくり~
実はこの時までにDXlibやSiv3D、Xenkoなどで色々とゲーム作りにトライしていたことはあるんです。
あるんですが、その当時使っていたパソコンが本当にクソスペックだったので、何をどうしても何も動かない状態が続き、気づけばやる気を失っていました。
「じゃあコマンドライン上で動くものを作ればいいじゃない!」と普通のゲーム開発者の方々は至極当然のように思うはずですが、この時の私は3Dゲーム、フルグラフィックこそがゲームたるものだ、と信じて疑わず、コマンドライン上で動くゲーム開発には見向きもしませんでした。(この時の私のカスみたいな判断にも死ぬほど後悔してます、ゲームだったら何でも作ってみればよかったのに...)まあそんなこんなで、しょうもないながらも何回か挫折を味わっていた私は「あてずっぽうで作るより、きちんと企画書・仕様書から作成して作るほうが挫折しにくいのでは?」と思い、実際に三時間くらい使って仕様書を完成させ、その仕様書の通りに作ろうと決心しました。
これは後から見ても成功でした。大枠での進捗管理が容易になったのもそうですが、自分が何をすべきなのかを明確化できたところも後々響いてきます。
結果、今回作るゲームのコンセプト・期間・としては以下のようになりました。(仕様書があったはずなんですが捨ててました)コンセプト...一分間でやる鬼ごっこ
製作期間...一ヶ月
使用ツール...Unity~その2:Unityの操作方法を覚える~
さてとにかく自分が何をすべきか、どれくらいの期間で作るかは決まりました。
ただこの時点ではUnityの使い方を何も知らない状況です。技術選定だけして後は野となれ山となれ方式ではいずれ計画は破綻します。
ということでこのチュートリアルをやりました。Unityは偉大。でもチュートリアルに何十日もかける余裕はないので、とりあえずこのチュートリアルは3日で終わらせました。
はじめてのUnity~その3:ゲームの大枠を作ろうとするも想像以上にコードが書けない~
さて、なんとかUnityのチュートリアルは終了したので、さっそくゲーム制作に取り掛かりました。
ゲームに必要な要素として、
- 3Dゲームである
- 一分間でゲームを終了させる
- 鬼はフィールドのランダム座標に一定の間隔でポップする
- フィールドの外に落ちてもゲームを終了させる
- 鬼はプレイヤーの座標を毎秒取得し、追いかける
大体こんな感じにリストアップしていたので、あとはこれに沿ってプロジェクトを進めていけばよいのですが。
しかし、私はここで拗らせてしまいます。「どうせならできる限り自分で作りたい!」
まだUnityのチュートリアルを終えたばかりのひよっこがそんなことを思うもんですから、速攻でプロジェクトが頓挫しかけました。
どのように作ればいいのかという経験・知識もない状態の人間にはそこまで自作で作りこむことなんてできません。できるわけがありません。その時の私はうんうん唸った結果、こうすることにしました。「出来合いのモデルは嫌だし自分でモデル作るか、、、」
~その4:もっともらしい理由を付けてモデリングに逃げた~
一見良さそうなこの考えも、期間が決まっているプロジェクトに扱ったことのないソフトをもう一つ扱うことが確定するモデリングの要素を導入するという点ではまごうことなき愚策です。しかし一度決まったことを曲げるのも自分の成長につながらないのでは、と思い自分なりに様々なサイトを閲覧しまくることによってblenderを使って大体二週間かけてモデルを完成させました。
足っぽいのをミラーで生成して、
何となく人っぽい形を作りました。でも手があまりリアリティがなかったので、最後にUV展開をしてテクスチャを作成しました。ただこれに関してはホントにガバガバなのでクソみたいなテクスチャしか作れませんでした。
そのテクスチャを作ったモデルに貼り付けて、なんとかUnity上にインポートすることができました。万歳。
ちなみにモデルの頂点数とかはこんな感じです。ほんとに初心者のモデリングだったので、パラメータは軒並みクソです。
モデリングは自分が思っている以上に意外と楽しかったです。ただ、
手の細かい成型ができなかったため、手がデスフェイサーみたいになってしまったり、モデルがこんな感じの形になってからしばらく戻せなかったり、
変形を間違えて恐ろしい形になった挙句この状態で保存をかけてしまい泣く泣く作り直す羽目になってしまったりとかなり時間をかけてはしまいました。しばらくモデリングはやりたくないです。~その5:さあ動かすぞと思ったもののやっぱりコードが書けないので、他力本願で行こう!~
まあ何とかモデルは完成しましたが、モデルの作成にここまでで二週間もかけてしまっていました。残りの一週間前後でやるのは残ったコードの実装です。さすがにモデル自作の時に思い知ったのか、ここらへんで私はスクリプト全て自作することを諦め、ほかの人のコードをガンガン流用することに決めました。でもほかの方が作ったソースを読みとって、どういう意味かを理解する、自分の環境に落とし込む、そんな作業をすると決めた時点でどうなるかは明白でした。
~その6:技術力不足で毎日徹夜のデスマーチ突入~
はい、その通りです。
「デスマーチ」の時間です。
学校から帰ってきたらすぐにパソコンの前に座ってうんうん苦しみながらほかの人のソースを読む。そのソースを自分のプロジェクトの環境に落とし込む。デバッグも忘れないでやる。
自分でアルゴリズムを考えて実装するというよりほかの人の考えをうまく自分と同じ領域に移しているだけであったため、かなり無為でつらい作業でした。でも時間がないとできない作業でしたので、生活リズムが毎日三時寝は当たり前みたいな、完全にリリース前のIT企業勤めのサラリーマンになってしまいました。そんな感じで必死こいて作ったソースがこれらです。ほんとにリファクタリングのこととか何一つ考えていないため、正直クソコードの羅列にしかなりませんが、皆様の反面教師となるのなら本望ということで、
後悔公開します。
作成したコード集
UI上に残り時間を表示するコードです。
TimerController.csusing System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using UnityEngine.SceneManagement; public class TimerController : MonoBehaviour { // GameObject TimerTex; public Text timerText; public float totalTime; float seconds; // Use this for initialization IEnumerator Start() { enabled = false; yield return new WaitForSeconds(3); //三秒待ってUpdate()を有効化 enabled = true; } // Update is called once per frame void Update() { totalTime -= Time.deltaTime; seconds = totalTime; timerText.text = "残り時間:" + seconds.ToString(); if(totalTime <= 0) { SceneManager.LoadScene("GameClear"); } } }カウントダウン表示を行うソースです。
CowntDown.csusing System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class CountDown : MonoBehaviour { Text text; void Start() { text = GameObject.Find("CountDown").GetComponent<Text>(); StartCoroutine(Count()); } IEnumerator Count() { yield return new WaitForSeconds(1f); text.text = ("2"); yield return new WaitForSeconds(1f); text.text = ("1"); yield return new WaitForSeconds(1f); text.text = ("Start!"); yield return new WaitForSeconds(1.0f); text.gameObject.SetActive(false); } }フィールドのランダム座標に敵を出現させるコードです。
Pop.csusing System.Collections; using System.Collections.Generic; using UnityEngine; public class Pop : MonoBehaviour { // 出現させる敵を入れておく [SerializeField] GameObject Enemy; // 次に敵が出現するまでの時間 [SerializeField] float appearNextTime; // この場所から出現する敵の数 [SerializeField] int maxNumOfEnemys; // 今何人の敵を出現させたか private int numberOfEnemys; // 待ち時間計測フィールド private float elapsedTime; IEnumerator Start() { numberOfEnemys = 0; elapsedTime = 0f; enabled = false; yield return new WaitForSeconds(3); enabled = true; } void Update () { // この場所から出現する最大数を超えてたら何もしない if (numberOfEnemys >= maxNumOfEnemys) { return; } // 経過時間を足す elapsedTime += Time.deltaTime; // 経過時間が経ったら if (elapsedTime > appearNextTime) { elapsedTime = 0f; AppearEnemy(); } Resources.UnloadUnusedAssets(); } // 敵出現メソッド void AppearEnemy() { float x = Random.Range(10f, 450f); float y = 116; float z = Random.Range(10f, 450f); Vector3 position = new Vector3(x,y, z); Instantiate(Enemy, new Vector3(x,y,z),Quaternion.identity); numberOfEnemys++; elapsedTime = 0f; } }敵の挙動について記述したコードです。NavMeshAgentを用いて簡単にプレイヤー追跡を行っています。
Enemy.csusing UnityEngine; using UnityEngine.AI; using System.Collections; using UnityEngine.SceneManagement; public class Enemy : MonoBehaviour { [SerializeField] public GameObject Player; private NavMeshAgent navAgent = null; IEnumerator Start() { GetComponent<NavMeshAgent>().enabled = true; Player = GameObject.Find("Player"); navAgent = GetComponent<NavMeshAgent>(); enabled = false; yield return new WaitForSeconds(3); //三秒待ってUpdate()を有効化 enabled = true; } private void Update() { navAgent.destination = Player.transform.position;//navMeshAgentの操作 } }ゲームオーバー時の挙動を記述したものです。
GameOverSceneChanger.csusing System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.SceneManagement; public class GameOverSceneChanger : MonoBehaviour { GameObject player; private ParticleSystem particle; //Exploder Exploder; void Start() { particle = GetComponent<ParticleSystem>(); //Exploder = player.GetComponent<Exploder>(); } // Use this for initialization IEnumerator OnControllerColliderHit (ControllerColliderHit other) { Debug.Log("Hit"); if (other.gameObject.CompareTag("Enemy")) { yield return new WaitForSeconds(0.3f); SceneManager.LoadScene("GameOver"); } if (other.gameObject.CompareTag("DeathZone")) { yield return new WaitForSeconds(0.01f); SceneManager.LoadScene("GameOver"); } } }プレイヤーの挙動について記述したコードです。このコードが一番他力本願です。当時は本当にどんなふうに実装したらいいかわからず、ほとんどがコピペみたいなソースになっています。
PlayerController.csusing UnityEngine; using System.Collections; public class PlayerController : MonoBehaviour { private CharacterController charaCon; private Vector3 moveDirection = Vector3.zero; public float MoveSpeed = 5.0f; public float RotateSpeed = 3.0F; public float RollSpeed = 1200.0f; public float gravity = 20.0F; public float jumpPower = 6.0F; IEnumerator Start() { charaCon = GetComponent<CharacterController>(); animCon = GetComponent<Animator>(); enabled = false; yield return new WaitForSeconds(3); enabled = true; } void LateUpdate() { var cameraForward = Vector3.Scale(Camera.main.transform.forward, new Vector3(1, 0, 1)).normalized; Vector3 direction = cameraForward * Input.GetAxis("Vertical") + Camera.main.transform.right * Input.GetAxis("Horizontal"); charaCon.Move(moveDirection * Time.deltaTime); if (Input.GetAxis("Vertical") == 0 && Input.GetAxis("Horizontal") == 0) { animCon.SetBool("Running", false); } else { Rotate(direction); animCon.SetBool("Running", true); } if (charaCon.isGrounded) { animCon.SetBool("Jumping", Input.GetKey(KeyCode.Space)); moveDirection.y = 0f; moveDirection = direction * MoveSpeed; if (Input.GetKey(KeyCode.Space) ) { moveDirection.y = jumpPower; } else { moveDirection.y -= gravity * Time.deltaTime; } } else { moveDirection.y -= gravity * Time.deltaTime; } } void Rotate(Vector3 Hope_Rotate) { Quaternion q = Quaternion.LookRotation(Hope_Rotate); transform.rotation = Quaternion.RotateTowards(transform.rotation, q, RollSpeed * Time.deltaTime); } }そんな感じでコードを書いて、あとはこれを適応させるだけだったんですがまだいろいろと作業が終わってなかったのを思い出し、コードの詳細な適応と共に以下の作業を行いました。
インポートしたモデルにSAColliderBuilderというアセットを適応したり、そういう作業を必死に行ったり、残りの数日でUIの部分を作成したり、実際にテストプレイしながらストレスのたまらないようなゲーム性を追求し、
何とか完成しました。今見るとボタンの所とか本当にUnity臭がすごいですね。
デモプレイの様子はこちらです。完成したゲームのリンクはこちらです。クソゲーですが、もし気になったら触ってみてほしいです。(Windowsのみ)
1 Minutes Tag!!! ver1.01 ~for Windows - Google ドライブ当時を振り返って・自分が得たもの
その当時はわからなかったんですが、結果的にサウンドの実装を全くしないまま終わってしまっていました。この時はとにかく動くものを作りあげる、ってところしか頭になかったため、今では臨場感に欠けるものができてしまったと反省しています。
対して、得たものですが、これは単純に自信がついたのと、様々な機会が増えたことです。
一ヶ月という期間で自分なりの考えをアウトプットできた、というやりがいは本当に何物にも代えられないものでしたし、拙くてもそれらを遂行することができた自分の実行力に自信が付きました。
また、逆求人のイベントに行く際の明確な提示材料ができ、様々な企業の方とお話しする機会をいただいたり、自分が幼少期熱中していたゲームを製作していた会社の方とお話しする機会をいただいたり、最終選考の機会をいただいたりと、本当に様々なものを得ることができました。やっぱり自分から行動するのは大事です。最後に
今この記事を読んでるゲームを開発しようか迷ってる皆さん。
「今すぐゲームを作りましょう」
拙くてもいいです。クソゲーでもいいです。いくらくだらなくてもいいです。作りましょう
「ゲームのアイデアはあるのに迷ってる?迷うくらいなら作りましょう」
私みたいにこんな
恥さらしゲームを作って平然としてる人間だっています。違いは行動するかしないかです。ゲーム作りのノウハウは学校で本当に学べますか?
学べませんよね?確かに基礎的なことは様々な部分で応用ができます。企画、計算、マネージメントなどには様々な教科で学んだことが活きたりします。そういう観点からすると学校の学習も本当に有意義な
はずのものです。
ですが、ゲーム作りのノウハウはゲーム専門学校でもない限り実際に自分で作ってみないことにはつかないのです。ゲーム作りが上手くなりたければゲームを作るしかないのです。そこを行動しないままにして丸く収まっているのは非常にもったいないです。いつかはビッグタイトルを作ってみたい!
と思っているならなおのこと自分で行動し、それを様々な場所にアウトプットしましょう。早いうちに恥をかいたり、失敗することこそが将来の大成功にきっとつながります。重ね重ね言いますが、今現在様々なシーンで活躍されているゲーム開発者とあなたの違いは
「目的のために行動しているか」
これ一つのみです。全力で取り組むのであれば、私もそうですが、あなたの周りの方々も、他のゲーム開発者の方々もきっと全力で応援します。するはずです。ゲームを作れるようになりたければ
とにかく自分で、自分の意思で行動しましょう
自分で簡単なゲームを最後まで作り上げること、それこそがあなたのゲーム作りの第一歩です。
ここまでの長文を読んで下さり、誠にありがとうございます。
これからのゲーム作り人生を、皆様がより有意義に過ごせますように。
- 投稿日:2019-12-22T23:51:26+09:00
[C#,Windows]他プロセスにマネージドなコードを実行させる
はじめに
Windowsでは古来よりDLLインジェクションと呼ばれる手法で他プロセスに任意のコードを実行させることができます。
DLLインジェクションはその名の通りDLLを他プロセスに読み込ませてDLLのエントリーポイント(
DllMain
)を実行させるという仕組みです。
仕組み上ネイティブのDLLを用意する必要があり、残念なことにマネージドなDLLを使用することはできません。マネージドなDLLを使用することができれば以下のようなメリットがあります。
- C#でコードが書ける
- (C#でコードが書けるので)WinFormsやWPFといったフレームワークを使用可能
- x86/x64のどちらのプロセスにもインジェクトできる
全てをC#で書きたい人には圧倒的なメリットですね
今回はどうにかしてマネージドなDLLをインジェクトする方法について解説します。
解説する手法を実装したコードは以下のリポジトリにあります。
yaegaki/Mogu最終的に以下のようなことができるようになります。
// インジェクトする側(ホスト)のメインメソッド static async Task Main(string[] args) { // メモ帳のプロセスIDを取得する var pid = (uint)Process.GetProcessesByName("notepad").First().Id; var injector = new Injector(); // メモ帳に自身のDLLをインジェクトし、DLL内のEntoryPoint関数を実行させる using (var con = await injector.InjectAsync(pid, c => EntryPoint(c))) { var buffer = new byte[1024]; while (con.IsConnected) { // メモ帳にインジェクトしたマネージドコードからの返答を待つ var count = await con.Pipe.ReadAsync(buffer, 0, buffer.Length, CancellationToken.None); var str = Encoding.UTF8.GetString(buffer, 0, count); Console.WriteLine($"recv:{str}"); } } } // インジェクトされた側で実行されるメソッド public static async ValueTask EntryPoint(Connection con) { var text = "Hello from notepad.exe!"; var buf = Encoding.UTF8.GetBytes(text); // 引数で渡されたConnectionを使用してホストと通信する。 await con.Pipe.WriteAsync(buf, 0, buf.Length); }方針
先に述べた通り通常の方法ではマネージドコードを他プロセスに実行させることができません。
そこで今回は他プロセスに.NET Coreをホストさせるコードを実行させてその.NET CoreにマネージドDLLを読み込ませるという手法をとります。参考: カスタム .NET Core ランタイム ホストを作成する - .NET Core | Microsoft Docs
解説
インジェクトする側(ホスト)/マネージド
コード: Mogu/Injector.cs
通常のDLLインジェクションと同様に
WriteProcessMemory
でDLLのパスを書き込みCreateRemoteThread
DLLをロードさせます。1
注意が必要な点として相手プロセスが32ビットか64ビットかでLoadLibarary
のアドレスが異なるということです。
ホストプロセスと同じビット数のプロセスにインジェクトする場合は特に気にする必要はなく、ホストプロセスでLoadLibary
のアドレスを取得すればそれを使用することができます。
違う場合はめんどくさいのでここを参考にしてください。
簡単に解説すると既にメモリ上に読み込まれたPEイメージから対象の関数のアドレスを取得しています。ホスト側はDLLをインジェクトするだけではなくインジェクトしたDLLに対象プロセス上で実行するマネージドメソッドを伝える必要があります。
様々な方法が考えられますが今回はメモリーマップドファイルを使用します。
メモリーマップドファイルに必要な情報を書き込み、インジェクトされた側はその情報を読み込みます。// 対象プロセスのPIDを含んだ名前のメモリーマップドファイルを作成。 // インジェクトされた側は自身のPIDを使用してこのメモリーマップドファイルを開く。 using (var sharedMemory = MemoryMappedFile.CreateNew(GetMemoryMappedFileName(pid), memorySize)) using (var accessor = sharedMemory.CreateViewAccessor()) { int position = 0; // アセンブリの位置、実行するメソッドが定義されている型、実行するメソッドの名前、通信用の名前付きパイプの名前を書き込む。 accessor.Write(position, assemblyLocation, out position); accessor.Write(position, typeName, out position); accessor.Write(position, methodName, out position); accessor.Write(position, pipeName, out position); // 書き終わってからインジェクトする。 // ..略.. }インジェクトするDLL/アンマネージド
コード: MoguHost/dllmain.cpp
.NET CoreをホストするアンマネージドなDLLです。
このDLLはアンマネージなものなので32ビット版と64ビット版を用意する必要があります。
アンマネージドのコードはあまり書きたくないのでここでは.NET CoreをホストしマネージドDLLのメソッドを実行までを担当します。
メモリーマップドファイルの内容を読み込んで指定されたメソッドを実行などは全てマネージド側で行います。公式のドキュメントを参考にコードを書きます。
coreclr_delegates.h
とhostfxr.h
は以下から取得できます。
nethost.dll
は以下の場所にあります。$(NetCoreTargetingPackRoot)/Microsoft.NETCore.App.Host.$(NETCoreSdkRuntimeIdentifier)/$(BundledNETCoreAppPackageVersion)/runtimes/$(NETCoreSdkRuntimeIdentifier)/native参考までに自分の環境では以下の場所です。
C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Host.win-x64\3.1.0\runtimes\win-x64\native実行するマネージドDLLはアンマネージドDLLと同じパスに固定の名前で配置されているという前提でコードを書きます。
例えばアンマネージドDLLがC:\XX\MoguHost_x64.dll
においてあればアンマネージドDLLはC:\XX\Mogu.dll
という風にします。
これによって簡単にロードすべきマネージドDLLのパスを取得することができます。MoguHost/dllmain.cpp#L113-L116
// 自身(アンマネージドDLL)のパスを取得 GetModuleFileNameW(hModule, path_buffer, max_path); string_t mogu_native_dll_path = path_buffer; // パスからディレクトリを取得 const auto mogu_directory = get_directory_path(mogu_native_dll_path); // ディレクトリに固定の文字列を加えることでマネージドDLLのパスとする const auto mogu_managed_dll_path = mogu_directory + L"\\Mogu.dll";.NET Coreがホストできれば次はマネージドなコードを実行します。
MoguHost/dllmain.cpp#L191-L199
MoguHost/dllmain.cpp#L63-L64// 既定の型のメソッド(Mogu.Injector.InjectedEntryPoint)を取得 entrypoint_fn entrypoint; const auto type_name = L"Mogu.Injector, Mogu"; const auto method_name = L"InjectedEntryPoint"; if (load_assembly_and_get_function_pointer(mogu_managed_dll_path.c_str(), type_name, method_name, nullptr, nullptr, (void**)&entrypoint) != 0) { FreeLibraryAndExitThread(hModule, -4); return nullptr; } // メソッドを実行 entrypoint(nullptr, 0);これでアンマネージドDLL側のコードの主要部分は終了です。
注意すべき点として既に.NET Coreがmuxerモードで実行されている場合はどうやってもホストに失敗するので諦めましょう。
また二度以上同じプロセスでホストさせる場合は通常の方法ではできません。
最初にホストさせたときに取得したポインタをプロセスに残しておきましょう。
ポインタをプロセスに残すのは少し面倒です。
DLLのグローバル変数として確保している場合、DLLがアンロードされると消えてしまいます。
そこでポインタを保持するだけのDLLを作成し、そのDLLに情報を保持させておきます。MoguHost/dllmain.cpp#L129-L142
// ポインタをキャッシュしているDLLをロード const auto store_lib = LoadLibraryW(mogu_store_path.c_str()); if (store_lib == nullptr) { return nullptr; } const auto store = reinterpret_cast<void(*)(void*)>(GetProcAddress(store_lib, "Store")); const auto load = reinterpret_cast<void*(*)()>(GetProcAddress(store_lib, "Load")); // キャッシュされているポインタを取得 auto cached = load(); if (cached != nullptr) { FreeLibrary(store_lib); // 既にキャッシュされている場合はそのポインタを使用する。 return reinterpret_cast<entrypoint_fn>(cached); }インジェクトするDLL/マネージド
やることは単純で以下のことを実行します。
- メモリーマップドファイルを開いて実行すべきメソッドの情報を取得する。
- 名前付きパイプでホストとの通信経路を確保する。
- メソッドを実行する。
実際にコードを見ていただければわかると思いますが非常にシンプルです。
.NET CoreにはAppDomainが実質存在しないので少し注意が必要です。まとめ
C#でDLLインジェクションをしたい人なんていない需要は未知数ですが今回の内容を実装するにあたって.NET Coreについての知見が深まりました。
ソースコードを拾ってきて自分でビルドするというのはハードル高めに思っていましたが、.NET Coreの各種ツールは意外と簡単にビルドできて驚きました。
一度自分でやってみるとなかなか面白いのではないかと思います。
DLLインジェクションだけでは全く意味がないのでフックする処理もC#で書けるようにしたかったのですが、安定して動かず記事にするのは一旦諦めました。
残骸は以下に置いています。
グローバルフックを用いた手法のほうが安全ですが簡単にするためにこの手法を使いました ↩
- 投稿日:2019-12-22T22:27:47+09:00
OutsystemsでCSVの読み込みと出力をしてみた
はじめに
会社のITメンバーで参加している2019年のアドベントカレンダーに、自身2つ目の記事を投稿します!
自己紹介も兼ねて、前回書いた記事はこちらになります。
https://qiita.com/tom-k7/items/d8ef19dccb42891a0698今回もOutsystemsについて書きます。
今はちょうどOutsystemsでCSV変換ツール(Webアプリ)を開発しているので、
OutsystemsでCSVの読み込み・出力の実装方法、あとは開発中にぶち当たった壁について共有させていただきます。やりたいこと
今開発しているツールでやりたいこととしては、
いろんな他社さんのサイトからダウンロードしたCSVファイルを
社内で使ってるシステムにインポートできるフォーマットのCSVファイルに変換したい。
なので、ダウンロードしたCSVファイルを読み込んで、
ごにょごにょ変換して新たなCSVファイルを出力するということをOutsystemsで実現します。Forge
CSVファイルの読み込み・出力にはCSVUtilのForgeコンポーネントを使います。
https://www.outsystems.com/forge/component-overview/636/csvutilこちらをダウンロードし、ServiceStudioにインストールしてDependencyに追加してください。
画面の開発
CSV処理をやる前に画面を作っちゃいます。
CSVファイルのアップロード
まずは読み込む対象のCSVファイルをアップロードする必要があります。
方法は2つあります。
①Uploadを使う
②RichWidgets\Popup_Uploadを使う
今回は①のほうが良いということになったので、Uploadを使います。
※ちなみにUploadウィジェットは、ListRecordsのようなリストの中では動かないのでご注意ください。変換処理を行うサーバアクションを呼ぶためのボタンを配置
ConvertボタンのDestinationに変換処理を行うConvertサーバアクションを指定し、
そのサーバアクション内で変換処理を行います。
CSV読み込み処理の実装
さて、ここからは本題のCSVの処理です。
まずはCSVファイルの読み込みを実装します。LoadConfigの設定
CSV読み込み時の設定をするためのCSVLoadConfig型のローカル変数を定義し、Assignで設定します。
主な設定内容は以下の通り。
- Encode:CSVファイルの文字コード。utf-8やshift-jisなどを設定。
- IsSkipHeader:CSVファイルにヘッダー行がある場合はTrue、ない場合はFalse。
- IsIgnoreColumnChange:カラム数が合わない場合にエラーとする場合はFalse、エラーとしない場合はTrue。
- FieldDelimiter:カラムの間の区切り文字。複数文字入れられるが、先頭の1文字しか適用されない。
- IsDisableDoubleQuote:カラムがダブルクオーテーションで囲われてる場合はFalse、そうでない場合はTrue。
読み込み用RecordListを定義
CSVを読み込んだ結果を格納するEntity/StructureのRecordのリストを、ローカル変数で定義します。
今回は自作のSourceCSVというStructureのRecordListにしています。
※StructureのListでなく、StructureのRecordのListにしないとうまくいきません。Extension処理の呼出し
ForgeのCSVUtilで定義されているLoadCSVRecordListサーバアクションを呼び出し、
引数には画面で作ったUploadのContentと、CSVLoadConfigのローカル変数、
Entity/StructureのRecordListをToObject()で変換した結果をそれぞれ設定します。
これで読み込みは完了です。CSV出力処理の実装
続いて、CSV出力のほうも実装していきます。
ExportConfigの設定
CSV出力時の設定をするためのCSVExportConfig型のローカル変数を定義し、Assignで設定します。
主な設定内容は以下の通り。
- IsShowHeader:CSVファイルにヘッダー行を入れる場合はTrue、入れない場合はFalse。
- FieldDelimiter:カラムの間の区切り文字。複数文字入れられるが、先頭の1文字しか適用されない。
- EncodeMode:カラムをダブルクオーテーションで囲うか否か。auto/quote/noquote/noquote_nocheckのいずれかを文字列で設定。
- LineSeparator:改行コード。CRだとChr(13)、LFだとChr(10)、CRLFはChr(13) + Chr(10)と設定。
出力用RecordListを定義
出力するCSVのデータを格納するためのEntity/StructureのRecordのリストを、ローカル変数で定義します。
画像は自作のOutputCSVというStructureのRecordListにしています。
出力用RecordListに読み込んだデータを変換して設定
↑で定義した出力用RecordListに対して、Loadしたデータが入っているRecordListの全件を設定します。
その際に値を変換したり、結合したり、不要なものは捨てたりして出力用に設定すれば変換ができますね。
画像ではListAppendAllで単純に項目の結合だけしてますが、実際にはここでいろんな変換をします。
かなり複雑なことをやる場合はForEachでループするケースもあると思います。Extension処理の呼出し
CSVUtilで定義されているExportRecordList2CSVサーバアクションを呼び出し、
引数にはEntity/StructureのRecordListをToObject()で変換した結果とCSVExportConfigのローカル変数をそれぞれ設定します。
変換後CSVファイルの出力(ダウンロード)
最後にExportRecordList2CSVで作成されたCSVデータをDownloadウィジェットに渡して終了です。
開発中に引っかかった罠
以上が実装方法になりますが、その他CSVUtilを使っててぶち当たった壁について共有したいと思います。
出力時にダブルクオーテーションで囲う方法がわかりづらい
「ExportConfigの設定」でダブルクオーテーションで囲う方法はすでに記載済みですが、
そこにたどり着くまでに結構かかりました。
だって、その項目の名前EncodeModeて。。
文字コードのことかと思うやん。わからんて。。
ていう愚痴でしたwwちなみにCSVExportConfig.EncodeModeに"quote"を指定するとダブルクオーテーションで囲ってくれます!
Export処理でNullReferenceException
CSVUtilのExportRecordList2CSVサーバアクションを呼び出した際にこんなエラーが出ました。
Object reference not set to an instance of an object.
NullReferenceExceptionです。Javaでいうヌルポですね。
CSVの項目としては基本型しか使ってないので、Outsystemsで基本型でNullってあるんだっけ?って感じでした。それでこのエラーはこれ以上詳細にはログに出ないので、いろいろ試してもうまくいかず途方に暮れた結果、
Extensionの中見るしかないな!ってなりました。
でExtensionダウンロードして、VisualStudioでsln開いてソース追っても原因わからず。
じゃあってことで、実際に渡してたデータをC#上で作って実行してみました!(^^)!(これが地味にめんどかった)やってみた結果、渡したデータのカラムが1個足りてないせいでNullReferenceExceptionになっていたことが判明。
そのカラム何だろうと思ってたら、データ変換するときに特殊なことをしているカラムでした。説明が意味不明だったらすいません
例えば読み込んだデータの値が"1"なら、変換後は"大学院"とするみたいなものをJSONファイルにKeyとValueのペアとして定義し、
JSONファイルをResourcesとしてモジュールにインポート。
そのJSONファイルをJSONDeserializeで読み込み、ForgeのHashTableを使ってKeyとValueのペアとしてメモリに格納しておき、
変換時はそのHashTableからgetした値を設定するという処理にしていました。ForgeのHashable:https://www.outsystems.com/forge/component-overview/21/hashtable
それが、JSONで定義したKeyにはないデータ(↑のJSONだと"5"とか空文字とか)が読み込むCSVに入っていたため、getの結果がNullとなり、
それをExportRecordList2CSVに渡したためにNullReferenceExceptionとなったということでした。Outsystemsで普通に実装してたらText型にNullは入らないのですが、
Extensionを介すとNullが入るんだなって知り、勉強になったなと思いました。最後に
OutsystemsでのCSV読み込み・出力について説明しました。
これからCSVUtilを使う方、同じ壁にぶつかった方の一助になればと思っています。
- 投稿日:2019-12-22T21:46:40+09:00
exeへの引数処理
練習とメモ用
定期的に機能増やす予定機能
引数1つの場合のみ対応
コンソールとドロップで実行可能
絶対パスとファイル名、拡張子が取得可能
.lnkの時に参照元のパスを取得
""で引数を渡されなくても実行可注意点
Windows Script Host Object Modelを参照しなければならないProgram.csusing System; namespace FileGet { class Program { static void Main(string[] args) { GetFile getFile = new GetFile(args); Console.ReadKey(); } } }GetFile.csusing System; using System.Linq; namespace FileGet { class GetFile { private IGetFileProcess pro; public GetFile(string[] args) { string[] Path = Environment.GetCommandLineArgs(); if (args.Any()) { pro = new FileArgument(args); } else if (Path.Length > 1) { pro = new FileDrop(Path); } else { Console.WriteLine("引数とドロップはありませんでした。"); return; } Console.WriteLine("FilePath:{0}", pro.FilePath); Console.WriteLine("FileName:{0}", pro.FileName); Console.WriteLine("FileType:{0}", pro.FileType); } } }IGetFileProcess.csnamespace FileGet { public interface IGetFileProcess { string FilePath { get; set; } string FileName { get;} string FileType { get;} } }FileDrop.csusing System.IO; using IWshRuntimeLibrary; namespace FileGet { class FileDrop : IGetFileProcess { public string FilePath { get; set; } public string FileName { get { return Path.GetFileName(@FilePath); } } public string FileType { get { return Path.GetExtension(@FilePath); } } public FileDrop(string[] arg) { FilePath = arg[2]; if (Path.GetExtension(@FilePath) == ".lnk") { WshShell Shell = new IWshRuntimeLibrary.WshShell(); IWshShortcut Shortcut = (IWshShortcut)Shell.CreateShortcut(FilePath); FilePath = Shortcut.TargetPath.ToString(); } } } }FileArgument.csusing System; using System.IO; using IWshRuntimeLibrary; namespace FileGet { class FileArgument : IGetFileProcess { public string FilePath { get; set; } public string FileName { get { return Path.GetFileName(@FilePath); } } public string FileType { get { return Path.GetExtension(@FilePath); } } public FileArgument(string[] arg) { FilePath = String.Join(" ", arg); if (FilePath.Contains("\"")) { FilePath = FilePath.Replace("\"", ""); } if (Path.GetExtension(@FilePath) == ".lnk") { WshShell Shell = new WshShell(); IWshShortcut Shortcut = (IWshShortcut)Shell.CreateShortcut(FilePath); FilePath = Shortcut.TargetPath.ToString(); } } } }
- 投稿日:2019-12-22T20:32:02+09:00
AutoFixture: C# でテストデータを自動的に作ってくれるライブラリの紹介
テストデータを手作業で全て作成している人はいませんか?そんなあなたにオススメなライブラリのご紹介です。
使用用途は主に Moq の戻りデータを自動生成したいときや、パラメーターを自動生成したいとき、他には Database のデータを自動生成したいときにも使えます。
Package Install
Visual Studio の Package Manager 画面か、 Command を使って Install できます。
Package Manager
Install-Package AutoFixture -Version 4.11.0.NET CLI
dotnet add package AutoFixture --version 4.11.0試しに使ってみる
モデルのテストデータを作ってみます。 MyModel を用意しました。
public class MyModel { public int Id { get; set; } public string Name { get; set; } public DateTimeOffset CreatedAt { get; set; } public MyInnerModel MyInner { get; set; } } public class MyInnerModel { public string[] Notes { get; set; } }AutoFixture を使って MyModel を作ってみます。
[Fact] public void MyModelTest() { var fixture = new Fixture(); var myModel = fixture.Create<MyModel>(); Assert.NotNull(myModel); }Breakpoint を設定して中身を見てみるとこんな感じで自動的に設定されています。
AutoFixture で出来ること
- 自動設定する値をカスタマイズ
- DataAnnotation を自動読み込み
- E.g. StringLength(3) が付与されている場合、自動的に 3 文字のデータが設定される。
- Entity Framework の小テーブルデータを自動作成
- きちんと外部キーの設定をしている場合のみ。
- 親テーブルを fixture.Create したあとに Add すれば子テーブルのデータも全部入る。
- 可能なら DB First (.edmx) より Code First を使ったほうがよいです。
リンク
Document: Cheat Sheet · AutoFixture/AutoFixture Wiki
NuGet Gallery: NuGet Gallery | AutoFixture 4.11.0
似たような機能を持つライブラリ
こっちも使ってみましたが、個人的には AutoFixture のほうが好きでした。
- 投稿日:2019-12-22T18:04:35+09:00
LeapMotionでLive2Dモデルの指を動かす
はじめに
Unity(C#)でLeapMotionから指の角度を取得してLive2Dモデルをリアルタイムに動かす方法
こうですか!!
— ときなし (@KalsaKey) December 11, 2019
思ったより簡単に指の曲がり具合がとれました!!!(個人差でパラメータ調整は必要かもしれないですあくまで私の手に最適化されてます)
使用モデルは鳥総帥さん(@bird_w_cooking)
の製作中モデルの腕をお借りしました #leapmotion #Live2D #Unity pic.twitter.com/U1mGrNsQTk環境
Unity : 2019.2.2f1
Live2D SDK : 4.0-beta.2
LeapMotion SDK : 4.4.0
Live2Dモデル書き出しファイル : 以下画像
今回使用した腕モデルはLive2Dイラストレーター・モデラーの鳥総帥様から許可を得て使用しています準備
Live2D
公式チュートリアルページのSDKをインポートの通りにSDKとモデルをインポート
LeapMotion
Unity向けSDKからUnityPackageをインポート
ここからが問題なんですよね
大抵はLeapHandControllerプレハブをヒエラルキーに入れると書いてあるんですが、少なくとも私の環境ではインポートの問題か何回試してもスクリプトがmissingだったため使えませんでした。
なのでSDK付属のサンプルシーンからコピペします。
参考にしたのは以下の記述です。Assets/LeapMotion/Core/Examples/Capsule Hands(Desktop)からLeapMotionControllerとHandModelsをコピペして自分のシーンに貼り付けます。
LeapMotion+Unityでグー・チョキ・パーを認識する より引用実際のコード
このスクリプトをLive2Dモデルにアタッチします
MovingArm.csusing System.Collections; using System.Collections.Generic; using UnityEngine; using Leap; using Live2D.Cubism.Core; using Live2D.Cubism.Framework; public class MoveArm : MonoBehaviour { private CubismModel _model; private Controller controller; private Dictionary<Leap.Finger.FingerType, CubismParameter> modelfingers; // Start is called before the first frame update void Start() { controller = new Controller(); _model = this.FindCubismModel(); modelfingers = new Dictionary<Leap.Finger.FingerType, CubismParameter>(); modelfingers.Add(Leap.Finger.FingerType.TYPE_INDEX, _model.Parameters[1]); modelfingers.Add(Leap.Finger.FingerType.TYPE_MIDDLE, _model.Parameters[2]); modelfingers.Add(Leap.Finger.FingerType.TYPE_RING, _model.Parameters[3]); modelfingers.Add(Leap.Finger.FingerType.TYPE_PINKY, _model.Parameters[4]); modelfingers.Add(Leap.Finger.FingerType.TYPE_THUMB, _model.Parameters[5]); } private void LateUpdate() { Frame frame = controller.Frame(); if (frame.Hands.Count != 0) { List<Hand> hand = frame.Hands; var fingers = hand[0].Fingers; foreach (Finger finger in fingers) { if (finger.Type == Leap.Finger.FingerType.TYPE_UNKNOWN) continue; var angle = Mathf.PI - finger.Direction.AngleTo(hand[0].Direction); var param = EditParam(angle, 0f, Mathf.PI, -30, 30); if (finger.Type == Leap.Finger.FingerType.TYPE_THUMB) { angle = finger.Direction.AngleTo(hand[0].PalmNormal); param = EditParam(angle, 1.0f, 1.27f, -30, 30); Debug.Log("THUMB " + angle + " " + param); } modelfingers[finger.Type].Value = param; } } } private float EditParam(float param,float leapmin,float leapmax, float modelmin,float modelmax) { return (param - leapmin) * ((modelmax - modelmin) / (leapmax - leapmin)) + modelmin; } }解説
using
LeapMotionの機能を使いたい→
using Leap;
Live2Dの機能を使いたい→using Live2D.Cubism.Core;
using Live2D.Cubism.Framework;
modelfingers って辞書はなに
Fingerクラスに取得したFinger型オブジェクトの指の種類(親指・人差し指・中指・薬指・小指・不明)が取得できるFingerTypeがあるので、モデルの各指パラメータの順番に合わせて(例えば今回モデルの人差し指は2番目の位置にあるので0開始で
_model.Parameters[1]
)各指の種類とモデルのパラメータオブジェクトを当てはめてる
angleってなに
finger.Direction
で指の差す角度(Vector)
hand[0].Direction
で手先の角度(Vector)
AngleTo()
で各ベクトル間の角度を取得
今回モデルの動き(Angleパラメータ最大(30)で指伸ばし最小(-30)で指曲げ)とAngleToで取得できる数値が逆だったため180度(Mathf.PI
)で反転してますEditParam(angle, 1.0f, 1.27f, -30, 30);
すいません取得した値をログでひろってきて値の最大最小を直接入力するゴリ押しをしました
親指だけ曲がる角度が大きく違うので……
ここはもっと方法があると思います最後
間違い、説明不足な点などありましたらコメントか編集リクエストをお願いします。
- 投稿日:2019-12-22T12:16:47+09:00
【C#】派生クラスにキャストで失敗
ベースクラスから派生クラスにキャストしようとしたときに、はまってしまったことをまとめておきます。
処理内容
メンバー変数を1つ保持しているベースクラス(CastBase.cs)と
1つ変数を追加した派生クラス(CastA.cs)を用意しました。using UnityEngine; /// <summary> /// ベースクラス /// </summary> public class CastBase { /// <summary> /// ベースクラスのメンバー変数 /// </summary> public int BaseMemberNum; }/// <summary> /// 派生クラス /// </summary> public class CastA : CastBase { public int AMemberNum; }2つのクラスを使って、以下のような処理しました。(Unity上で動かしています。)
実行はTestMethod内のキャストで失敗し、null参照でエラーを吐いてしまいます。using UnityEngine; /// <summary> /// メインクラス /// </summary> public class Main : MonoBehaviour { void Start() { var castBase = new CastBase(); //ベースクラスでインスタンス化 var castA = new CastBase();//ベースクラスでインスタンス化 TestMethod(castBase, castA); } //ベースクラスを引数として受け渡すメソッド void TestMethod(CastBase castBase, CastBase castA) { Debug.Log(castBase.BaseMemberNum); var castedA = castA as CastA; //キャスト失敗。castedAにはnullが返ってきます。 Debug.Log(castedA.BaseMemberNum); //null参照でエラー Debug.Log(castedA.AMemberNum); } }修正方法はcastAをインスタンス化するときにベースクラスではなく、
CastA型でインスタンス化する必要があります。
CastA型で宣言をし、あらかじめCastAの領域を確保しないとキャストに失敗してしまうようです。using UnityEngine; /// <summary> /// メインクラス /// </summary> public class Main : MonoBehaviour { void Start() { var castBase = new CastBase(); //ベースクラスでインスタンス化 var castA = new CastA(); // CastA型でインスタンス化する TestMethod(castBase, castA); } void TestMethod(CastBase castBase, CastBase castA) { Debug.Log(castBase.BaseMemberNum); var castedA = castA as CastA; Debug.Log(castedA.BaseMemberNum); Debug.Log(castedA.AMemberNum); } }同じクラスに書かれると、なんてこともない問題ですが、実際に直面したのは、宣言とキャストする場所が離れてしまっていて、なかなか原因を発見することができませんでした。
- 投稿日:2019-12-22T11:00:11+09:00
UnityでExcelを読み込む【ExcelDataReader】
環境
- Unity 2019.2.13
- NuGetForUnity 2.0.0
導入方法
1. NuGetをインストールする
https://github.com/GlitchEnzo/NuGetForUnity/releases
2. ExcelDataReaderをインストールする
Excelのデータを
System.Data.DataSet
として受け取るのなら、
ExcelDataReader.DataSet
も一緒にインストールするサンプルコード
https://github.com/tani-shi/unity-excel
using System; using System.Collections.Generic; using System.Data; using System.IO; using System.Linq; using ExcelDataReader; using UnityEngine; public class Excel { public struct Sheet { public string name; public Cell[] cells; public Cell GetCell (int row, int column) { return cells.FirstOrDefault (c => c.row == row && c.column == column); } public Cell[] GetRowCells (int row) { return cells.Where (c => c.row == row).ToArray (); } public Cell[] GetColumnCells (int column) { return cells.Where (c => c.column == column).ToArray (); } } public struct Cell { public int row; public int column; public string value; } public string error { get { return _error; } } public Sheet[] Sheets { get { return _sheets; } } private Sheet[] _sheets = null; private string _error = string.Empty; private string _name = string.Empty; public static bool TryRead (string path, out Excel excel) { excel = Read (path); return string.IsNullOrEmpty (excel._error); } public static Excel Read (string path) { var excel = new Excel (); try { using (var stream = File.Open (path, FileMode.Open, FileAccess.Read)) { var reader = ExcelReaderFactory.CreateOpenXmlReader (stream); if (reader != null) { excel.ParseDataSet (reader.AsDataSet ()); } } } catch (Exception e) { Debug.LogError (e); excel._sheets = new Sheet[] { }; excel._error = e.ToString (); } return excel; } public Sheet GetSheet (string name) { return _sheets.FirstOrDefault (s => s.name == name); } private void ParseDataSet (DataSet dataSet) { var sheetList = new List<Sheet> (); foreach (DataTable table in dataSet.Tables) { var sheet = new Sheet (); sheet.name = table.TableName; var cellList = new List<Cell> (); for (int row = 0; row < table.Rows.Count; row++) { for (int column = 0; column < table.Columns.Count; column++) { var cell = new Cell (); cell.row = row; cell.column = column; cell.value = table.Rows[row][column].ToString (); cellList.Add (cell); } } sheet.cells = cellList.ToArray (); sheetList.Add (sheet); } _sheets = sheetList.ToArray (); } }using UnityEngine; using UnityEditor; public static class ExcelDemo { private const string kSampleExcelPath = "Xlsx/Sample.xlsx"; [MenuItem("ExcelDemo/Read Sample Excel")] private static void ReadSampleExcel () { Excel excel; Debug.Log("EXCEL READING START"); Debug.Log("=================="); if (Excel.TryRead(kSampleExcelPath, out excel)) { foreach (var sheet in excel.Sheets) { foreach (var cell in sheet.cells) { Debug.Log(string.Format("{0}:{1}:{2} value={3}", sheet.name, cell.row, cell.column, cell.value)); } } } Debug.Log("=================="); Debug.Log("EXCEL READING END"); } }
- 投稿日:2019-12-22T10:59:14+09:00
C# - 通知領域に常駐してタスクバーに接するFormを力技で表示する - マルチディスプレイ対応
環境
Windows10。例によってライブラリレスです。
スクショ
アイコンは面倒なので、ただの青い四角にしてます。
アイコンをクリックすると、表示・非表示を切り替えます。例として、右配置と上配置のスクショを載せておきます。
右配置
上配置
やっていること
以下のような感じ。
Form
は起動時に生成しておく。(1). 通知領域に常駐する(
NotifyIcon
)
(2). クリックするとタスクバーを探す
(2-1). タスクバーをWin32API使って検出する(さらに、プロセスが正規のパスにあるexplorer.exe
であることもチェックする)
(2-2). タスクバーの矩形領域(スクリーン上の座標とサイズ)を取得する
(2-3). タスクバーの所属するスクリーンを判定し、座標とサイズをもとに、タスクバーが左右上下のどこに配置されているかを判定する
(3). クリックされた位置とタスクバーの配置情報をもとに、表示するForm
の位置を決めるソースコード
using System; using System.ComponentModel; using System.Diagnostics; using System.Drawing; using System.IO; using System.Runtime.InteropServices; using System.Text; using System.Windows.Forms; internal class TaskbarInfo { public IntPtr WindowHandle{get; private set;} bool _lockInfo; // trueを設定時、情報更新を停止させる Rectangle _windowRect; Rectangle _screenRect; public enum TaskbarDockStyle { Top=0, Bottom=1, Left=2, Right=3 } // ----------------------------------------------------------- public void UpdateInfo() { UpdateInfo(false); } public bool UpdateInfo(bool throwError) { bool retCode; NativeMethods.WINDOWINFO wi; wi = MyGetWindowInfo(WindowHandle, out retCode); if ( retCode ) { _windowRect = wi.rcWindow.rect; // タスクバーが配置されているスクリーンを取得する Point p = new Point(_windowRect.X + _windowRect.Width/2, _windowRect.Y + _windowRect.Height/2); Screen screen = Screen.FromPoint(p); _screenRect = screen.Bounds; } else { // failed throw new Win32Exception( Marshal.GetLastWin32Error() ); } return retCode; } public void LockInfo() { UpdateInfo(); _lockInfo = true; // trueを設定時、情報更新を停止させる } public void UnlockInfo() { UpdateInfo(); _lockInfo = false; } // ----------------------------------------------------------- public Rectangle Rect{ get { if ( !_lockInfo ) { UpdateInfo(); } return _windowRect; } } public Size Size{ get { if ( !_lockInfo ) { UpdateInfo(); } return _windowRect.Size; } } public TaskbarDockStyle Dock{ get{ if ( !_lockInfo ) { UpdateInfo(); } // ※※※ 以下、UpdateInfoが呼ばれないように、プロパティではなくフィールドを直接参照すること。 // タスクバーの移動やスクリーン設定の変更などのタイミングによっては // おそらく、情報の整合性が取れない場合がありえる。 // その場合は一番使われていそうな bottom を返すようにする。 if ( _screenRect.Width == _windowRect.Width ) { if ( _screenRect.Top == _windowRect.Top ) { return TaskbarDockStyle.Top; } else if ( _screenRect.Bottom == _windowRect.Bottom ) { return TaskbarDockStyle.Bottom; } } if ( _screenRect.Height == _windowRect.Height ) { if ( _screenRect.Left == _windowRect.Left ) { return TaskbarDockStyle.Left; } else if ( _screenRect.Right == _windowRect.Right ) { return TaskbarDockStyle.Right; } } return TaskbarDockStyle.Bottom; } } // ----------------------------------------------------------- static readonly string PrimaryTaskbarClassName = "Shell_TrayWnd"; static readonly string TaskbarExeName = "explorer.exe"; static class NativeMethods { [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)] public static extern IntPtr FindWindowEx(IntPtr parentWnd, IntPtr previousWnd, string className, string windowText); [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)] public static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount); [DllImport("user32.dll", SetLastError = true)] public static extern int GetWindowThreadProcessId(IntPtr hWnd, out int lpdwProcessId); [DllImport("user32.dll", SetLastError = true)] public static extern int GetWindowInfo(IntPtr hwnd, ref WINDOWINFO pwi); [StructLayout(LayoutKind.Sequential)] public struct WINDOWINFO { public int cbSize; public RECT rcWindow; public RECT rcClient; public int dwStyle; public int dwExStyle; public int dwWindowStatus; public uint cxWindowBorders; public uint cyWindowBorders; public short atomWindowType; public short wCreatorVersion; } [StructLayout(LayoutKind.Sequential)] public struct RECT { public int left; public int top; public int right; public int bottom; public int width{get{return right-left;}} public int height{get{return bottom-top;}} public Rectangle rect{get{return new Rectangle(left,top,width,height);}} } } private TaskbarInfo(IntPtr windowHandle) { _lockInfo = false; WindowHandle = windowHandle; UpdateInfo(true); } public static TaskbarInfo GetPrimaryTaskbarInfo() { string expectedExePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), TaskbarExeName).ToLowerInvariant(); IntPtr hWnd = IntPtr.Zero; while ( IntPtr.Zero != (hWnd = NativeMethods.FindWindowEx(IntPtr.Zero, hWnd, PrimaryTaskbarClassName, ""))) { int pid; NativeMethods.GetWindowThreadProcessId(hWnd, out pid); Process p = Process.GetProcessById(pid); ProcessModule pm = p.MainModule; // ※MainModuleでよいのかよくわからない //Console.WriteLine(pm.FileName); if ( pm.FileName.ToLowerInvariant() == expectedExePath ) { // c:\windows\explorer.exe がつくった正規のタスクバーと判定した return CreateTaskbarInfo(hWnd); } } return null; // failed } static TaskbarInfo CreateTaskbarInfo(IntPtr windowHandle) { try { return new TaskbarInfo(windowHandle); } catch(Win32Exception e) { Console.WriteLine(e); } return null; } public Rectangle CalcNearRect(Point baseP, Size sz) { bool backup = _lockInfo; try { _lockInfo = true; var dock = Dock; Point p = new Point(); switch ( dock ) { case TaskbarDockStyle.Left: p.X = _windowRect.Right; p.Y = baseP.Y - sz.Height/2; break; case TaskbarDockStyle.Right: p.X = _windowRect.Left - sz.Width; p.Y = baseP.Y - sz.Height/2; break; case TaskbarDockStyle.Top: p.X = baseP.X - sz.Width/2; p.Y = _windowRect.Bottom; break; case TaskbarDockStyle.Bottom: p.X = baseP.X - sz.Width/2; p.Y = _windowRect.Top - sz.Height; break; } /* if ( p.X + sz.Width > _screenRect.Right ) { p.X = _screenRect.Right - sz.Width; } if ( p.X < _screenRect.Left ) { p.X = _screenRect.Left; } if ( p.Y + sz.Height > _screenRect.Bottom ) { p.Y = _screenRect.Bottom - sz.Height; } if ( p.Y < _screenRect.Top ) { p.Y = _screenRect.Top; } */ return new Rectangle(p, sz); } finally { _lockInfo = backup; } } // if GetWindowInfo failed, retCode = false. static NativeMethods.WINDOWINFO MyGetWindowInfo(IntPtr hWnd, out bool retCode) { var wi = new NativeMethods.WINDOWINFO(); wi.cbSize = Marshal.SizeOf(wi); retCode = (NativeMethods.GetWindowInfo(hWnd, ref wi) != 0); return wi; } } class JohchuTest : Form { NotifyIcon notifyIcon; Button btn; JohchuTest() { this.Visible = false; this.ShowInTaskbar = false; this.WindowState = FormWindowState.Minimized; this.ControlBox = false; this.Text = ""; this.FormBorderStyle = FormBorderStyle.FixedToolWindow; this.StartPosition = FormStartPosition.Manual; //this.Visible = false; this.Size = new Size(200, 150); btn = new Button(){Text="Exit"}; btn.Click += (s,e)=>{MyExit();}; btn.Location = new Point((200-btn.Width)/2, (150-btn.Height)/2); Controls.Add(btn); notifyIcon = new NotifyIcon(); notifyIcon.Icon = Create16x16Icon(); notifyIcon.Visible = true; notifyIcon.MouseClick += NotifyIcon_MouseClick; var menu = new ContextMenuStrip(); menu.Items.AddRange(new ToolStripMenuItem[]{ new ToolStripMenuItem("E&xit", null, (s,e)=>{MyExit();}, "Exit") }); notifyIcon.ContextMenuStrip = menu; //HandleCreated+=(s,e)=>{Console.WriteLine("HandleCreated event occured.");}; //Load+=(s,e)=>{Console.WriteLine("Load event occured.");}; //Shown+=(s,e)=>{Console.WriteLine("Shown event occured.");}; //Activated+=(s,e)=>{Console.WriteLine("Activated event occured.");}; } void NotifyIcon_MouseClick(object sender, MouseEventArgs e) { if ( e.Button == MouseButtons.Left ) { if ( this.WindowState == FormWindowState.Normal ) { this.WindowState = FormWindowState.Minimized; } else { try { this.Opacity = 0; // 移動前に表示されてしまうので透過させておく this.WindowState = FormWindowState.Normal; MoveFormTo(Cursor.Position); } finally { this.Opacity = 1; } } } } bool MoveFormTo(Point p) { TaskbarInfo taskbar = TaskbarInfo.GetPrimaryTaskbarInfo(); Rectangle rect = taskbar.CalcNearRect(p, Size); this.Location = rect.Location; if ( taskbar == null ) { return false; } return true; } // ------------------------------------------- static Icon Create16x16Icon() { Bitmap bmp = new Bitmap(16,16); using ( Graphics g = Graphics.FromImage(bmp) ) { g.Clear(Color.Blue); } return Icon.FromHandle(bmp.GetHicon()); } void MyExit() { var e = new CancelEventArgs(); notifyIcon.Visible = false; Application.Exit(e); if (e.Cancel) { Console.WriteLine("Application.Exit is canceled."); notifyIcon.Visible = true; } } [STAThread] static void Main(string[] args) { //Application.EnableVisualStyles(); Application.Run(new JohchuTest()); } }マルチディスプレイ(マルチスクリーン)で注意すべきこと
- マルチ画面になると、画面の左上=(0,0)とは限らなくなる。
- 主画面(
PrimaryScreen
)が右側や下側にくる場合がありえる。マルチディスプレイにおけるタスクバー
- 通知領域は主タスクバー(
Shell_TrayWnd
)にしか表示されない。PrimaryScreen
以外の画面にShell_TrayWnd
を置くことができる。
(つまり、主画面に主タスクバーがいない場合がある。)感想
.NETではタスクバーは取り残されているような気がする。
(NotifyIcon
はあるものの、お作法的なものがあまり見つけられない。)
わりと需要ある機能だと思うが、標準で機能提供されてないものか。参考サイト
- 投稿日:2019-12-22T10:06:20+09:00
ASP.NET Coreで静的コンテンツを提供する
はじめに
今回は、HTML、CSS、画像、および、JavaScript などの静的ファイルはアプリケーションで扱う方法について調べてみました。
扱わないアプリなんてほぼ皆無だと思いますので、これは必須な知識のはず。なお、環境を.NET Core 3.1に上げました。
wwwroot内のファイルの場合
Startup.csの
Configure
メソッドにある、UseStaticFilesメソッドの呼び出しをすることで、wwwrootフォルダ以下にあるファイルを提供することができます。app.UseStaticFiles();view側で、ここにあるファイルにアクセスする際は、以下のような記述をします。
<img src="~/images/sample.jpeg" alt="サンプル画像" />
~/
がwwwroot
を指します。ですからこれで、wwwroot/images/example.jpg を参照することになります。wwwroot外のファイルの場合
wwwroot内のファイルに加えて、別のフォルダ内の静的ファイルも提供したい場合は、Configureメソッドで、以下のようなコードを書きます。
app.UseStaticFiles(); app.UseStaticFiles(new StaticFileOptions { FileProvider = new PhysicalFileProvider( Path.Combine(Directory.GetCurrentDirectory(), "StaticFiles")), RequestPath = "/StaticFiles" });上の例は、StaticFiles フォルダの例です。最初の "StaticFiles" は、物理フォルダの名前で、2番目の"/StaticFiles"は、ルーティングのパスを指定します。
ここでは同じ名前にしましたが、別の名前でもかまいません。では、試してみます。プロジェクトの直下に、StaticFiles フォルダを作成し、その下に Images フォルダを作成します。
このフォルダの下に、任意の画像ファイルを入れます。チュートリアルで作成したindex.cshtml に以下のタグを追加します。
<img src="/staticfiles/images/sample.jpeg" alt="サンプル画像" />実行してみます。
無事、画像が表示されました。試しに、
app.UseStaticFiles(new StaticFileOptions { FileProvider = new PhysicalFileProvider( Path.Combine(Directory.GetCurrentDirectory(), "StaticFiles")), RequestPath = "/StaticFiles" });の部分をコメントアウトしてみます。
画像は表示されません。staticsfiles フォルダにはアクセスできないのが確認できます。
HTTP 応答ヘッダーの設定
まず、以下のコマンドで、Microsoft.AspNetCore.Httpパッケージをインストールします。
dotnet add package Microsoft.AspNetCore.Http --version 2.2.2次に、StartUp.cs の
Configure
メソッドを書き換えます。using Microsoft.AspNetCore.Http; … var cachePeriod = 600; app.UseStaticFiles(new StaticFileOptions { OnPrepareResponse = ctx => { ctx.Context.Response.Headers.Append("Cache-Control", $"public, max-age={cachePeriod}"); } });こうすると、Response Headerに
Cache-Control
の値を設定できます。この例では、600秒間キャッシュを有効にしています。ブラウザで確認すると、確かにCache-Control が設定されているのが確認できます。
上の例では、
PhysicalFileProvider
での指定がないので、wwwroot
配下のファイルに限定されます。ディレクトリ参照の有効化
通常のWebアプリではこの機能は不要ですが、時には、ディレクトリの参照を有効にしたい場合があります。
Startup.Configure で
UseDirectoryBrowser
メソッドを呼び出すと、ディレクトリの参照を有効にできます。app.UseStaticFiles(new StaticFileOptions { FileProvider = new PhysicalFileProvider( Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "myimages")), RequestPath = "/images" }); app.UseDirectoryBrowser(new DirectoryBrowserOptions { FileProvider = new PhysicalFileProvider( Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "myimages")), RequestPath = "/images" });上のコードでは、
app.UseStaticFiles
で、/wwwroot/myimages フォルダ配下の静的ファイルを提供し、app.UseDirectoryBrowser
で、ディレクトリの参照を有効化することで、ブラウザ上でフィルを閲覧できるようになります。
"/images"がルーティングのパスを指定です。以下、ブラウザの表示例です。
- 投稿日:2019-12-22T03:32:23+09:00
Parallelクラスでお手軽な並列処理
会社の人にParallelクラスの存在を教えてもらったのでメモ。
.NET Framework 4で追加されたかなり前から存在する機能なのでありふれた内容です。概要
Parallelクラスを利用することでループ内を手軽に並列化できる。
メリット
- 通常のfor, foreachと書き方が殆ど変わらない点が非常にお手軽。
- Thread, Task, await, asyncを使わずに並列処理を実現できる。
注意点
当然ではあるが並列処理なので同じファイルへのアクセスなどは気をつける必要がある。Lockをかけるとか。
通常のforeach
List<int> list = new List<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; foreach (int value in list) { Console.Write(value); }実行結果123456789
逐次処理なので1~9まで順番にコンソール出力されます。
Parallel.ForEachで並列化
List<int> list = new List<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; // 並列処理オプションの設定 ParallelOptions parallelOptions = new ParallelOptions(); parallelOptions.MaxDegreeOfParallelism = 4; // 並列処理実行 Parallel.ForEach(list, parallelOptions, (value) => { Console.Write(value); });実行結果156894723
並列処理なので実行毎に結果が変わります。
順番を気にする必要のない処理であれば非常に手軽に高速化できます。
「MaxDegreeOfParallelism」の設定は実行環境によると思うが3~4が良いという意見をよく見る。あとがき
お手軽すぎる。C#好き。
- 投稿日:2019-12-22T03:32:23+09:00
[C#] Parallelクラスでお手軽な並列処理
会社の人にParallelクラスの存在を教えてもらったのでメモ。
.NET Framework 4で追加されたかなり前から存在する機能なのでありふれた内容です。概要
Parallelクラスを利用することでループ内を手軽に並列化できる。
メリット
- 通常のfor, foreachと書き方が殆ど変わらない点が非常にお手軽。
- Thread, Task, await, asyncを使わずに並列処理を実現できる。
注意点
当然ではあるが並列処理なので同じファイルへのアクセスなどは気をつける必要がある。Lockをかけるとか。
通常のforeach
List<int> list = new List<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; foreach (int value in list) { Console.Write(value); }実行結果123456789
逐次処理なので1~9まで順番にコンソール出力されます。
Parallel.ForEachで並列化
List<int> list = new List<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; // 並列処理オプションの設定 ParallelOptions parallelOptions = new ParallelOptions(); parallelOptions.MaxDegreeOfParallelism = 4; // 並列処理実行 Parallel.ForEach(list, parallelOptions, (value) => { Console.Write(value); });実行結果156894723
並列処理なので実行毎に結果が変わります。
順番を気にする必要のない処理であれば非常に手軽に高速化できます。
「MaxDegreeOfParallelism」の設定は実行環境によると思うが3~4が良いという意見をよく見る。まとめ
お手軽すぎる。C#好き。
- 投稿日:2019-12-22T03:32:23+09:00
[.NET] Parallelクラスでお手軽な並列処理
会社の人にParallelクラスの存在を教えてもらったのでメモ。
.NET Framework 4で追加されたかなり前から存在する機能なのでありふれた内容です。概要
Parallelクラスを利用することでループ内を手軽に並列化できる。
メリット
- 通常のfor, foreachと書き方が殆ど変わらない点が非常にお手軽。
- Thread, Task, await, asyncを使わずに並列処理を実現できる。
注意点
当然ではあるが並列処理なので同じファイルへのアクセスなどは気をつける必要がある。Lockをかけるとか。
通常のforeach
List<int> list = new List<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; foreach (int value in list) { Console.Write(value); }実行結果123456789
逐次処理なので1~9まで順番にコンソール出力されます。
Parallel.ForEachで並列化
List<int> list = new List<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; // 並列処理オプションの設定 ParallelOptions parallelOptions = new ParallelOptions(); parallelOptions.MaxDegreeOfParallelism = 4; // 並列処理実行 Parallel.ForEach(list, parallelOptions, (value) => { Console.Write(value); });実行結果156894723
並列処理なので実行毎に結果が変わります。
順番を気にする必要のない処理であれば非常に手軽に高速化できます。
「MaxDegreeOfParallelism」の設定は実行環境によると思うが3~4が良いという意見をよく見る。まとめ
お手軽すぎる。C#好き。
- 投稿日:2019-12-22T03:32:23+09:00
C#のParallelクラスでお手軽な並列処理
会社の人にParallelクラスの存在を教えてもらったので自分用メモ。
.NET Framework 4で追加されたかなり前から存在する機能なのでありふれた内容です。概要
Parallelクラスを利用することでループ内を手軽に並列化できる。
メリット
- 通常のfor, foreachと書き方が殆ど変わらない点が非常にお手軽。
- Thread, Task, await, asyncを使わずに並列処理を実現できる。
注意点
当然ではあるが並列処理なので同じファイルへのアクセスなどは気をつける必要がある。Lockをかけるとか。
通常のforeach
List<int> list = new List<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; foreach (int value in list) { Console.Write(value); }実行結果123456789
逐次処理なので1~9まで順番にコンソール出力されます。
Parallel.ForEachで並列化
List<int> list = new List<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; // 並列処理オプションの設定 ParallelOptions parallelOptions = new ParallelOptions(); parallelOptions.MaxDegreeOfParallelism = 4; // 並列処理実行 Parallel.ForEach(list, parallelOptions, (value) => { Console.Write(value); });実行結果156894723
並列処理なので実行毎に結果が変わります。
順番を気にする必要のない処理であれば非常に手軽に高速化できます。
「MaxDegreeOfParallelism」の設定は実行環境によると思うが3~4が良いという意見をよく見る。まとめ
お手軽すぎる。C#好き。
- 投稿日:2019-12-22T01:26:37+09:00
ついにでたOculusQuestハンドトラッキング×VRMでアバター空間満喫してみた
はじめに
楽しみに待っていたOculusQuest ハンドトラッキング対応版 OculusIntegrationが公開されたので、
早速どんなのか試してみました。
とにかく早くQuest触りたいという欲求に忠実に品質度外視・検証速度重視/QCDのD偏重した記録をまとめたものになります。
それゆえに内容は今後修正・加筆されることがあります。検証環境
- Unity 2018.4.14f1
- macOS Catalina Version 10.15
- Oculus Quest Version12
- Oculus Integration Version12.0
- FinalIK Version19.0
組込手順(概要)
- OculusDeveloper公式のハンドトラッキングのページにそって組み込みする https://developer.oculus.com/documentation/quest/latest/concepts/unity-handtracking/ (Google翻訳使うとだいたい読める日本語になるので手順に沿って進める)
- OVRCameraRigのCenterEyeAnchor,LeftHandAnchor,RightHandAnchorをFinalIKのSpine/HeadTarget,Spine/Pelvis,LeftArm/Target,RightArm/Targetにそれぞれ設定
- OVRLipSyncをVRMのVRM Blend Shape ProxyのAIUEOに接続
- 不定期に瞬きするスクリプトをVRM Blend Shape ProxyのBLINKに接続
- OVRHandの指状態を手に反映
- ここだけズルした。VRMの手boneのスケールを0にして非表示にしつつ、OVRHandの手をそのまま使用。
作例
VRMモデル動かしてみた。
Unity/Oculus IntegrationにHandTrackingきたので早速×VRMで試してみた。外部コントローラなしのQuestのみで指5指動く破壊力スゴイ #Oculus #Handstracking pic.twitter.com/I6w7iw3idK
— sa_w_ara△ (@sa_w_ara) December 20, 2019
左指ピンチでメニュー表示ON/OFFするとこんな感じ pic.twitter.com/IAjcJvBJNd
— sa_w_ara△ (@sa_w_ara) December 20, 2019
- OVRHand Behaviourから取得できる各種値をそのままデバッグ表示してみた
- 各指の閉じ具合、各指のピンチon/off、精度High/Lowが取得できる。
- 指boneのtransformも使えるので個別の関節状態はそちら参照することなるかと。
OVRHandクラスで取得される各種値の確認。
— sa_w_ara△ (@sa_w_ara) December 21, 2019
FingerIsPinching, GetFingerPinchStreangthはよく使いそう。 pic.twitter.com/uSNSkBAcg8
- おまけ: ハンドトラッキングしつつ自前計算Particleやってみたもの, 5000triangles
- 全指個別で粒子触れるようにしたかったのだけれど、全く速度意識しないと重くて使い物にならなかったので省略。
Quest HandTracking×パーティクル楽しい
— sa_w_ara△ (@sa_w_ara) December 21, 2019
人差指に退ける力・中指に引き寄せる力持ってみたのだけど、
VRだと粒子移動に奥行き知覚がついて空間の開放感がとても良い #OculusQuest #HandTracking pic.twitter.com/IyxEC7ZnQY
- おまけ: さらに360天球画像をパーティクルに投影してみた版
- こっちのshaderを若干修正して天球をパーティクル群にリアルタイム投影 https://github.com/uisawara/unity-projection360
#theta360 で撮影した全天球写真を球状パーティクルに投影して、それを #ocululsquest +ハンドトラッキングで触って遊ぶ pic.twitter.com/HwqKJCwdvN
— sa_w_ara△ (@sa_w_ara) December 21, 2019所感
- ハンドトラッキングの認識精度わりとよい
- とはいえ手・指を入力装置として使う場合のUIUXの考えかた自体が未成熟なのでそこを今後詰めていく。
- ユーザーの意図的操作か・無意識動作かをうまく切り分ける仕組みは必要。
- 検知できる指の特性
- 手の認識される範囲が結構広くて、下ろしたつもりの手指でピンチ操作した扱いになることがある。
- 手の高さと組み合わせて、意図的に手を上げての操作のみ対象にしたほうがよいのかも。
- 右手指で2進数作ると2,6あたりの認識が苦手っぽい。(普通にやる姿勢ではないだろうから実用充分)
- 人差し指中指・薬指小指でVの字作るのはできた。
謝辞
- 3Dアバター: セシル変身 https://twitter.com/cecilproject