若遊戲包含裝備、坐騎等系統,需對圖片源進行即時合成。合成 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;
}
為何將朝向定義為依賴屬性?因需在 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 依賴屬性實現任意動作下的正確朝向顯示。