- 投稿日:2019-11-28T23:24:18+09:00
C# - 力技で四則演算式をLL(1)構文解析する
yaccとかCompilerProviderクラスとかを使わずに、四則演算式を解析するプログラムを作成してみた。
字句解析器を分離してないので無駄にハマった・・。参考サイト
https://blog.tiqwab.com/2017/01/04/recursive-descent-parser.html
今回の四則演算のBNF風メモ
E -> T E2 E2 -> "+" T E2 | "-" T E2 | empty T -> F T2 T2 -> "*" F T2 | "/" F T2 | empty F -> "(" E ")" | numberサンプルコード
上のBNFに沿って文字列を解析して、演算します。
エラーとかオーバーフローは放置です。using System; using System.IO; using System.Text; class TestLL { MemoryStream ms; TestLL() { } bool Parse(string expr) { ms = new MemoryStream(Encoding.UTF8.GetBytes(expr)); int x=0; if (Expr_E(ref x)){ Console.WriteLine(x); return true; } return false; } bool Expr_E(ref int y) { int x1=0; string ope=""; int x2=0; if ( Expr_T(ref x1) ) { y = x1; if ( Expr_E2(ref ope, ref x2) ) { if (ope=="+"){y+=x2;} else if(ope=="-"){y-=x2;} return true; } } throw new Exception("Expr error"); } bool Expr_E2(ref string yOpe, ref int y) { int x1=0; string ope=""; int x2=0; if ( TokenCheck("+") ) { yOpe = "+"; if ( Expr_T(ref x1) ) { y = x1; if ( Expr_E2(ref ope, ref x2) ) { if (ope=="+"){y+=x2;} else if(ope=="-"){y-=x2;} return true; } } } else if ( TokenCheck("-") ) { yOpe = "-"; if ( Expr_T(ref x1) ) { y = x1; if ( Expr_E2(ref ope, ref x2) ) { if (ope=="+"){y+=x2;} else if(ope=="-"){y-=x2;} // else{y=y;} return true; } } } else{ return true; } throw new Exception("Expr error"); } bool Expr_T(ref int y) { int x1=0; string ope=""; int x2=0; if ( Expr_F(ref x1) ) { y = x1; if ( Expr_T2(ref ope, ref x2) ) { if (ope=="*"){y*=x2;} else if(ope=="/"){y/=x2;} return true; } } throw new Exception("Expr error"); } bool Expr_T2(ref string yOpe, ref int y) { int x1=0; string ope=""; int x2=0; if ( TokenCheck("*") ) { yOpe = "*"; if ( Expr_F(ref x1) ) { y=x1; if ( Expr_T2(ref ope, ref x2) ) { if (ope=="*"){y*=x2;} else if(ope=="/"){y/=x2;} return true; } } } else if ( TokenCheck("/") ) { yOpe = "/"; if ( Expr_F(ref x1) ) { y=x1; if ( Expr_T2(ref ope, ref x2) ) { if (ope=="*"){y*=x2;} else if(ope=="/"){y/=x2;} return true; } } } else{ return true; } throw new Exception("Expr error"); } bool Expr_F(ref int y) { if ( TokenCheck("(") ) { if ( Expr_E(ref y) ) { if ( TokenCheck(")")) { return true; } } } else if ( TokenIntCheck(ref y) ) { return true; } ms.Position = pos; throw new Exception("Expr error"); } // --------------------------------------------- bool TerminalCheck() { int b = ms.ReadByte(); if ( b < 0 ) { return true; } else { ms.Position--; return false; } } bool TokenIntCheck(ref int x) { long pos = ms.Position; int i = 0; int b; x = 0; //Console.WriteLine("CheckNum"); while ( ( b = ms.ReadByte() ) >= 0 ) { if ('0'<=b&&b<='9') { x *= 10; x += b-'0'; i++; } else if ('a'<=b&&b<='z') { ms.Position = pos; return false; } else if ('A'<=b&&b<='Z') { ms.Position = pos; return false; } else { break; } } if (i==0){ ms.Position = pos; return false; } if (b>=0) { ms.Position--; } Console.WriteLine("Num"); return true; } bool TokenCheck(string s) { byte[] t = Encoding.UTF8.GetBytes(s); //Console.WriteLine("CheckToken('"+s+"')"); long pos = ms.Position; int i = 0; int b; while ( i<t.Length ) { b = ms.ReadByte(); if (b==t[i]) { i++; } else { ms.Position = pos; return false; } } Console.WriteLine("Token('"+s+"')"); return true; } [STAThread] static void Main(string[] args) { string exprStr = "1+2+3"; if (args.Length>=1) { exprStr = String.Join("",args); } Console.WriteLine("Input: "+exprStr); var t = new TestLL(); t.Parse(exprStr); } }実行結果
Input: 1+2+3 6Input: 1+2*3 7Input: 1+2*(3-1) 5Input: 7/2 3※除算は整数除算なので、
7/2
は3.5にはならず、3になります。
- 投稿日:2019-11-28T22:36:36+09:00
C#でタスクスケジューラに登録しようとした話
TL;DR
- 最上位権限でタスクスケジューラに登録したい
- ログオフ(サインアウト)しておいても動いてほしいから
- エクスポートしたXMLファイルをもとに作って、登録すればなんとかいけた
現在作業中のユーザで登録してみる
まずは普通に登録してみます。
// タスクスケジューラの登録解除 runProcess(@"/c schtasks.exe /delete /TN SampleTask /F"); // タスクスケジューラの登録 var command = "/c schtasks.exe /create /TN SampleTask /TR " + Directory.GetCurrentDirectory() + @"\SampleTask.bat "; // TODO 必要に応じて、ここでcommandに色々な指定を追加する // 登録 Process proc = new Process(); proc.StartInfo.FileName = Environment.GetEnvironmentVariable("ComSpec"); //"cmd.exe"; proc.StartInfo.UseShellExecute = false; proc.StartInfo.RedirectStandardOutput = true; proc.StartInfo.RedirectStandardInput = false; proc.StartInfo.CreateNoWindow = true; proc.StartInfo.Arguments = command; proc.Start(); var results = proc.StandardOutput.ReadToEnd(); // TODO 必要ならresultsからログ出力とか proc.WaitForExit(); proc.Close(); // 完了通知 MessageBox.Show("登録しました");さて。
これだと、ログインしている状態じゃないと動きません。
それでもいいなら、十分です。XMLを利用して、登録してみる
「いちいち色々な設定を記載して登録するのとか面倒じゃない?」
ということで、一度テンプレートとなるようなXMLを作成してからタスクを登録する方法です。テンプレートとなるXMLの作成
- 普通にWindowsのタスクスケジューラを起動し、適当なタスクを作成します。
- 作成したタスクを選択して、右クリックなり、画面右なりから「エクスポート」を選択します。
- 作成したタスクを削除します。
登録
XMLをタスクスケジューラに登録します。
といっても、command部分を以下のようにするだけです。var command = "/c schtasks.exe /create /XML " + Path.Combine(Environment.CurrentDirectory, "template.xml") + " /TN SampleTask";仮に、テンプレートを読み込んで編集したい場合があれば、XMLを読み込んだ後、追加したり変更したりします。
XmlDocument document = new XmlDocument(); document.Load(inputXmlPath); // テンプレートとなるXMLをロード var task = document.GetElementsByTagName("Task")[0]; DateTime now = DateTime.Now; // 今日の日付 var date = document.GetElementsByTagName("Date")[0]; date.InnerText = now.ToString("yyyy-MM-ddTHH:mm:ss"); // Author var author = document.GetElementsByTagName("Author")[0]; author.InnerText = $@"{Environment.UserDomainName}\{Environment.UserName}"; // バッチのパス var command = document.GetElementsByTagName("Command")[0]; command.InnerText = Directory.GetCurrentDirectory() + @"\SampleTask.bat"; // todo などなど適当にXMLに追加したり、変更したりする // 保存 document.Save(outputXmlPath); // テンプレートに上書きでもいいが、他の名前で保存しておく(つまり、登録時のパスはoutputXmlPathを使う)XMLを利用して、SYSTEMユーザで登録してみる
サインアウト時にも動くようにしたいので、登録時のユーザをSYSTEMにしてみます。
"schtasks.exe /create /XML " + Path.Combine(Environment.CurrentDirectory, "sample.xml") + " /TN SampleTask /RU "" /RP ""これで実行ユーザがSYSTEM(たぶん、最上位)になる。
ただし、ツールを起動したときのアカウントの権限次第ですが、タスクが登録されているかどうかの確認でエラーになります。
ITaskService taskservice = null; Boolean exist = false; try { taskservice = new TaskScheduler.TaskScheduler(); taskservice.Connect(null, null, null, null); ITaskFolder containingFolder = taskservice.GetFolder("\\"); // 存在確認 containingFolder.GetTask("SampleTask"); exist = true; } catch (Exception ex) { // こっちに来る exist = false; } finally { if (taskservice != null) { System.Runtime.InteropServices.Marshal.ReleaseComObject(taskservice); } } return exist;タスクが登録されているかどうかを判定する必要がないなら、このままでもOKです。
XMLを利用して、ユーザにIDとパスを入力させつつ登録してみる
色々こねくり回してます。
前提として、エクスポートするタスクの設定は、最低でも以下のようにしています。
まず、バッチファイルをもう一つ作ります。
保存パスを動的に変更したいなら、バッチファイルを動的に作ります。string str = "echo off" + Environment.NewLine + "set USR_INPUT_STR=" + Environment.NewLine + "set /P USR_INPUT_STR=\"ユーザIDを入力してください: \"" + Environment.NewLine + "schtasks /create /XML " + Path.Combine(Environment.CurrentDirectory, "template.xml") + " /TN SampleTask /RU %USR_INPUT_STR% /RP \"\"" + Environment.NewLine + "timeout 5 /nobreak"; // 最後5秒待っているのは趣味(すぐ閉じないで、結果を見たかったから) string outputBatPath = Path.Combine(Environment.CurrentDirectory, @"Register.bat"); // 保存先のパス StreamWriter sw = new StreamWriter( outputBatPath, false, Encoding.GetEncoding("shift_jis")); // 内容を書き込む sw.Write(str); // 閉じる sw.Close();次に、Processも少し設定をいじります。
string outputBatPath = Path.Combine(Environment.CurrentDirectory, @"Register.bat"); // 保存先のパス Process proc = new Process(); proc.StartInfo.Verb = "RunAs"; proc.StartInfo.FileName = Environment.GetEnvironmentVariable("ComSpec");; proc.StartInfo.UseShellExecute = true; proc.StartInfo.CreateNoWindow = true; proc.StartInfo.Arguments = "/c " + outputBatPath; try { proc.Start(); proc.WaitForExit(); proc.Close(); } catch (System.ComponentModel.Win32Exception) { //「ユーザーアカウント制御」ダイアログでキャンセルされたなどによって //起動できなかった時 return true; } return false;この方法をとると、ユーザIDとパスワードをユーザに入力させることができます。
とはいえ、逆に管理アカウントのパスワードを知らないとダメだったりするので、他のを使った方が良い場合もあると思います。とりあえず、私がやりたかったことはこれで実現できたということで。
- 投稿日:2019-11-28T22:35:07+09:00
PNG画像の解像度を保持しておいて、設定し直したら予期せぬ値になっていた話
環境
Windows 10 Pro
Visual Studio 2017 Professional
Magick.NET-Q16-AnyCPU v7.14事象
- PNG画像を読み込んで、オリジナルの解像度を取得しておく
- 途中、色々な処理をする(別拡張子で保存したりとかもする)
- 最後に、手順1で取得したオリジナルの解像度をppiで設定する → あら不思議、おかしな値が入ってるじゃないの
結論
事象を読んで気づいた方もいらっしゃるかと思いますが、問題があったのはここ。
- PNG画像を読み込んで、オリジナルの解像度を取得しておく
ここを、以下のようにすればいい。
- PNG画像を読み込んで、単位をppiにしてからオリジナルの解像度を取得しておく
なぜこのようなことが?
私も今回調べて初めて知りましたが、PNGはピクセルの物理サイズは持っていますが、その単位がppiではないようです。
なので、物理的なピクセルサイズをinch(2.54)で割ってあげれば、一応ppiは出せます。が、端数は出る。この辺りのことは、私もさらっとしか調べていませんが、興味のある方は「PNG pHYs」みたいなキーワードとかで調べてみてください。
(そして、この記事の内容が間違っていたら教えてください…)再現コード(抜粋)
意図せぬ値が設定されていたコード
string inPath = ""; // 入力画像のパス string outPath = ""; // 最終出力画像のパス double densityX; using(MagickImage img = new MagickImage(inPath)) { // オリジナルのdensityを取得 densityX = img.Density.X; } /* 色々処理 */ string tempPath = ""; // 色々処理した結果の一時ファイルのパス using(MagickImage img = new MagickImg(tempPath) { // 解像度を設定 img.Density = new Density(densityX, DensityUnit.PixelsPerInch); // 保存 img.Write(outPath); }修正後
string inPath = ""; // 入力画像のパス string outPath = ""; // 最終出力画像のパス double densityX; using(MagickImage img = new MagickImage(inPath)) { // 単位をPixelsPerInchに替えてからdensityを取得 Density density = mImg.Density.ChangeUnits(DensityUnit.PixelsPerInch); densityX = density.X; } /* 色々処理 */ string tempPath = ""; // 色々処理した結果の一時ファイルのパス using(MagickImage img = new MagickImg(tempPath) { // 解像度を設定 img.Density = new Density(densityX, DensityUnit.PixelsPerInch); // 保存 img.Write(outPath); }
- 投稿日:2019-11-28T22:34:30+09:00
C#のImageMagickでカラーモードがRGBの画像を保存しなおしたらグレーになった
環境
Windows 10 Pro
Visual Studio 2017 Professional
Magick.NET-Q16-AnyCPU v7.14事象再現
パッと見にはグレースケールの画像なんだけど、Photoshop等で確認するとRGBになっている画像がありました。
※本来の画像の一部をQiitaの記事用に切り抜いて使っています。元データには模様がついています。この画像をC#のMagick.Netを使ってリサイズした画像がこちらです。
いや、グレースケールになっとるやないかい。
もちろんすべての画像で起こるわけではないです。
RGBそれぞれの値が全画素で一致している場合に勝手に変換してくれている印象です。
パッと見グレースケールでも、各ピクセルのどこか1つでもRGB値に差異があればRGB保持のままだと思います。変換部分のコードは以下になります。
string outPath = @"C:\temp"; // 出力先フォルダ string filepath = @"C:\temp\test.tiff"; // 入力画像 int toHeight = 50; // 拡大後の高さ string filename = Path.GetFileNameWithoutExtension(filepath); string ext = Path.GetExtension(path); using (var img = new ImageMagick.MagickImage(path)) { int wid = img.Width; int hei = img.Height; double scale = ((double)toHeight / hei); double newWid = scale * wid; // フィルター指定 img.FilterType = ImageMagick.FilterType.Lanczos; // リサイズ実行 img.Resize((int)Math.Ceiling(newWid), toHeight); // 保存 img.Write(Path.Combine(outPath, $"{filename}_out{ext}")); }解決方法
PreserveColorType();
を使う「Preserve=保持する」とかそういう意味だと思ったので、使ってみたらいけました。
SetAttribute("colorspace:auto-grayscale", "false");
を使うどこかで見た記事だと、
PreserveColorType()
の中でも同じことをやっているそうな。
colorspace:auto-grayscaleについては、ImageMagickのコマンドラインオプションにも説明が載っています。
ColorType = ImageMagick.ColorType.TrueColor;
を使う(tiff画像の場合)コマンドラインオプションの説明を読むと、以下のような記述があったので、試したらいけました。
PNG and TIF do not need this define. With PNG, just use PNG24:image. With TIF, just use -type truecolor.
- 投稿日:2019-11-28T17:55:15+09:00
DEBUG定数のパターンを増やしたい
.netでよく使いがちな
#if DEBUG XXXXXXXX #endifってやつをDEBUGかReleaseか以外の判断をさせたい場合の対応方法です。
対象のプロジェクトを右クリックしてプロパティを開きます。
こんな感じで何かしらの文字列を入れておくと、、、、
いい感じに切り替えができます。
- 投稿日:2019-11-28T17:08:52+09:00
RPG戦闘のAIを総当たりで実装する(そして、インターフェースを用いた設計)
この記事は C# Advent Calendar 2019 の1日目です。
私はRPGを作るのが大好きです。特に、ちょっと複雑な効果を持ったユニークなスキルを実装するのが好きです。しかし、そういったスキルを敵に使わせる時には悩ましい問題があります。
「攻撃力の高いスキル」と「自分のHPを回復するスキル」があったとして、どちらを使うと良いでしょうか?それは場合によります。1撃でライバルを倒せるならば前者ですし、逆に自分が1撃で倒れそうなら後者かもしれません。
実際にゲームシステムを組みながら、AIをどのような発想で実装するとよいか、その際にプログラムの設計で気を付けることなどを紹介します。オブジェクト指向などの設計のための参考にしていただければ幸いです。
ゲームシステム
今回実装するゲームは以下のようなシステムです:
- プレイヤー1体と敵1体がいて、お互いに相手のHPが0になることを目指す
- プレイヤーと敵は1ラウンドに一回行動できる。
- プレイヤーはAIによって自動で行動する。
- 敵は固定の行動しかとらない。
- プレイヤーは複数のスキルから何らかの手段でスキルを選んで使用できる。
- スキルには攻撃力が、戦闘参加者には防御力がある。
- (ダメージ) = (攻撃力) - (防御力)
- 決着がつくとプログラムは終了する。
- 戦闘の様子はコンソール ウィンドウに出力される。
ユーザーが操作できる部分すらなく寂しい感じですが、今回はAIを実装したいだけなのでバッサリ割愛しました。
下準備
まずはゲームの全体の流れを作成します。
つまり、バトルの参加者に関する情報の初期化や、
ゲームの勝利条件の判定などのことです。まだ定義していないクラスが多数登場しますので、この後ひとつづつ実装していきます。
Program.csclass Program { public static void Main() { // HPが低いが防御が高い敵と、HPが高く防御が低い敵を作成 // BattleContext.Enemy にどちらを渡すかによって、対戦相手を差し替えることができる var enemy1 = new EnemyBattler() { Hp = 45, Defense = 25, }; var enemy2 = new EnemyBattler() { Hp = 100, Defense = 0, }; // バトルの制御全体にわたって必要になる情報を保持するクラス var context = new BattleContext() { Enemy = enemy1, Player = new PlayerBattler() { Hp = 100, Defense = 0, Skills = new Skill[] { new SingleAttackSkill(87), // ここでスキルの攻撃力を設定 new TripleAttackSkill(39), // ここでスキルの攻撃力を設定 } } }; Console.WriteLine($"プレイヤーのHP:{context.Player.Hp}"); Console.WriteLine($"敵のHP:{context.Enemy.Hp}"); while(true) { context.Player.Act(context); if(context.Enemy.Hp <= 0) { Console.WriteLine("敵は倒れた!"); Console.WriteLine("プレイヤーの勝ち"); return; } context.Enemy.Act(context); if(context.Player.Hp <= 0) { Console.WriteLine("プレイヤーは倒れた!"); Console.WriteLine("敵の勝ち"); return; } } } }次に定義するのは、バトル全体にわたって必要になる機能をまとめる
BattleContext
クラスです。BattleContext.csclass BattleContext { public EnemyBattler Enemy { get; set; } public PlayerBattler Player { get; set; } }独特で多様なスキルをたくさん作るためにも、戦況に関わる情報はなるべくどこからでも書き換えできるように、
BattleContext
のプロパティに押し込めて様々なクラスに受け渡します。敵とプレイヤーの情報が同時に必要になる場面はいくらかあるので、こうして固めておいて、メソッドの引数の定義が簡潔になることを狙っています(パラメータ オブジェクトといいます)。次は、バトルの主役である
EnemyBattler
,PlayerBattler
を定義します。まずはそれらの基底クラスとして、敵にもプレイヤーにもあるHPと防御力を持たせたBattler
クラスを定義します。Battler.csclass Battler { public int Hp { get; set; } public int Defense { get; set; } }そして、
EnemyBattler
クラスを定義します。このクラスは、ターンが回ってきたときの行動を実行するAct
メソッドを持ちます。敵のAIとして、プレイヤーに対して119の固定ダメージを及ぼす攻撃をさせることにします。EnemyBattler.csclass EnemyBattler : Battler { public void Act(BattleContext context) { Console.WriteLine("敵の攻撃"); Console.WriteLine("プレイヤーに 119 のダメージ"); context.Player.Hp -= 119; } }つぎに
PlayerBattler
クラスを定義します。このクラスも敵と同じ役割であるAct
メソッドを持ちますが、行動内容としてスキルを適当に選び、実行します。今回は、持っているスキルから先頭のものを必ず使うようにしましょう。PlayerBattler.csclass PlayerBattler : Battler { public Skill[] Skills { get; set; } public void Act(BattleContext context) { Skills[0].Run(context, context.Enemy); } }詳しくは前述の
Program.cs
に書かれていますが、スキルの配列には次のものを決め打ちで渡します:
- 0番目は一回攻撃のスキルで、威力87
- 1番目は三回攻撃のスキルで、威力39
スキルとは、次のようなクラスです。
Skill.csabstract class Skill { public abstract void Run(BattleContext context, Battler target); }
Skill
には、SingleAttackSkill
,TripleAttackSkill
という2つのバリエーションがあります。SingleAttackSkill
は、敵に一回だけ攻撃するスキルです。SingleAttackSkill.cssealed class SingleAttackSkill : Skill { public int Power { get; private set; } public SingleAttackSkill(int power) { Power = power; } public override void Run(BattleContext context, Battler target) { Console.WriteLine("あなたは狙いを定めて敵を撃ちぬいた!"); var damage = Power - target.Defense; // ダメージ計算 target.Hp -= damage; // 実際にHPを減らす Console.WriteLine($"敵に{damage}のダメージ!"); } }
TripleAttackSkill
は、敵に3回連続で攻撃するスキルです。TripleAttackSkill.cssealed class TripleAttackSkill : Skill { public int Power { get; private set; } public TripleAttackSkill(int power) { Power = power; } public override void Run(BattleContext context, Battler target) { Console.WriteLine("あなたは敵の体へ銃を3連射した!"); var singleDamage = Power - target.Defense; // ダメージ計算 target.Hp -= singleDamage * 3; // 実際にHPを減らす Console.WriteLine($"敵に {singleDamage} のダメージ!"); Console.WriteLine($"敵に {singleDamage} のダメージ!"); Console.WriteLine($"敵に {singleDamage} のダメージ!"); } }ひとまず実行
上記のサンプルでは、
BattleContext.Enemy
プロパティにenemy1
変数の内容を設定してあります。このまま実行すると次のようになります:プレイヤーのHP:100 敵のHP:45 あなたは狙いを定めて敵を撃ちぬいた! 敵に62のダメージ! 敵は倒れた! プレイヤーの勝ち用意した一回攻撃のスキルは威力が
87
で、敵の防御力によって25
軽減されましたが、それでも敵のHP45
を超えるダメージを与えて倒すことができました。
BattleContext.Enemy
プロパティにenemy2
変数の内容を代入するように書き換えてみてください。それを実行すると次のようになります:プレイヤーのHP:100 敵のHP:100 あなたは狙いを定めて敵を撃ちぬいた! 敵に87のダメージ! 敵の攻撃 プレイヤーに 119 のダメージ プレイヤーは倒れた! 敵の勝ち用意した一回攻撃のスキルは威力が
87
で、敵の防御力は0
なのでダメージは減りませんでしたが、それでも敵のHP100
を超えるダメージを与えられなかったので倒しきれず、反撃でやられてしまいました。そこで、
PlayerBattler
の選択するスキルを0番目のスキルではなく1番目のスキルに変えてみるとどうでしょうか。書き換える場所は、PlayerBattler.cs
のAct
メソッドの中です。1番目のスキルには「三回攻撃」が割り当てられているはずです。これで実行してみましょう。プレイヤーのHP:100 敵のHP:100 あなたは敵の体へ銃を3連射した! 敵に 39 のダメージ! 敵に 39 のダメージ! 敵に 39 のダメージ! 敵は倒れた! プレイヤーの勝ち敵に
39*3
のダメージを与え、これはHP100
を超えているので倒すことができました。しかし、いつも三回攻撃のスキルを選べば良いわけではありませんよね。対戦相手を
enemy1
に戻すと次のような結果になります:プレイヤーのHP:100 敵のHP:45 あなたは敵の体へ銃を3連射した! 敵に 14 のダメージ! 敵に 14 のダメージ! 敵に 14 のダメージ! 敵の攻撃 プレイヤーに 119 のダメージ プレイヤーは倒れた! 敵の勝ち三回攻撃スキルは一回のダメージが
39
ですが、敵の防御力15
により軽減され、ダメージは14*3=42
しか与えられませんでした。これだとHP45
を削り切れないので、反撃でやられてしまいました。こうなるようにルールを作ったので、プレイヤーは適切なスキルを考えて選択する必要があるわけです。
AIにやらせたいよね
でも、このように適切なスキルを選ばなければならないのは敵も同じです。敵キャラクターの行動はプレイヤーに選択させるわけにはいかないため、AIでスキルを決定する必要があるはずです。さて、AIに適切なスキルを選ばせるためにはどうすればよいのでしょうか?
攻撃力が高いスキルを選ぶのがよいでしょうか?でも、攻撃回数が多くて攻撃力の低いスキルの方が強いかもしれません。もしかしたら自分に攻撃力アップの状態変化がついているかもしれませんし、ほかにも、このターンは攻撃せずに敵に毒状態などを与えた方がいいのかもしれません。どんなスキルもシンプルな考え方で評価できる方法はないでしょうか?
今回紹介するのは、すべてのスキルに対して、それを使った結果をシミュレーションし、攻撃結果だけを評価する方法です。サンプルプログラムでは、敵側ではなくプレイヤーキャラクターが自動で適切なスキルを選ぶことができるAIを作ってみましょう(それはプレイヤーキャラクターとは言わない気がしますが悪しからず)。
スキルをシミュレーションするAI
今回紹介する方法では、プレイヤーのAIは次のように実装します。引数として、選択肢となるスキルの配列を渡し、戻り値としてそのスキルの配列の中で最も効果的なものを選んで返します。詳細はこの後すぐ説明します。
PlayerAi.csclass PlayerAi { public Skill DetermineSkill(BattleContext context, Skill[] skills) { // 最も優先度が高い候補を保持する変数。より優先度の高いスキルが現れれば、その都度更新される (Skill, int priority) candidate = (null, -context.Enemy.Hp); foreach (var skill in skills) { // A. シミュレーション中に敵が受けるダメージを実際には反映しないためのクローン var clone = new EnemyBattler() { Hp = context.Enemy.Hp, Defense = context.Enemy.Defense }; // B. スキルを実際に適用してみる skill.Run(context, clone); // C. スキルの仕様結果を評価する。 // 敵のHPが少ないほど好ましい状況のはず var priority = -clone.Hp; if (candidate.priority < priority) { candidate = (skill, priority); } } return candidate.Item1; } }このコードについて詳しく見てみましょう。
B. 本当にスキルを適用しているだけ
コメント
B.
のところを見ると、本当にスキルを実行して試していることが分かりますね。A. スキルは敵のコピーに対して使用する
ただし、スキルの対象者として本物の
Enemy
を渡すわけにはいきません。そうしてしまうと、使うべきスキルが確定するころには敵キャラクターは全種類のスキルを喰らった後の満身創痍の状態になってしまい、それはこのゲームではルール違反です。ですので、元のEnemyBattler
のパラメータをコピーした新しいEnemyBattler
を作成します。この2つは完全に別のオブジェクトですので、コピーの方のHPが書き換わっても元のオブジェクトのHPは書き換わりません。このようなコピーを作ることを「クローンする」といいます。「クローン」と「クローンでないもの」の違いは以下のような感じです:
// 元のオブジェクト。 var source = new EnemyBattler() { Hp = 100 }; // 変数 source を変数 notClone に代入しただけ。クローンじゃない。 // この2つの変数は参照先が同じ var notClone = source; // notClone.Hp を書き換えると source.Hp も書き換わってしまう。 notClone.Hp = 99; // 変数 source のメンバー変数の値だけを引き継ぐ新しいオブジェクト。これがクローン。 // この2つの変数は参照先が違う var clone = new EnemyBattler() { Hp = source.Hp }; // clone.Hp を書き換えても、 source.Hp は書き換わらない。 clone.Hp = 50;C. スキルを適用した結果を評価する
スキルを適用したら、実際にどれだけ有効だったかを評価します。最も評価が高かったスキルをAIが実際に使うように制御するわけです。
スキルがどれだけ有効だったか、その評価基準はゲームのルールに依存します。多くのRPGは相手のHPを最も良く削るものを選ぶでしょうし、ひょっとすると、プレイヤーのお金を盗むことが最優先事項である敵キャラなどもいるかもしれません。
今回は、敵のHPを最も削ることができるスキルを選ぶことにしましょう。
B.
でスキルを適用したので、変数clone
の表す敵キャラクターはHPが減っているはずです。そこで、HPの正負を逆転したものをそのまま、そのスキルの優先度としましょう(HPが大きいほど、優先度が下がりますからね)。そして、優先度が最も高いスキルを最後に選ぶのです。変数
canndidate
に、最も高かった優先度とその時のスキルを記録しておき、最後に残ったスキルが最も優先度の高いスキルとなりますので、それがAIの計算結果となります。AIの呼び出し側
PlayerBattler
クラスを以下のように書き換えましょう。PlayerBattler.csclass PlayerBattler : Battler { public Skill[] Skills { get; set; } private readonly PlayerAi ai; public PlayerBattler() { ai = new PlayerAi(); } public void Act(BattleContext context) { var skill = ai.DetermineSkill(context, Skills); skill.Run(context, context.Enemy); } }
PlayerBattler
はPlayerAi
を持ち、使うスキルを決定したいときはこのクラスに依頼します。スキルを発動する部分はほぼ今まで通りですが、前もって決まったスキルを選ぶのではなくAIから返ってきたスキルを呼び出す、という点は今までと異なります。新しいAIを実行
新しいAIを搭載した
PlayerBattler
を戦わせてみましょう。対戦相手をenemy1
にして実行してみます。レイヤーのHP:100 敵のHP:45 あなたは狙いを定めて敵を撃ちぬいた! 敵に62のダメージ! あなたは敵の体へ銃を3連射した! 敵に 14 のダメージ! 敵に 14 のダメージ! 敵に 14 のダメージ! あなたは狙いを定めて敵を撃ちぬいた! 敵に62のダメージ! 敵は倒れた! プレイヤーの勝ち……何かがおかしい気がしますが、最終的にはAIが「1回攻撃」を選択し、敵を倒すことができました。次は対戦相手を
enemy2
にしてみましょう。プレイヤーのHP:100 敵のHP:100 あなたは狙いを定めて敵を撃ちぬいた! 敵に87のダメージ! あなたは敵の体へ銃を3連射した! 敵に 39 のダメージ! 敵に 39 のダメージ! 敵に 39 のダメージ! あなたは敵の体へ銃を3連射した! 敵に 39 のダメージ! 敵に 39 のダメージ! 敵に 39 のダメージ! 敵は倒れた! プレイヤーの勝ち今度は敵の防御力に合わせて「3回攻撃」を選びました。確かに、戦況が最も良くなるスキルを選ぶことができているようです。余裕があれば、新しい対戦相手を追加してみると面白いです。HPが高すぎて倒しきれない相手であっても、可能な限りHPをたくさん削れるスキルを選ぶはずです。
AIの実行中にメッセージが表示されてしまう
もうお気づきかもしれませんが、ここまでの実装だと、スキルのシミュレーション中にメッセージが表示されてしまいます。先ほどの例では、良く見ると1回行動するためにスキル3回ぶんのメッセージが表示されてしまっているのが分かると思います。全てのスキルを試しているので、スキルが2個あればメッセージはスキル(2+1)回ぶん表示されてしまうわけです。
この問題を回避するためには、メッセージの表示先を切り替えられるようにする必要があります。そして、「コンソールに表示する」モードと、「どこにも表示しない」モードを用意したいところです。今回の例だと主な表示先がコンソールでしたが、美麗なグラフィックのコンシューマーゲームだったとしても、スキルのキラキラしたエフェクトがシミュレーション中に全種類再生されたらカッコ悪いですから、やはり「どこにも表示しない」モードは必要になります。
表示先を切り替える機能は、インターフェースを用いたテクニックによってシンプルに実装できます。
メッセージの表示先を差し替えられるようにしよう
インターフェースを用いたやり方を紹介する前に、フラグやメソッドを用いた実装方法について考えてみましょう。
フラグを使った方法はどうか?
今のところ、メッセージを画面に表示するためには
Console.WriteLine
メソッドを使っていますね。Console.WriteLine("あなたは狙いを定めて敵を撃ちぬいた!"); var damage = Power - target.Defense; target.Hp -= damage; Console.WriteLine($"敵に{damage}のダメージ!");これから実装したい「モード切り替え」機能はどのように実装するとよいでしょうか?試しに、
bool
型のフラグを1つ用意して、true
のときはコンソールに表示し、false
のときはどこにも表示しない、と決めたとするとどうなるでしょうか?そのフラグIsShown
は、バトル中のどこからでもアクセスできるつもりのオブジェクトBattleContext
に持たせるとよいでしょう。すると、メッセージを表示する部分は以下のようになります:if (context.IsShown) Console.WriteLine("あなたは狙いを定めて敵を撃ちぬいた!"); var damage = Power - target.Defense; target.Hp -= damage; if (context.IsShown) Console.WriteLine($"敵に{damage}のダメージ!");
Console.WriteLine
を呼び出すかどうかを、BattleContext.IsShown
フラグの状態によって分岐しています。しかしこの方法だと、Console.WriteLine
を呼び出す部分全てでif文を追加しなければなりません。これをゲームの完成までずっと、必ず忘れずに続けるのはなかなかに苦痛です。メソッドを使った方法
先ほどのフラグを使った方法では、if文で分岐をするという処理が繰り返し登場していました。繰り返し登場する処理をメソッドによって共通化するというのはよい考えです。そのメソッドを
BattleContext
クラスに足してみるとどうなるでしょう。そのメソッドは以下のようなものです:BattleContext.cs// 前略 public void Talk(string message) { // isShownというprivateフィールドをBattleContextに追加しておく。 if(isShown) Console.WriteLine(message); } // 後略呼び出し側は以下のようになります:
context.Talk("あなたは狙いを定めて敵を撃ちぬいた!"); var damage = Power - target.Defense; target.Hp -= damage; context.Talk($"敵に{damage}のダメージ!");なかなかすっきりした記述になりましたね。これなら面倒がらずに書くことができそうです。
しかしこの書き方にも問題はあります。
BattleContext
は元々、バトルの制御に必要な情報をまとめるのが責務であり、そのためのプロパティが用意されています。そこにこういった実際に何らかの処理を行うメソッドが追加された場合、そのメソッドが元々あったプロパティに不正な値を代入したりしないよう気を付けなければなりません。今回は単純なメソッドなのでよいかもしれませんが、今後もずっとそうとは限りません。ゲーム開発はどんな仕様が正解なのかがはじめからは定まっていませんから、仕様変更により
BattleContext
の実装の信頼性が少しづつ不安定になっていくかもしれません。インターフェースを使った方法
インターフェースを使って、メッセージの表示先を
Skill
側が意識しなくて済むようにしてみましょう。さしあたっての目標は、以下のようなメソッド呼び出しを:Console.WriteLine("あなたは狙いを定めて敵を撃ちぬいた!");以下のように書き換え、メッセージ データがどのような機能へ流れ着くのかを隠蔽します。
// Talkメソッド自体は表示の作業はせず、あくまでどのような機能へデータを流すかを制御するだけ。 // もはや Console.WriteLine を読んでいるのかどうかを把握することは、呼び出し側の責任ではない context.View.Talk("あなたは狙いを定めて敵を撃ちぬいた!");そのようなインターフェースとして、以下のようなものを定義します。これが「メッセージを表示する機能」を表すインターフェースとなります。
IView.csinterface IView { void Talk(string text); }その実装……つまり「特定の方法でメッセージを表示するクラス」は、「コンソールに表示する」モードと「どこにも表示しない」モードの2つのためのクラスが必要です。
ConsoleView.cs// コンソールに表示するモード class ConsoleView : IView { public void Talk(string text) { Console.WriteLine(text); } }NullView.cs// どこにも表示しないモード class NullView : IView { public void Talk(string text) { // 何もしない } }
IView
インターフェースを実装するオブジェクトは、BattleContext
クラスに持たせることで、バトルの制御コード内のどこからでもアクセスできるようにしましょう。BattleContext
はあくまで情報をまとめる以外の責任は持たず、何か管轄外の要求が来た場合はView
プロパティに設定されたオブジェクトに丸投げするつもりです。BattleContext.csclass BattleContext { public EnemyBattler Enemy { get; set; } public PlayerBattler Player { get; set; } public IView View { get; private set; } // コンストラクター引数から受け取って、読み取り専用プロパティに設定する // View プロパティの内容を後から書き換えることのない設計にするつもりのため public BattleContext(IView view) { View = view; } }そして、
Program.cs
でBattleContext
を生成している部分を書き換えます。IView
を実装するオブジェクトとして、ConsoleView
を生成して渡してあげます。Program.cs(変更前)// 前略 var context = new BattleContext() { Enemy = enemy1, Player = new PlayerBattler() { Hp = 100, Defense = 0, Skills = new Skill[] { new SingleAttackSkill(87), new TripleAttackSkill(39), } } }; // 後略Program.cs(変更後)// 前略 var context = new BattleContext(new ConsoleView()) { Enemy = enemy2, Player = new PlayerBattler() { Hp = 100, Defense = 0, Skills = new Skill[] { new SingleAttackSkill(87), new TripleAttackSkill(39), } } }; // 後略この後は、
Console.WriteLine
を呼び出している部分をcontext.View.Talk
に置き換えていく作業となります。スキルの発動に関係ない部分でも全て置き換えておくことをお勧めしますし、今回は全て置き換えた場合で説明します。なかなか大変な作業ですし、実際の開発ではこういう仕様変更が起きる可能性を考えて前もってインターフェースを用いて差し替えられるようにしておくと良いかもしれません。そうすると良いのは、今回必要になったモードの他にもたとえば「iPhoneで動かすためのモード」「ゲームエンジンを用いてグラフィカルに表示するモード」などの様々な新しい要求が起きても対応できることです。
さて、ここまでの作業だと、動作は何も変わらないはずです。実行してみましょう(このようなリファクタリング作業では、動作が変わっていないことの確認は重要です):
プレイヤーのHP:100 敵のHP:100 あなたは狙いを定めて敵を撃ちぬいた! 敵に87のダメージ! あなたは敵の体へ銃を3連射した! 敵に 39 のダメージ! 敵に 39 のダメージ! 敵に 39 のダメージ! あなたは敵の体へ銃を3連射した! 敵に 39 のダメージ! 敵に 39 のダメージ! 敵に 39 のダメージ! 敵は倒れた! プレイヤーの勝ち次に、
BattleContext
を生成するときにNullView
を渡すようにしてみましょう。Program.cs// 前略 var context = new BattleContext(new NullView()) { Enemy = enemy2, Player = new PlayerBattler() { Hp = 100, Defense = 0, Skills = new Skill[] { new SingleAttackSkill(87), new TripleAttackSkill(39), } } }; // 後略すると、今度は実行しても画面には何も表示しなくなるはずです。
これで下準備ができました。今度は、スキルのシミュレーション中は画面に何も表示せず、実際に発動するときにはちゃんと表示をするようにしたいところです。
この要求を満たすために修正した
PlayerAi
クラスは以下のようになります:PlayerAi.csclass PlayerAi { public Skill DetermineSkill(BattleContext context, Skill[] skills) { (Skill, int priority) candidate = (null, -context.Enemy.Hp); // *修正* シミュレーション中に発動するスキルのメッセージを表示しないようにするためのクローン var cloneContext = new BattleContext(new NullView()) { Enemy = context.Enemy, Player = context.Player }; foreach (var skill in skills) { // シミュレーション中に敵が受けるダメージを実際には反映しないためのクローン // 割愛しているが、実際はプレイヤーのクローンも生成しておいたり、 // 敵のクローンはBattleContext.Enemyなどにもsetしておいたほうが // 独特なスキルをたくさん実装する際に安全 var clone = new EnemyBattler() { Hp = context.Enemy.Hp, Defense = context.Enemy.Defense }; // *修正* BattleContext を渡す場所には、メインの BattleContext ではなく // NullView を持たせてあるクローンのほうの BattleContext を渡す skill.Run(cloneContext, clone); var priority = -clone.Hp; if (candidate.priority < priority) { candidate = (skill, priority); } } return candidate.Item1; } }今回は、
EnemyBattler
だけでなくBattleContext
のクローンも作成しています。BattleContext.Enemy
プロパティとBattleContext.Player
プロパティの中身は元々のBattleContext
の中身を雑に代入していますが、これはクローンになっていないので、このプロパティを経由してHPを変更したりすると元々のBattleContext
に影響が出てしまいます。実際には全てのメンバーについて、その子のメンバー、孫のメンバーというふうに再帰的に潜って完全に切り離されたクローンを作るべきです。そして、シミュレーションのためにスキルを実行する際には、元々の
BattleContext
ではなく、NullView
を持たせてあるクローンの方を渡す必要があります。こうすることによって、スキルのシミュレーションをする時に限って画面への表示を禁止することができます。さあ、この状態で実行してみましょう。対戦相手が
enemy2
ならば、次のようになるはずです。プレイヤーのHP:100 敵のHP:100 あなたは敵の体へ銃を3連射した! 敵に 39 のダメージ! 敵に 39 のダメージ! 敵に 39 のダメージ! 敵は倒れた! プレイヤーの勝ちきちんと適切なスキルを選べていますし、しかもシミュレーション中にスキルを実際に試していることはバレずに済んでいます!お疲れさまでした。
まとめ
総当たりのAIも悪くない
RPGなどにおけるゲームAIを作る際、全てのパターンを試してみる、というのは悪くない方法です。いわゆる「総当たり」というやつです。この方法の問題点は最終的なスキルを決定するまでに時間がかかることですが、それが顕著になるパターンもいくつか考えられます:
- スキルの個数が数十個にも及ぶ場合
- スキルの効果が膨大な数の条件に応じて変動する場合
- 何回も連続で行動でき、スキルを使う順番によっても戦況が大きく変わる場合
- スキルが2個で行動回数2回だったとしても、「使うか使わないか」「順番」によって6パターン試さなければなりません
そのような状態に陥った時には、私の場合は次に、行動パターンをランダムに打ち切る方法を使います。一部のスキルをランダムに、「評価する価値もなく不採用だ」と見なして切り捨てることで、シミュレーションの手間を省きます。時折、非常に強力なスキルを使うのを不意に諦めてしまって妙な感じになるかもしれませんが、強すぎるAIにするとゲームにならないですし、最強のスキルをひたすら撃つ敵ばかりになるとつまらないので、容認することにしています。
他にも色々な最適化方法があるかと思いますが、総当たりの手法を改善して効率的にしたものを使う、という発想はやはり有効と考えています。
インターフェースを使おう
特定の処理を後で差し替えられるようにしたいとき、インターフェースを用いるのはよい方法です。特に今回は、メソッドに切り出して共通化するだけでも実現できましたが、後々の保守のことも考えてあえてインターフェースを用いた切り出し方にしました。それはなぜかというと、
BattleContext
に元々あった機能と、新たに追加された機能のあいだの相互作用に気を配らなければならない可能性を排除するためでした。インターフェースに切り出す作業をすることには、他にも様々な狙いがあります。詳しくは、「SOLID原則」について調べてみてください。
余談:決め打ちのAIという手段もあり
今回紹介したAIの実装はとても汎用的なもので、どんなスキルを用意しても評価方法を準備しておけば、適切なスキルを選ぶことができます。一方で、これから私たちが作ろうとしているゲームは、そんなに複雑なスキルがたくさん登場しないかもしれません。そういったときは、敵キャラクターごとに決まったルールでスキルを選ぶようにしてもよいでしょう。たとえば:
- Nターン目は必ず決まったスキルを選ぶ
- HPが半分以下になったターンだけは通常と違う固定のスキルを選ぶ
- スキルを1つしか持たないので、必ずそれを使う
この方法はシンプルなだけでなく、ゲームデザインがしやすい長所もあります。今回紹介した方法は、「Nターン目は必ず決まったスキルを選ぶ」のような柔軟な行動パターンを作るためには更にひと手間がいる特徴があります。
皆さんのゲームデザインに合ったAIを作ってみてください。
おわり
みんなもRPGつくろうね!
- 投稿日:2019-11-28T14:51:20+09:00
Microsoft Message Analyzerが廃止
一先ず個人用メモ
Microsoft Message Analyzerが廃止に
netsh traceでパケットキャプチャしてWireSharkで解析出来るようにフォーマット変換しようと思ったら、MMAが配信終了してました。ビックリ。
https://docs.microsoft.com/en-us/openspecs/blog/ms-winintbloglp/dd98b93c-0a75-4eb0-b92e-e760c502394f代替案
https://chentiangemalc.wordpress.com/2018/10/08/convert-netsh-trace-etl-to-pcap-with-powershell/
こんなの作ってくれてる人がいたので利用してみたいと思います。
利用したらまた書き直そう(多分)
- 投稿日:2019-11-28T14:12:17+09:00
Windowsで他プロセスを操る
オスロアドベント2日目。3日目も私です。(何個書くんだろ…?)
はじめに
PCでアプリを立ち上げた時の初期設定とかルーチンワークとかめんどくさくないですか?私の場合は特殊な環境にある恋声の初期設定がめんどくさかったです。そこでそれを自動化 + α したのでその際どうやったかを書いていきます。いわゆるRPAですね。(新しいアプリではこの方法を使えないものがあるので注意)
その結果がこれです事前準備
私は、GUIも作りたかったのでC#を使いました。C++でも私の紹介する方法は使えるので参考にしてください。また、アプリケーションを操作するために、アプリがどういう構造をしているか調べるツールを導入する必要があります。以下が開発に使ったものです。
- Visual Studio
- WinSpector(ダウンロードページに飛びます)
自動化
実際の例として恋声を例としてやっていきます。
操作する要素を知る
まず、ボタンをクリックするなら押すボタンを特定しなければなりません。ここで、 WinSpector を使って特定します。
WinSpector を開き、Window をクリックすると現在開いているウィンドウ(とその要素)の一覧が表示されます。
その中から目的のものを見つけ出します。基本的に、階層、要素の名前、クラス名で判断します。日本語は文字化けするので文字数を手掛かりに見つけます。目的のものが見つかったら、クラス名とタイトルを記録しておきましょう。C#側から探すときの手掛かりとなります。名前が無かったらめんどくさいですが、名前がないことを覚えておきましょう。
もし、文字化けしてわからなかったら、以下のように枠で囲ったところから目的の要素までドラッグ&ドロップをしてやると、丸で囲んだところみたいに薄い灰色になるので使ってみるといいです。
C#で要素を取得
それでは、プログラム側から要素を取得していきます。いったん目的のトップウィンドウからすべての子孫要素を取得して、そこから使いまわすという方針でいきます。ここで使うWin32API関数は以下の5つです。、ここから要素のことをウィンドウとか書いたりしますが気にしないでください。
using System.Runtime.InteropServices; /// <summary> /// 指定したクラス名、タイトルを持つ要素のハンドラを取得 /// </summary> /// <param name="lpClassName">指定するクラス名(Winspectorで表示されている)</param> /// <param name="lpWindowName">指定するタイトル(Winspectorで表示されている)</param> /// <returns>指定した要素のハンドル。指定したものが無ければ0が戻る</returns> [DllImport("user32.dll")] private static extern IntPtr FindWindow(string lpClassName, string lpWindowName); /// <summary> /// 指定した子要素を取得。ひとつづつしか取れないので、第二引数でどの子要素を取るか指定 /// </summary> /// <param name="hWnd">親要素のウィンドウハンドラ</param> /// <param name="hwndChildAfter">このハンドラの次の子要素を取得</param> /// <param name="lpszClass">クラス名を指定。nullで全て可。</param> /// <param name="lpszWindow">タイトルを指定。nullで全て可。</param> /// <returns>指定した子要素のハンドル</returns> [DllImport("user32.dll")] private static extern IntPtr FindWindowEx(IntPtr hWnd, IntPtr hwndChildAfter, string lpszClass, string lpszWindow); /// <summary> /// 指定したハンドルのクラス名を取得 /// </summary> /// <param name="hWnd">指定する要素のハンドラ</param> /// <param name="lpClassName">ここにクラス名が返ってくる</param> /// <param name="nMaxCount">文字数の制限</param> /// <returns>返った文字数</returns> [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] private static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount); /// <summary> /// 指定したハンドルのタイトル名の長さを取得 /// </summary> /// <param name="hWnd">指定する要素のハンドラ</param> /// <returns>タイトルの文字数</returns> [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] private static extern int GetWindowTextLength(IntPtr hWnd); /// <summary> /// 指定したハンドルのタイトルを取得 /// </summary> /// <param name="hWnd">指定する要素のハンドラ</param> /// <param name="lpString">ここにタイトルが返ってくる</param> /// <param name="nMaxCount">文字数制限</param> /// <returns>返った文字数</returns> [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] private static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);ここで出てくる
[DllImport("user32.dll")]
というのは Win32API の関数を使いますよといった合図みたいなものです。
操作するのに使いそうな情報を集めた以下のような構造体かクラスがあると便利です。class Window { public string ClassName; public string Title; public IntPtr hWnd; }それでは以上のことを組み合わせて、指定の要素と、その子要素すべてを列挙したリストを返す関数を作ります。
using System.Linq; /// <summary> /// 指定の要素と、その子要素すべてを列挙したリストを返す /// </summary> /// <param name="parent">指定する要素</param> /// <param name="dest">元々あるリスト</param> /// <returns>parentとその子要素をdestに追加したリスト</returns> public static List<Window> GetAllChildWindows(Window parent, List<Window> dest) { dest.Add(parent); EnumChildWindows(parent.hWnd).ToList().ForEach(x => GetAllChildWindows(x, dest)); return dest; } private static IEnumerable<Window> EnumChildWindows(IntPtr hParentWindow) { IntPtr hWnd = IntPtr.Zero; while ((hWnd = FindWindowEx(hParentWindow, hWnd, null, null)) != IntPtr.Zero) { yield return GetWindow(hWnd); } } public static Window GetWindow(IntPtr hWnd) { int textLen = GetWindowTextLength(hWnd); string windowText = null; if (0 < textLen) { StringBuilder windowTextBuffer = new StringBuilder(textLen + 1); GetWindowText(hWnd, windowTextBuffer, windowTextBuffer.Capacity); windowText = windowTextBuffer.ToString(); } StringBuilder classNameBuffer = new StringBuilder(256); GetClassName(hWnd, classNameBuffer, classNameBuffer.Capacity); return new Window() { hWnd = hWnd, Title = windowText, ClassName = classNameBuffer.ToString() }; }こうしてやると、以下のようにして先ほどのスクショにあったボタンの要素は取得できます。
using System.Threading; using System.Linq; IntPtr koigoe = Process.GetProcessesByName("koigoe")[0].MainWindowHandle; List<Window> all = GetAllChildWindow(koigoe, new List<Window>()); Window button = all.Where(x.ClassName == "Button" && x.Title == "OPEN");取得した要素に対して操作する
操作する要素を取得できたので、今度はそれを操作していきます。やばそうに聞こえますがそこまでやばくありません。まあ、私の場合これで恋声をバグらせてしまって何回か設定をぶっ飛ばしてしまいましたが。
要素への操作は以下の Win32API 関数で全て行えます。using System.Runtime.InteropServices; /// <summary> /// プロセス間通信。メッセージを送る。 /// </summary> /// <param name="hWnd">送り先の要素のハンドラ</param> /// <param name="Msg">メッセージの種類</param> /// <param name="wParam">メッセージの中身1</param> /// <param name="lParam">メッセージの中身2</param> /// <returns>結果</returns> [DllImport("user32.dll")] private static extern int SendMessage(IntPtr hWnd, uint Msg, uint wParam, uint lParam);この関数に適切な引数を与えて使えば、全て操作を行うことができます。以下の表が私が実際に使った組み合わせです。
内容 Msg wParam lParam マウスの左を押す 0x0201 0x00000001 0x000A000A マウスの左を離す 0x0202 0x00000000 0x000A000A コンボボックスを指定の
インデックスに設定0x014E 指定の
インデックス0x00000000 参考記事のところに一覧がのったページがあります。
高負荷の罠
高負荷時に、ウィンドウを開く・閉じる等やや重い動作があることをすると、その後の
SendMessage()
がうまく動作しないことがあります。このせいで成果物がかなり不安定になりました。(今は解消しています)その対処法を書きます。安定化のすすめ
問題は、ウィンドウを開閉するときにラグが生じるというとこです。なので、ウィンドウが安定するまで待てば安定するはず!→安定しました。
恋声は、2種類でウィンドウを制御していたので Win32API を2つ使います。using System.Runtime.InteropServices; /// <summary> /// ハンドラがさすウィンドウが存在するか /// </summary> /// <param name="hWnd">確かめるハンドラ</param> /// <returns>存在するならtrue</returns> [DllImport("user32.dll")] public static extern bool IsWindow(IntPtr hWnd); /// <summary> /// 指定したウィンドウの指定した属性を調べる /// </summary> /// <param name="hWnd">ウィンドウのハンドラ</param> /// <param name="nIndex">属性指定</param> /// <returns>結果</returns> [DllImport("user32.dll")] public static extern uint GetWindowLong(IntPtr hWnd, int nIndex);恋声では、ウィンドウを生成・破棄するタイプと、 style の visible を制御するタイプの二種類がありました。前者は大丈夫だと思いますが、後者のやり方だけ書きます。
using System.Runtime.InteropServices; using System; if(GetWindowLong(some_hWnd, -16) % 0x20000000 / 0x10000000 == 1){ Console.WriteLine("visibleだよ"); } else{ Console.WriteLine("visibleじゃないよ"); //ifの計算の結果は0となっている。 }おわりに
今回紹介したものは古いし、簡単にできるRPAツールが最近出てきているので枯れた技術かなぁと思いつつも複雑なことをやったり、他のことに組み合わせたりするのに使えるのかなぁと思ったりしてます。ではよい自動化ライフを。
参考記事
- 投稿日:2019-11-28T11:38:00+09:00
【Unity(C#),Python】API通信勉強メモ③簡易版ログイン機能の実装
今回やること
Unity側から入力した情報を登録 & ログインする機能を作ります。
前回同様、Flaskでローカルにアプリケーションサーバーを立てて利用します。【前回】:【Unity(C#),Python】API通信勉強メモ②Flaskでローカルサーバー立ち上げ
なんもわからんなりの解釈が山盛りなのでマサカリ、オールオッケーです。
特にセキュリティ面に関してはエンジニアと名乗るのが恥ずかしいくらい疎いので
超巨大マサカリで一刀両断してもらってもしっかりと受け止めます。実際に作成したもの
ID、パスワードを入力してのアカウント登録が行えて、
ログイン画面で実際にログインっぽいことが可能です。行っていることのイメージです。
DBに情報が保存されているので、Editorを閉じてもアカウント情報は消えません。(たぶん)
Unity側
Unity側が行う処理としては入力した情報をローカルサーバーに送って、
レスポンスに応じてテキストを表示するだけです。登録ボタンに関する処理using System.Collections; using UnityEngine.Networking; using UnityEngine; using System.Text; using UnityEngine.UI; public class RegistraionHTTPPost : MonoBehaviour { [SerializeField, Header("LogText")] Text m_logText; [SerializeField, Header("IDInputField")] InputField m_idInputField; [SerializeField, Header("PassInputField")] InputField m_passInputField; //接続するURL private const string RegistrationURL = "http://localhost:5000/registration"; //ゲームオブジェクトUI > ButtonのInspector > On Click()から呼び出すメソッド public void Registration() { StartCoroutine(RegistrationCoroutine(RegistrationURL)); } IEnumerator RegistrationCoroutine(string url) { //POSTする情報 WWWForm form = new WWWForm(); form.AddField("user_id", m_idInputField.text, Encoding.UTF8); form.AddField("password", m_passInputField.text, Encoding.UTF8); //URLをPOSTで用意 UnityWebRequest webRequest = UnityWebRequest.Post(url, form); //UnityWebRequestにバッファをセット webRequest.downloadHandler = new DownloadHandlerBuffer(); //URLに接続して結果が戻ってくるまで待機 yield return webRequest.SendWebRequest(); //エラーが出ていないかチェック if (webRequest.isNetworkError) { //通信失敗 Debug.Log(webRequest.error); m_logText.text = "通信エラー"; } else { //通信成功 Debug.Log("Post"+" : "+webRequest.downloadHandler.text); m_logText.text = webRequest.downloadHandler.text; } } }ローカルサーバーに対して情報を送る処理は下記箇所が担っています。
ローカルサーバー側が受け取る情報として
form
にuser_id
、password
などをリクエスト情報として追加しています。リクエスト時に送る情報//POSTする情報 WWWForm form = new WWWForm(); form.AddField("user_id", m_idInputField.text, Encoding.UTF8); form.AddField("password", m_passInputField.text, Encoding.UTF8);詳細に理解できてはいませんが、
form
というのはリクエストの種類(POST,GETなど)に加えて、何かしらの情報を渡せるもののようです。
WWWForm
はPOST専用のクラスです。ローカルのアプリケーションサーバー(Flask)
こっちは本当に難しくて、
そもそも私は何をやればいいんだろうという状態が長く続いてしんどかったです。まずはアカウント情報を登録する上でDB(データベース)というものを利用する必要があるとわかりました。
DBって何?
データベース(英: database, DB)とは、検索や蓄積が容易にできるよう整理された情報の集まり。 通常はコンピュータによって実現されたものを指すが、紙の住所録などをデータベースと呼ぶ場合もある。コンピュータを使用したデータベース・システムでは、データベース管理用のソフトウェアであるデータベース管理システムを使用する場合も多い。
【引用元】:ウィキペディア(Wikipedia)
データベースってのはソフトウェアのことらしいです。
そのデータはどこにあってどういう仕組みで成り立っているのか完全に理解するために深堀りすると、
帰ってこられなくなるって偉い人が言ってたので深くは考えません。
【参考リンク】:そもそもデータベースって何で出来ていて、どこの何にどう保存されるのでしょうか。。DBにもいろいろと種類があって、今回利用するのはRDB(リレーショナルデータベース)っぽいです。
SQLAlchemy
データベースを実際に操作するにはSQLという言語を用いるのですが、それをPython内からやってくれる、というライブラリ
【引用元】:はじめての Flask #4 ~データベースをSQLAlchemyでいじってみよう~
だそうです。便利ですね~。今回はこちらを使います。
実装
いよいよFlask及びDBの実装です。
import hashlib from flask import * from sqlalchemy import create_engine, Column, String, Integer from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker, scoped_session app = Flask(__name__) engine = create_engine('sqlite:///user.db') Base = declarative_base() # DBの設定 class User(Base): __tablename__ = 'users' id = Column(Integer, primary_key=True, unique=True) user_id = Column(String) password = Column(String) Base.metadata.create_all(engine) SessionMaker = sessionmaker(bind=engine) session = scoped_session(SessionMaker) # DBにIDとパスワード登録する @app.route("/registration", methods=["POST"]) def registration(): user = request.form["user_id"].strip() check_users = session.query(User).filter(User.user_id == user).all() if check_users: return "そのユーザー名は使用済みです" else: user = User(user_id=request.form["user_id"], password=str(hashlib.sha256( request.form["password"].strip().encode("utf-8")).digest())) session.add(user) session.commit() return str(user.user_id.strip() + "様\nご登録ありがとうございます") # ログインできるID、パスワードの組合わせかどうかDBを見て照合 @app.route("/login", methods=["POST"]) def login_check(): user = request.form["user_id"].strip() check_users = session.query(User).filter(User.user_id == user).all() try: for login_user in check_users: login_user_pass = login_user.password if login_user_pass == str(hashlib.sha256( request.form["password"].strip().encode("utf-8")).digest()): return "ログイン完了です" else: return "パスワードが異なります" except: return "登録情報が異なります" if __name__ == "__main__": app.run(debug=True) # User.__table__.drop(engine) # テーブル削除用テーブルの消し方
下記箇所のコメントアウトを解除して動かせば消えます。
User.__table__.drop(engine) # テーブル削除用Pylintと仲良くする
VSCでpyhtonを書いているのですが、FlaskがPylintと仲良くしてくれませんでした。
そのため、Setting.jsonを開いて下記設定を追記しました。
"python.linting.pylintArgs": [ "--load-plugins", "pylint_flask" ],ハッシュ化
今回、ハッシュ化してセキュリティ対策もばっちりだぜ!ってのをやってみたかったんですが、
現状、②の箇所しかできていないので全く意味がないような気がしてます。
POSTを使うだけではセキュリティ不十分だよ~って記事がいっぱい出てくるので
やりとりする情報は全てハッシュ化しないとダメなのかな~と勝手に思ってます。
この辺り、詳しく知ってる方いたら教えてください。Basic認証とDigest認証
セキュリティうんぬんを調べている際に知りました。
認証にも種類があるそうです。Digest認証はセキュリティの観点でBasic認証より優れています。しかし、すべての環境に対応しているわけではありません。ページを利用するユーザーの環境がある程度分かっていて、対応しているブラウザを使っている場合には問題はありません。しかし、不特定多数のユーザーに向けたページで設定をする場合、Digest認証は向いていません。
一方Basic認証はセキュリティ面でDigest認証に劣っています。しかし、あらかじめセキュリティ対策が行われている環境下、例えばSSLやローカルネットワーク内などで利用する分には特に問題はないでしょう。ユーザーの環境にも左右されません。
このように、不特定多数のユーザーが使うページにユーザー認証を設定する場合はSSLと合わせたBasic認証、管理者など接続する環境が特定されている場合にはDigest認証、と状況によって使い分けるのが一般的です。
【引用元】:Basic認証(基本認証)とDigest認証、それぞれの役割と違いについて
今回実装したものはBasic認証と呼ばれるものに該当するのでしょうか。
よくわかりませんので、"お前が作ったのはどちらでもない"とかでいいので知りたいです。参考リンク
- 投稿日:2019-11-28T06:24:36+09:00
PHPでC#アプリケーションとデータをやり取りするためのAPIを作ってみる
はじめに
当記事では、C#アプリケーションとデータをやり取りするAPIを作成して実際に動かしてみます。
C#アプリケーションから、ライブラリを使用して直接データベースに接続することも可能ですが、
C#等の高級言語は「リバースエンジニアリングしやすい」といった特徴や、そもそも外部からの接続を許してしまうデータベースはセキュアではないので、基本的にはAPIを通す必要があります。環境
Visual Studio 2019 Professional
Windows 10 1903
.NET Framework: 4.7.2
XAMPP: 7.3.7
PHP: 7.3.7
Apache: 2.4.39
MariaDB: 10.3.16データベース
テーブルを新規作成します。
今回は例として以下の構造で作成しました。
PHP側
今回作成するのは以下3つのファイルです。
class.php database.php index.phpPHPのフルソースコードは以下の通りです。
今回はあくまで一例として行いますので、複数のデータが見つかった場合や例外エラーの処理は一切していません。ソースコード
database.php<?php class database{ public $db; private $db_host = "localhost"; private $db_user = "root"; private $db_password = ""; private $db_name = "testdb"; public function connect() { $this->db = new PDO("mysql:host=" . $this->db_host . ";dbname=" . $this->db_name, $this->db_user, $this->db_password); if(!$this->db){ die("Failed to connect"); } } } ?>class.php<?php include "database.php"; class _main_ extends database { public function __construct() { $this->connect(); } public function InsertData($data) { if (empty($data)) { die('{"result":"failed"}'); } else { $query = $this->db->prepare("INSERT INTO data_table (data) VALUES ('$data')"); $query->execute(); die('{"result":"success"}'); } } public function GetData() { $query = $this->db->prepare("SELECT * FROM data_table LIMIT 1"); $query->execute(); $result = $query->fetch(PDO::FETCH_ASSOC); if (isset($result)) { $data = $result["data"]; die('{"result":"success", "data":"' . $data . '"}'); } else { die('{"result":"failed"}'); } } } ?>index.php<?php //ini_set( 'display_errors', 1 ); //error_reporting(E_ALL); include "class.php"; $main = new _main_; if(isset($_POST['type'])) $Type = $_POST['type']; if(isset($_POST['data'])) $Data = $_POST['data']; if(isset($Type)) { switch (strip_tags($Type)) { case "t_insert_data": $main->InsertData($Data); break; case "t_get_data": $main->GetData(); break; } } ?>C#側
.NET Frameworkを使用します。
Jsonを扱うので、Newtonsoft Jsonを予めNuGetでインストールして下さい。
ソースコード
using(WebClient wc = new WebClient()) { }
としているのは、必ずリソースが破棄されるからです。
NameValueCollection
に値を格納し、それを渡す感じです。
Encoding.Default.GetString(wc.UploadValues(URL, Values));
でPOSTし、返ってきた値を関数の返り値としています。
一応、WebException
はキャッチします。Handle.csusing Newtonsoft.Json; using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; using System.Net; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace datatest { class Handler { //ポスト先のURL public static string URL { get; set; } //コンストラクタ public Handler() { } public static string DoPost(NameValueCollection Values) { try { using(WebClient wc = new WebClient()) { return Encoding.Default.GetString(wc.UploadValues(URL, Values)); } } catch (WebException e) { MessageBox.Show(e.Message); Dictionary<string, object> ERROR = new Dictionary<string, object>(); HttpWebResponse response = (HttpWebResponse)e.Response; switch (response.StatusCode) { case HttpStatusCode.NotFound: ERROR.Add("result", "net_error_not_found"); break; case HttpStatusCode.RequestEntityTooLarge: ERROR.Add("result", "net_error_request_entry_too_large"); break; case HttpStatusCode.ServiceUnavailable: ERROR.Add("result", "net_error_service_unavailable"); break; case HttpStatusCode.Forbidden: ERROR.Add("result", "net_error_forbidden"); break; default: ERROR.Add("result", "net_error_unknown" + Environment.NewLine + e.Message); break; } return JsonConvert.SerializeObject(ERROR); } } } }フォームがロードされたタイミングでハンドラーのURLをセットし、
NameValueCollection
に各値を格納してHandler.DoPost()
するだけです。
Stringとして帰ってくるので、JObject.Parse()
でパースする必要があります。Form1.csusing Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace datatest { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void Form1_Load(object sender, EventArgs e) { Handler.URL = "http://localhost/api/index.php"; } private static void InsertData(string data) { var values = new NameValueCollection(); values["type"] = "t_insert_data"; values["data"] = data; string result = Handler.DoPost(values); if (result != "") { var jobj = JObject.Parse(result); switch ((string)jobj["result"]) { case "success": MessageBox.Show("Success!"); break; case "failed": MessageBox.Show("Failed!"); break; default: MessageBox.Show("Unknown error!"); break; } } } private static void GetData() { var values = new NameValueCollection(); values["type"] = "t_get_data"; string result = Handler.DoPost(values); MessageBox.Show(result); if (result != "") { var jobj = JObject.Parse(result); switch ((string)jobj["result"]) { case "success": MessageBox.Show("Success!" + Environment.NewLine + "Data: " + (string)jobj["data"]); break; case "failed": MessageBox.Show("Failed!"); break; default: MessageBox.Show("Unknown error!"); break; } } } private void BtnInsert_Click(object sender, EventArgs e) { InsertData("SETO_KOUJI"); } private void BtnGet_Click(object sender, EventArgs e) { GetData(); } } }実行結果
無事データのやり取りを行うことができました。
このとき、データベースの中身は以下のようになっています。正しい値を取得できていますね。
最後に
今回は、C#アプリケーションとデータベースでのやり取りにAPIを使用してみました。
APIを使用しても、肝心の通信の中身が暗号化されていなければセキュアであるとは言えないですし、中間者攻撃も容易にできてしまいます。
実際に実装して運用する際は十分な注意を払う必要がありますね。通信内容の暗号化に関しては、セッションを利用して共通鍵方式で暗号化するのも手段の一つだと思います。
- 投稿日:2019-11-28T03:41:54+09:00
Unityでゲームを作り始めた
作ろうとしてるゲーム
2Dのシューティングゲームを作ろうと思う
使用環境
Windows10 Home
Intel Core i7-9700K
Unity 2019.2.11f1 Personal
Visual Studio 2019
Android Studio
この投稿の主な目的
自分が作ったゲームを広めるためと、改善のための意見がほしい。
ゲーム制作において解決したことなども書いていければと思っている。
基本的なことをおさらいするというのはネットに記事がいっぱいあるだろうからこの投稿ではやらない。
ではさっそくPrefabから元のオブジェクトへの戻し方
赤矢印のNormal_EnemyというPrefabがあったときに、ヒエラルキーのCreate Emptyで空のオブジェクトを作成する。
Normal_Enemyをさっき作った空のオブジェクトの子オブジェクトにする。
空のオブジェクトをプロジェクトウィンドウにドラック&ドロップすることでPrefabにする。
先ほどプロジェクトウィンドウに置いたPrefabを削除するとヒエラルキーのGameObjectとNormal_Enemyの文字が茶色になるので、子オブジェクトにしていたNormal_Enemyを元に戻してGameObjectを削除すると戻すことができる。
うまくいかないことがおきた
プログラム内で判定させているのに、赤い丸の敵オブジェクトが範囲から出てしまう。
おそらく単なるミスだと思うけど......とおもったらわかった
このオブジェクトは180度回転させてるからtransform.Translateで移動させようとするとx方向とy方向が逆になってしまうからおかしくなってしまっていた。
なのでこのオブジェクトを移動させるときは
transform.Translate(-x,y,0)としなければならない。今日はここまでにする。
- 投稿日:2019-11-28T02:53:07+09:00
Commonクラスを自動で生成する仕組みを作ると人生が楽になる【Unity】
UnityでC#スクリプトを生成した後毎回デフォルトで書かれているコードを決まったネームスペースやregionなど、チームのルールに沿って書き直す経験はありませんか。毎回同じコードを書くのは手間ですし、うっかりルールを守れていない状態で処理を書いていたなんてこともありますよね。
この問題は自分でテンプレートをカスタマイズする事で解決する事ができます。
1. UnityEditorをダウンロードしたパスからScriptTemplatesを探す
2. 81-C# Script-NewBehaviourScript.cs.txt を好みで書き換える
using System.Collections; using System.Collections.Generic; using UnityEngine; public class #SCRIPTNAME# : MonoBehaviour { // Start is called before the first frame update void Start() { #NOTRIM# } // Update is called once per frame void Update() { #NOTRIM# } }あとはUnityでスクリプト生成すると
上記で書き換えた通りにコードが書き換えられていると思います。using System.Collections; using System.Collections.Generic; using UnityEngine; namespace Common { /// <summary> /// /// </summary> public class NewBehaviourScript : MonoBehaviour { #region 定数 #endregion #region 変数 #endregion #region プロパティ #endregion #region ライフサイクル void Awake() { } void Start() { } void Update() { } void LateUpdate() { } #endregion } }おわりに
書き換えるだけでなくテンプレートを追加することもできます。
- 投稿日:2019-11-28T02:24:50+09:00
【C#】時間を出力する
プログラムの意味
ほんだのばいくを見ていたら、ふと目をやると、コメント欄で、この動画の見どころとして動画の長さを毎秒書いている人がいたんで、そういう人向けのプログラムを書いたお。
https://www.youtube.com/watch?v=6oYhz4OUVvcusing System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.IO; namespace TimeSeriesWriter { class Program { static void Main(string[] args) { try { string[] timeSeparate = Console.ReadLine().Split(':'); Console.Clear(); int timeHour = 0; int timeMinute = 0; int timeSecond = 0; if (timeSeparate.Count() == 3) { timeHour = int.Parse(timeSeparate[0]); timeMinute = int.Parse(timeSeparate[1]); timeSecond = int.Parse(timeSeparate[2]); } else if (timeSeparate.Count() == 2) { timeMinute = int.Parse(timeSeparate[0]); timeSecond = int.Parse(timeSeparate[1]); } else { timeSecond = int.Parse(timeSeparate[0]); } string minute = null; string second = null; for (int i = 0; i <= timeMinute; i++) { minute = i.ToString(); int count = 0; for (int j = 0; j < 60; j++) { if (i == timeMinute && j > timeSecond) { break; } count++; second = j.ToString(); if (j < 10) { second = string.Concat("0", second); } Console.Write(minute + ":" + second + " "); if (count % 7 == 0) { Console.Write("\n"); } } Console.Write("\n"); } Console.ReadKey(true); } catch (Exception e) { Console.WriteLine(e.Message); Console.ReadKey(true); } } } }出力結果
0:00 0:01 0:02 0:03 0:04 0:05 0:06 0:07 0:08 0:09 0:10 0:11 0:12 0:13 0:14 0:15 0:16 0:17 0:18 0:19 0:20 0:21 0:22 0:23 0:24 0:25 0:26 0:27 0:28 0:29 0:30 0:31 0:32 0:33 0:34 0:35 0:36 0:37 0:38 0:39 0:40 0:41 0:42 0:43 0:44 0:45 0:46 0:47 0:48 0:49 0:50 0:51 0:52 0:53 0:54 0:55 0:56 0:57 0:58 0:59 1:00 1:01 1:02 1:03 1:04 1:05 1:06 1:07 1:08 1:09 1:10 1:11 1:12 1:13 1:14 1:15 1:16 1:17 1:18 1:19 1:20 1:21 1:22 1:23 1:24 1:25 1:26 1:27 1:28 1:29 1:30 1:31 1:32 1:33 1:34 1:35 1:36 1:37 1:38 1:39 1:40 1:41 1:42 1:43 1:44 1:45 1:46 1:47 1:48 1:49 1:50 1:51 1:52 1:53 1:54 1:55 1:56 1:57 1:58 1:59 2:00 2:01 2:02 2:03 2:04 2:05 2:06 2:07 2:08 2:09 2:10 2:11 2:12 2:13 2:14 2:15 2:16 2:17 2:18 2:19 2:20 2:21 2:22 2:23 2:24 2:25 2:26 2:27 2:28 2:29 2:30 2:31 2:32 2:33 2:34 2:35 2:36 2:37 2:38 2:39 2:40 2:41 2:42 2:43 2:44 2:45 2:46 2:47 2:48 2:49 2:50 2:51 2:52 2:53 2:54 2:55 2:56 2:57 2:58
- 投稿日:2019-11-28T00:43:36+09:00
ref readonly(inパラメーター修飾子)からReadOnlySpanを構築する
C# 7.2から追加された読み取り専用参照(
ref readonly T
)ですが、残念ながらそのままReadOnlySpan<T>
を構築出来ません。読み取り専用ではない通常の参照(
ref T
)であれば、Span<T>
をMemoryMarshal.CreateSpan
で構築出来ますが、ReadOnlySpan<T>
を構築するMemoryMarshal.CreateReadOnlySpan
の引数はref T
となっているため、読み取り専用参照(ref readonly T
)からは構築出来ません。
また、.NET Standard 2.0ではMemoryMarshal.CreateReadOnlySpan
がありません。ReadOnlySpanを構築する方法
読み取り専用参照から通常の参照を得るため、
System.Runtime.CompilerServices.Unsafe.AsRef(in reference)
を使用します。
NugetでSystem.Runtime.CompilerServicesを追加する必要があります。.NET Standard 2.0 向け
MemoryMarshal.CreateReadOnlySpan
が無いため、ポインターからReadOnlySpan<T>
を構築します。
Unsafeの許可が必要になります。netstandard2.0public static ReadOnlySpan<T> CreateReadOnlySpan<T>(in T reference, int length) where T : unmanaged { unsafe { return new ReadOnlySpan<T>(Unsafe.AsPointer(ref Unsafe.AsRef(in reference)), length); } }.NET Standard 2.1 向け
netstandard2.1public static ReadOnlySpan<T> CreateReadOnlySpan<T>(in T reference, int length) where T : unmanaged { return MemoryMarshal.CreateReadOnlySpan(ref Unsafe.AsRef(in reference), length); }他のユーティリティメソッド
オマケです。
Unsafeクラスのメソッドはほぼ通常の参照(ref T
)しか引数に取らないため、読み取り専用参照を扱うメソッドを用意してみました。public static class ReadOnlyRefUnsafe { [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ReadOnlySpan<T> CreateReadOnlySpan<T>(in T reference, int length) where T : unmanaged { #if BEFORE_NET_STANDARD21 unsafe { return new ReadOnlySpan<T>(Unsafe.AsPointer(ref Unsafe.AsRef(in reference)), length); } #else return MemoryMarshal.CreateReadOnlySpan(ref Unsafe.AsRef(in reference), length); #endif } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ref readonly T Add<T>(in T source, int elementOffset) => ref Unsafe.Add(ref Unsafe.AsRef(in source), elementOffset); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ref readonly T Add<T>(in T source, IntPtr elementOffset) => ref Unsafe.Add(ref Unsafe.AsRef(in source), elementOffset); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ref readonly T AddByteOffset<T>(in T source, IntPtr byteOffset) => ref Unsafe.AddByteOffset(ref Unsafe.AsRef(in source), byteOffset); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool AreSame<T>(in T left, in T right) => Unsafe.AreSame(ref Unsafe.AsRef(in left), ref Unsafe.AsRef(in right)); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ref readonly TTo As<TFrom, TTo>(in TFrom source) => ref Unsafe.As<TFrom, TTo>(ref Unsafe.AsRef(in source)); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static IntPtr ByteOffset<T>(in T origin, in T target) => Unsafe.ByteOffset(ref Unsafe.AsRef(in origin), ref Unsafe.AsRef(in target)); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsAddressGreaterThan<T>(in T left, in T right) => Unsafe.IsAddressGreaterThan(ref Unsafe.AsRef(in left), ref Unsafe.AsRef(in right)); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsAddressLessThan<T>(in T left, in T right) => Unsafe.IsAddressLessThan(ref Unsafe.AsRef(in left), ref Unsafe.AsRef(in right)); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static T Read<T>(in byte source) => As<byte, T>(in source); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ref readonly T Subtract<T>(in T source, int elementOffset) => ref Unsafe.Subtract(ref Unsafe.AsRef(in source), elementOffset); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ref readonly T Subtract<T>(in T source, IntPtr elementOffset) => ref Unsafe.Subtract(ref Unsafe.AsRef(in source), elementOffset); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ref readonly T SubtractByteOffset<T>(in T source, IntPtr byteOffset) => ref Unsafe.SubtractByteOffset(ref Unsafe.AsRef(in source), byteOffset); }
- 投稿日:2019-11-28T00:15:37+09:00
C# - Windows常駐アプリ(タスクトレイ) - Form表示なし
タスクトレイにアプリを置く方法
参考サイト#1をベースに作成(
ほぼそのまま)。アイコンを準備する必要があるので、それ用のソフトを持っていない・フリーソフトを入れたくない場合は、手前味噌ですが下記あたりで適当に対応ください。
- https://qiita.com/kob58im/items/850da88ebed27c05022a
- https://qiita.com/kob58im/items/6f319988e3cdb745cf88
- https://qiita.com/kob58im/items/1d3420b51ff6cb5c3871サンプルコード
using System; using System.Drawing; using System.Windows.Forms; class TaskTrayTest : Form { TaskTrayTest() { this.ShowInTaskbar = false; NotifyIcon icon = new NotifyIcon(); icon.Icon = new Icon("Output.ico"); // Note: この行を省略するとタスクトレイにアイコンが表示されない。 icon.Visible = true; icon.Text = "常駐テスト"; var menu = new ContextMenuStrip(); var menuItem = new ToolStripMenuItem(); menuItem.Text = "E&xit"; menuItem.Click += (sender,e)=>{Application.Exit();}; menu.Items.Add(menuItem); icon.ContextMenuStrip = menu; } [STAThread] static void Main(string[] args) { Console.WriteLine("Entring Main()."); new TaskTrayTest(); Console.WriteLine("Instance is created."); Application.Run(); Console.WriteLine("Exit Main()."); } }正直、原理がよくわからない
■分かっていること
-NotifyIcon
が、タスクトレイにアイコンを表示させるためのクラス。(Microsoft Docs参照1)
-Application.Run()
が、メッセージループを回している。(Microsoft Docs参照)■気を付けるべきこと
- タスクトレイからFormを表示させたりする場合は、アプリの終了のさせ方に気を付ける必要がありそう。参考サイト3参照。■よくわからないこと(未確認)
-NotifyIcon
のやっていること。(ILSpyでみてみようとしたが難しそう)
-Application.Run
で回しているメッセージループのメッセージは誰が受け取っているのか?誰も受け取っていない?
- タスクトレイに置くだけなら、Form
要らんかも。参考サイト
- C#: タスクトレイに常駐するアプリの作り方
- フォームを表示させずにトレイアイコンを表示する - dobon.net
- アプリケーション(自分自身)を終了させる - dobon.net
- 通知領域のアイコン情報のレジストリについて
スタートアップへの登録
参考サイト
まだ試せていない
[C#] スタートアップメニューにショートカットを登録する方法
日本語版は機械翻訳が酷かったので英語のほうにしてます。 ↩