PowerShellで遊んでみた：ブロック崩しゲームの実装 (2)

## はじめに

こんにちは！今回は、PowerShellを使ってブロック崩しゲームを実装する方法を紹介します。.NETのWindows Forms を利用して、グラフィカルなゲーム画面を作成し、キー入力操作とスコア機能を追加します。

前回の記事ではコンソールの画面に出力していたのを、今回は.NETのWindows Forms を画面のフレームワークとして採用しました。

https://qiita.com/Tadataka_Takahashi/items/39dac06de4cc89d461ba

## 目次
1. 要件
2. 仕様
3. 環境設定
4. ゲームの基本構造
5. ゲームオブジェクトの実装
6. キー入力の処理
7. スコア機能の実装
8. ゲームロジックの実装
9. 画面の描画
10. ゲームループの実装
11. 完全なソースコード
12. まとめ

## 1. 要件

このプロジェクトでは、以下の要件を満たすブロック崩しゲームを作成します：

1. PowerShellを使用して実装すること
2. グラフィカルなユーザーインターフェースを持つこと
3. プレイヤーがパドルを操作してボールを打ち返せること
4. 複数のブロックを配置し、ボールが当たると消えること
5. スコア機能を実装すること
6. ゲームオーバーとクリア条件を設定すること

```plantuml
@startuml
left to right direction
skinparam actorStyle awesome
skinparam usecaseArrowThickness 2
skinparam usecaseBorderThickness 2

actor "プレイヤー" as Player

rectangle "ブロック崩しゲーム" {
  usecase "ゲームを開始する" as UC1
  usecase "パドルを操作する" as UC2
  usecase "ボールを打ち返す" as UC3
  usecase "ブロックを破壊する" as UC4
  usecase "スコアを獲得する" as UC5
  usecase "ゲームオーバーになる" as UC6
  usecase "ゲームをクリアする" as UC7
  usecase "ハイスコアを記録する" as UC8
}

Player --> UC1
Player --> UC2
Player --> UC3

UC2 ..> UC3 : <<include>>
UC3 ..> UC4 : <<include>>
UC4 ..> UC5 : <<include>>
UC1 ..> UC6 : <<extend>>
UC1 ..> UC7 : <<extend>>
UC7 ..> UC8 : <<include>>

@enduml
```

## 2. 仕様

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/7c10b7a9-24f8-3280-00b4-63d1433ae517.png)

上記の要件を満たすため、以下の仕様で実装を行います：

1. 使用技術：
   - PowerShell
   - .NET Framework（System.Windows.Forms, System.Drawing）

2. ゲーム画面：
   - サイズ：400x500ピクセル
   - 背景色：黒

3. ゲームオブジェクト：
   - パドル：
     - サイズ：幅100ピクセル、高さ10ピクセル
     - 初期位置：画面下部中央
     - 移動速度：5ピクセル/フレーム
   - ボール：
     - サイズ：直径10ピクセル
     - 初期位置：画面中央
     - 初期速度：X方向2ピクセル/フレーム、Y方向-2ピクセル/フレーム
   - ブロック：
     - サイズ：幅50ピクセル、高さ20ピクセル
     - 配置：7列3行（計21個）

4. 操作方法：
   - 左矢印キー：パドルを左に移動
   - 右矢印キー：パドルを右に移動

5. スコアシステム：
   - 初期スコア：0点
   - ブロック1個につき10点加算

6. ゲーム終了条件：
   - ゲームオーバー：ボールが画面下端に到達
   - クリア：すべてのブロックを破壊

7. その他：
   - フレームレート：約60FPS（タイマー間隔15ミリ秒）
   - 衝突判定：単純な矩形判定を使用

これらの仕様に基づいて、以降のセクションでゲームの実装を進めていきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/14105494-67b6-c5d7-ee23-eb2f61bd3e2d.png)

## 3. 環境設定

PowerShellとWindows Formsを使用するため、特別な環境設定は必要ありません。Windows 10以降のOSであれば、標準でPowerShellが利用可能です。

## 4. ゲームの基本構造

まず、ゲームの基本構造を作成します。Windows Formsを使用してゲームウィンドウを作成し、ゲームオブジェクト（パドル、ボール、ブロック）を定義します。

```powershell
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing

$form = New-Object System.Windows.Forms.Form
$form.Text = "PowerShell ブロック崩し"
$form.Size = New-Object System.Drawing.Size(400, 500)
$form.StartPosition = "CenterScreen"
$form.BackColor = [System.Drawing.Color]::Black

$graphics = $form.CreateGraphics()
```

## 5. ゲームオブジェクトの実装

次に、ゲームオブジェクトを定義します。パドル、ボール、ブロックをそれぞれ構造体として実装します。

```powershell
$paddle = @{
    X = 150
    Y = 450
    Width = 100
    Height = 10
    Speed = 5
}

$ball = @{
    X = 200
    Y = 250
    Size = 10
    SpeedX = 2
    SpeedY = -2
}

$blocks = @()
$blockWidth = 50
$blockHeight = 20
for ($i = 0; $i -lt 7; $i++) {
    for ($j = 0; $j -lt 3; $j++) {
        $blocks += @{
            X = $i * ($blockWidth + 10) + 25
            Y = $j * ($blockHeight + 10) + 50
            Width = $blockWidth
            Height = $blockHeight
            IsVisible = $true
        }
    }
}
```

## 6. キー入力の処理

キー入力の処理方法を改善します。`GetAsyncKeyState` を使用してキー入力を検出します。

```powershell
Add-Type @"
using System;
using System.Runtime.InteropServices;

public class KeyboardInput {
    [DllImport("user32.dll")]
    public static extern short GetAsyncKeyState(int vKey);
}
"@

function IsKeyPressed($key) {
    return ([KeyboardInput]::GetAsyncKeyState($key) -band 0x8000) -ne 0
}
```

## 7. スコア機能の実装

スコア機能を実装します。ゲーム開始時のスコアを0とし、ブロックを壊すたびにスコアが増加するようにします。

```powershell
$score = 0

function UpdateScore {
    $script:score += 10
}
```

## 8. ゲームロジックの実装

ゲームロジックを実装します。ボールの移動、衝突判定、スコア管理などを行います。

```powershell
function MoveBall {
    $ball.X += $ball.SpeedX
    $ball.Y += $ball.SpeedY

    # 壁との衝突判定
    if ($ball.X -le 0 -or $ball.X -ge ($form.Width - $ball.Size)) {
        $ball.SpeedX *= -1
    }
    if ($ball.Y -le 0) {
        $ball.SpeedY *= -1
    }

    # パドルとの衝突判定
    if ($ball.Y -ge $paddle.Y - $ball.Size -and 
        $ball.X -ge $paddle.X -and 
        $ball.X -le $paddle.X + $paddle.Width) {
        $ball.SpeedY *= -1
    }

    # ブロックとの衝突判定
    for ($i = 0; $i -lt $blocks.Count; $i++) {
        if ($blocks[$i].IsVisible -and 
            $ball.X -ge $blocks[$i].X -and 
            $ball.X -le $blocks[$i].X + $blocks[$i].Width -and 
            $ball.Y -ge $blocks[$i].Y -and 
            $ball.Y -le $blocks[$i].Y + $blocks[$i].Height) {
            $blocks[$i].IsVisible = $false
            $ball.SpeedY *= -1
            UpdateScore  # スコアを更新
            break
        }
    }
}

function MovePaddle {
    if (IsKeyPressed([System.Windows.Forms.Keys]::Left)) {
        $paddle.X -= $paddle.Speed
    }
    if (IsKeyPressed([System.Windows.Forms.Keys]::Right)) {
        $paddle.X += $paddle.Speed
    }
    
    $paddle.X = [Math]::Max(0, [Math]::Min($form.Width - $paddle.Width, $paddle.X))
}
```

## 9. 画面の描画

ゲームオブジェクトとスコアを描画する関数を実装します。

```powershell
function DrawGame {
    $graphics.Clear([System.Drawing.Color]::Black)
    
    # パドルの描画
    $graphics.FillRectangle([System.Drawing.Brushes]::White, $paddle.X, $paddle.Y, $paddle.Width, $paddle.Height)
    
    # ボールの描画
    $graphics.FillEllipse([System.Drawing.Brushes]::White, $ball.X, $ball.Y, $ball.Size, $ball.Size)
    
    # ブロックの描画
    foreach ($block in $blocks) {
        if ($block.IsVisible) {
            $graphics.FillRectangle([System.Drawing.Brushes]::Blue, $block.X, $block.Y, $block.Width, $block.Height)
        }
    }

    # スコアの描画
    $font = New-Object System.Drawing.Font("Arial", 12)
    $brush = [System.Drawing.Brushes]::White
    $graphics.DrawString("Score: $score", $font, $brush, 10, 10)
}
```

## 10. ゲームループの実装
最後に、ゲームループを実装して、ゲームを動作させます。

