RPGツクールMVで効果音(SE)が鳴っているかどうかの判定および特定の効果音だけを停止する方法 他
RPGツクールMVいじり二日目です。昨日の記事の続きです。
結論から言うと、効果音(SE)が鳴っているかどうかの判定はできませんでしたが、
やりたいこと(効果音を多重で再生せずループさせる)はできました。
RPGツクールMVの内情はちんぷんかんぷんなので、誤りや慣習などはご教示いただければと思います。
効果音(SE)が鳴っているかどうかの判定
冒頭でも言った通り、うまく動作しなかったのですが、経緯だけご紹介します。
まずrpg_managers.jsのファイルの先頭あたりに以下の一文を追加しておきます。
const path = require('path');
rpg_managers.jsに以下のメソッドを追加します。
AudioManager.isSEPlaying = function(sename) { if (sename) { var seBuffers = this._seBuffers.filter( function(audio) { var audioname = path.basename(audio.url, path.extname(audio.url)); return ((audioname === sename) && audio.isPlaying()) }); return (seBuffers.length != 0) } else { return false } };
そのまんまなのですが、SE名が一致し、かつ再生中のエントリを
管理配列であるthis._seBuffersから検索して取得しています。
1つでも見つかれば再生中、そうでなければ停止中と判定しています。
あまりわかっていませんが実機検証の結果、効果音(SE)再生直後などは、実際には再生が開始されていてもこの結果がfalseになることがあるみたいで、再生し始めのときに不当にfalseが数回返る→数回分は効果音が被って再生されてしまう…という結果になってしまいました。これを回避する方法は、サウンドバッファのシーク位置とかを見ればいけそうな気がしますがめんどくさくて見つからなかったということにしてます。それよりも、後述する方法を使えばもっとシンプルにやりたいことが実現できそうでした。
効果音(SE)を二重以上で被らずに再生させる方法
単純に、WebAudioオブジェクトのplay系関数の変数にtrueを渡すだけでうまくいきそうな気がしました。
rpg_managers.jsに以下の内容を追加します。
AudioManager.playSeLoop = function(se) { if (se.name) { this._seBuffers = this._seBuffers.filter(function(audio) { return audio.isPlaying(); }); var buffer = this.createBuffer('se', se.name); this.updateSeParameters(buffer, se); buffer.play(true); this._seBuffers.push(buffer); } };
なんてことはない。通常のplaySeと比べplayに渡す引数をfalse→trueと変更しているだけです。
これにより効果音の被りを気にする必要はなくなりました。
または、コアスクリプトの関数を書き換えてもいいのであれば、以下のようにplaySeメソッドの
第二引数でループするかどうかを指定できるようにし、
引数なし(つまり既存でコールされている部分)についてはデフォルト引数で
必ずループさせない(既存と同じ動作)ようにさせても良いです。
個人的にはこちらの方法を推奨します。似たような関数を2つ作成したくはないので。
// 既存のplaySeメソッドに第二引数を追加 AudioManager.playSe = function(se, bLoop=false) { if (se.name) { this._seBuffers = this._seBuffers.filter(function(audio) { return audio.isPlaying(); }); var buffer = this.createBuffer('se', se.name); this.updateSeParameters(buffer, se); buffer.play(bLoop); this._seBuffers.push(buffer); } };
もともとやりたいことはSE再生中は、再度同じSEを鳴らさないということで、それを実現する
ためにSEの再生時間分だけフレームをカウントしていました。
つまりループ再生ができれば上述した課題は自動的に解決され、フレームをカウントする必要がなくなります。
# 勢いで書いたエントリなのでもうちょっと検証しますが。
特定の効果音(SE)だけ停止する方法
先日のエントリだと、プレイヤーが停止したらすべての効果音を停止させる動きをしていましたが、
関係ないSEも止めちゃうってそりゃないでしょということで、足跡の効果音だけを停止させることにします。
rpg_managers.jsに以下の関数を追加します。
AudioManager.stopPlayedSE = function(sename) { if (sename) { for ( var i = 0; i < this._seBuffers.length; ++i ) { var file = path.basename(this._seBuffers[i].url, path.extname(this._seBuffers[i].url)) if ( file == sename ) { this._seBuffers[i].stop() // 止めるべきSEを発見した this._seBuffers.splice( i, 1 ) // 止めた要素は削除してしまう break } } } }
filterを使うことなども考えましたが、とりあえず線形検索して最初に見つかった要素を
対象にして効果音の停止および管理配列からの削除をさせることにしました。
どうやらこの配列、再生中の効果音(SE)が入るようで、効果音はそこまで多重で再生しない=配列サイズも
そんなに多くはならない=線形探索しても性能への影響はない、と考えています。
# これもちょっと検証が必要な気がしますけど。
昨日の記事は上記の内容を更新しておきます。
RPGツクールMVでプレイヤーが移動しているときだけ足音を鳴らす方法
ワケあってRPGツクールMVを触ることになりました。
RPGツクールに触った機会と言えば・・・
- 中学生時代にプレイしたスーファミのRPGツクール2
- はじめてパソコンに触ったときにプレイしたRPGツクール2000
あぁ年がバレるぅ~~~。友達とワイワイ作ったりプレイしたり、楽しかったなぁ……とノスタルジーに浸るのは置いといて。
初日ということで、とりあえずプレイヤーが移動したときに足音を鳴らす機能を実装してみました。後々必要になりそうだったので。
ネット軽く調べてみたところ、歩数やキーの押下状況で音の再生/停止をコントロールする方法を見つけましたが、
以下の観点から別の方法でコントロールすることにしました。
- 再生時間の長いSEでも自然に聞こえるようにしたい (歩数コントロールだと、場合によって音がかぶりそう(たぶんね))
- マウスクリック、スマホのタップでも正常にSEを鳴らしたい (←だと方向キーは押してない状態になるので、判定条件に方向キーの押下状況を使いたくない)
なお当方RPGツクールの慣習などをほとんど知りませんので、誤り等ありましたら教えてくださいね~。
前書き
考え方
- プレイヤーの移動を検知したとき、SE再生中フラグがすでにONなら何もしない。SE再生中フラグがOFFなら、SEをループで鳴らし、SE再生中フラグをONにする。
- プレイヤーが移動をやめたらSEを停止し、SE再生中フラグをOFFにする。
実装
自作メソッドをrpg_managers.jsに追加します。
効果音ごとに再生を停止するメソッドと、効果音をループ再生させるメソッドです。
rpg_managers.jsの先頭に以下を追加。
const path = require('path');
また以下をrpg_managers.jsの任意の位置に追加。
AudioManager.stopPlayedSE = function(sename) { if (sename) { for ( var i = 0; i < this._seBuffers.length; ++i ) { var file = path.basename(this._seBuffers[i].url, path.extname(this._seBuffers[i].url)) if ( file === sename ) { this._seBuffers[i].stop() // 止めるべきSEを発見した this._seBuffers.splice( i, 1 ) // 止めた要素は削除してしまう break } } } }
次に、コアスクリプトの関数を書き換えてしまっていいのかわかりませんが、
AudioManager.playSeメソッドを改造します。
以下のように第二引数でループするかどうかを指定できるようにし、
引数なし(つまり既存でコールされている部分)についてはデフォルト引数で
必ずループさせない(既存と同じ動作)ようにさせます。
// 既存のplaySeメソッドに第二引数を追加 AudioManager.playSe = function(se, bLoop=false) { if (se.name) { this._seBuffers = this._seBuffers.filter(function(audio) { return audio.isPlaying(); }); var buffer = this.createBuffer('se', se.name); this.updateSeParameters(buffer, se); buffer.play(bLoop); this._seBuffers.push(buffer); } };
次にプレイヤーが移動しているかどうかを判定するコモンイベントを1つ作成します。
const PREV_MOVING=101, CUR_MOVING=102, STOP_COUNTER=104 { if ( this.character(-1).isMoving() ){ $gameVariables.setValue( CUR_MOVING, 1 ) // プレイヤーが移動中かどうかのフラグは「移動中」 $gameVariables.setValue( STOP_COUNTER, 0 ) // isMoving()連続false(停止)回数は0にリセット } else { // isMoving()が一定回数連続でfalse(停止)にならない限り「停止中」とは判定せず、前回の結果をそのまま使う。 $gameVariables.setValue( CUR_MOVING, ($gameVariables.value(STOP_COUNTER) <= 2) ? $gameVariables.value(PREV_MOVING) : 0 ) $gameVariables.setValue( STOP_COUNTER, $gameVariables.value(STOP_COUNTER) + 1 ) // isMoving()連続false(停止)回数を+1 } $gameVariables.setValue( PREV_MOVING, $gameVariables.value(CUR_MOVING) ) // 次回向け、前回の移動中フラグを保存する }
プレイヤーが移動しているかどうかは「this.character(-1).isMoving()」で判定できるのですが、
これをそのまま使うと思った通りに動作しません。
方向キーを押しっぱなしにしていても、グリッドとグリッドの間で一瞬停止期間があるようで、
そのまま実装すると、プレイヤーはずっと移動しているのに、上述した停止期間にひっかかって足跡のSEが所々で
停止してしまうのです。そのため、ここでは連続してisMoving()がfalse(プレイヤーの移動停止)を返してきたときに、
はじめて本当にプレイヤーの動作が停止したと判定することにします。
ここでは連続して3回isMoving()がfalseを返したら停止と判断しています。
次に、足音を鳴らしたいマップにイベントを作成し、トリガーを「並列処理」にします。
イベントの実行内容の1つ目は、↑で作成したコモンイベントを呼び出します。
実行内容の2つ目で、スクリプトを実行します。
なお以下の項目については、ご自分の環境に合わせて変更してください。
- playSeLoopメソッドおよびstopPlayedSEメソッドに渡す変数。SE名(ファイルの拡張子を除いたもの)、ボリューム、ピッチ、パン。
const CUR_MOVING=102, SE_PLAYED=103, $bPlayerMoving = $gameVariables.value(CUR_MOVING) { if ( $bPlayerMoving ) { if ( $gameVariables.value(SE_PLAYED) != 1 ){ AudioManager.playSe({"name":"stepsound","volume":100,"pitch":100,"pan":0}, true) $gameVariables.setValue( SE_PLAYED, 1 ) } } else { AudioManager.stopPlayedSE("stepsound"); $gameVariables.setValue( SE_PLAYED, 0 ) } this.wait(1) }
効果音は停止までループして再生させています。
this.wait(1)はノーウェイトだと重そうなのと、1FPSに一度処理する、とタイミングが決まっていた方が
何かと都合が良いと思ったためです。RPGツクールの常識、よくわかんねぇ。
実装は以上です。
今後は以下を考えてます。
- 足元のマップチップの種別やリージョンID?とやらで判定する
ところで
RPGツクールってRubyでガリガリ書けるようになったと聞きましたが、また元に戻ったんですかね?
マルチデバイス対応でHTML5 + JavaScriptで動作しているというのは納得なのですが。
できれば全部スクリプトで組ませて欲しいですねぇ。
せめて1つのスクリプトの命令で今の100倍くらいは行数を書けるようにして欲しいっす。
2019/07/24追記
と思ったらこんなプラグインを作成しておられる方がいらっしゃるではありませんか!
ありがとうございます。使わせていただいてます。
翠さんのエディタ内で12行以上のスクリプトを書けるプラグイン +エラー時に該当箇所を標準出力する機能を追加しました - ツクマテ
はてなブログ(無料版)にnend(クリック報酬型アド)の広告を表示する方法
はじめに
はてなブログの場合、仕組み上Google AdSenseの審査が通らず、別の広告を探す必要があります。
そこで自分が目を付けたのが「nend」です。
はてなブログの無料版でも広告を貼り付けることができますよ。
特徴
- Google AdSenseと同様、クリック報酬型の広告
- 報酬はGoogle AdSenseの半分くらい (Google AdSenseに次いで2番目に高い報酬)
- 審査が簡単に、素早く通る
- ただしスマホ専用!
まずはnendにアカウントを登録
以下のURLにアクセスします。
2019/07/05現在のレイアウトとなりますが「広告主様」→「会員登録」と進んで、[メディアパートナーご登録]画面から必要な情報を登録しましょう!
nend.net
ここで入力した登録メールアドレスに仮登録の通知が来ますので、メールに記載されているURLをクリックして登録を完了させます。
すると審査(本登録)が開始されるのでしばらく待ちましょう。1日くらいで審査(本登録)を通過するはずです。
広告枠を作成する
nendでは、広告を貼り付ける場所ごとに広告枠を作成する必要があります。
nendにログインして[広告枠の管理]→[新規サイト/アプリの作成]と進み、
ご自分の運用されているサイト名やURL、サイトの紹介なんかを入力してください。
作成すると、これまた審査に入りますが、これは早ければ数分で完了します。
まずはテスト用のバナー広告を貼り付けてみよう!
↑で広告枠の審査がおりるまで、こちらを進めておきましょう。
この工程を完了させておけば、後にテスト用広告コードを本番用広告コードに書き換えるだけで、本番の広告を表示することができます。
最初に、はてなブログの管理画面から「デザイン」に移動します。
続いて、「工具」アイコンをクリックします。
今回はタイトルと記事の間に広告を載せることにしますので、「記事上」をクリックします。
テスト用広告コードを貼り付けます。コードは以下のサイトに記載されてます。
スマートフォン広告なら日本最大級のnend(スマホweb広告/アプリ広告/インフィード広告・ネイティブ広告/インタースティシャル広告対応)
2019/07/05時点ですが、サイズ320x50バナーの広告コードは以下です。
現在はコードが変わっている可能性もありますので、うまく広告が表示されない場合は、↑のサイトにアクセスして最新のコードをコピペしてみてくださいね!
<script type="text/javascript"> var nend_params = {"media":82,"site":58536,"spot":127513,"type":1,"oriented":1}; </script> <script type="text/javascript" src="https://js1.nend.net/js/nendAdLoader.js"></script>
nendの広告はスマホ専用なので、スマホの設定も必要です。
「スマホ」アイコンをクリックして[PCと同じHTMLを表示する]にチェックをつけておきます。
スマホからWebページにアクセスして広告が表示されるか確認する
うまく表示されない場合はブラウザを変えて試してみてください。
自分はSafariではうまく表示されず、Chromeなら表示されました。
(たぶんSafariには広告ブロックアプリを入れているからだと思いますが~)
成功していれば赤枠の部分が表示されます。(青枠はもともと(無料版だから?)表示されている広告です)
本番用広告コードに変更する
nendの[広告枠の管理]から作成した広告ユニットのステータスが「アクティブ」になっていることを確認したあと、「広告枠」をクリックします。
アクティブになっていなかったらまだ審査がおりていないので、しばらく待ちましょうね!
その後コードが2つ表示されると思いますが、「広告コード」の方のコードをコピーし、↑でペーストしたテスト用のバナー広告のコードと差し替えてください。
あとはスマホから本番用の広告が表示されているかを確認してみてください。
プライバシーポリシー および お問い合わせについて
広告の配信について
当サイトは第三者配信の広告サービス「Google Adsense グーグルアドセンス」を利用しています。
広告配信事業者は、ユーザーの興味に応じた広告を表示するためにCookie(クッキー)を使用することがあります。
Cookie(クッキー)を無効にする設定およびGoogleアドセンスに関する詳細は「https://policies.google.com/technologies/ads?hl=ja」をご覧ください。
当サイトへのコメントについて
当サイトでは、スパム・荒らしへの対応として、コメントの際に使用されたIPアドレスを記録しています。
これはブログの標準機能としてサポートされている機能で、スパム・荒らしへの対応以外にこのIPアドレスを使用することはありません。また、メールアドレスとURLの入力に関しては、任意となっております。全てのコメントは管理人が事前にその内容を確認し、承認した上での掲載となりますことをあらかじめご了承下さい。加えて、次の各号に掲げる内容を含むコメントは管理人の裁量によって承認せず、削除する事があります。
・特定の自然人または法人を誹謗し、中傷するもの。
・極度にわいせつな内容を含むもの。
・禁制品の取引に関するものや、他者を害する行為の依頼など、法律によって禁止されている物品、行為の依頼や斡旋などに関するもの。
・その他、公序良俗に反し、または管理人によって承認すべきでないと認められるもの。
免責事項
当サイトで掲載している画像の著作権・肖像権等は各権利所有者に帰属致します。権利を侵害する目的ではございません。記事の内容や掲載画像等に問題がございましたら、各権利所有者様本人が直接メールでご連絡下さい。確認後、対応させて頂きます。
当サイトからリンクやバナーなどによって他のサイトに移動された場合、移動先サイトで提供される情報、サービス等について一切の責任を負いません。
当サイトのコンテンツ・情報につきまして、可能な限り正確な情報を掲載するよう努めておりますが、誤情報が入り込んだり、情報が古くなっていることもございます。
当サイトに掲載された内容によって生じた損害等の一切の責任を負いかねますのでご了承ください。
お問い合わせ
お問い合わせがある方は、本エントリーのコメント欄に投稿をお願い致します。
C# + Ironyで構文解析を行う (電卓を作ってみます)
皆様新年明けましておめでとうございます。
相変わらずの更新ペースですが、本年もよろしくお願い致します。
挨拶はこの辺にしてさっそく本題。
なぜ構文解析をするのか?
事の発端ですが、うちにはC/C++で書かれたソースコードが山ほどあり、構文解析してヘンな部分を静的解析でもできたら素敵だな~と考えたのが始まりですが、まぁ難易度的に出来る出来ないは置いといて、この字句解析/構文解析という分野、なかなかちゃんと理解できず、曖昧な知識のまま今に至っているので、ちょいとお勉強したいなとも思ったのです。
Ironyとは?
パーサージェネレーターです。
有名なところではyacc/lex、bison/flexがありますね。
これはyacc/lex独自の文法にBNFを食わせて、字句解析/構文解析を行うC言語のプログラムを出力するものです。
今回使うIronyは、BNFの定義をIronyのクラスインスタンスなどを使って、C#のコード上で指定できるものです。
すべてC#のコードで表現できるので、yacc/lexのような独自の文法が不要になります。
C++のboostにも、spiritというC++のコードでBNFを指定し、字句解析/構文解析を行うクラスインスタンスを作成するみたいなライブラリがありました。
さすがC++!クラス/関数テンプレートさえあればなんでもできるんや!魔境へようこそ!って感じですねぇ…。
目標
とりあえず月並みですが、電卓を作ってみたいと思います。概要は以下。
- 四則演算+べき乗計算
- 計算の優先順位のためのカッコ対応
- マイナス値およびプラス値(-1とか+1とか)
- 整数のみ
- 複数の文を定義できる。文ごとに計算され、結果は合算値を表示。
- //や/* */のコメント部分や、空白文字は無視
こんな式↓を計算して結果を返すものを作ります。
2 + 3 *(((1 + -2)*(-1)) + 3 ) * ( 2 + 4/2);
準備
当方、Visual Studioのバージョンは2017 Communityです。
C#で適当なプロジェクトを作って(自分はフォームアプリケーションにしました)、
NuGetでIronyのライブラリを取り込んでおく必要があります。やり方は以下。
Visual Studioの[ツール]→[NuGet パッケージ マネージャー]→[ソリューションのNuGetパッケージの管理]を選択、 「Irony」で検索して、「Irony」と「Irony.Interpreter」をインストールします。
BNFのイメージ
詳しくないのであくまでイメージとして記載します。
構文木のトップをProgramとして、複数の文(Statement)を書けるようにします。
文とは計算式の最後にセミコロン(";")をつけたものであり、セミコロン区切りで複数の計算式を定義できるようにします。
式(Expr)は四則演算やカッコなどを含む計算式です。演算子の優先順位を意識する必要があるため、Exprで+, -を解決して、その下にTermを作って*, /を解決して、その下にFactor...とやるのが一般的なようですが、なんか名前がわかりづらいので、ここではExprAddSubやExprMulDivなどの名前にしています。Numberは整数です。
Program ::= Statement* Statement ::= ExprAddSub ";" ExprAddSub ::= ExprMulDiv | ExprAddSub "+" ExprAddSub | ExprAddSub "-" ExprAddSub ExprMulDiv ::= Expr | ExprMulDiv "*" ExprMulDiv | ExprMulDiv "/" ExprMulDiv | ExprMulDiv "**" ExprMulDiv Expr ::= "(" ExprAddSub ")" | UnOp "(" ExprAddSub ")" | UnOp Number UnOp ::= "+" | "-"
文法定義部分のC#コード
まずは文法を表現するクラスを定義します。
基本的には上記BNFをまんまコードに落とし込んでいくことになります。
まずはクラスの始まりと、コメントや空白文字、改行などを無視するコード。
using Irony.Interpreter; using Irony.Parsing; [Language("MyNumbersGrammar")] class NumbersGrammar : InterpretedLanguageGrammar { public NumbersGrammar() : base(true) { // 一行コメント(//)や、複数行コメント(/* ... */)、その他文法上無視するものの指定 var singleLineComment = new CommentTerminal("SingleLineComment", "//", "\r", "\n", "\u2085", "\u2028", "\u2029"); var delimitedComment = new CommentTerminal("DelimitedComment", "/*", "*/"); NonGrammarTerminals.Add(singleLineComment); NonGrammarTerminals.Add(delimitedComment);
続いて、終端文字です。
今回は整数と、それ以外のこまごまとして演算子や記号を定義します。
// 1. Terminals NumberLiteral Number = new NumberLiteral("number"); KeyTerm AddOperator = ToTerm("+"); KeyTerm SubOperator = ToTerm("-"); KeyTerm MulOperator = ToTerm("*"); KeyTerm DivOperator = ToTerm("/"); KeyTerm PowOperator = ToTerm("**"); KeyTerm LeftParen = ToTerm("("); KeyTerm RightParen = ToTerm(")"); KeyTerm EndOfSentence = ToTerm(";");
肝の部分の非終端記号です。
上記BNFの非終端記号を1つずつ定義していきます。
NonTerminalの第一引数は構文木を解析したときに、要素を見分けるための文字列です。適当で良いです。
第二引数には、構文木(Abstract Syntax Tree, 以下AST)を解析したあと、それをどう評価するか、ノードごとに評価するコードを持つクラスを指定します。
これらのクラスは後述します。
// 2. Non-Terminals var Program = new NonTerminal("Program", typeof(NumbersGrammarProgramNode)); var Statement = new NonTerminal("Statement", typeof(NumbersGrammarStatementNode)); var ExprAddSub = new NonTerminal("ExprAddSub", typeof(NumbersGrammarExprNode)); var ExprMulDiv = new NonTerminal("ExprMulDiv", typeof(NumbersGrammarExprNode)); var Expr = new NonTerminal("Expr", typeof(NumbersGrammarExprNode)); var ParenExpr = new NonTerminal("ParenExpr", typeof(NumbersGrammarParenExprNode)); var UnExpr = new NonTerminal("UnExpr", typeof(NumbersGrammarUnExprNode)); var UnOp = new NonTerminal("UnOp", typeof(NumbersGrammarUnOpNode) );
BNFのルールを書いていきます。
// 3. BNF Program.Rule = MakeStarRule(Program, Statement); Statement.Rule = ExprAddSub + EndOfSentence; ExprAddSub.Rule = ExprMulDiv | ExprAddSub + AddOperator + ExprAddSub | ExprAddSub + SubOperator + ExprAddSub ; ExprMulDiv.Rule = Expr | ExprMulDiv + MulOperator + ExprMulDiv | ExprMulDiv + DivOperator + ExprMulDiv | ExprMulDiv + PowOperator + ExprMulDiv ; Expr.Rule = ParenExpr | UnExpr | Number; ParenExpr.Rule = LeftParen + ExprAddSub + RightParen; UnExpr.Rule = UnOp + ParenExpr | UnOp + Number; UnOp.Rule = ToTerm("+") | "-";
最後に、このルールのトップをthis.Rootに指定し、
演算子の優先順位をRegisterOperators関数で指定します(※)
あとはASTを作成するフラグもONにします。
(※)補足。RegisterOperatorsでの演算子の優先順位がうまく動かないので、BNFとしては"*", "/"が"+", "-"よりも先に評価されるような文法にしています。
this.Root = Program; RegisterOperators(1, "+", "-"); RegisterOperators(2, "*", "**", "/"); LanguageFlags = LanguageFlags.CreateAst; } }
これで入力された文字列を読み取り、構文木を作成することができます。
ASTを評価する部分のC#のコード
非終端記号毎に、AstNodeのサブクラスを作成します。
重要な点として、これらのクラスは必ずアクセス修飾子をpublicにする必要があります。
今回はInit関数、DoEvaluate関数をオーバーライドしています。
Init関数でノード情報を取得し、DoEvaluate関数で評価した結果(今回は主に整数)を取得します。
トップレベルの非終端記号Programに対応するクラスです。
Programは複数のStatementを所持できるので、AstNodeのリストで管理しています。
public class NumbersGrammarProgramNode : AstNode { // プログラム全体は複数の数字を結果として持つ public List<AstNode> numList = new List<AstNode>(); public override void Init(AstContext context, ParseTreeNode treeNode) { base.Init(context, treeNode); ParseTreeNodeList nodes = treeNode.GetMappedChildNodes(); foreach ( var node in nodes ) { this.numList.Add(AddChild("numList", node)); } } protected override object DoEvaluate(ScriptThread thread) { thread.CurrentNode = this; // すべての文を評価した結果得られた整数値の合計を返す int result = 0; foreach ( var num in this.numList ) { result += (int)num.Evaluate(thread); } thread.CurrentNode = Parent; return result; } }
次にStatementですが、これは式の最後にセミコロンをつけたものなので、
式のAstNodeだけを所持するようにします。
public class NumbersGrammarStatementNode : AstNode { public AstNode exprNode = null; public override void Init(AstContext context, ParseTreeNode treeNode) { base.Init(context, treeNode); ParseTreeNodeList nodes = treeNode.GetMappedChildNodes(); this.exprNode = AddChild("exprNode", nodes[0]); // 数字のみ取り出す。nodes[1]の";"は無視。 } protected override object DoEvaluate(ScriptThread thread) { thread.CurrentNode = this; // 単一式を評価して、整数を取得する int number = (int)exprNode.Evaluate(thread); thread.CurrentNode = Parent; return number; } }
式Exprを評価します。
式は構文によって、値が1つだったり、3つだったりするので、
取得した要素分だけ配列サイズを確保して、ノード情報を所持するようにします。
public class NumbersGrammarExprNode : AstNode { public AstNode[] numberNodes = null; public override void Init(AstContext context, ParseTreeNode treeNode) { base.Init(context, treeNode); ParseTreeNodeList nodes = treeNode.GetMappedChildNodes(); this.numberNodes = new AstNode[nodes.Count]; for (int i = 0; i < nodes.Count; ++i) { this.numberNodes[i] = AddChild("numberNode", nodes[i]); } } protected override object DoEvaluate(ScriptThread thread) { thread.CurrentNode = this; int number = 0; if (this.numberNodes.Length == 1) { // 要素1つの場合 number = (int)this.numberNodes[0].Evaluate(thread); } else { // 要素3つの場合(と仮定) int nLeft = (int)this.numberNodes[0].Evaluate(thread); int nRight = (int)this.numberNodes[2].Evaluate(thread); // 演算子はこれ以上評価できないので、取得した文字列を直接参照する。 string strBinOp = this.numberNodes[1].Term.Name; if (strBinOp.Equals("+")) { number = nLeft + nRight; } else if (strBinOp.Equals("-")) { number = nLeft - nRight; } else if (strBinOp.Equals("*")) { number = nLeft * nRight; } else if (strBinOp.Equals("/")) { number = nLeft / nRight; } else { // **(べき乗)と仮定する。 number = (int)Math.Pow(nLeft, nRight); } } thread.CurrentNode = Parent; return number; } }
続いてカッコ付の式を評価するクラス。
public class NumbersGrammarParenExprNode : AstNode { public AstNode middleExprNode = null; public override void Init(AstContext context, ParseTreeNode treeNode) { base.Init(context, treeNode); ParseTreeNodeList nodes = treeNode.GetMappedChildNodes(); this.middleExprNode = AddChild("middleExprNode", nodes[1]); // 両端の(と)は無視して、間だけを評価する } protected override object DoEvaluate(ScriptThread thread) { thread.CurrentNode = this; int result = (int)this.middleExprNode.Evaluate(thread); thread.CurrentNode = Parent; return result; } }
次に、負値(とオマケで明示的な正の値)を式として評価するクラス。
public class NumbersGrammarUnExprNode : AstNode { public AstNode unOpNode = null; public AstNode termNode = null; public override void Init(AstContext context, ParseTreeNode treeNode) { base.Init(context, treeNode); ParseTreeNodeList nodes = treeNode.GetMappedChildNodes(); this.unOpNode = AddChild("unOpNode", nodes[0]); this.termNode = AddChild("termNode", nodes[1]); } protected override object DoEvaluate(ScriptThread thread) { thread.CurrentNode = this; // "-"なら-1を掛けて負の値にする。それ以外は+とする。 int result = (int)this.termNode.Evaluate(thread) * (((string)this.unOpNode.Evaluate(thread)).Equals("-") ? -1 : +1); thread.CurrentNode = Parent; return result; } }
負値および正の値の前につける"-"または"+"を評価するクラス。
public class NumbersGrammarUnOpNode : AstNode { public AstNode unOpNode = null; public override void Init(AstContext context, ParseTreeNode treeNode) { base.Init(context, treeNode); ParseTreeNodeList nodes = treeNode.GetMappedChildNodes(); this.unOpNode = AddChild("unOpNode", nodes[0]); } protected override object DoEvaluate(ScriptThread thread) { thread.CurrentNode = this; string result = (string)this.unOpNode.Term.ToString(); thread.CurrentNode = Parent; return result; } }
電卓を使用してみる!
最後に、上記コードで作成した電卓に値を渡し、結果を取得するコードをご紹介します。
Main関数に書いたり、フォームアプリケーションでボタンが押されたときのコールバック関数に書くと良いでしょう。
また自分は入力データをtbExprというテキストボックスから取得し、
tbResultというテキストボックスに結果を出力しています。
var grammar = new NumbersGrammar(); var language = new LanguageData(grammar); try { var app = new ScriptApp(language); int result = (int)app.Evaluate(this.tbExpr.Text); this.tbResult.Text = result.ToString(); } catch (ScriptException ex) { Console.Error.WriteLine("{0} {1}", ex.Location, ex.Message); }
電卓に渡す値の例
機能を一通り使った式
2 + 3 *(((1 + -2)*(-1)) + 3 ) * ( 2 + 4/2);
結果
50
複数の文を渡す
セミコロンで区切れば、複数の式を渡すこともできます。
2 + 3 *(((1 + -2)*(-1)) + 3 ) * ( 2 + 4/2); ((2 + -2 )/3 + 5) * 2 + 1;
結果
61
コメント
以下のように、コメントは無視されます。
2 + 3 *(((1 + -2)*(-1)) + 3 ) * ( 2 + 4/2); ((2 + -2 )/3 + 5) * 2 + 1; // 100 * 5 /* 300 * 5; 400 * 5; */ +5;
結果
66
途中から解説が面倒になってコードを張り付けるだけみたいな感じになってしまいました。
解説も非常に適当で、間違ってそうなところがちらほらと・・・。
後日修正するかもしれません。修正するんじゃないかな?さぼったらごめんなさい。
AutoIt+OpenCVでデスクトップから任意の画像を曖昧検索(テンプレートマッチング)する
AutoItで画像の曖昧検索がしたい
UWSCにはchkimgという画像検索関数が標準で用意されていました。また有志の方がchkimgxという画像の曖昧検索を行う外部ライブラリを作成されていたようですが、アップローダーなどでのみ配布されていたようで現在では入手が難しくなっています。
一方、AutoItには画像検索をする標準関数が存在せず、searchImageという外部ライブラリを使う必要があるようです。
しかしこのsearchImage、2008年頃までしかメンテされていないようで、自分の環境でもいまいちちゃんと動いてくれず、単にdllの配置場所を間違えてたかもしれない件は置いといて、まぁ自分で画像マッチング出来た方が曖昧さ加減を調整出来たり、判定方法を自由にカスタマイズ出来たり、今後のことを考えると自作した方がいいかなぁなんて思いまして、OpenCVを触ってみることにしました。
こちらの環境です。
OS | Windows 10 |
コンパイラ | Visual Studio Community 2017 |
CMake | 3.12.1(64-bit版) |
OpenCV, OpenCV contrib | 3.4.3 |
AutoIt | v3.3.14.5 |
Visual Studio Community 2017のインストール
なにはともあれ、コンパイラをインストールします。
CMakeのインストール
OpenCVのビルドには、CMakeも必要です。インストールします。
ご自分の環境合わせてインストールしてください。自分の場合は「cmake-3.12.2-win64-x64.msi」でした。
https://cmake.org/download/
CMakeインストール中に「Add CMake to the system PATH for all users」を選択して、環境変数PathにCMakeのパスを追加してください。
↑を忘れてしまった場合は、手動で環境変数Pathに以下のCMakeのパスを追加してください。
C:\Program Files\CMake\bin
OpenCV, OpenCV contribのビルド
まずは以下からソースコードをダウンロードしましょう。
OpenCV contrib
Releases · opencv/opencv_contrib · GitHub
「zip」と書かれたリンクをクリックしてダウンロードします。
上記ページのopencvのファイル名(例:opencv-3.4.3-vc14_vc15.exe)にあるvc14とかvc15とかが、
Visual Studioのバージョンに対応しているので、今後バージョンが上がった場合は注意して確認してみてください。
vc14 | Visual Studio 2015 |
vc15 | Visual Studio 2017 |
OpenCVのビルド
ここでは例として、「C:\temp」に上記ファイルをダウンロードします。
「opencv-3.4.3-vc14_vc15.exe」をダブルクリックすると「opencv」というフォルダ名で展開されます。
「opencv_contrib-3.4.3.zip」をダブルクリックすると「opencv_contrib-3.4.3」というフォルダ名で展開されます。
[スタート]メニューからcmake-guiを探して起動します。
[Where is the source code]に「C:/temp/opencv/sources」を指定します。
[Where to build the binaries]に「C:/temp/opencv/build」を指定します。
手動でパスを入力する場合、パス区切り文字は「/」にしてください。
入力したら[Configure]ボタンを押下します。
今回は、AutoItをデフォルト32-bitで動作させているので、OpenCVも32-bitでビルドします。
そのため今回は「Win64が付いていない」プロジェクトを選択します。
再びCMake-guiの画面に戻ります。
32-bit版の場合、cuda関連のビルドが通らないという噂がありますので、「WITH_CUDA」のチェックを外します(最近のは最初から外れてるかも)
また「OPENCV_EXTRA_MODULES_PATH」に「C:/temp/opencv_contrib-3.4.3/modules」を指定します。
何度も言いますが、パス区切り文字は「/」にしてください。
コンソールに「Configuring done」が出力されたら成功です。
続いて「Generate」ボタンを押下します。
C:\temp\opencv\build直下にOpenCV.slnが出来ているので、Visual Studioで開きます。
INSTALLプロジェクトをReleaseビルドしてください。
出力コンソールにこんな感じの表示が出れば成功です。
========== すべてリビルド: 134 正常終了、0 失敗、0 スキップ ==========
AutoItで使用するテンプレートマッチングを行うDLLの作成
Visual Studioで新規プロジェクトを作成します。
DLLのプロジェクトを作るか、プロジェクトの[構成プロパティ]→[全般]→[構成の種類]を「ダイナミック ライブラリ(.dll)」などにしてください。
同じプロジェクトのプロパティページの[C/C++]→[全般]→[追加のインクルードディレクトリ]に以下を入れます。
C:\temp\opencv\build\install\include
[リンカー]→[全般]→[追加のライブラリディレクトリ]に以下を入れます。
C:\temp\opencv\build\install\x86\vc15\lib
#include <opencv2/highgui/highgui.hpp> #include <opencv2/imgproc/imgproc.hpp> #include <map> using namespace std; #include <Windows.h> /** * @brief ウィンドウハンドルhwndのイメージcv::Matを作成する。 * @param [in] hwnd ウィンドウハンドル。デスクトップを対象にしたい場合、GetDesktopWindow()の戻り値を渡すべし。 * @param [in] nPosLeft 検索対象となる左上のx座標 * @param [in] nPosTop 検索対象となる左上のy座標 * @param [in] nDestWidth 検索対象となる範囲のnPosLeftからの幅 * @param [in] nDestHeight 検索対象となる範囲のnPosTopからの高さ */ cv::Mat hwnd2mat(HWND hwnd, int nPosLeft, int nPosTop, int nDestWidth, int nDestHeight) { HDC hwindowDC = GetDC(hwnd); HDC hwindowCompatibleDC = CreateCompatibleDC(hwindowDC); HBITMAP hbwindow = NULL; cv::Mat src; BITMAPINFOHEADER bi; SetStretchBltMode(hwindowCompatibleDC, COLORONCOLOR); RECT windowsize; // get the height and width of the screen GetClientRect(hwnd, &windowsize); // ここはデスクトップの幅と高さ int srcheight = windowsize.bottom; int srcwidth = windowsize.right; src.create(nDestHeight, nDestWidth, CV_8UC3); // create a bitmap hbwindow = CreateCompatibleBitmap(hwindowDC, nDestWidth, nDestHeight); if (hbwindow == NULL) { src.data = NULL; return src; } bi.biSize = sizeof(BITMAPINFOHEADER); // http://msdn.microsoft.com/en-us/library/windows/window/dd183402%28v=vs.85%29.aspx bi.biWidth = nDestWidth; bi.biHeight = -nDestHeight; bi.biPlanes = 1; bi.biBitCount = 24; bi.biCompression = BI_RGB; bi.biSizeImage = 0; bi.biXPelsPerMeter = 0; bi.biYPelsPerMeter = 0; bi.biClrUsed = 0; bi.biClrImportant = 0; // use the previously created device context with the bitmap SelectObject(hwindowCompatibleDC, hbwindow); // copy from the window device context to the bitmap device context StretchBlt(hwindowCompatibleDC, 0, 0, nDestWidth, nDestHeight, hwindowDC, nPosLeft, nPosTop, nDestWidth, nDestHeight, SRCCOPY); // change SRCCOPY to NOTSRCCOPY for wacky colors ! GetDIBits(hwindowCompatibleDC, hbwindow, 0, nDestHeight, src.data, (BITMAPINFO *)&bi, DIB_RGB_COLORS); // copy from hwindowCompatibleDC to hbwindow // avoid memory leak if (hbwindow != NULL) { DeleteObject(hbwindow); hbwindow = NULL; } if (hwindowCompatibleDC != NULL) { DeleteDC(hwindowCompatibleDC); hwindowCompatibleDC = NULL; } if (hwindowDC != NULL) { ReleaseDC(hwnd, hwindowDC); hwindowDC = NULL; } return src; } extern "C" { /** * @brief 教師画像が指定されたデスクトップ範囲に存在するかあいまい検索(テンプレートマッチング)する * @param [in] pszFilePath 教師(テンプレート)画像のフルパス * @param [in] nPosLeft 検索対象となるデスクトップの左上のx座標 * @param [in] nPosTop 検索対象となるデスクトップの左上のy座標 * @param [in] nDestWidth 検索対象となる範囲のnPosLeftからの幅 * 4の倍数でないと必ずマッチングしないため、4の倍数に増やす方向で調整をする。 * @param [in] nDestHeight 検索対象となる範囲のnPosTopからの高さ * @param [out] pDetectedPosX 検知した場合のX座標。検知してない場合なにも入らない。 * @param [out] pDetectedPosY 検知した場合のY座標。検知してない場合なにも入らない。 * @retval 0 正常終了(見つかった) * @retval 1 正常終了(見つからなかった) * @retval 10 教師画像ロード失敗 * @retval 11 デスクトップ情報確保失敗 * @retval 12 テンプレートマッチングでエラー */ DllExport int searchImg(const char* pszFilePath, int nPosLeft, int nPosTop, int nDestWidth, int nDestHeight, int* pDetectedPosX, int* pDetectedPosY) { int nActualWidth = nDestWidth + (4 - (nDestWidth % 4)); // 4の倍数に増やす方向で合わせる cv::Mat template_image = cv::imread(pszFilePath); if (template_image.data == NULL) { return 10; } cv::Mat srcDesktop = hwnd2mat(GetDesktopWindow(), nPosLeft, nPosTop, nActualWidth, nDestHeight); if (srcDesktop.data == NULL) { return 11; } cv::Mat result; try { cv::matchTemplate(srcDesktop, template_image, result, cv::TM_CCORR_NORMED); } catch (cv::Exception ex) { return 12; } // Multiple results // 最もそれっぽい画像選ぶためのMap。同じ値の場合最初に検知したものを優先するため、insert自体しないこととする。 // 比較をgreator<float>にすることにより降順に並ぶ。 map<float, cv::Point, greater<float> > dectetedPointMap; const float threshold = 0.981f; // TODO:ここも指定できるようにしたい for (int y = 0; y < result.rows; ++y) { for (int x = 0; x < result.cols; ++x) { if (result.at<float>(y, x) > threshold) { if (dectetedPointMap.find(result.at<float>(y, x)) != dectetedPointMap.end()) { // すでに同じ値がある場合、最初に見つかったものを採用するため、mapには追加しない continue; } dectetedPointMap.insert(pair<float, cv::Point>(result.at<float>(y, x), cv::Point(x,y))); } } } if (dectetedPointMap.empty()) { // 見つからなかった return 1; } else { // 最も似ている点(複数ある場合は最初に見つかった点)を採用する *pDetectedPosX = (dectetedPointMap.cbegin()->second.x + template_image.cols / 2); *pDetectedPosY = (dectetedPointMap.cbegin()->second.y + template_image.rows / 2); return 0; } } }
とりあえず複数見つかった場合は以下のルールに従って、単一の結果を返すようにします。返すデータはマッチングした画像の中心座標です。
- もっとも類似度の高いもの
- 最初に見つかったもの
ビルドしてdllを作成します。
includeやlibの設定が間違っていなければビルドが通るはず(通らなかったらごめんなさい)。
AutoItからDLLを呼び出す
AutoItのインストール手順などは省略します。
適当にAutoItのファイルを作成し、以下のDLLをAutoItのファイルと同じ位置に配置します。
上記で作成した自作のDLL |
opencv_ccalib342.dll |
opencv_core342.dll |
opencv_highgui342.dll |
opencv_imgcodecs342.dll |
opencv_imgproc342.dll |
AutoItのコードは以下です。
まずはDLL関数のラッパー関数。
; @param [in] $hDll DllOpen()済のハンドル。ここファイル名を渡すこともできるが、そうすると毎回オープン/クローズ ; するせい(?)なのか、連続して呼び出すとアプリケーションエラーが発生するので、必ずハンドルを渡すこと。 ; @param [in] $x デスクトップ上のx座標 ; @param [in] $y デスクトップ上のy座標 ; @param [in] $width $xからの幅 ; @param [in] $height $yからの幅 ; @retval -1 DllCallでエラー発生 ; @retval 0 イメージが正常に判定し、見つかった。イメージの中心座標が$detectedXと$detectedYに格納される。 ; @retval 1 イメージを正常に判定し、見つからなかった ; @retval 10 イメージのファイルが見つからなかった ; @retval 11 デスクトップ画面の取得で失敗 ; @retval 12 マッチング処理でエラー発生 Func searchImageCore($hDll, $imgFileName, $x, $y, $width, $height, ByRef $detectedX, ByRef $detectedY) Dim $ret = DllCall($hDll, _ "int:cdecl", _ "searchImg", _ "str", $imgFileName, _ "int", $x, _ "int", $y, _ "int", $width, _ "int", $height, _ "int*", $detectedX, _ "int*", $detectedY) If (@error <> 0) Then ConsoleWrite( "DllCall error!." & @LF ) MsgBox(0, "error", "DllCall returned " & @error & " imgFile=" & $imgFileName & @CRLF) Return -1 EndIf ; 0 -> 見つかった ; 1 -> 見つからなかった ; その他 -> エラー発生 ; 検知した座標はおそらく($x,$y)からのもの。今回ほしいのは実際のデスクトップ上の座標なので、加算して格納する。 If ($ret[0] = 0) Then $detectedX = $x + $ret[6] $detectedY = $y + $ret[7] EndIf Return $ret[0] EndFunc ;==>searchImageCore
次に、ラッパー関数を呼び出すAutoItコード。
Global $g_hDll = DllOpen( "<自作のDLLファイル名>.dll" ) Local detectedX = 0 Local detectedY = 0 Local $ret = searchImageCore($g_hDll, _ "abc.bmp", _ 0, _ 0, _ 1920, _ 1080, _ $detectedX, _ $detectedY) If ($ret = 0) Then ; 見つかった ConsoleWrite("searchImage detected! dx=" & $detectedX & " dy=" & $detectedY & @CRLF) Else ; 普通に見つからなかったか、なんらかのエラー発生。見つからなかったとして帰る。 If ($ret <> 1) Then ; 普通に見つからない場合以外はログを出しておく ConsoleWriteError("[Error] searchImage returned " & $ret & " imgFile=" & $imgFile & @CRLF) EndIf EndIf
思ったより長くなってしまった。あぁ疲れた…。
AutoItでDLLを呼び出す方法
つい先日からUWSCでスマホゲーWindows上の作業を自動化していたのですが、
UWSCは最近公式サイトがリンク切れになったり、32-bitのバイナリしかなかったり、VMのゲストOSでの動作がうまくいかなかったりと、
将来的にいろいろ不安なので、AutoItに乗り換えられないか色々試してます。
AutoItについてはWikipediaでもみてください。
https://ja.wikipedia.org/wiki/AutoIt
こちらの環境です。
OS | Windows 10 |
C++コンパイラ | Visual Studio 2017 Community |
AutoIt | v3.3.14.5 |
AutoItは世界ではわりとメジャーだそうで、困ったことがあっても調べればすぐ解決方法が出てくるのも魅力ですね~。
今回は自作のDLL呼び出しでちょっと詰まったので、解決方法をご紹介します。
C++(DLL)のソースコード
サンプルで簡単な足し算の関数を定義します。
結果を戻り値で受け取るバージョンと、引数経由で受け取るバージョンの2つ。
これをVisualStudio等でビルド・dllを作成し、AutoItのソースコードと同じパスに配置しておいてください。
#define DllExport __declspec( dllexport ) extern "C" { DllExport int Add(int a, int b) { return (a + b); } DllExport void AddRef(int a, int b, int* pResult) { *pResult = (a + b); } }
AutoItのソースコード
Add関数の呼び方
Dim $result = DllCall("NinaImageSearch.dll", "int:cdecl", "Add", "int", 3, "int", 5) If (@error <> 0) Then MsgBox(0, "error", "DllCall returned " & @error) Exit @error Else MsgBox(0, "Result", "$result=" & $result[0]) EndIf
ポイント
- 第一引数にはDLLのファイル名(またはDllOpenした結果(ハンドル))を渡す
- 第二引数には関数の戻り値の型(※)を指定する
- 呼び出す関数の呼び出し規約がcdeclの場合は「型:cdecl」と指定する
- 第三引数に関数名を指定する
- それ以降は型名(※)と値のペアを指定する
- エラーが発生した場合はマクロ「@error」に0以外の数値が入る
- 戻り値がintだろうと、結果は配列で返るので値を参照するときは$result[0]として参照する
ポイント多いですね!
この呼び出しを成功させるだけで、30分くらい格闘した覚えがあります…。
(※)型一覧はこのあたりをみてください。
https://open-shelf.appspot.com/AutoIt3.3.6.1j/html/functions/DllCall.htm
AddRef関数の呼び方
変数の参照(リファレンス)を渡して、そこに結果を格納する方法ですが、こいつがわかりづらい。
Dim $sum Dim $ret = DllCall("NinaImageSearch.dll", "none:cdecl", "AddRef", "int", 3, "int", 20, "int*", $sum) If (@error <> 0) Then MsgBox(0, "error", "DllCall returned " & @error) Exit @error Else $sum = $ret[3] MsgBox(0, "Result", "$sum=" & $sum) EndIf
ポイント
- 戻り値voidの場合は「none」という識別子を使う。(第二引数)
- 変数の参照(リファレンス)を渡すときは型の末尾に「*」を指定する(第八引数)
- DllCallに成功しても、なんと$sumには値が入らない!
- 結果は$retに格納されており、$retは以下のような配列構造になっている。
- $ret[0] ←戻り値(noneの場合は空文字)
- $ret[1] ←3 AddRef関数の第一引数に渡す値
- $ret[2] ←20 AddRef関数の第二引数に渡す値
- $ret[3] ←23 これがAddRef関数の戻り値!
- つまり必要に応じて結果($ret[3])を変数に格納して使う。ってなんじゃこのわかりづらいのは~!
この呼び出しを成功させるだけで、1時間くらい格闘した覚えがあります…。
その他疑問
呼び出し規約をstdcallとかにすると、DllCallが3(関数が見つからない)が返ってくる。なぜだ。