首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

C# WinForms 經典貪吃蛇遊戲完整實作教學

     編輯:關於C語言

前言

貪吃蛇是程式設計入門經典專案,涵蓋遊戲迴圈、輸入處理、碰撞偵測、狀態管理、繪圖更新等核心概念。本文提供完整的 C# WinForms 實作,架構清晰分離:FormMain (主視窗/遊戲控制)、Snake (蛇身邏輯)、SnakeNode (節點繪製)、SnakeFood (食物管理)。

專案架構設計


GreedySnake/
├── FormMain.cs        // 主視窗、遊戲迴圈、輸入、UI
├── Snake.cs           // 核心邏輯類別
│   ├── Snake          // 蛇身管理:移動、成長、碰撞檢測
│   ├── SnakeNode      // 單一節點:座標、繪製、清除
│   └── SnakeFood      // 食物:生成、顏色、移除
└── Program.cs         // 進入點

核心類別實作

1. SnakeNode - 蛇身基本單元


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;
        }
    }
}

2. Snake - 蛇身邏輯核心


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;
    }
}

3. SnakeFood - 食物管理


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);
    }
}

關鍵技術點解析

1. 雙緩衝防止閃爍


// 在 Form 建構子或設計器設定
this.DoubleBuffered = true;
panelGame.DoubleBuffered = true; // 需透過反射或自訂控制項

// 或自訂 DoubleBufferedPanel
public class DoubleBufferedPanel : Panel
{
    public DoubleBufferedPanel() => DoubleBuffered = true;
}

2. 遊戲迴圈時機控制

  • Thread.Sleep:簡單但不精確,受排程影響
  • Stopwatch + SpinWait:高精確度,佔用 CPU
  • Timer:System.Timers.Timer 或 System.Windows.Forms.Timer (UI 執行緒)
  • 建議:簡單遊戲用 Sleep 足夠;進階用 Task.Delay + 非同步迴圈

3. 碰撞偵測優化


// 當前 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))) // 瞬間完成

4. 擴充功能建議

功能實作方向
高分記錄JSON/XML 序列化儲存,啟動時載入
關卡/障礙物關卡資料結構,初始化佔用格子
道具系統Food 基底類別,衍生 SpeedUp/SlowDown/Invincible
雙人對戰兩條蛇、分割螢幕或同螢幕、獨立輸入處理
AI 自動玩BFS/A* 尋路算法,避開牆壁與自己
網路對戰SignalR/WebSocket 同步狀態,伺服器權威判定

現代化重構方向

使用 Task 非同步迴圈


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();

分離邏輯與渲染 (MVC/MVVM)

  • Model:Snake, Food, GameState - 純邏輯、可單元測試
  • View:GameRenderer - 負責 Draw/Clear,支援 GDI+/WPF/Unity
  • Controller:輸入處理、遊戲迴圈控制、狀態機

總結

這個貪吃蛇實作展示了 WinForms 遊戲開發的核心要素:遊戲迴圈、輸入處理、狀態管理、碰撞偵測、繪圖更新、資源清理。架構上將蛇身、食物、節點封裝為獨立類別,符合單一職責原則。進階開發可引入非同步迴圈、雙緩衝、空間索引優化、MVC 分層,並擴充關卡、道具、AI、網路等功能。掌握這些基礎,即可進階開發更複雜的 2D 遊戲專案。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved