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

C# 開發 WPF/Silverlight 動畫與遊戲系列教程(十八):完美精靈之八面玲瓏(僅限 WPF)②

     編輯:關於C#

為何要合成大圖?

若遊戲包含裝備、坐騎等系統,需對圖片源進行即時合成。合成 2 張大圖的效率遠高於合成 272 張小圖。本例中主角由身體(衣服)與武器兩部分組成,因此定義交錯陣列 PartImage 快取已載入的角色裝備合成圖:

/// <summary>
/// 角色圖片快取
/// 交錯陣列範例 PartImage[a,b][c,d]
/// a: 衣服代號(本例即身體代號,換衣服等於換角色身體)
/// b: 武器代號
/// c: 角色朝向代號
/// d: 當前動作幀在合成圖中的列數
/// 本例:1-5列站立,6-13列跑動,14-20列攻擊,21-26列施法,27-34列死亡
/// PartImage = new BitmapSource[10, 20][,]; // 初步設定 10 個角色、20 種武器
/// </summary>
public static BitmapSource[,][,] PartImage = new BitmapSource[10, 20][,];

例如 PartImage[0,6] 代表 0 號角色拿 6 號武器的八方向所有動作幀;PartImage[4,0] 代表 4 號角色空手的八方向所有動作幀。若有帽子、坐騎則擴展為 BitmapSource[,][,,,]。雖可用 Hashtable 或 Dictionary 替代,但數字鍵對應物件值的場景下,交錯陣列更清晰、優雅且高效。

核心方法:裝備合成與切幀快取

EquipPart() 方法將上一節合成的角色大圖與武器大圖組裝,切割成單幀存入 PartImage:

/// <summary>
/// 拼裝角色+裝備後切割成系列幀圖片並存入記憶體(裝備角色)
/// </summary>
public static BitmapSource[,] EquipPart(int[] Equipment, int rowNum, int colNum, int totalWidth, int totalHeight, int singleWidth, int singleHeight) {
    // Equipment[0] 衣服代號,Equipment[1] 武器代號
    if (PartImage[Equipment[0], Equipment[1]] == null) {
        BitmapSource[,] bitmap = new BitmapSource[rowNum, colNum];
        // 載入角色衣服大圖
        BitmapSource bitmapSource = new BitmapImage(new Uri(@"Images\Body" + Equipment[0].ToString() + ".gif", UriKind.Relative));
        // 若武器非 0,載入武器大圖並合成
        if (Equipment[1] != 0) {
            BitmapSource bitmapSource1 = new BitmapImage(new Uri(@"Images\Weapon" + Equipment[1].ToString() + ".gif", UriKind.Relative));
            DrawingVisual drawingVisual = new DrawingVisual();
            Rect rect = new Rect(0, 0, totalWidth, totalHeight);
            DrawingContext drawingContext = drawingVisual.RenderOpen();
            drawingContext.DrawImage(bitmapSource, rect);
            drawingContext.DrawImage(bitmapSource1, rect);
            drawingContext.Close();
            RenderTargetBitmap renderTargetBitmap = new RenderTargetBitmap(totalWidth, totalHeight, 0, 0, PixelFormats.Pbgra32);
            renderTargetBitmap.Render(drawingVisual);
            bitmapSource = renderTargetBitmap;
        }
        // 切割單幀
        for (int i = 0; i < rowNum; i++) {
            for (int j = 0; j < colNum; j++) {
                bitmap[i, j] = new CroppedBitmap(bitmapSource, new Int32Rect(j * singleWidth, i * singleHeight, singleWidth, singleHeight));
            }
        }
        PartImage[Equipment[0], Equipment[1]] = bitmap;
        return bitmap;
    } else {
        // 記憶體中已存在則直接返回,極大提升效能
        return PartImage[Equipment[0], Equipment[1]];
    }
}

此方法將 5100×1200 像素的角色身體大圖與同尺寸武器大圖合成,再按 150×150 切割存入陣列。

精靈控件關鍵屬性定義

為對接 EquipPart(),精靈控件需新增屬性:

// 當前調用圖片源(二維陣列):第一維朝向 0 上 4 下,順時針 0-7;第二維幀數
public BitmapSource[,] Source { get; set; }
// 方向數量,預設 8
public int DirectionNum { get; set; }
// 當前動作狀態
public Actions Action { get; set; }
// 先前動作狀態
public Actions OldAction { get; set; }
// 各動作對應幀列範圍(暫 5 個動作)
public int[] EachActionFrameRange { get; set; }
// 每方向總列數
public int DirectionFrameNum { get; set; }
// 當前動作起始列
public int CurrentStartFrame { get; set; }
// 當前動作結束列
public int CurrentEndFrame { get; set; }
// 合成大圖總寬/高
public int TotalWidth { get; set; }
public int TotalHeight { get; set; }
// 單張圖片寬/高,預設 150
public int SingleWidth { get; set; }
public int SingleHeight { get; set; }
// 裝備代碼陣列
public int[] Equipment { get; set; } // [0]衣服 [1]武器 [2]頭盔 ...

