前言
用 WinForm 控制項 而非繪圖介面 實作簡易坦克遊戲,適合初學者理解控制項操作、鍵盤事件、ImageList 動畫、多執行緒非同步移動、基本碰撞邏輯。本篇完整提供程式碼、圖片資源需求、執行步驟與擴充方向。
遊戲設計概覽
| 元素 | 實作方式 |
| 坦克本體 | Button 控制項 (設定 ImageList、FlatStyle=Flat、BackColor=Transparent) |
| 四方向圖片 | ImageList 元件 (4 張圖:上、下、左、右,索引 0-3) |
| 移動控制 | KeyPress 事件 (W/A/S/D 對應上/左/下/右) |
| 發射子彈 | K 鍵 → 動態建立 Label (紅色小方塊) + 獨立執行緒移動 |
| 子彈移動 | ParameterizedThreadStart 傳遞 (Label, 方向) → 迴圈移動 + Sleep |
| 邊界檢測 | 螢幕座標超出螢幕範圍 → 執行緒結束、Dispose Label |
| 右鍵選單 | ContextMenuStrip → 結束遊戲 |
準備工作
- 建立 Windows Forms App (.NET Framework) 專案
- 工具箱拖入:Button (name: btnTank)、ImageList (name: imgTank)、ContextMenuStrip (name: cmsMain)
- 準備 4 張坦克圖片 (建議 48x48 px,透明背景 PNG):
tank_up.png, tank_down.png, tank_left.png, tank_right.png
- ImageList 設定:
ImageSize = 48, 48、ColorDepth = Depth32Bit、依序加入 4 張圖 (索引 0=下、1=右、2=左、3=上)
- Button 設定:
ImageList = imgTank, ImageIndex = 0, FlatStyle = Flat, FlatAppearance.BorderSize = 0, BackColor = Transparent, KeyPreview = True (表單層級)
- ContextMenuStrip 加入項目「結束遊戲」→ Click 事件呼叫
Application.Exit()
- Form 設定:
ContextMenuStrip = cmsMain, FormBorderStyle = None (可選全螢幕)、WindowState = Maximized
完整程式碼 (Form1.cs)
using System;
using System.Collections;
using System.Drawing;
using System.Threading;
using System.Windows.Forms;
namespace TankBattle
{
public partial class Form1 : Form
{
private int _screenWidth;
private int _screenHeight;
private string _currentDirection = "down"; // 預設朝下
public Form1()
{
InitializeComponent();
// 取得螢幕尺寸
_screenWidth = Screen.PrimaryScreen.Bounds.Width;
_screenHeight = Screen.PrimaryScreen.Bounds.Height;
// 允許跨執行緒存取控制項 (教學簡化,正式建議用 Invoke)
Control.CheckForIllegalCrossThreadCalls = false;
// 表單鍵盤預覽
this.KeyPreview = true;
this.KeyPress += Form1_KeyPress;
// 初始坦克位置置中
btnTank.Left = (_screenWidth - btnTank.Width) / 2;
btnTank.Top = (_screenHeight - btnTank.Height) / 2;
}
private void Form1_KeyPress(object sender, KeyPressEventArgs e)
{
int step = 10; // 移動步距
switch (char.ToLower(e.KeyChar))
{
case 'w': // 上
_currentDirection = "up";
btnTank.ImageIndex = 3;
btnTank.Top = Math.Max(0, btnTank.Top - step);
break;
case 's': // 下
_currentDirection = "down";
btnTank.ImageIndex = 0;
btnTank.Top = Math.Min(_screenHeight - btnTank.Height, btnTank.Top + step);
break;
case 'a': // 左
_currentDirection = "left";
btnTank.ImageIndex = 2;
btnTank.Left = Math.Max(0, btnTank.Left - step);
break;
case 'd': // 右
_currentDirection = "right";
btnTank.ImageIndex = 1;
btnTank.Left = Math.Min(_screenWidth - btnTank.Width, btnTank.Left + step);
break;
case 'k': // 發射
FireBullet();
break;
case 'x': // 退出 (備選)
Application.Exit();
break;
}
}
private void FireBullet()
{
// 建立子彈 Label
var bullet = new Label
{
BackColor = Color.Red,
Size = new Size(4, 4),
// 子彈從坦克中心發射
Left = btnTank.Left + btnTank.Width / 2 - 2,
Top = btnTank.Top + btnTank.Height / 2 - 2,
Visible = true
};
this.Controls.Add(bullet);
bullet.BringToFront();
// 參數打包
var args = new ArrayList { bullet, _currentDirection };
// 建立並啟動執行緒
var thread = new Thread(new ParameterizedThreadStart(BulletThreadProc));
thread.IsBackground = true; // 主程式結束時自動終止
thread.Start(args);
}
private void BulletThreadProc(object parameters)
{
var args = (ArrayList)parameters;
var bullet = (Label)args[0];
var direction = (string)args[1];
bool outOfBounds = false;
while (!outOfBounds)
{
// 依方向移動
switch (direction)
{
case "up": bullet.Top -= 8; break;
case "down": bullet.Top += 8; break;
case "left": bullet.Left -= 8; break;
case "right": bullet.Left += 8; break;
}
// 邊界檢測
if (bullet.Top < 0 || bullet.Top > _screenHeight ||
bullet.Left < 0 || bullet.Left > _screenWidth)
{
outOfBounds = true;
}
Thread.Sleep(30); // 移動速度控制
}
// 清理資源
try
{
if (bullet.IsHandleCreated) bullet.Invoke(new Action(bullet.Dispose));
}
catch { /* 忽略 */ }
Thread.CurrentThread.Abort();
}
private void 結束遊戲ToolStripMenuItem_Click(object sender, EventArgs e)
{
Application.Exit();
}
}
}
執行畫面與操作
- W / S / A / D:坦克上下左右移動,圖片自動切換方向
- K:發射紅色子彈 (Label),沿當前方向飛行直到螢幕邊界消失
- 右鍵 → 結束遊戲:離開程式
- X:鍵盤快速退出
已知限制與改進方向
| 限制 | 改進方向 |
| Control.CheckForIllegalCrossThreadCalls = false 不安全 | 改用 bullet.Invoke(() => bullet.Top += 8) 或 BeginInvoke 回 UI 執行緒更新 |
| 子彈無碰撞檢測、無敵人、無分數 | 加入敵人坦克 (Button)、碰撞判斷 bullet.Bounds.IntersectsWith(enemy.Bounds)、分數 Label |
| 執行緒每顆子彈一條,大量子彈效能差 | 改用單一 Timer/Task 迴圈統一更新所有子彈位置 (物件池模式) |
| 畫面閃爍、無雙重緩衝 | 表單設定 DoubleBuffered = true、或改用 Panel + Paint 事件繪圖 |
| 無遊戲迴圈、幀率不固定 | 引入 System.Windows.Forms.Timer (約 60fps) 統一更新邏輯與繪圖 |
進階擴充:最小遊戲迴圈重構
// 1. 定義遊戲物件基底
abstract class GameObject
{
public Rectangle Bounds { get; protected set; }
public abstract void Update();
public abstract void Draw(Graphics g);
}
// 2. 坦克、子彈、敵人繼承 GameObject
// 3. 主遊戲迴圈 (Timer Tick 事件)
private void GameLoop_Tick(object sender, EventArgs e)
{
// 更新邏輯
foreach (var obj in _gameObjects) obj.Update();
// 碰撞檢測
CheckCollisions();
// 觸發重繪
gamePanel.Invalidate();
}
// 4. Panel Paint 事件統一繪圖
private void GamePanel_Paint(object sender, PaintEventArgs e)
{
foreach (var obj in _gameObjects) obj.Draw(e.Graphics);
}
教學重點總結
- 控制項即遊戲物件:Button、Label、PictureBox 皆可當作 Sprite,適合入門理解座標、Bounds、Visible。
- ImageList 動畫:透過
ImageIndex 切換幀,無需手動載入圖片。
- 鍵盤事件:
KeyPress (字元) vs KeyDown/KeyUp (鍵碼),遊戲常用 KeyDown 支援長按連發。
- 多執行緒 UI 更新:必須回 UI 執行緒 (Invoke/BeginInvoke),
CheckForIllegalCrossThreadCalls = false 僅供教學。
- 參數化執行緒:
ParameterizedThreadStart 傳遞多參數 (物件陣列/ArrayList/Tuple)。
- 資源清理:動態建立控制項必須
Dispose(),否則記憶體洩漏。