2016年09月22日

ProcessingとopenFrameworksを試してみた

of03.png

Arduboyの画面に、PCで選択した画像ファイルを表示するようなツールを作ってみたいと思います。
この場合はArduboyとPC間で通信をすることになります。
PC側を何で開発するかちょっと迷ったのですが、画像処理(2値化)などもすることを考えると、とりあえずProcessing(Proce55ing)かなあと思い、ちょっと使ってみました。

Processing.org

ProcessingはArduinoの兄弟のような開発環境で、IDEも似ています。
ベースの言語はJavaです。
画像処理関係の機能が充実しており、画像処理フィルタ機能が標準で使えます。

filter() \ Language (API) \ Processing 2+

IDEそのものはArduinoにそっくりなのですが、Arduinoと違い、現在のバージョン(v3)ではステップ実行や変数の確認ができるデバッガが付いています。

まずは画像ファイルを読み込んでウインドウに表示させるプログラムを作ってみました。
思ったより手こずりましたが、できたのが以下です。
スペースキーを押すとファイル選択ダイアログが立ち上がり、画像ファイルを選択すると表示できます。
PImage img;
String fName;

void setup()
{
size( 512, 512 );
background(0, 0, 0);
}

void keyReleased() {
if (key == ' ') {
noLoop();
selectInput("Select an image file", "fileSelected");
while(fName==null) {delay(200);}
if (fName != null) {
img = loadImage(fName);
clear();
redraw();
}
loop();
}
}

void fileSelected(File selection) {
if (selection == null) {
println("Window was closed or the user hit cancel.");
fName = null;
} else {
fName = selection.getAbsolutePath();
}
}

void draw()
{
if (img != null) {
image(img, 0, 0 );
}
}


ただ・・・このプログラム、一応は動くのですが、2回目以降に画像を開くとき、画像を開いた直後には表示されません。
redraw()をコールすれば、直後でdraw()で新しい画像が描かれると思ったのですが、何かタイミングが合っていないようです。

また、私にとっては言語として、どうも気持ちの悪い点がいくつかあります。たとえば

・keyReleased()で使用しているkeyのように、グローバル変数が多い。
・手続き型言語の印象が強い。img.drawじゃなくてimage(img)とか。
・ループの一時停止と再開を意味するnoLoop(), loop()という表現も古臭い。
・ライブラリを見る限り、GUI関係がちょっと弱そう。

などなど・・・
また、最近Arduboyシミュレータを使うためにVisual Studioばかり使っていたためか、ProcessingのIDEも不便に感じるようになってしまいました。


そんなわけで他の選択肢を探してネットを見ていたところ、「openFrameworks」に行き当たりました。Processingの影響を受けたC++ライブラリですが、Mac、Linux、WindowsのほかiOSやAndroidにも対応しているようです。
Windowsでは、VisualStudioの上で開発が行えます。

openFrameworks

日本語の情報はProcessingに比べると少ないのですが、嬉しいことに日本語の書籍で、無料でダウンロードできるものがあります。

Beyond Interaction メディアアートのためのopenFrameworksプログラミング入門 | 株式会社ビー・エヌ・エヌ新社

というわけで、こちらを使ってみることにしました。
ライブラリはダウンロードして展開するだけですが、それ以外にVisual Studio用のプラグインが必要です。

これはVisual Studio内からダウンロードできます。
まず「ツール」→「拡張機能と更新プログラム…」を選択します。
of01.png


そして、openFrameworksを検索してダウンロードします。
of02.png


これで完了です。

ダウンロードしたライブラリには、サンプルが沢山付属しています。
GUIツールキットも用意されています。
of03.png


プラグインをインストールしたので、新規のプロジェクトを作成する際にはテンプレートの選択肢にopenFrameworksが出てきます。
of04.png


なお初回は、openFrameworksを展開したディレクトリの場所を指定するよう求められました。
of05.png