```powershell
$timer = New-Object System.Windows.Forms.Timer
$timer.Interval = 15
$timer.Add_Tick({
    MoveBall
    MovePaddle
    DrawGame

    # ゲームオーバー判定
    if ($ball.Y -gt $form.Height) {
        $timer.Stop()
        [System.Windows.Forms.MessageBox]::Show("ゲームオーバー`nスコア: $score", "PowerShell ブロック崩し")
        $form.Close()
    }

    # クリア判定
    if (-not ($blocks | Where-Object { $_.IsVisible })) {
        $timer.Stop()
        [System.Windows.Forms.MessageBox]::Show("クリア！おめでとうございます！`nスコア: $score", "PowerShell ブロック崩し")
        $form.Close()
    }
})

$form.Add_Shown({$timer.Start()})
$form.Add_FormClosing({$timer.Stop()})
$form.ShowDialog()
```

## 11. 完全なソースコード

### クラス図

```plantuml
@startuml
skinparam classAttributeIconSize 0

class Game {
  -Form form
  -Graphics graphics
  -Timer timer
  -Paddle paddle
  -Ball ball
  -Block[] blocks
  -int score
  +Start()
  +Stop()
  -MoveBall()
  -MovePaddle()
  -DrawGame()
  -UpdateScore()
  -CheckCollisions()
  -CheckGameOver()
  -CheckGameClear()
}

class Form {
  +Show()
  +Close()
}

class Graphics {
  +Clear()
  +FillRectangle()
  +FillEllipse()
  +DrawString()
}

class Timer {
  +Start()
  +Stop()
}

abstract class GameObject {
  +int X
  +int Y
  +int Width
  +int Height
}

class Paddle {
  +int Speed
  +Move()
}

class Ball {
  +int Size
  +int SpeedX
  +int SpeedY
  +Move()
}

class Block {
  +bool IsVisible
}

class KeyboardInput {
  {static} +IsKeyPressed()
}

Game "1" *-- "1" Form
Game "1" *-- "1" Graphics
Game "1" *-- "1" Timer
Game "1" *-- "1" Paddle
Game "1" *-- "1" Ball
Game "1" *-- "1..*" Block
Game ..> KeyboardInput

GameObject <|-- Paddle
GameObject <|-- Ball
GameObject <|-- Block

@enduml
```

### シーケンス図

```plantuml
@startuml
skinparam sequenceArrowThickness 2
skinparam roundcorner 20
skinparam maxMessageSize 60

actor Player
participant Game
participant Form
participant Timer
participant Paddle
participant Ball
participant Blocks
participant KeyboardInput

Player -> Game: Start Game
activate Game

Game -> Form: Create Window
activate Form

Game -> Timer: Start
activate Timer

loop Every Timer Tick
    Timer -> Game: Tick
    
    Game -> KeyboardInput: Check Input
    activate KeyboardInput
    KeyboardInput --> Game: Input State
    deactivate KeyboardInput
    
    Game -> Paddle: Move
    activate Paddle
    Paddle --> Game: New Position
    deactivate Paddle
    
    Game -> Ball: Move
    activate Ball
    Ball --> Game: New Position
    deactivate Ball
    
    Game -> Game: Check Collisions
    
    alt Ball collides with Paddle
        Game -> Ball: Bounce
    else Ball collides with Block
        Game -> Blocks: Remove Block
        Game -> Game: Update Score
    else Ball misses Paddle
        Game -> Game: Game Over
    end
    
    Game -> Form: Redraw
    activate Form
    Form --> Game: Draw Complete
    deactivate Form
    
    alt All Blocks Cleared
        Game -> Game: Level Complete
    end
end

Game -> Timer: Stop
deactivate Timer

Game -> Form: Close
deactivate Form

deactivate Game

@enduml
```

以下は、これまでの実装をまとめた完全なソースコードです。

```powershell
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing

Add-Type @"
using System;
using System.Runtime.InteropServices;

public class KeyboardInput {
    [DllImport("user32.dll")]
    public static extern short GetAsyncKeyState(int vKey);
}
"@

$form = New-Object System.Windows.Forms.Form
$form.Text = "PowerShell ブロック崩し"
$form.Size = New-Object System.Drawing.Size(400, 500)
$form.StartPosition = "CenterScreen"
$form.BackColor = [System.Drawing.Color]::Black

$graphics = $form.CreateGraphics()

$paddle = @{
    X = 150
    Y = 450
    Width = 100
    Height = 10
    Speed = 5
}

$ball = @{
    X = 200
    Y = 250
    Size = 10
    SpeedX = 2
    SpeedY = -2
}

$blocks = @()
$blockWidth = 50
$blockHeight = 20
for ($i = 0; $i -lt 7; $i++) {
    for ($j = 0; $j -lt 3; $j++) {
        $blocks += @{
            X = $i * ($blockWidth + 10) + 25
            Y = $j * ($blockHeight + 10) + 50
            Width = $blockWidth
            Height = $blockHeight
            IsVisible = $true
        }
    }
}

