貪吃蛇是程式設計入門經典專案,涵蓋遊戲迴圈、輸入處理、碰撞偵測、狀態管理、繪圖更新等核心概念。本文提供完整的 C# WinForms 實作,架構清晰分離:FormMain (主視窗/遊戲控制)、Snake (蛇身邏輯)、SnakeNode (節點繪製)、SnakeFood (食物管理)。
GreedySnake/
├── FormMain.cs // 主視窗、遊戲迴圈、輸入、UI
├── Snake.cs // 核心邏輯類別
│ ├── Snake // 蛇身管理:移動、成長、碰撞檢測
│ ├── SnakeNode // 單一節點:座標、繪製、清除
│ └── SnakeFood // 食物:生成、顏色、移除
└── Program.cs // 進入點
using System.Drawing;
using System.Windows.Forms;
public class SnakeNode : IDisposable
{
public Control Canvas { get; set; }
public int CenterX { get; set; }
public int CenterY { get; set; }
public int Radius { get; set; }
public Color NodeColor { get; set; } = Color.Blue;
private bool _disposed = false;
public SnakeNode(Control canvas, int x, int y, int radius)
{
Canvas = canvas;
CenterX = x;
CenterY = y;
Radius = radius;
}
public void Draw(Color? color = null)
{
if (_disposed || Canvas?.IsDisposed == true) return;
using (Graphics g = Canvas.CreateGraphics())
using (SolidBrush brush = new SolidBrush(color ?? NodeColor))
{
int x = CenterX - Radius;
int y = CenterY - Radius;
int diameter = Radius * 2;
g.FillEllipse(brush, x, y, diameter, diameter);
}
}
public void Clear()
{
Draw(Canvas.BackColor); // 用背景色覆蓋
}
public void Dispose()
{
if (!_disposed)
{
Clear();
Canvas = null;
_disposed = true;
}
}
}
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
public class Snake : IDisposable
{
private Control _canvas;
private readonly List _nodes = new List();
private int _radius;
private int _step; // 移動步長 = 2 * Radius
// 方向常數 (位元旗標,便於判斷反向)
private const int UP = 0x0001, DOWN = 0x0010, LEFT = 0x0100, RIGHT = 0x1000;
private int _direction = RIGHT;
private int _nextDirection = RIGHT;
// 頭尾座標緩存
public int HeadX => _nodes[0].CenterX;
public int HeadY => _nodes[0].CenterY;
public int TailX => _nodes[_nodes.Count - 1].CenterX;
public int TailY => _nodes[_nodes.Count - 1].CenterY;
public int Count => _nodes.Count;
public bool IsDead { get; private set; }
public bool IsOutOfBounds { get; private set; }
public Snake(Control canvas, int initialLength, int radius)
{
_canvas = canvas;
_radius = radius;
_step = radius * 2;
// 初始化蛇身 (水平向右)
int startX = radius;
int startY = radius;
for (int i = 0; i < initialLength; i++)
{
var node = new SnakeNode(canvas, startX + i * _step, startY, radius);
_nodes.Insert(0, node); // 頭在索引 0
}
}
public void Draw()
{
foreach (var node in _nodes)
node.Draw();
}
public void Clear()
{
foreach (var node in _nodes)
node.Clear();
}
// 變更方向 (防止 180 度反向)
public void ChangeDirection(int newDirection)
{
int combined = _direction | newDirection;
// UP+DOWN=0x0011, LEFT+RIGHT=0x1100 為反向組合
if (combined != 0x0011 && combined != 0x1100)
_nextDirection = newDirection;
}
// 移動一步
public void Move()
{
if (IsDead) return;
_direction = _nextDirection;
// 計算新頭部位置
int newHeadX = HeadX;
int newHeadY = HeadY;
switch (_direction)
{
case UP: newHeadY -= _step; break;
case DOWN: newHeadY += _step; break;
case LEFT: newHeadX -= _step; break;
case RIGHT: newHeadX += _step; break;
}
// 碰撞檢測:牆壁
if (newHeadX < 0 || newHeadX >= _canvas.Width ||
newHeadY < 0 || newHeadY >= _canvas.Height)
{
IsDead = true;
IsOutOfBounds = true;
return;
}
// 碰撞檢測:自己 (排除尾巴,因為尾巴會移動)
for (int i = 0; i < _nodes.Count - 1; i++)
{
if (_nodes[i].CenterX == newHeadX && _nodes[i].CenterY == newHeadY)
{
IsDead = true;
return;
}
}
// 移動:加頭、畫頭、清尾、除尾
var newHead = new SnakeNode(_canvas, newHeadX, newHeadY, _radius);
_nodes.Insert(0, newHead);
newHead.Draw();
// 尾巴處理:正常移動時移除尾巴,吃食物時保留 (Grow 處理)
if (!_growing)
{
var oldTail = _nodes[_nodes.Count - 1];
oldTail.Clear();
_nodes.RemoveAt(_nodes.Count - 1);
}
else
{
_growing = false;
}
}
private bool _growing = false;
public void Grow() => _growing = true; // 下一步移動時不縮尾巴
public void Dispose()
{
foreach (var node in _nodes)
node.Dispose();
_nodes.Clear();
_canvas = null;
}
}
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
public class SnakeFood : IDisposable
{
private Control _canvas;
private readonly List _foods = new List();
private readonly Random _random = new Random();
private int _radius;
private int _step;
private readonly Color[] _colors = { Color.Red, Color.Green, Color.Yellow };
private int _maxCount = 5;
public int CurrentCount => _foods.Count;
public SnakeFood(Control canvas, int maxCount, int radius)
{
_canvas = canvas;
_maxCount = maxCount;
_radius = radius;
_step = radius * 2;
}
public void AddFood()
{
if (_foods.Count >= _maxCount) return;
// 對齊網格
int maxX = _canvas.Width / _step;
int maxY = _canvas.Height / _step;
int gridX = _random.Next(0, maxX);
int gridY = _random.Next(0, maxY);
int x = _radius + gridX * _step;
int y = _radius + gridY * _step;
// 避免生成在蛇身上 (需外部檢查或這裡傳入蛇身參考)
var food = new SnakeNode(_canvas, x, y, _radius)
{
NodeColor = Color.Green // 固定綠色代表加分食物
};
food.Draw();
_foods.Add(food);
}
public bool TryEat(int headX, int headY, out SnakeNode eatenFood)
{
for (int i = 0; i < _foods.Count; i++)
{
var food = _foods[i];
if (food.CenterX == headX && food.CenterY == headY)
{
eatenFood = food;
food.Clear();
_foods.RemoveAt(i);
return true;
}
}
eatenFood = null;
return false;
}
public void Clear()
{
foreach (var food in _foods)
food.Clear();
_foods.Clear();
}
public void Dispose()
{
Clear();
_canvas = null;
}
}
using System;
using System.Drawing;
using System.Threading;
using System.Windows.Forms;
public partial class FormMain : Form
{
private const int DefaultSpeed = 80; // ms
private int _speed = DefaultSpeed;
private const int InitialLength = 18;
private const int Radius = 5;
private const int MaxFood = 5;
private Snake _snake;
private SnakeFood _food;
private int _greenEaten = 0;
private bool _gameRunning = false;
// UI 控制項 (設計器產生)
// panelGame, panelInfo, lblScore, lblLength, lblSpeed, btnStart, menuStrip...
public FormMain()
{
InitializeComponent();
this.KeyPreview = true; // 表單優先接收按鍵
this.KeyDown += FormMain_KeyDown;
}
private void FormMain_KeyDown(object sender, KeyEventArgs e)
{
if (!_gameRunning || _snake == null) return;
int dir = e.KeyCode switch
{
Keys.W or Keys.Up => Snake.UP,
Keys.S or Keys.Down => Snake.DOWN,
Keys.A or Keys.Left => Snake.LEFT,
Keys.D or Keys.Right => Snake.RIGHT,
_ => -1
};
if (dir != -1)
_snake.ChangeDirection(dir);
}
private void btnStart_Click(object sender, EventArgs e)
{
StartGame();
}
private void StartGame()
{
// 清理上一局
_snake?.Dispose();
_food?.Dispose();
_snake = new Snake(panelGame, InitialLength, Radius);
_food = new SnakeFood(panelGame, MaxFood, Radius);
_greenEaten = 0;
_gameRunning = true;
_snake.Draw();
_food.AddFood(); // 初始一顆食物
UpdateUI();
btnStart.Enabled = false;
btnRestart.Enabled = true;
// 遊戲迴圈在背景執行緒
ThreadPool.QueueUserWorkItem(GameLoop);
}
private void GameLoop(object state)
{
while (_gameRunning && !_snake.IsDead)
{
// 1. 移動蛇
_snake.Move();
// 2. 檢查吃食物
if (_snake.IsDead) break;
if (_food.TryEat(_snake.HeadX, _snake.HeadY, out var food))
{
if (food.NodeColor == Color.Green)
{
_snake.Grow();
_greenEaten++;
}
// 可擴充:不同顏色不同效果
}
// 3. 定期生成食物
if (_food.CurrentCount < MaxFood && _random.Next(0, 10) < 3)
_food.AddFood();
// 4. 更新 UI (跨執行緒)
this.Invoke((Action)UpdateUI);
// 5. 控制速度
Thread.Sleep(_speed);
}
// 遊戲結束
this.Invoke((Action)GameOver);
}
private void UpdateUI()
{
lblLength.Text = $"長度: {_snake.Count}";
lblScore.Text = $"分數: {_greenEaten * 10}";
lblFood.Text = $"食物: {_food.CurrentCount}";
}
private void GameOver()
{
_gameRunning = false;
btnStart.Enabled = true;
btnRestart.Enabled = false;
string msg = _snake.IsOutOfBounds ? "撞牆了!" : "咬到自己了!";
MessageBox.Show($"{msg}\n最終分數: {_greenEaten * 10}\n再玩一局?",
"遊戲結束", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
private void btnRestart_Click(object sender, EventArgs e) => StartGame();
private void menuSpeedEasy_Click(object sender, EventArgs e) => SetSpeed(160, "簡單");
private void menuSpeedNormal_Click(object sender, EventArgs e) => SetSpeed(80, "普通");
private void menuSpeedHard_Click(object sender, EventArgs e) => SetSpeed(40, "困難");
private void SetSpeed(int speed, string label)
{
_speed = speed;
lblSpeed.Text = $"速度: {label}";
// 更新選單勾選狀態...
}
protected override void OnFormClosing(FormClosingEventArgs e)
{
_gameRunning = false;
_snake?.Dispose();
_food?.Dispose();
base.OnFormClosing(e);
}
}
// 在 Form 建構子或設計器設定
this.DoubleBuffered = true;
panelGame.DoubleBuffered = true; // 需透過反射或自訂控制項
// 或自訂 DoubleBufferedPanel
public class DoubleBufferedPanel : Panel
{
public DoubleBufferedPanel() => DoubleBuffered = true;
}
System.Timers.Timer 或 System.Windows.Forms.Timer (UI 執行緒)Task.Delay + 非同步迴圈
// 當前 O(n) 線性搜尋,蛇身長時可優化
// 使用 HashSet 或 2D 陣列 記錄佔用格子
// 移動時:新頭加入、舊尾移除,O(1) 判斷碰撞
private readonly HashSet _occupied = new HashSet();
// 初始化時填入
// 移動時:
_occupied.Add(new Point(newHeadX, newHeadY));
if (!_growing)
_occupied.Remove(new Point(oldTailX, oldTailY));
// 碰撞檢測:
if (_occupied.Contains(new Point(newHeadX, newHeadY))) // 瞬間完成
| 功能 | 實作方向 |
|---|---|
| 高分記錄 | JSON/XML 序列化儲存,啟動時載入 |
| 關卡/障礙物 | 關卡資料結構,初始化佔用格子 |
| 道具系統 | Food 基底類別,衍生 SpeedUp/SlowDown/Invincible |
| 雙人對戰 | 兩條蛇、分割螢幕或同螢幕、獨立輸入處理 |
| AI 自動玩 | BFS/A* 尋路算法,避開牆壁與自己 |
| 網路對戰 | SignalR/WebSocket 同步狀態,伺服器權威判定 |
private async Task GameLoopAsync(CancellationToken token)
{
while (!token.IsCancellationRequested && !_snake.IsDead)
{
_snake.Move();
// ... 邏輯 ...
this.Invoke((Action)UpdateUI);
await Task.Delay(_speed, token); // 可取消、不阻塞執行緒
}
GameOver();
}
// 啟動
var cts = new CancellationTokenSource();
_ = GameLoopAsync(cts.Token);
// 停止
cts.Cancel();
Snake, Food, GameState - 純邏輯、可單元測試GameRenderer - 負責 Draw/Clear,支援 GDI+/WPF/Unity這個貪吃蛇實作展示了 WinForms 遊戲開發的核心要素:遊戲迴圈、輸入處理、狀態管理、碰撞偵測、繪圖更新、資源清理。架構上將蛇身、食物、節點封裝為獨立類別,符合單一職責原則。進階開發可引入非同步迴圈、雙緩衝、空間索引優化、MVC 分層,並擴充關卡、道具、AI、網路等功能。掌握這些基礎,即可進階開發更複雜的 2D 遊戲專案。