テンプレートには最初から必要なファイルが含まれています。
プログラム本体はofApp.cpp / ofApp.hに記述します。
of06.png


openFrameworksで、先ほどと同様に画像ファイルを選択して表示するプログラムを書いてみました。
テンプレートがあるので、実際に書いた部分は青字の部分だけです。

#pragma once

#include "ofMain.h"

class ofApp : public ofBaseApp{

public:
void setup();
void update();
void draw();

void keyPressed(int key);
void keyReleased(int key);
void mouseMoved(int x, int y );
void mouseDragged(int x, int y, int button);
void mousePressed(int x, int y, int button);
void mouseReleased(int x, int y, int button);
void mouseEntered(int x, int y);
void mouseExited(int x, int y);
void windowResized(int w, int h);
void dragEvent(ofDragInfo dragInfo);
void gotMessage(ofMessage msg);
void processOpenFileSelection(ofFileDialogResult openFileResult);
ofImage img;

};


#include "ofApp.h"

//--------------------------------------------------------------
void ofApp::setup(){
ofSetWindowShape(512, 512);
ofBackground(0,0,0);

}

//--------------------------------------------------------------
void ofApp::update(){

}

//--------------------------------------------------------------
void ofApp::draw(){
if (img.isAllocated())
img.draw(0, 0);
}

//--------------------------------------------------------------
void ofApp::keyPressed(int key){

}

//--------------------------------------------------------------
void ofApp::keyReleased(int key){
if (key == ' ') {
ofFileDialogResult openFileResult = ofSystemLoadDialog("Select a jpg or png");
if (openFileResult.bSuccess) {
processOpenFileSelection(openFileResult);

}
}


}

void ofApp::processOpenFileSelection(ofFileDialogResult openFileResult) {

ofFile file(openFileResult.getPath());

if (file.exists()) {
string fileExtension = ofToUpper(file.getExtension());
if (fileExtension == "JPG" || fileExtension == "PNG") {
img.load(openFileResult.getPath());
}
}

}
//--------------------------------------------------------------
void ofApp::mouseMoved(int x, int y ){

}

//--------------------------------------------------------------
void ofApp::mouseDragged(int x, int y, int button){

}

//--------------------------------------------------------------
void ofApp::mousePressed(int x, int y, int button){

}

//--------------------------------------------------------------
void ofApp::mouseReleased(int x, int y, int button){

}

//--------------------------------------------------------------
void ofApp::mouseEntered(int x, int y){

}

//--------------------------------------------------------------
void ofApp::mouseExited(int x, int y){

}

//--------------------------------------------------------------
void ofApp::windowResized(int w, int h){

}

//--------------------------------------------------------------
void ofApp::gotMessage(ofMessage msg){

}

//--------------------------------------------------------------
void ofApp::dragEvent(ofDragInfo dragInfo){

}

とりあえず、openFrameworksを使ってみようかと思います。
タグ:openFrameworks
posted by boochow at 21:04| Comment(0) | Arduino | このブログの読者になる | 更新情報をチェックする

2016年09月11日

インベーダーゲームを作ってみる(6)サウンド

インベーダーゲームの解説、最後はサウンドです。

インベーダーの効果音は7種類しかありません。

@インベーダーの行進音
A自機弾の発射音
Bインベーダー命中音
CUFO出現音
DUFO命中音
E自機被弾音
F1500点での自機+1の音

オリジナルのインベーダーゲームは、これらの音を全て専用の回路で生成しています。
以下の記事にそれぞれの回路図が出ています。

Space Invaders sound emulation

これによると、@Fは有名なタイマーICのNE556、CはTIのサウンドチップSN76477、ABDEはオペアンプLM3900を使っているそうです。また、AEはノイズ音が混じりますが、これはCMOS ICの発振回路とシフトレジスタで生成しているそうです。
説明には4066とありますが、シフトレジスタなので4006の間違いでしょう。こちらのマニュアルのP8のパーツリストにも4006とあります。)

