プログラミングの最近のブログ記事
前回、XAMLを使用せずにプログラムコードからStoryboardを構築するのにちょっとつまづいたので、整理も兼ねて手順を書いておきます。
Storyboardを構築する主な流れは次のようになります。
- Storyboardクラスのインスタンスを作成
- DoubleAnimation等のアニメーションクラスのインスタンスを作成
- アニメーションをStoryboardの子に追加
- 2で作成したアニメーションの各種プロパティを設定
- アニメーションを適用するターゲットとプロパティを設定
- Storyboardをビジュアルツリー内のどこかのリソースに追加
- Beginメソッドでアニメーションの開始
(順番は多少前後してもかまいません)
2のアニメーションクラスですが、1つのプロパティに対しては1つしか適用できません。青→黒のような単純な色変化だけならColorAnimationクラスでいいのですが、青→赤→緑→青のように複雑な変化をする場合は、ColorAnimationUsingKeyFramesを使う必要があります。時間がかぶらないようにColorAnimationを複数作成して、同じターゲットに対して適用しようとしたら怒られました。
5のアニメーションを適用するターゲットですが、円の色を変化させるのでEllipseのインスタンスを設定してみたら駄目でした。正しくは、EllipseのFillプロパティに設定したSolidColorBrushをターゲットにします。
アニメーションを適用するプロパティは、SolidColorBrush.ColorPropertyフィールドを使って指定します。今まで各クラスにある****Propertyフィールドが何のためにあるのか謎でしたが、やっと使い道が分かりました。
6のリソースに追加ですが、StoryboardはUIElementではないので、CanvasのChildrenに登録することはできません。Resourcesプロパティの子にする必要があります。また、リソースに登録する際にユニークな名前を付ける必要があり、同じオブジェクトのリソース内で名前が重複してはいけません。
実は前回はこのStoryboardを登録するリソースの場所がよく分かっておらず、描画領域のCanvasのリソースに登録しています。円のオブジェクトの数だけStoryboardも作成しているので、新しいStoryboardを作るたびにGuid.NewGuid()でユニークな名前を生成していました。しかし、よく見たらEllipseクラスにもResourcesプロパティはあったので、Ellipseのリソースに登録すれば名前で悩む必要はありませんでした。
EllipseごとにStoryboardを作成するのではなく、1つのStoryboardで全てのEllipseのアニメーションを管理することも試みましたが、これはとても面倒なのでやめました。まず、Storyboardに新しいアニメーションを登録するには、アニメーションの再生を止める必要があります。このときPauseでは駄目で、Stopさせる必要があります。また、1つのStoryboardには1つのタイムラインしかないので、各アニメーションのタイミングのズレは、アニメーションの設定で吸収する必要があります。
Ellipseを作成して色を変化させるサンプルは次のようになります。
現在のSilverlight2では次のようなことができません。
1.ビットマップ画像操作
ラスター画像のピクセル単位での読み書きができません。
2.任意の音声出力
実行時に音声を生成して出力はできません。
3.3Dグラフィックス表示
3D関係の描画機能は一切ありません。
ビットマップが扱えないのは私としてはかなり致命的で、Silverlightを使ってやりたいと思っていたことの半分くらいは無理でした。短冊のように画像が並ぶお絵描き掲示板とか作りたかったんですけどね!一応、Silverlight Raytracing Sampleのように自力でPNGエンコードすれば、ビットマップを表示することは可能ですが、これをリアルタイムで実行するのはキツそうです。
2の音声の動的生成は最初から出来るとは思っていませんでしたが、可能ならばFM音源シミュレータのような「音」を自分で作って遊ぶようなものを作ってみたかったです。
3Dグラフィックスは、機能としては提供されていませんが、テクスチャ無しのグーローシェーディングくらいなら自力描画でそこそこいけるでしょう。テクスチャ付きでQuakeを作っている人もいるので、初代プレイステーション程度の表現はいけるかもしれません。ちなみに来年リリース予定のSilverlight3では、3Dグラフィックス対応とのことです。
まっとうなペイントツールを作るのは厳しそうなので、ベンチマークも兼ねて動くペイントツールを作ってみました。
Silverlightでのキーボード入力は、キーが押されたときに発生するKeyDownイベントと離されたときに発生するKeyUpイベントで処理します。
ゲーム等でキーを押しっぱなしにする操作を使用する場合は、あるキーが現在押されているのかの状態を知る必要がありますが、その情報を直接的に知ることはできません。(ShiftやCtrl等の修飾的に用いるキーのみ取得可能)
現在のキーの押下状態を取得するには、KeyDownイベントとKeyUpイベントでキー毎の押下状態フラグをON/OFFして、自分で管理する必要があります。
特にキーを限定しない、汎用的な押下状態チェックは次のようになります。
public partial class Page : UserControl { Dictionary<Key,DateTime> KeyState = new Dictionary<Key,DateTime>(); public Page() { InitializeComponent(); this.KeyDown += new KeyEventHandler(Page_KeyDown); this.KeyUp += new KeyEventHandler(Page_KeyUp); this.LostFocus += new RoutedEventHandler(Page_LostFocus); } bool IsKeyDown(Key key) { return KeyState.ContainsKey(key); } void Page_KeyDown(object sender, KeyEventArgs e) { if (!KeyState.ContainsKey(e.Key)) { KeyState[e.Key] = DateTime.Now; } } void Page_KeyUp(object sender, KeyEventArgs e) { KeyState.Remove(e.Key); } void Page_LostFocus(object sender, RoutedEventArgs e) { KeyState.Clear(); } }
ここでは、KeyDownイベントで押されたキーをDictionaryに登録し、KeyUpイベントで削除しています。単純に押されたキーの記録だけならDictionaryはValueの部分が冗長ですが、そのような目的に最適なHashSetがSilverlightでは使えないので、DictionaryのKeyだけを利用してValueの部分は無視して代用します。しかし、せっかくValueがあるのに使わないのはもったいないので、ここでは押されたときの時刻をValueとして使っています。これを使って、キーが押されている時間を調べることもできます。(ゲームでは実時間よりもフレーム数で管理した方が便利かもしれません)
また、あるキーを長時間押し続けると、OSのオートリピート処理により、同じキーに対するKeyDownイベントが繰り返し発生します。このときKeyUpイベントはキーが離されるまで発生しないので、KeyDownイベントとKeyUpイベントの発生回数は非対称になります。
キーボード入力に関して3つ注意点があります。
1つ目の注意点ですが、KeyDownイベントもKeyUpイベントも、フォーカスを得ていないと発生しません。IEでウェブサイトを表示しているときにTabを押すとページ内のリンク等を点線の枠が移動しますが、これがフォーカスを表しています。キーボードからの操作は、このフォーカスに対して行われます。
フォーカスを得るには、Silverlightプラグインの画面内をユーザにクリックしてもらう必要があります。よくブラウザ上で動くゲームで、ゲーム内の操作は全てキーボードで行うのにタイトル画面のメニュー選択はマウスでしか行えないという、ユーザビリティの観点からは残念なゲームがありますが、これもフォーカスを得るための苦肉の策となっています。
2つ目は、フルスクリーンモードでは使用できるキーが著しく制限されます。フルスクリーンでも使えるキーは、矢印キー、スペース、Tab、PageUp、PageDown、Home、End、Enterだけとなっています。OSや特定アプリケーションそっくりの画面を出してパスワードを入力させるような悪用を避けるためだそうです。矢印とスペースが使えるので、なんとかゲームにも使えそうですが、矢印以外に2ボタン以上必要なゲームは厳しいでしょう。
3つ目の注意点は、各ブラウザの操作コマンドに設定されているショートカットキーは、ブラウザの操作が優先されるのでSilverlightでは使えません。ただし、Ctrl+C等は(IE上では)使えるので、どのキーが使えないのか各ブラウザで確認する必要があります。とりあえず、Ctrlキーやファンクションキー(F1~F12)は使用を避けた方がいいでしょう。
効果音も基本的にはBGMと同じで、MediaElementを使います。
ただし、常に1つの曲が流れているBGMとは違い、効果音は同時にいくつ鳴るのか実行時にならないと分からないので、画面構成用のXAMLに静的に登録してしまうのではなく、動的にMediaElementオブジェクトを作成します。(使用する効果音が非常に少なかったり同じ音が同時に複数鳴らなければ静的に登録する手も有り)
まずはPage.xamlに効果音を登録するための入れ物となるCanvasを作ります。効果音は実行時に頻繁に追加/削除を繰り返すので、効果音専用にCanvasを用意すると便利です。
<UserControl x:Class="SoundTest.Page" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Width="400" Height="300"> <Canvas x:Name="LayoutRoot" Background="White"> <Canvas x:Name="SpriteCanvas"/> <Canvas x:Name="SoundCanvas"/> <MediaElement x:Name="Music"/> </Canvas> </UserControl>
UserControlは子要素としてコントロールを1つしか持てないので、まずは全体の入れ物となるCanvas(LayoutRoot)を作って、その中にスプライト用Canvas、サウンド用Canvas…と入れ子にします。
効果音に使うファイルは、リソースとして埋め込む方法やWebサイトにxapと一緒に置いて実行時にダウンロードさせる方法があります。ここでは実装が簡単なリソースに埋め込む方法を使います。
まず、プロジェクトに効果音として使いたいmp3(またはwma)を登録します。登録したファイルは、ソリューションエクスプローラで「ビルドアクション」を「埋め込まれたリソース」にします。(XAMLに直接MediaElementを書く場合は「埋め込まれたリソース」では駄目で「Resource」にする理由がよく分かっていません)
リソースとして埋め込んだmp3は、GetManifestResourceStreamで読み込みます。
public partial class Page : UserControl { Stream Sound; void Page_Loaded(object sender, RoutedEventArgs e) { System.Reflection.Assembly asm = this.GetType().Assembly; Sound = asm.GetManifestResourceStream("SoundTest.Resources.sound.mp3"); }
GetManifestResourceStreamに指定するリソース名は、名前空間からフルネームで指定します。大文字と小文字は区別され、フォルダの区切りは.(ピリオド)を使用します。この例ではSoundTestプロジェクトにResourcesフォルダを作って、その中にsound.mp3を入れています。
音を鳴らすときは、MediaElementオブジェクトを作成し、先ほど取得したStreamをソースに指定してPlayメソッドを呼びます。
private void PlaySound() { MediaElement me = new MediaElement(); me.SetSource(Sound); me.MediaEnded += new RoutedEventHandler(me_MediaEnded); SoundCanvas.Children.Add(me); me.Play(); } void me_MediaEnded(object sender, RoutedEventArgs e) { MediaElement me = sender as MediaElement; SoundCanvas.Children.Remove(me); }
実はMediaElementはビジュアルツリーに登録しなくても、オブジェクトの参照を保持してさえいれば、音声は再生されます。しかし、ビジュアルツリーに登録しないと、再生が終わってもMediaEndedイベントが発生しないようです。
私の環境では、SoundCanvas.Children.Countが120を超えたあたりでエラーが発生しました。正確に数えた人によると、123まではOKで124個目でエラーが発生したそうです。ドキュメントにはMediaElementが100を超えるようなら制限を設けるように書いてあるだけなので、この限界値が環境依存なのかは不明です。
Silverlightで扱えるオーディオデータは、MP3(.mp3)とWindows Media Audio(.wma)の2種類だけです。WAVE(.wav)やMIDI(.mid)等は使えません。
使い方はスプライトのときとほとんど一緒で、MediaElementコントロールをCanvasの子として登録するだけです。他のコントロール同様XAMLで登録できます。
<Canvas x:Name="LayoutRoot"> <MediaElement x:Name="Music" Source="music.mp3" AutoPlay="True"/> </Canvas>
Sourceで再生するファイルはリソースとしてプロジェクトに登録しておく必要があります。その際、ビルドアクションが「埋め込まれたリソース」では駄目で、「Resource」にする必要がありました。(日本語版 Silverlight tools RC0を使用)
AutoPlay="True"は、このページが読み込まれると同時に、自動的に再生を開始する設定です。
BGMとして使用する場合はループ再生させる必要がありますが、何故かMediaElementにはループ設定がなく、MediaEndedイベントを使って自分でループを実装する必要があります。
public Page() { InitializeComponent(); Music.MediaEnded += new RoutedEventHandler(Music_MediaEnded); } void Music_MediaEnded(object sender, EventArgs e) { Music.Position = new TimeSpan(0); Music.Play(); }
つなぎ目の無いシームレスなループは無理なようで、ループする際に一瞬音が途切れてしまいます。
MediaEndedイベントを使わずに、ループ終了位置の後ろにループ開始位置のデータを連結したファイルを用意し、Positionがループ終了位置を越えていたらループ開始位置+越えた分に設定しなおすという方法を試しましたが、これでも少し途切れてしまいました。こちらの方が、MediaEndedイベントよりは多少マシで無音部分は1/2~1/3程度に軽減されました。
短いフレーズをループで繰り返すようなBGMは諦めた方がいいかもしれません。これはBGMよりも、継続的に鳴り続ける効果音で致命的なので、何か対策があればいいのですが。
ところで、主に画像を定義するXAMLでBGMも指定できるというのは、意外と便利そうです。例えばゲームのシーン毎にBGMを指定するのに、従来ならばBGM設定を画像とは別に管理する必要がありましたが、画像と同時に扱えれば管理がとても楽になります。フラグによってBGMが変化するというような柔軟な対応は難しいかもしれませんが、MediaElementに付ける名前の命名規則である程度は対応可能だと思います。
Silverlightはユーザの入力イベントに対してハンドラを記述するイベント駆動型プログラミングモデルが基本となっていますが、アクションゲームのようなプログラムではユーザからの入力が全くない場合でも常に動き続けなければいけないので、このプログラミングモデルとは相性が良くありません。
一般的にゲームプログラムは、メインの処理をループで回して常にゲーム処理を動かしておく構造にします。このときのメインループを、ゲームループと呼んだりします。通常のWindowsプログラムでは、メッセージループと呼ばれるメインループが存在するので、そこをメッセージがないときでも止まらないように作ればゲームループを簡単に実装することができます。
しかし、SilverlightではメインループはSilverlightプラグインの内部で処理されていて、我々の手の届かない場所にあります。そこでタイマ等の一定間隔で発生するイベントをゲームループのかわりに使います。
ゲームループの代用になるのは、主に次の3つです。
- System.Windows.Media.CompositionTarget.Renderingイベント
- DispatcherTimer
- StoryBoard
1のCompositionTarget.Renderingイベントというのは、Silverlightの画面更新時に発生するイベントで、処理落ちがなければ一定間隔で常に発生しています。このイベントの発生間隔は、Application.Host.Settings.MaxFrameRateで設定可能で、デフォルトでは60fpsになっています。(ただし元となるタイマの分解能が1msなので、60fpsに設定しても16ms間隔の62.5fpsになる)
2のDispatcherTimerは、発生間隔を100ナノ秒単位で指定できるタイマイベントですが、Silverlightでは1のCompositionTarget.Renderingが呼ばれるタイミングが最小分解能となっています。デフォルトの60fps設定の場合、16msが最小分解能となっており、DispatcherTimerに17msを設定すると実際には32ms間隔でしかイベントが発生しません。
3のStoryBoardは空のStoryBoardを作成し、Completedイベントが発生するたびにBeginメソッドで繰り返し開始することで、タイマイベントのように動作します。これは、DispatcherTimerのIntervalを0に設定するのと実質的に同じことで、どちらを使っても大差ないと思います。
特に理由がなければ、1のRenderingイベントが一番いいと思います。このイベントの使い方は次のようになります。
public partial class Page : UserControl
{
public Page()
{
InitializeComponent();
CompositionTarget.Rendering += new EventHandler(CompositionTarget_Rendering);
}
void CompositionTarget_Rendering(object sender, EventArgs e)
{
OnTimer();
}
フレームレートの設定はHTMLからSilverlightプラグインを呼び出すときのパラメータで与えることもできますが、Appクラスで明示的に設定してしまった方がいいでしょう。
public partial class App : Application
{
public App()
{
this.Startup += this.Application_Startup;
this.Exit += this.Application_Exit;
this.UnhandledException += this.Application_UnhandledException;
this.Host.Settings.MaxFrameRate = 60; // 60fps
InitializeComponent();
}
デフォルト値が60なので60fpsなら何も指定しなくてもいいのですが、プラグインのパラメータでフレームレートを落としてゲームの速度を遅くするチートが可能になってしまうので、60fpsでも明示的に指定しておいた方がいいと思います。
XAMLは画像を定義するだけでなく、簡単なアニメーションも登録することができます。
XAMLアニメーションの解説の多くは、単純に図形の位置や向きが変化したり色や透明度が変化するだけのものが多いですが、ちょっと工夫するとXAMLだけでも、パラパラ漫画のように画像を順番に切り替えていくパラパラアニメーションも作ることができます。
私が試して成功した方法は以下の4つです。
- ObjectAnimationUsingKeyFramesを使ってImageのSourceプロパティを変更して画像を切り替え
- アニメーションを1枚に連結した画像をImageBrushに登録し、Transformでブラシの位置をずらしながら、1コマ分の大きさのRectangleを表示
- Canvas.clipに1コマ分の大きさのRectangleGeometryを登録し、その中でImageを移動させて連結画像の1コマ分をクリップ表示
- Canvas上の同じ位置に全ての画像を重ねて配置し、1枚ずつ順番に表示されるようにObjectAnimationUsingKeyFramesでVisibilityプロパティを切り替え
1は一番素直だと思いますが、Sourceプロパティを変更するたびにPNG画像の読み込みが発生するので、たぶん一番重いと思います。Image専用の手法で、ベクトル画像には使えない欠点もあります。また、VisualStudioでXAMLを編集しようとすると落ちたりして、扱いづらかったです。
2と3は考え方はほとんど一緒で、連結画像から1コマ分だけをどうやって抜き出すのかの実現方法が違います。2はラスター画像専用ですが、3はベクトル画像にも適用できます。
4はStoryboardの記述が複雑になりますが、もしかすると一番軽いかもしれません。ベクトル画像をアニメーションさせる場合は、これがベストではないかと思います。
3の方法で作ったパラパラアニメは以下のようになります。
<UserControl x:Class="SilverlightAnimationTest.Walk" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" IsHitTestVisible="False"> <Canvas x:Name="LayoutRoot"> <Canvas Canvas.Left="-45" Canvas.Top="-73"> <Canvas.Clip> <RectangleGeometry Rect="0,0,75,75"/> </Canvas.Clip> <Image Name="walking" Source="walking.png" Stretch="None"/> </Canvas> <Canvas.Triggers> <EventTrigger RoutedEvent="Canvas.Loaded"> <BeginStoryboard> <Storyboard> <DoubleAnimationUsingKeyFrames Storyboard.TargetName="walking" Storyboard.TargetProperty="(Canvas.Left)" Duration="0:0:1.2" RepeatBehavior="Forever"> <DiscreteDoubleKeyFrame Value="0" KeyTime="0:0:0.0"/> <DiscreteDoubleKeyFrame Value="-75" KeyTime="0:0:0.2"/> <DiscreteDoubleKeyFrame Value="-150" KeyTime="0:0:0.4"/> <DiscreteDoubleKeyFrame Value="-225" KeyTime="0:0:0.6"/> <DiscreteDoubleKeyFrame Value="-300" KeyTime="0:0:0.8"/> <DiscreteDoubleKeyFrame Value="-375" KeyTime="0:0:1.0"/> </DoubleAnimationUsingKeyFrames> </Storyboard> </BeginStoryboard> </EventTrigger> </Canvas.Triggers> </Canvas> </UserControl>
ここで使用しているwalking.pngは以下のようにアニメーションを横に連結した画像で、1コマは75*75のサイズとなっています。
入れ子になっている内側のCanvasは、原点を人物の足元にするためにマイナスの座標を設定しています。また1コマ分を抜き出すためのクリップGeometryもここに設定してあります。
アニメーションの対象が添付プロパティの場合は、このようにTargetPropertyの引数に括弧を付ける必要があります。
これを組み込んだ実物のサンプルは続きで。
Silverlightで画面上にキャラクタ(スプライト)を表示する方法は、DirectDrawやDirect3Dのようにバックバッファに対して自力で描画するのとは全く違った考え方で、表示したいオブジェクトをビジュアルツリーに登録するだけで、後はSilverlightが勝手に描画してくれます。一度登録したら明示的に削除しなければ、ずっと表示されたままになります。昔のハードウェアスプライトを知っている人には、馴染み深いと思います。
まず準備として、Page.xamlにCanvasを設置します。Silverlight2にはレイアウトコントロールとして、Canvas, Grid, StackPanelがありますが、GridやStackPanelは適当に子要素を登録すれば勝手に座標をいい感じに調節して表示してくれるのに対して、Canvasは自分で細かく座標を指定して使います。スプライト表示にはCanvasを使います。ウィザードが自動生成した初期状態のPage.xamlには空っぽのGridコントロールが配置されていますが、これは必要ないのでそのままCanvasに置き換えてしまいます。(Gridの子要素としてCanvasを設置してもいい)
<UserControl x:Class="SpriteTest.Page" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Width="400" Height="300"> <Canvas x:Name="LayoutRoot" Background="White"> </Canvas> </UserControl>
スプライトのパターンデータは、プログラム上でハードコーディングすることも可能ですが、通常はXAMLファイルとして用意します。XAMLは主にベクトル画像を表現するファイルですが、ImageオブジェクトとしてPNGを貼り付ければ、ラスター画像も簡単に扱えます。
XAMLファイルをSilverlightアプリケーションに組み込むには、SilverlightユーザーコントロールとしてC#のクラスにしてしまう方法と、リソースに登録する方法があります。
ユーザーコントロールにするには、[プロジェクト]-[新しい項目の追加]-[Silverlightユーザーコントロール]からプロジェクトに追加します。ここでMario.xamlを追加すると、Marioというクラスが自動的に定義されて、コード上でMario型のオブジェクトを扱うことができます。新しいMario型のインスタンスの作成も普通に Mario mario = new Mario() で作ることができます。XAML中に書かれている子要素もクラスのメンバとして定義されるので、簡単にアクセスできます。
リソースとして登録する場合は、外部のエディタ等でXAMLを予め用意しておいて、[プロジェクト]-[既存項目の追加]で加えます。また、加えたXAMLファイルのプロパティを開き、ビルドアクションを「埋め込まれたリソース」に変更します。
リソースの場合はC#のクラスが定義されないので、インスタンスの作成がちょっと面倒で、XamlReader.Load()にXAMLを読み込ませて作成してもらうことになります。
Assembly asm = this.GetType().Assembly;
Stream s = asm.GetManifestResourceStream("SpriteTest.mario.xaml");
StreamReader reader = new StreamReader(s);
UIElement mario = XamlReader.Load(reader.ReadToEnd()) as UIElement;
スプライトのインスタンスが作成できたら、CanvasのChildrenコレクションに追加します。Page.xamlを上記のように書いた場合、CanvasにLayoutRootという名前が付いているので、そのままLayoutRootでアクセスできます。
LayoutRoot.Children.Add(mario);
表示順(Zオーダー)はChildrenコレクション内の順番に依存していて、後から追加したものほど手前に表示されます。Canvas.ZIndex添付プロパティで制御でき、ZIndexが大きいほど手前に表示されます。ZIndexが等しい場合は、Childrenコレクション内の順番に依存し、後から追加したものほど手前に表示されます。(2008/11/05訂正)
スプライトを表示する座標はCanvasの添付プロパティを使って設定します。
Canvas.SetLeft(mario, 100); // mario.SetValue(Canvas.LeftProperty, 100)でも可
Canvas.SetTop(mario, 80); // mario.SetValue(Canvas.TopProperty, 80)でも可
普通のオブジェクト指向的な発想では mario.X = 100; mario.Y = 80; としたいところですが、Silverlight(WPF)では添付プロパティという方法を使います。添付プロパティのメリットはまだちゃんと理解していませんが、Canvasの子要素になったときにしか使わないLeft, Topを子に持たせるのは無駄だから、親が管理するといった感じでしょうか。(Gridの子要素になったときは、Gridの行と列を表すRow, Columnが添付プロパティになります)
スプライトを消去するときは、Childrenコレクションから削除します。
LayoutRoot.Children.Remove(mario);
同じスプライトを複数同時に表示する場合は、表示したい数だけMarioのインスタンスを作成します。ただし、既にあるオブジェクトからクローンを作成する機能がないようなので、必要な数だけXAMLを読み込む必要があります。毎回XAMLの解析が入って無駄な感じがしますが、我慢するしか無さそうです。
なお、表示する数が決まっているもの(自機、スコア、ゲームオーバーの文字…etc)は、Page.xamlに最初から登録しておいて、VisibilityプロパティやOpacityプロパティで表示のON/OFFを切り替えるのが一般的なようです。
Page.xamlに何かUI要素を追加して、x:Name属性で名前を付けると、Pageクラス内でその名前でアクセスできます。
例えば次のようにPage.xamlにRectangleを加えると
<Rectangle x:Name="hoge" Fill="Red" Width="50" Height="50"/>
Page.xaml.csでhogeを介してRectangleを操作できます。
hoge.Fill = new SolidColorBrush(Colors.Green);
このときhogeはSystem.Windows.Shapes.Rectangleクラスのインスタンスで、Pageクラスのメンバとなっています。
C#はC++のように宣言にうるさい言語のはずなのに、突然hogeが使えるようになっているのが気持ち悪いですが、このカラクリの秘密はobjディレクトリの中にあります。
私は自作プログラムを動かすためのプラットフォームとしてSilverlightを使いたいのですが、今のところ日本語でのSilverlight入門記事は、XAMLで画面をデザインしてそれに対するイベントハンドラとしてプログラムコードを書くという、XAMLが主でプログラムコードが従の関係のものばかりです。実際、Silverlightはそのような構造のアプリケーションに最も向いていて、ExpressionとVisualStudioの組み合わせで簡単に構築することができます。
しかし、私がやりたいのはプログラムコードが主で、XAMLはほとんど使わないか画像ファイル代わりに使うという構造のアプリケーションです。そのような構造のアプリケーション構築に関する記事が見当たらないので、自分で勉強しながら、学んだことを少しずつ書いていこうと思います。
CGIプログラムは通常はWebサーバから呼び出されて実行されるので、高機能なデバッガが利用できないため、とてもデバッグがしにくいです。
しかし、Pythonの標準ライブラリには、CGIのデバッグに役立つ2つのモジュールが用意されていて、デバッグがかなり楽にできます。
まず最初に紹介するのが、cgitbです。このモジュールを使うと、CGIプログラムで捕捉されない例外が発生したときに、トレースバック情報をHTMLで出力してくれます。使い方は非常に簡単で、
import cgitb; cgitb.enable()
と最初の方に書いておくだけです。後は例外が発生したときに、勝手になんとかしてくれます。
実際にcgitbの出力がどんな感じになるのか、サンプルを用意しました。
http://niisaka.s33.xrea.com/cgi-bin/test/python-cgi.cgi
このCGIは1行コメントを投稿して、10行まで保存しますが、コメント欄が空欄のまま投稿すると例外が発生します。(フォームにinput要素があっても値が空の場合はFieldStorageには登録されないため)
例外が発生すると、例外の詳細と前後のソースコードが表示されます。この場合は、form['comment']から暗黙的に呼び出された FieldStorage.__getitem__() でKeyError例外が発生しています。
ユーザにソースコードを見せたくない場合は、cgitb.enable(display=0, logdir="/tmp") のようにすると、/tmpディレクトリにファイルとして出力され、ブラウザ上には出力されなくなります。また、context引数で出力されるソースコードの行数を指定できます。(標準ではcontext=5)
もう1つとても便利なのが、CGIHTTPServerモジュールです。これはCGI実行機能付きの簡易的なWebサーバとして動くモジュールで、Apache等をインストールしなくてもローカルでCGIのテストができます。
使用方法は、コマンドプロンプトから python -m CGIHTTPServer とするだけです。 このときのカレントディレクトリがWebサーバのドキュメントルートとなります。標準ではポート8000に設定されているので、ブラウザから localhost:8000 を開くことでアクセスできます。
CGIスクリプトは、cgi-binという名前のディレクトリより下に置く必要があり、拡張子は.pyでなければなりません。また、cgi-bin以下のディレクトリでは.py以外のファイルにはアクセスできなくなっています。画像などを表示する場合は、cgi-bin以外のディレクトリに配置しなければなりません。
このモジュールには1つ困った問題があり、CGIスクリプトが実行されるときにカレントディレクトリがドキュメントルートのままになっています。通常はCGIスクリプトのあるディレクトリがカレントディレクトリになるので、実際にApache等で動かした場合と動作が異なってしまいます。CGIHTTPServerを修正してしまうか、CGIスクリプト側でカレントディレクトリを設定しなおすといいかもしれません。
このような素晴らしい機能が標準で付いてくるのもPythonの魅力ですね。
CGIと言えばいまだにPerlが主流ですが、PythonはCGI用のライブラリも標準で用意されており、特別なパッケージを別途インストールする必要無しに、簡単にCGIを作成することができます。
CGI作成に使うのは、その名の通りcgiモジュールです。
cgiモジュールの使い方は非常に簡単で、import cgiをした後に form = cgi.FieldStorage() とするだけで、フォームの内容を取得できます。メソッドがPOSTかGETかだとか、そんなことは一切気にする必要はありません。この一行だけで、後はformを介してフォームの値にアクセスできます。
例えば掲示板の名前入力欄にnameという名前を付けていれば、form['name'].valueでname欄に入力された値が取得できます。'name'が存在しない場合はKeyErrorの例外が投げられますが、getvalueメソッドを使って name = form.getvalue('name', '名無しさん') のようにすると例外の代わりにデフォルト値を返してくれます。
'name'に該当する項目が1つだけならば、form['name'].valueは文字列を返しますが、チェックボックス等は同じ名前で複数の項目を使用することがあります。その場合に同名の項目が2つ以上選択されていると、form['name'].valueはリストを返します。同名の項目数に関わらず、常に単一の文字列を返して欲しい場合はgetfirstメソッドを、逆に常にリストで返して欲しい場合はgetlistメソッドを使います。
フォームからファイルをアップロードした場合は、form['name']はファイルオブジェクトになります。アップロードされたファイルの読み取りは普通のファイルと同じように行います。オリジナルのファイル名はform['name'].filenameで、MIMEタイプはform['name'].typeで取得できます。
投稿サイズに制限を設けたい場合は、import cgiの後でcgi.FieldStorage()を呼ぶ前に、cgi.maxlen = 102400 のようにバイト単位で指定します。フォームの内容がこのサイズを超えていると、cgi.FieldStorage()がValueErrorの例外を投げます。(ただし他のエラーでもValueErrorを投げるので、厳密に判定したければエラーメッセージを調べる必要がある)
掲示板CGI等でよく使う、<>&"のエスケープもcgiモジュールに用意されていて、s = cgi.escape(s) でエスケープされた文字列が得られます。デフォルトでは"をエスケープしてくれないので、それもエスケープして欲しい場合は、 s = cgi.escape(s, True) のようにします。
Pythonの強力なライブラリのおかげで、単純な掲示板くらいならあっという間に作ることができます。CGIを配布する場合も、余計なライブラリは必要ないのでライセンス等を気にしなくていいのは気楽です。
floatからintへ変換すると、小数点以下が丸められます。このとき小数点以下の数字をどう扱うかについてはいくつか考え方がありますが、C++の場合は単純に切り捨てられます。
切り捨てというのは、言い方を変えると「0に向かって丸める」とも言います。10進表記で小数点以下が無かったことになります。一般的に小数点以下切り捨てと言ったら、この方法を指します。
この変換をグラフにしてみると、次のようになります。
この図からも分かるように、正と負の値で丸められる向きが逆になり、値が0となる範囲だけが他よりも広くなっています。
この性質は、座標のような値を扱うときには注意しないといけません。例えばマウスカーソルの座標をパース変換してゲーム画面内の整数グリッド位置(チェス盤のマス目等)を計算する場合に、0の座標値を持つグリッドの幅が2倍になり、正と負の座標値で1マスずれることになります。
一方、70~80年代に流行していたBASIC(主にMicrosoft製)では、浮動小数点数から整数への変換は床関数が使われていました。
床関数は、その数以下で最大の整数に変換されます。負の無限大に向かって丸めるとも言います。この方法では、正の値は切り捨てられ、負の値は切り上げられます。
床関数のグラフは次のようになります。
先ほどの切り捨てとは違い、0の周囲でも同じ規則でグラフが変化しています。
座標のような値の場合は、切り捨てよりも床関数の方が合っています。床関数ではfloatの値に+1したら変換後のintの値も必ず+1されますが、切り捨てではfloatの値が0を跨ぐときだけintの値が変化しません。
逆にオフセットのような値は、床関数よりも切り捨ての方が合っています。
IronPythonの勉強ついでに、.NET Frameworkを勉強するためにひさしぶりにC#を使ってみたのですが、VisualC#のIntelliSense(入力支援機能)にカルチャーショックを受けましたw
私が以前C#を使ったときは、テキストエディタ(C#未対応)+コマンドラインコンパイラを使っていたので、入力支援なんて受けられませんでしたが、今回使ってみたVisualC#2008のIntelliSenseはVisualC++とは比較にならないほどの使いやすさでした。
これは単にC#とC++の文法上の差というよりも、全てがグローバルな名前空間に置かれているWin32APIと、名前空間を細かく分けている.NET Frameworkの違いが大きいと思います。
.NET Frameworkのクラスライブラリは、初めて見たときはやりすぎじゃないかと思ったほど細かく、そして何重にも入れ子にして名前空間を区切ってありますが(クラスが1つあるだけの名前空間なんてのがある)、これがIntelliSenseとの相性抜群です。1つの名前空間には少数の識別子しかないので、頭の1~3文字を入力すれば目的の候補を選ぶことができます。System.Windows.Forms.Buttonなどという長い名前でも、実際にタイプするのはsys.wi.f.buだけで済みます。(もちろん名前空間なのでusing System.Windows.FormsしておけばButtonだけでもいい)
今まで、入力支援無しでC#を使ったときの印象で、わざわざ選択する価値は無いなんて結論付けていてすみませんでした。これは素晴らしいものです。言語仕様、ライブラリ、エディタと三すくみでよく練られた開発環境だと思います。
後は.NETアプリケーションの起動の遅ささえなんとかなれば、今日からでもC#メインに移行してもいいんですけどね。ネイティブコードを吐いてくれるC#コンパイラとかあったら最高です。(将来的な脱IA32のためにMSからの提供は無いかな?)
IronPythonはVisualStudioのフォームデザイナを使って、Windowsフォームアプリケーションも作れるので、試しに電卓を作ってみました。
フォームデザイナでコントロールのプロパティを編集すると、ちゃんとPythonのソースコードが変化します。ちょっと感動。
実は私は.NET Frameworkを使うのはこれが初めてですが、かなり使いやすい印象を受けました。Pythonの文法も簡単だし、フォームデザイナも使いやすいし、この組み合わせは最強のRADツールになるかもしれません。
今回作ってみた電卓アプリケーションをせっかくなので置いておきます。
ソースコードと実行ファイル入りです。IronPythonはexeを出力するので、実行の際にはIronPythonのインストールは必要ないようです。(.NET Framework 2.0は必要)
PythonをMicrosoftが.NET Framework用に実装したのが、IronPythonです。
IronPythonはオリジナルのPythonと高い互換性を保ちつつ、.NET Frameworkのクラスライブラリも使えてしまう、とんでもない物です。C#やVB.NETとの相互連携も容易で、お互いにオブジェクトのやり取りができるみたいです。
さて、ここまでなら私も( ´_ゝ`)フーンという感じですが(C#とか使ってないし)、さすがMicrosoft、凄い物を用意しています。
それがIronPython Studioです。
名前から想像できる通り、これはIronPython用のIDE環境です。単独で動くものと、VisualStudio2008に統合可能なものの2種類あります。
元はVisualStudio SDK(VSを拡張するためのSDK)に入っているIronPython Integrationサンプルで、それが単独のプロジェクトとして独立したのが、IronPython Studioみたいです。
しかし、現在公開されているIronPython Studio 1.0ではインストールしてもWPF Applicationしか選択できなかったり(その他のテンプレートはエラーが出ている模様)、そのWPF Applicationもインストールしただけではビルドすらできなかったりと、まだまだ使い物にならない状態です。
現状ではVisualStudio SDKに含まれるサンプルの方がちゃんと動きます。ただし、こちらはソースコードでの提供となるので、自分でビルドしないと使えませんし、VS2008か2005のStandard Edition以上が必要となります。
実際にVisualStudio 2008上でPythonプロジェクトを作成して簡単なスクリプトを書いてみました。(私が初めて書いたPythonスクリプト)
コードが色分け表示されたり、IntelliSenseで入力支援が受けられるのはもちろんのこと、デバッガまでちゃんと対応しているのが驚きです。
ちょっとしたスクリプトの編集にVSを持ち出すのは大袈裟な感じですが、大規模なスクリプトの開発には強力な武器になりそうです。
現在Microsoftが開発中のSilverlight(Flashモドキ)でもIronPythonが使えるみたいなので、このギャグみたいなプログラミング言語が、近い将来にメジャーな言語になっているかもしれませんね。
ちなみにIronRubyも開発されているので、Ruby大好きっ子の人も安心してください :-)
数日前からPythonの勉強を始めました。
Ruby等、他にも選択肢がある中でPythonを選んだ理由は……boostライブラリにPython関係のライブラリがあるので(それが何のためのライブラリなのかは知りませんが)、なんとなくC++との連携が便利そうだったから。
Pythonの言語仕様に一通り目を通してみた感想は、文法がシンプルで分かりやすく、変数どころか構造体すら宣言無しで使えて扱いやすそうな感じです。C++が扱える人ならば、3日もあれば文法は理解できるでしょう。
個人的に感動したのが、他の言語では簡単な操作のわりに面倒な2つの変数の値の入れ替え(swap)が、Pythonでは
x, y = y, x
と一行で書けてしまいます。
これは右辺で(y, x)のタプル(無名の構造体に相当する)が一時オブジェクトとして作成され、左辺ではシーケンスのアンパックという操作でタプルが分解されて個別の変数に代入しています。
CGI用のライブラリも標準で用意されているので、しばらくはPerlで書かれているCGIの移植でもしてみようと思います。
5.0、6.0、2003とProfessional Editionを使ってきたのですが、2008ではStandard Editionでも十分な機能があるので、今回はStandardにしました。(参考リンク1、参考リンク2)
Professional Editionで出来てStandard Editionで出来ないことをまとめると
1.Officeアプリケーション開発
2.モバイルデバイスサポート
3.単体テスト
4.SQLデバッグ
5.リモードデバッグ
こんなところです。1~3はVC++では制限のある機能なので、VC++がメインの私には恩恵も少なそうです。(そもそもOfficeアプリケーションやモバイル開発に興味がありませんが)
上記参考リンク2で「ガイド付き最適化のプロファイル」にチェックが入っていませんが、ビルドメニューから呼び出せないだけで、プロジェクトのプロパティからは設定可能なので、多少面倒ですが利用は可能です。
IronPythonもStandard Editionで使えることを確認しました。
さて、このアップグレード版ですが、実はアップグレード元の対象として無償提供されているExpress Editionが明記されています。(ソース)
VisualStudioを初めて購入する場合でも、合法的に安いアップグレード版が購入できます。
ちなみに、買ったばかりのまっさらなPCにアップグレード版をインストールしても、アップグレード元のメディアやプロダクトキー等は一切要求されませんでした。今後、再インストールするときも楽で助かりますが、これでいいんだろうかw
アス比固定拡大ツールではC++のソースからDirect3DインターフェースのVtblにアクセスするために#define CINTERFACEをしているのですが、これを定義しているとD3DXのヘッダでエラーが出るようです。
D3DXはインライン関数も多く含まれていますが、該当箇所を見てみると__cplusplusしか考慮されておらず、CINTERFACEが定義されているとメソッドの呼び出しでコンパイルエラーが発生します。
CINTERFACE d3dx9でググっても6件しかヒットしないようなマニアックな仕様では、MSの人達に忘れられていても仕方ないかな?3年前からCINTERFACEを使っている私も、今日まで気付きませんでしたし。
SDKのヘッダに手を加えるわけにもいかないし、VisualC++では#undef __cplusplusは無視されてしまうので、D3DXを使用する部分だけソースを分けないと駄目っぽいです。
Windowsでゲームプログラムを作成する際に、忘れがちだけど必要な処理をまとめておきます。
- タイマー精度の設定
timeBeginPeriodでタイマの精度を1msに設定します。この設定はtimeGetTime以外にも、GetTickCountやSleepの精度にも影響します。終了時にはtimeEndPeriodで元に戻しておきましょう。
これを忘れると動きがスムーズでなくなったり、fpsが低くなったりします。また、単独で起動したときよりも、メディアプレーヤーと同時に起動したときに動きがスムーズになるという症状が現れます。
- スクリーンセーバーの起動抑制
スクリーンセーバーが起動する直前に、アクティブなウィンドウに対してWM_SYSCOMMANDメッセージで通知されます。wParamがSC_SCREENSAVEのときはスクリーンセーバー、SC_MONITORPOWERのときはモニターのパワーオフです。このメッセージをDefWindowProcに渡さないようにすると、スクリーンセーバーの起動を抑制することができます。
スクリーンセーバーの起動は、キーボードやマウスの入力で判定していて、ゲームコントローラは全く関知しません。そのため、ゲームコントローラをメインで使うゲームの場合は、何も対策していないとゲーム中にスクリーンセーバーが起動してしまいます。
このメッセージは、自分のウィンドウがアクティブのときにしか送られてきません。最小化されていたり、他のウィンドウがアクティブのときはスクリーンセーバーが起動します。
Vista以降のOSでは、スクリーンセーバーの設定で「再開時にログオン画面に戻る」にチェックが入っていると、このメッセージでスクリーンセーバーの起動を抑制することはできないようです。
- IMEの無効化
ゲーム中でIMEを使用しないのならば、ImmDisableIMEでIMEを無効にしておきましょう。IMEが有効のままだと、ゲーム中に日本語入力をONにすると、それ以降キーを押すたびに左上の方で文字がチラチラ表示されてしまいます。
- フルスクリーン時にタイトルバーを無くす
フルスクリーンモードのときは、ウィンドウスタイルをタイトルバーの無いものに設定します。
WindowsXPやVistaでLunaが有効に設定されているとタイトルバーの角が丸くなり、このままDirect3Dでフルスクリーンに設定すると、画面の左上と右上に隙間ができてしまいます。この部分をマウスでクリックすると、フォアグラウンドウィンドウが切り替わってしまいます。
- Winキーの無効化
Winキーを押すと、スタートメニューが開きます。ゲーム中にうっかり押してしまったときにスタートメニューが開いてしまっては困るので、これも無効にしておきましょう。
一番簡単で確実な方法は、IDirectInputDevice8::SetCooperativeLevelのdwFlagsにDISCL_NOWINKEYを設定します。
どれも1~2行程度で済む簡単な処理ですが、忘れるとゲームプレイに支障をきたすことがあるので、必ず設定しておきましょう。
アスペクト比固定拡大ツールは、フックを仕込んでアプリケーションからDirect3Dへのアクセスをトラップしています。
ただし、DirectXは全てCOMクラスで実装されているので、よくあるAPIフックのようにDLLのインポートアクセステーブル(IAT)を書き換える方法ではフックすることができません。
そこで、まずはプロキシDLLを使ってDirect3DCreate9(8)の呼び出しをトラップし、そこから返されるIDirect3D9のVtblを書き換えています。プロキシDLLを使ったフックはD3DSpyでも使われています。
Direct3DCreate9にフックを仕込む手段は、アプリケーションのエントリポイントで一度停止させてからd3d9.dllのIATを書き換える等の方法もありますが、その方法ではアプリケーションをランチャーから起動させる必要があって面倒なので、今回はプロキシDLLを採用しました。(元になったのがネトゲ用に作った窓化ツールだったというのも理由)
COMクラスは、各メソッドへのポインタが並んだテーブルへのポインタを持っています。これはVisualC++の仮想関数と互換性のある形式で、C++からはこのテーブルの存在は意識する必要がないのですが、CでCOMクラスを扱う場合はlpVtblというメンバを持った構造体として定義されます。例えばBeginSceneをCから呼ぶ場合は、lpD3DDev->lpVtbl->BeginScene(lpD3DDev)となります。 *1
C++の形式ではVtblに直接アクセスする手段が無いので、C形式の方がフックしやすいです。C++の場合でも、d3d9.hをインクルードする前に#define CINTERFACEと書いておくと、定義がC形式に切り替わります。
メインの処理は大雑把に言うと、CreateDeviceをフックして指定された解像度のレンダーターゲットテクスチャを作成して、それをバックバッファのように見せかけています。Presentの段階で本物のバックバッファに比率を調節して拡大コピーします。
ただし、BeginStateBlockとEndStateBlockがVtblを書き換えるという方法で実装されているので注意が必要です。せっかくVtblを書き換えてフックを仕込んでも、これらのメソッドが呼ばれるとVtblが初期化されてしまいます。対策として、BeginStateBlockが呼ばれた時点でEndStateBlockにフックを仕掛け、EndStateBlockの後でVtblを元にもどすようにしました。
また、マウスカーソルは最初はGetCursorPos等をフックして座標変換するつもりでしたが、その方法では上手く動作しなかったので、本物のカーソルは非表示にして座標変換した位置に自分でカーソルを描画しています。しかし、いくら調べてもアニメーションカーソルに関する資料が見つからなかったので、今はカーソルはアニメーションしません。
エラー処理って大事だけど面倒ですよね。初期化関数なんて真面目にエラー処理していたら、本来のコードよりもエラー処理の方が多くなったなんてのもよくある話です。
エラーコードやクラスを定義したりするのも面倒ですし、それらとエラーメッセージの対応を取るのも凄く面倒です。エラーが発生したかどうかだけが重要で、その内容はどうでもいい場合も多く、関数の戻り値をTRUE/FALSEやS_OK/E_FAILのように2値にしている方も多いと思います。
しかし、成功か失敗かの2値だけではほとんどデバッグの役に立ちませんし、各所でエラーメッセージを出そうとすると汚いコードになりがちです。いちいちエラーメッセージを出すのが面倒になって、全てのエラーを「初期化エラー」の一言で片付けていませんか?
そこで、半歩進んだエラー処理として、例外処理を使ったとても簡単な方法を紹介します。

