概要
PowerShellで簡単なゲームを作りたい。
PowerShellはゲームを作るのには向いていない。
なぜこんなことをするかというと、
OSに標準で入っている機能だけでゲームを作るということに魅力を感じるからだ。
作ったゲーム
テトリスを一般化したもの。
ブロックが4x4の領域にランダムに生成されたものになっている。
1ライン揃えるのも難しい。代わりに、自動落下せず、上にも移動できる。
Cキーで自分のタイミングで固着できる。それでも難しい。
必要なもの
- RGB配列を一定間隔で画面に表示
- キー入力を取得
これらは、PowerShellから利用できる.NET Frameworkの System.Windows.Forms
、System.Drawing
の機能を利用することで実現できる。
雛形となるコード
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
$bmp = [Drawing.Bitmap]::new(32, 32)
$bmpScale = 16
$imageData = [byte[]]::new($bmp.Width * $bmp.Height * 4)
function ClearImageData($r, $g, $b) {
for ($i = 0; $i -lt $imageData.Length; $i += 4) {
$imageData[$i + 0] = $b
$imageData[$i + 1] = $g
$imageData[$i + 2] = $r
$imageData[$i + 3] = 255
}
}
function SetPixelToImageData($x, $y, $r, $g, $b) {
$i = ($y * $bmp.Width + $x) * 4
$imageData[$i + 0] = $b
$imageData[$i + 1] = $g
$imageData[$i + 2] = $r
$imageData[$i + 3] = 255
}
function DrawImageData($graphics) {
$bmpData = $bmp.LockBits(
[Drawing.Rectangle]::new(0, 0, $bmp.Width, $bmp.Height),
[Drawing.Imaging.ImageLockMode]::WriteOnly,
$bmp.PixelFormat
)
$ptr = $bmpData.Scan0
$bytes = [Math]::Abs($bmpData.Stride) * $bmp.Height
[Runtime.InteropServices.Marshal]::Copy($imageData, 0, $ptr, $bytes)
$bmp.UnlockBits($bmpData)
$graphics.InterpolationMode = [Drawing.Drawing2D.InterpolationMode]::NearestNeighbor
$graphics.PixelOffsetMode = [Drawing.Drawing2D.PixelOffsetMode]::Half
$graphics.DrawImage($bmp, 0, 0, $bmp.Width * $bmpScale, $bmp.Height * $bmpScale)
}
$keys = [int[]]::new(256)
function ShowForm() {
$form = [Windows.Forms.Form]::new()
$form.FormBorderStyle = [Windows.Forms.FormBorderStyle]::FixedDialog
$form.Text = 'Game'
$form.ClientSize = [Drawing.Size]::new($bmp.Width * $bmpScale, $bmp.Height * $bmpScale)
$form.StartPosition = 'CenterScreen'
$form.Topmost = $true
[System.Windows.Forms.Form].GetMethod('SetStyle',
[Reflection.BindingFlags]::NonPublic -bor
[Reflection.BindingFlags]::Instance
).Invoke($form, @(
[Windows.Forms.ControlStyles]::DoubleBuffer -bor
[Windows.Forms.ControlStyles]::AllPaintingInWmPaint
$true
))
$form.Add_KeyDown({ param ($sender, $event); $keys[$event.KeyValue] = 1 })
$form.Add_KeyUp({ param ($sender, $event); $keys[$event.KeyValue] = 0 })
$form.Add_Paint({ param ($sender, $event); Update $event.Graphics })
$timer = [Windows.Forms.Timer]::new()
$timer.Interval = 100
$timer.Add_Tick({ $form.Invalidate($true) })
$timer.Start()
$form.ShowDialog()
}
function Update($graphics) {
ClearImageData 0 0 0
# ...
DrawImageData $graphics
}
ShowForm
これであとは Update
部分に更新処理を書けばゲームが作れる。
例えば、左キーが押されたかどうかは、
if ($keys[[int][Windows.Forms.Keys]::Left]) { }
で判定できる。
例えば、(5, 5)の位置に赤いピクセルを表示したい場合は、
SetPixelToImageData 5 5 255 0 0
とすればよい。
チラつき対策
チラつきをなくすためには Form
をダブルバッファリングにしなければいけない。
ダブルバッファリングにするためには Form
を SetStyle
で変更する必要がある。
Form
の SetStyle
は protected
なので継承しないと呼べない。
しかし、PowerShellは、現時点で、
に書かれているような事情により、Add-Type
で追加されたクラスをクラス定義時に認識できない。
なので、PowerShellでは Add-Type
されたクラスを継承することができない。
そこで、
[System.Windows.Forms.Form].GetMethod('SetStyle',
[Reflection.BindingFlags]::NonPublic -bor
[Reflection.BindingFlags]::Instance
).Invoke($form, @(
[Windows.Forms.ControlStyles]::DoubleBuffer -bor
[Windows.Forms.ControlStyles]::AllPaintingInWmPaint
$true
))
と、メソッドを取得して明示的に呼び出すことで回避している。
別解として、
Add-Type -ReferencedAssemblies System.Windows.Forms @'
using System.Windows.Forms;
public class DoubleBufferedForm : Form {
public DoubleBufferedForm() {
this.SetStyle(
ControlStyles.DoubleBuffer |
ControlStyles.AllPaintingInWmPaint,
true
);
this.UpdateStyles();
}
}
'@
と、C#のコードで継承し Add-Type
し、$form
を、
$form = [DoubleBufferedForm]::new()
で生成し回避することもできる。
起動
PowerShellのスクリプトはそのままでは起動できない。
ファイル game.bat
を作り、
powershell -ExecutionPolicy Unrestricted -File %~n0.ps1
のように記載しておけば、game.bat
経由で起動できるようになる。
リポジトリ
まとめ
PowerShellで簡単なゲームが作れた。