俗にゲームの音を「ピコピコ音」と言いますが、インベーダーゲームのサウンドは後にピコピコ音の代名詞となるファミコンなどよりも、アナログ回路で生成した電子音の比率が高かったようです。
以下のビデオはおそらく実機だと思いますが、今聴いても味わいがありますね。



まずはオリジナルの効果音を分析してみましょう。

最初は行進音です。

上のビデオの音声トラックを取り出し、Audacityで増幅・ノイズ除去したものを聴いてみます。
前半はそのまま、後半は2オクターブ上にトランスポーズしています。



音階は、「ラ・ソ・ファ・ミ」あるいは「ミ・レ・ド・シ」というように聞こえなくもありません。
いずれにせよ短調ですね。

インベーダーが減るに連れてテンポが上がりますが、音長(ゲートタイム)自体は変化せず、発音の間隔だけが狭まっていきます。
演奏速度としては、最後は最初の約10倍になるように設定しました。


次に、インベーダーに命中したときの「プチュン」という音です。
これも上のビデオから取り出したものを聴いてみます。
最初は通常再生、2番目は1/2倍速のスロー再生です。



波形を図にすると、下のようになっています。
iv10.png

全体で0.3秒ほどの音ですが、前半0.1秒くらいが低い音がさらに低くなる「プン」という音、その後は高い音からだんだん下がっていく「チューーーン」という音になっています。
また、末尾のところで同じ「チューーン」という音が、残響のようにもう一度小さく入っています。
なので、続けて鳴らすと「プチュンチュン」という具合に聴こえます。

このほか、UFO飛来音は4オクターブくらいの音域をサイン波でピッチを上げ下げするとそれらしくなります。
命中したときの音も、音程が低くなるだけで基本的には同様です。
なお、自機弾の発射音や自機爆発音は、ノイズ成分が必要なのでArduboyでは作るのは難しいです。


プログラム内での音声処理は、以下の通りです。
まず、各効果音の周波数の時間変化を配列に入れます。
データの末尾は「-1」とします。
int16_t snd1[5] = {   // laser
NOTE_B6, NOTE_C7, 0, NOTE_B6, -1
};


これとは別に、構造体で音の特性(ゲートタイム、再生速度、リピートするか等)と状態(再生中か等)を保持します。
enum SoundState {
SoundReady,
SoundPlaying,
SoundDone
};

struct sound_fx_t {
int16_t *data; // array of frequencies, use -1 as end marker
uint8_t gate_time; // note gate time in msec
uint8_t clk; // ticks per note
boolean loop; // true for loop play
uint8_t idx;
uint8_t clk_cnt;
enum SoundState status;
};


サウンドの状態は「Ready」→「Playing」→「Done」と遷移します。
それぞれ、再生前、再生中、再生完了を表します。

Playing状態では、再生速度(clk)ごとに配列のデータを1つずつ再生します。
clkが大きくなれば、演奏速度はゆっくりになります。


サウンド関連の関数は以下の6つを用意しています。

void sound_play(struct sound_fx_t *s)
loop()から各サウンドにつき1回ずつ呼び出すことでサウンドの状態を更新します。
音声チャンネルは1つだけですので、全ての効果音の処理をした結果として、基本的には最後に再生された音だけが聴こえます。
逆に言えば、サウンドの優先順位はsound_playを呼び出す順番で制御できます。後に呼び出されたものが優先です。

void sound_start(struct sound_fx_t *s)
サウンドの再生を開始します。(再生前→再生中へ遷移)

void sound_stop(struct sound_fx_t *s)
サウンドの再生を強制的に完了させます。(再生中→再生完了へ遷移)

void sound_restart(struct sound_fx_t *s)
再生が完了または中止されたサウンドを再び再生可能にします。(再生完了→再生前へ遷移)