Actions 列舉定義五大動作:

public enum Actions {
    Stop = 0,    // 停止
    Run = 1,     // 跑動
    Attack = 2,  // 戰鬥
    Magic = 3,   // 施法
    Death = 9    // 死亡
}

精靈初始化與生命線程改進

載入素材(Body0/1.gif、Weapon1/2.gif)後初始化主角:

QXSpirit Spirit = new QXSpirit();
private void InitSpirit() {
    Spirit.X = 300; Spirit.Y = 400;
    Spirit.Timer.Interval = TimeSpan.FromMilliseconds(150);
    Spirit.Equipment[0] = 0; Spirit.Equipment[1] = 1;
    Spirit.EachActionFrameRange = new int[] { 5, 8, 7, 6, 8 };
    Spirit.DirectionFrameNum = 34;
    Spirit.TotalWidth = 5100; Spirit.TotalHeight = 1200;
    Spirit.Source = Super.EquipPart(Spirit.Equipment, Spirit.DirectionNum, Spirit.DirectionFrameNum,
        Spirit.TotalWidth, Spirit.TotalHeight, Spirit.SingleWidth, Spirit.SingleHeight);
    Carrier.Children.Add(Spirit);
}

生命線程加入動作變更檢測與幀推進器:

int _FrameCounter = 1;
public int FrameCounter { get { return _FrameCounter; } set { _FrameCounter = value; } }

private void Timer_Tick(object sender, EventArgs e) {
    if (OldAction != Action) ChangeAction();
    Body.Source = Source[(int)Direction, FrameCounter];
    FrameCounter = FrameCounter == CurrentEndFrame ? CurrentStartFrame : FrameCounter + 1;
}

動作切換核心邏輯

ChangeAction() 根據當前動作設定切圖參數:

private void ChangeAction() {
    switch (Action) {
        case Actions.Stop:
            Timer.Interval = TimeSpan.FromMilliseconds(150);
            CurrentStartFrame = 0;
            CurrentEndFrame = EachActionFrameRange[0] - 1;
            OldAction = Actions.Stop;
            break;
        case Actions.Run:
            Timer.Interval = TimeSpan.FromMilliseconds(150);
            CurrentStartFrame = EachActionFrameRange[0];
            CurrentEndFrame = EachActionFrameRange[0] + EachActionFrameRange[1] - 1;
            OldAction = Actions.Run;
            break;
        case Actions.Attack:
            Timer.Interval = TimeSpan.FromMilliseconds(120);
            CurrentStartFrame = EachActionFrameRange[0] + EachActionFrameRange[1];
            CurrentEndFrame = EachActionFrameRange[0] + EachActionFrameRange[1] + EachActionFrameRange[2] - 1;
            OldAction = Actions.Attack;
            break;
        case Actions.Magic:
            Timer.Interval = TimeSpan.FromMilliseconds(100);
            CurrentStartFrame = EachActionFrameRange[0] + EachActionFrameRange[1] + EachActionFrameRange[2];
            CurrentEndFrame = EachActionFrameRange[0] + EachActionFrameRange[1] + EachActionFrameRange[2] + EachActionFrameRange[3] - 1;
            OldAction = Actions.Magic;
            break;
        case Actions.Death:
            Timer.Interval = TimeSpan.FromMilliseconds(150);
            CurrentStartFrame = EachActionFrameRange[0] + EachActionFrameRange[1] + EachActionFrameRange[2] + EachActionFrameRange[3];
            CurrentEndFrame = CurrentStartFrame + EachActionFrameRange[4] - 1;
            OldAction = Actions.Death;
            break;
    }
    FrameCounter = CurrentStartFrame;
}

關鍵屬性:Direction 依賴屬性

為何將朝向定義為依賴屬性?因需在 Storyboard 移動動畫中即時修改方向,使尋路移動更平滑:

// 精靈當前朝向:0 上 4 下,順時針 0-7(依賴屬性)
public double Direction {
    get { return (double)GetValue(DirectionProperty); }
    set { SetValue(DirectionProperty, value); }
}
public static readonly DependencyProperty DirectionProperty = DependencyProperty.Register(
    "Direction", typeof(double), typeof(QXSpirit));

階段成果與下一步

目前可看到主角站立動作,但移動時方向固定朝北(0)。下節將利用 Direction 依賴屬性實現任意動作下的正確朝向顯示。

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