$score = 0

function IsKeyPressed($key) {
    return ([KeyboardInput]::GetAsyncKeyState($key) -band 0x8000) -ne 0
}

function UpdateScore {
    $script:score += 10
}

function MoveBall {
    $ball.X += $ball.SpeedX
    $ball.Y += $ball.SpeedY

    if ($ball.X -le 0 -or $ball.X -ge ($form.Width - $ball.Size)) {
        $ball.SpeedX *= -1
    }
    if ($ball.Y -le 0) {
        $ball.SpeedY *= -1
    }

    if ($ball.Y -ge $paddle.Y - $ball.Size -and 
        $ball.X -ge $paddle.X -and 
        $ball.X -le $paddle.X + $paddle.Width) {
        $ball.SpeedY *= -1
    }

    for ($i = 0; $i -lt $blocks.Count; $i++) {
        if ($blocks[$i].IsVisible -and 
            $ball.X -ge $blocks[$i].X -and 
            $ball.X -le $blocks[$i].X + $blocks[$i].Width -and 
            $ball.Y -ge $blocks[$i].Y -and 
            $ball.Y -le $blocks[$i].Y + $blocks[$i].Height) {
            $blocks[$i].IsVisible = $false
            $ball.SpeedY *= -1
            UpdateScore
            break
        }
    }
}

function MovePaddle {
    if (IsKeyPressed([System.Windows.Forms.Keys]::Left)) {
        $paddle.X -= $paddle.Speed
    }
    if (IsKeyPressed([System.Windows.Forms.Keys]::Right)) {
        $paddle.X += $paddle.Speed
    }
    
    $paddle.X = [Math]::Max(0, [Math]::Min($form.Width - $paddle.Width, $paddle.X))
}

function DrawGame {
    $graphics.Clear([System.Drawing.Color]::Black)
    $graphics.FillRectangle([System.Drawing.Brushes]::White, $paddle.X, $paddle.Y, $paddle.Width, $paddle.Height)
    $graphics.FillEllipse([System.Drawing.Brushes]::White, $ball.X, $ball.Y, $ball.Size, $ball.Size)
    
    foreach ($block in $blocks) {
        if ($block.IsVisible) {
            $graphics.FillRectangle([System.Drawing.Brushes]::Blue, $block.X, $block.Y, $block.Width, $block.Height)
        }
    }

    $font = New-Object System.Drawing.Font("Arial", 12)
    $brush = [System.Drawing.Brushes]::White
    $graphics.DrawString("Score: $score", $font, $brush, 10, 10)
}

$timer = New-Object System.Windows.Forms.Timer
$timer.Interval = 15
$timer.Add_Tick({
    MoveBall
    MovePaddle
    DrawGame

    if ($ball.Y -gt $form.Height) {
        $timer.Stop()
        [System.Windows.Forms.MessageBox]::Show("ゲームオーバー`nスコア: $score", "PowerShell ブロック崩し")
        $form.Close()
    }

    if (-not ($blocks | Where-Object { $_.IsVisible })) {
        $timer.Stop()
        [System.Windows.Forms.MessageBox]::Show("クリア！おめでとうございます！`nスコア: $score", "PowerShell ブロック崩し")
        $form.Close()
    }
})

$form.Add_Shown({$timer.Start()})
$form.Add_FormClosing({$timer.Stop()})
$form.ShowDialog()
```

## 12. まとめ

以上で、PowerShellを使用したブロック崩しゲームの実装が完了しました。このコードを実行すると、シンプルながらも楽しいブロック崩しゲームをプレイすることができます。

主な特徴：
1. Windows Formsを使用したグラフィカルなインターフェース
2. 改善されたキー入力処理（`GetAsyncKeyState`の使用）
3. スコア機能の実装（ブロックを壊すとスコアが増加）
4. ゲームオーバーとクリア時のスコア表示

PowerShellは通常、システム管理やスクリプト作成に使用されますが、このように創造的な用途にも適用できることがわかります。さらに改良を加えて、以下のような機能を実装してみるのも面白いでしょう：

- レベルデザイン（複数のステージ）
- サウンドエフェクト
- パワーアップアイテム
- 難易度設定
- ハイスコア機能

このプロジェクトを通じて、PowerShellの柔軟性と.NET Frameworkとの連携の可能性を探ることができました。システム管理者やIT専門家だけでなく、プログラミング初心者にとっても、PowerShellの可能性を広げる良い例となるでしょう。

ゲーム開発の楽しさを体験しながら、プログラミングスキルを向上させることができます。ぜひ、このコードをベースに自分なりのアイデアを追加して、オリジナルのゲームを作成してみてください。

Happy coding!