boolean sound_ready(struct sound_fx_t *s)
サウンドが再生前ならtrue、そうでなければfalseを返します。

void sound_set_tempo(struct sound_fx_t *s, uint8_t t)
サウンドの再生テンポを指定します。再生はt+1 ticks(1/60秒)ごとに1データずつ進行します。0が最速です。
タグ:Arduboy
posted by boochow at 23:43| Comment(0) | Arduino | このブログの読者になる | 更新情報をチェックする

インベーダーゲームを作ってみる(5)状態遷移

インベーダーゲームに登場するオブジェクトは、これまでに説明したインベーダーと陣地を除くと
@自機
AUFO
B自機の弾
C敵の弾
があります。

これらのオブジェクトはいずれも、
出現→移動→(爆発)→消滅→出現・・・
という状態遷移をします。

プログラム中でも、オブジェクトごとに細かい違いはあるものの、大体この遷移に沿っています。
流れを把握する上で役立つと思いますので、これらの状態遷移を簡単に解説しておきます。

オブジェクトの基本的な状態は以下の2つです。

・READY
オブジェクトが画面に出現していない状態です。
トリガー(たとえば自機弾なら、発射ボタンを押す)によってACTIVEへ移行します。
移行する前に初期設定(座標の設定など)が必要です。

・ACTIVE
オブジェクトが画面に出現し、動くことができる状態です。
ACTIVEなオブジェクトの処理はオブジェクトの種類に依存します。
オブジェクトの消滅(たとえばUFOが画面端へ到達したとき)や爆発により、他の状態へ移行します。
どの状態へ移行しても、消滅したオブジェクトは最終的にREADYへ移行します。

iv09.png


ACTIVE状態からの移行は、以下の3つのパターンがあります。

(1)直接READYへ移行する
オブジェクトを画面から消去済みの場合、単にREADYへ移行します。

(2)RECYCLEへ移行する
次の画面更新のタイミングでオブジェクトを画面から消去する必要がある場合、RECYCLEへ移行します。
RECYCLE状態では、オブジェクトを画面から消去し、READYへ移行します。

(3)EXPLOSIONへ移行する
爆発のイメージを描画する場合はEXPLOSIONへ移行します。
プログラム内では、オブジェクトによって状態の名称は異なりますが、意味合いは同じです。
EXPLOSION状態はオブジェクトの爆発イメージを画面に描画後、タイマーでRECYCLEへ移行します。

状態はプログラム内では整数で表現しており、
EXPLOSION > RECYCLE = 1 > ACTIVE = 0 > READY = -1
となっています。
この整数はEXPLOSIONからRECYCLEへのカウントダウンタイマーを兼ねており(手抜き)、EXPLOSION > 状態 > RECYCLEのときは、ループ内で1ずつカウントダウンしていきます。


オブジェクトの状態遷移は、主に オブジェクト名_update() という関数で処理しています。
例としてufo_update()は以下のようになっています。
void ufo_update(struct ufo_t *u) {
switch (u->status) {
case OBJ_ACTIVE:
if (u->wait_ctr++ > UFO_WAIT) {
u->wait_ctr = 0;
ufo_move(u);
}
break;
case OBJ_READY:
ufo_appear(u);
break;
case OBJ_RECYCLE:
ufo_erase(u);
u->status = OBJ_READY;
break;
case UFO_EXPLOSION:
DRAW_BITMAP(u->x, UFO_TOP, ufo_score_img[u->img_idx], UFO_W, UFO_H, WHITE);
default:
u->status--;
}
}

statusが状態を表す変数です。
UFOは毎秒60回動かすと早すぎるので、wait_ctrでウエイトを入れています。
ufo_appear()はUFO出現条件が整っているかチェックし、整っていればUFOを出現させます。
UFO_EXPLOSIONでの処理は、爆発イメージを描画します。
UFO_EXPLOSIONからOBJ_RECYCLEまでカウントダウンする動作はdefaultの中で行っています。
なお、UFO_EXPLOSIONでの処理は最後にbreakが付いていないので、u->status--;まで実行されることに注意してください。
タグ:Arduboy
posted by boochow at 14:06| Comment(0) | Arduino | このブログの読者になる | 更新情報をチェックする

2016年09月10日

Arduinoベースのゲーム機:Gamebuino、Tiny Arcade/Tiny Screen他

Arduboyのことを調べていたら、他にもArduinoベースのゲーム機が出ているのを知ったのでメモしておきます。

ゲーム機のようなものをまとめてみる - inajob's blog

中でも、GamebuinoはディスプレイはNokia 5110のLCDで解像度は84×48ドットと、Arduboyよりさらに低いですが、色々なゲームが既に作られています。

Games - Gamebuino Wiki

ArduboyでGamebuinoのゲームを流用するためのライブラリも既に作られており、パックマンやインベーダーが実際に移植されていました。

Arduboy meets Gamebuino?! - Arduboy / Development - Community

Gamebuino ライブラリの移植 - 日本語 - Community

余談ですが、Gamebuino用のインベーダーゲームの作者は、今回作ったインベーダーゲームのキャラクタデザインで参考にしたPixel Invadersの作者さんでした。


いろいろなゲーム機の中では、個人的には、96x64ピクセルのカラーOLEDを使ったTinyScreenに魅力を感じますが、$75とやや高価ですね。

TinyScreen Video Game Kit - TinyCircuits

Arduinoベースではありませんが、ESP8266を用いたものもあります。NodeMCU2.2インチTFT LCDを組み合わせたもののようです。
NodeMCUなので、プログラミングはLuaを使うことになります。

WiFiBoy.Org

他には、Pocket C.H.I.P.というさらにパワフルなゲーム機もあります。
こちらはCPU周りはRaspberry Piと同程度でOSもLinuxです。

が、まあここまで強力なものが欲しければ、スマホやタブレットでいいのでは、という気もします。
タグ:Arduboy
posted by boochow at 16:47| Comment(0) | Arduino | このブログの読者になる | 更新情報をチェックする

2016年09月06日

インベーダーゲームを作ってみる(4)陣地の破壊

今回は、前回触れた「インベーダーゲームで強く印象に残った処理」のその2、「陣地が敵弾にだんだん崩される様子」の処理です。

陣地が敵弾や自機弾で崩れていく処理はどのように行っているのか、YouTubeのビデオを見ながら考えました。
そして、おそらくこうだろう、と推測した処理が以下です。

(1)弾の当たり判定は、弾の移動先のピクセルの有無で判定する。
(2)当たりと判定したら、弾を中心に爆発の画像を描画する。この際、黒いピクセルは描かず、白いピクセルだけを描く(ORで描画)。
(3)次に、爆発の画像を消去する。このときは、先程描いた白いピクセルの部分だけを、黒いピクセルで描く(NOT ANDで描画)。結果、陣地が爆発の画像の形で型抜きしたようになる。
iv08.png

これを繰り返すことで、陣地がだんだん削られていきます。
陣地そのものの処理は、ゲーム開始時に画面に絵として描画するだけです。

今回作ったインベーダーゲームは、自機弾・敵弾ともに上記の処理を行っており、陣地が8×6ピクセルしかない割には、結構雰囲気のある崩れ方を表現できています。

なお、当たり判定を画面上のドットの有無で行うというのは、一般的には誤判定が起こりやすいため、あまり使われません。
今回は陣地への当たり判定に使っていますが、敵弾と自機弾が衝突した場合も、陣地に衝突したと誤判定して弾が消滅します。
実は、敵弾と自機弾の衝突については専用の判定処理は行っておらず、上記の誤判定をそのまま弾同士の衝突処理として用いています。
タグ:Arduboy
posted by boochow at 23:50| Comment(0) | Arduino | このブログの読者になる | 更新情報をチェックする
人気記事