当前位置: 首页 > news >正文

福永网站开发城阳网站开发公司

福永网站开发,城阳网站开发公司,《网页制作与网站建设》,学习网站建设的书前言2048是一款益智小游戏#xff0c;得益于其规则简单#xff0c;又和 2的倍数有关#xff0c;因此广为人知#xff0c;特别是广受程序员的喜爱。本文将再次使用我自制的“准游戏引擎” FlysEngine#xff0c;从空白窗口开始#xff0c;演示如何“手撸” 2048小游戏得益于其规则简单又和 2的倍数有关因此广为人知特别是广受程序员的喜爱。本文将再次使用我自制的“准游戏引擎” FlysEngine从空白窗口开始演示如何“手撸” 2048小游戏并在编码过程中感受 C#的魅力和 .NET编程的快乐。说明 FlysEngine是封装于 Direct2D重复本文示例只需在 .NETCore3.0下安装 NuGet包 FlysEngine.Desktop即可。并不一定非要做一层封装才能用只是 FlysEngine简化了创建设备、处理设备丢失、设备资源管理等“新手劝退”级操作首先来看一下最终效果 小游戏的三原则在开始做游戏前我先聊聊 CRUD程序员做小游戏时我认为最重要的三大基本原则。很多时候我们有做个游戏的心但发现做出来总不是那么回事。这时可以对照一下看是不是违反了这三大原则中的某一个MVC应用程序驱动而非事件驱动动画MVC或者 MVP……关键是将逻辑与视图分离。它有两大特点视图层完全没有状态;数据的变动不会直接影响呈现的画面。也就是所有的数据更新都只应体现在内存中。游戏中的数据变化可能非常多应该积攒起来一次性更新到界面上。这是因为游戏实时渲染特有的性能所要求的游戏常常有成百上千个动态元素在界面上飞舞这些动作必须在一次垂直同步如 16ms或更低的时间内完成否则用户就会察觉到卡顿。常见的反例有 knockout.js它基于 MVVM也就是数据改变会即时通知到视图( DOM)导致视图更新不受控制。另外 MVC还有一个好处就是假如代码需要移植平台时如 C#移植到 html5只需更新呈现层即可模型层所有逻辑都能保留。应用程序驱动而非事件驱动应用程序驱动的特点是界面上的动态元素之所以“动”是由应用程序触发——而非事件触发的。这一点其实与 MVC也是相辅相成。应用程序驱动确保了 MVC的性能不会因为依赖变量重新求值次数过多而影响性能。另外如果界面上有状态就会导致逻辑变得非常复杂比如变量之间的依赖求值、界面上某些参数的更新时机等。不如简单点搞直接全部重新计算全部重新渲染绝对不会错细心的读者可能发现最终效果 demo中的总分显示就有 bug开始游戏时总分应该是 4而非 72。这就是由于该部分没有使用应用程序驱动求值导致逻辑复杂导致粗心……最终导致出现了 bug。在 html5的 canvas中实时渲染的“心脏”是 requestAnimationFrame()函数在 FlysEngine中“心脏”是 RenderLoop.Run()函数using var form new RenderWindow { ClientSize new System.Drawing.Size(400, 400) }; form.Draw (RenderWindow sender, DeviceContext ctx) { ctx.Clear(Color.CornflowerBlue); }; RenderLoop.Run(form, () form.Render(1, PresentFlags.None)); // 心脏动画动画是小游戏的灵魂一个游戏做得够不够精致有没有“质感”除了 UI把关外就靠我们程序员把动画做好了。动画的本质是变量从一个值按一定的速度变化到另一个值using var form new RenderWindow { StartPosition System.Windows.Forms.FormStartPosition.CenterScreen }; float x 0; form.Draw (w, ctx) { ctx.Clear(Color.CornflowerBlue); var brush w.XResource.GetColor(Color.Red); ctx.FillRectangle(new RectangleF(x, 50, 50, 50), brush); ctx.DrawText($x {x}, w.XResource.TextFormats[20], new RectangleF(0, 0, 100, 100), brush); x 1.0f; }; RenderLoop.Run(form, () form.Render(1, PresentFlags.None));运行效果如下 然而如果用应用程序驱动——而非事件驱动做动画代码容易变得混乱不堪。尤其是多个动画、动画与动画之间做串联等等。这时代码需要精心设计将代码写成像事件驱动那么容易下文将演示如何在 2048小游戏中做出流畅的动画。2048小游戏回到2048小游戏我们将在制作这个游戏慢慢体会我所说的“小游戏三原则”。起始代码这次我们创建一个新的类 GameWindow继承于 RenderWindow不像之前直接使用 RenderWindow类这样有利于分离视图层const int MatrixSize 4; void Main() { using var g new GameWindow() { ClientSize new System.Drawing.Size(400, 400) }; RenderLoop.Run(g, () g.Render(1, PresentFlags.None)); } public class GameWindow : RenderWindow { protected override void OnDraw(DeviceContext ctx) { ctx.Clear(new Color(0xffa0adbb)); } }OnDraw重载即为渲染的方法提供了一个 ctx参数对应 Direct2D中的 ID2D1DeviceContext类型可以用来绘图。其中 0xffa0adbb是棋盘背景颜色它是用 ABGR的顺序表示的运行效果如下 棋盘首先我们需要“画”一个棋盘它分为背景和棋格子组成。这部分内容是完全静态的因此可以在呈现层直接完成。棋盘应该随着窗口大小变化而变化因此各个变量都应该动态计算得出。 如图 2048游戏区域应该为正方形因此总边长 fullEdge应该为窗口的高宽属性的较小者以刚好放下一个正方形代码表示如下float fullEdge Math.Min(ctx.Size.Width, ctx.Size.Height);方块与方块之间的距离定义为总边长的 1/8再除以 MatrixSize也就是4此时单个方块的边长就可以计算出来了为总边长 fullEdge减去5个 gap再除以 MatrixSize代码如下float gap fullEdge / (MatrixSize * 8); float edge (fullEdge - gap * (MatrixSize 1)) / MatrixSize;然后即可按循环绘制 4行 4列方块位置使用矩阵变换可以让代码更简单foreach (var v in MatrixPositions) { float centerX gap v.x * (edge gap) edge / 2.0f; float centerY gap v.y * (edge gap) edge / 2.0f; ctx.Transform Matrix3x2.Translation(-edge / 2, -edge / 2) * Matrix3x2.Translation(centerX, centerY); ctx.FillRoundedRectangle(new RoundedRectangle { RadiusX edge / 21, RadiusY edge / 21, Rect new RectangleF(0, 0, edge, edge), }, XResource.GetColor(new Color(0x59dae4ee))); }注意 foreach(varvinMatrixPositions)是以下代码的简写for (var x 0; x MatrixSize; x) { for (var y 0; y MatrixSize; y) { // ... } }由于 2048将多次遍历 x和 y因此定义了一个变量 MatrixPositions来简化这一过程static IEnumerableint inorder Enumerable.Range(0, MatrixSize); static IEnumerable(int x, int y) MatrixPositions inorder.SelectMany(y inorder.Select(x (x, y)));运行效果如下 加入数字方块数据方块由于是活动的为了代码清晰需要加入额外两个类 Cell和 Matrix。Cell类Cell是单个方块需要保存当前的数字 N其次还要获取当前的颜色信息class Cell { public int N; public Cell(int n) { N n; } public DisplayInfo DisplayInfo N switch { 2 DisplayInfo.Create(), 4 DisplayInfo.Create(0xede0c8ff), 8 DisplayInfo.Create(0xf2b179ff, 0xf9f6f2ff), 16 DisplayInfo.Create(0xf59563ff, 0xf9f6f2ff), 32 DisplayInfo.Create(0xf67c5fff, 0xf9f6f2ff), 64 DisplayInfo.Create(0xf65e3bff, 0xf9f6f2ff), 128 DisplayInfo.Create(0xedcf72ff, 0xf9f6f2ff, 45), 256 DisplayInfo.Create(0xedcc61ff, 0xf9f6f2ff, 45), 512 DisplayInfo.Create(0xedc850ff, 0xf9f6f2ff, 45), 1024 DisplayInfo.Create(0xedc53fff, 0xf9f6f2ff, 35), 2048 DisplayInfo.Create(0x3c3a32ff, 0xf9f6f2ff, 35), _ DisplayInfo.Create(0x3c3a32ff, 0xf9f6f2ff, 30), }; }其中 DisplayInfo类用来表达方块的文字颜色、背景颜色和字体大小struct DisplayInfo { public Color Background; public Color Foreground; public float FontSize; public static DisplayInfo Create(uint background 0xeee4daff, uint color 0x776e6fff, float fontSize 55) new DisplayInfo { Background new Color(background), Foreground new Color(color), FontSize fontSize }; }文章中的“魔法”数字 0xeee4daff等和上文一样是颜色的 ABGR顺序表示的。通过一个简单的 Create方法即可实现默认颜色、默认字体的代码简化无需写过多的 if/else。注意我特意使用了 struct而非 class关键字这样创建的是值类型而非引用类型可以无需分配和回收堆内存。在应用或游戏中内存分配和回收常常是最影响性能和吞吐性的指标之一。Nswitch{...}这样的代码是 C# 8.0的 switchexpression特性下文将继续大量使用可以通过表达式——而非语句的方式表达一个逻辑可以让代码大大简化。该特性现在在 .NETCore3.0项目中默认已经打开某些支持的早期版本需要将项目中的 LangVersion属性设置为 8.0才可以使用。根据 2048的设计文档和参考其它项目一个方块创建时有 90%机率是 2 10%机率是 4这可以通过 .NET中的 Random类实现static Random r new Random(); public static Cell CreateRandom() new Cell(r.NextDouble() 0.9 ? 2 : 4);使用时只需调用 CreateRandom()即可。Matrix类Matrix用于管理和控制多个 Cell类。它包含了一个二维数组 Cell[,]用于保存 4x4的 Cellclass Matrix { public Cell[,] CellTable; public IEnumerableCell GetCells() { foreach (var c in CellTable) if (c ! null) yield return c; } public int GetScore() GetCells().Sum(v v.N); public void ReInitialize() { CellTable new Cell[MatrixSize, MatrixSize]; (int x, int y)[] allPos MatrixPositions.ShuffleCopy(); for (var i 0; i 2; i) // 2: initial cell count { CellTable[allPos[i].y, allPos[i].x] Cell.CreateRandom(); } } }其中 ReInitialize方法对 Cell[,]二维数组进行了初始化然后在随机位置创建了两个 Cell。值得一提的是 ShuffleCopy()函数该函数可以对 IEnumerableT进行乱序然后复制为数组static class RandomUtil { static Random r new Random(); public static T[] ShuffleCopyT(this IEnumerableT data) { var arr data.ToArray(); for (var i arr.Length - 1; i 0; --i) { int randomIndex r.Next(i 1); T temp arr[i]; arr[i] arr[randomIndex]; arr[randomIndex] temp; } return arr; } }该函数看似简单能写准确可不容易。尤其注意 for循环的终止条件不是 i0而是 i0这两者有区别以后我有机会会深入聊聊这个函数。今天最简单的办法就是——直接使用它即可。最后回到 GameWindow类的 OnDraw方法如法炮制将 Matrix“画”出来即可// .. 继之前的OnDraw方法内容 foreach (var p in MatrixPositions) { var c Matrix.CellTable[p.y, p.x]; if (c null) continue; float centerX gap p.x * (edge gap) edge / 2.0f; float centerY gap p.y * (edge gap) edge / 2.0f; ctx.Transform Matrix3x2.Translation(-edge / 2, -edge / 2) * Matrix3x2.Translation(centerX, centerY); ctx.FillRectangle(new RectangleF(0, 0, edge, edge), XResource.GetColor(c.DisplayInfo.Background)); var textLayout XResource.TextLayouts[c.N.ToString(), c.DisplayInfo.FontSize]; ctx.Transform Matrix3x2.Translation(-textLayout.Metrics.Width / 2, -textLayout.Metrics.Height / 2) * Matrix3x2.Translation(centerX, centerY); ctx.DrawTextLayout(Vector2.Zero, textLayout, XResource.GetColor(c.DisplayInfo.Foreground)); }此时运行效果如下 如果想测试所有方块颜色可将 ReInitialize()方法改为如下即可public void ReInitialize() { CellTable new Cell[MatrixSize, MatrixSize]; CellTable[0, 0] new Cell(2); CellTable[0, 1] new Cell(4); CellTable[0, 2] new Cell(8); CellTable[0, 3] new Cell(16); CellTable[1, 0] new Cell(32); CellTable[1, 1] new Cell(64); CellTable[1, 2] new Cell(128); CellTable[1, 3] new Cell(256); CellTable[2, 0] new Cell(512); CellTable[2, 1] new Cell(1024); CellTable[2, 2] new Cell(2048); CellTable[2, 3] new Cell(4096); CellTable[3, 0] new Cell(8192); CellTable[3, 1] new Cell(16384); CellTable[3, 2] new Cell(32768); CellTable[3, 3] new Cell(65536); }运行效果如下 嗯看起来……有那么点意思了。引入事件把方块移动起来本篇也分两部分事件和方块移动逻辑。事件首先是事件要将方块移动起来我们再次引入大名鼎鼎的 Rx全称 Reactive.NET NuGet包 System.Reactive。然后先引入一个基础枚举用于表示上下左右enum Direction { Up, Down, Left, Right, }然后将键盘的上下左右事件转换为该枚举的 IObservableDirection流可以写在 GameWindow构造函数中然后调用该“流”的 .Subscribe方法直接订阅该“流”var keyUp Observable.FromEventPatternKeyEventArgs(this, nameof(this.KeyUp)) .Select(x x.EventArgs.KeyCode); keyUp.Select(x x switch { Keys.Left (Direction?)Direction.Left, Keys.Right Direction.Right, Keys.Down Direction.Down, Keys.Up Direction.Up, _ null }) .Where(x x ! null) .Select(x x.Value) .Subscribe(direction { Matrix.RequestDirection(direction); Text $总分{Matrix.GetScore()}; }); keyUp.Where(k k Keys.Escape).Subscribe(k { if (MessageBox.Show(要重新开始游戏吗, 确认, MessageBoxButtons.OKCancel) System.Windows.Forms.DialogResult.OK) { Matrix.ReInitialize(); // 这行代码没写就是文章最初说的bug其根本原因也许忘记了就是因为这里不是用的MVC/应用程序驱动 // Text $总分{Matrix.GetScore()}; } });每次用户松开上下左右四个键之一就会调用 Matrix的 RequestDirection方法马上说松下 Escape键则会提示用户是否重新开始玩然后重新显示新的总分。注意我再次使用了 C# 8.0的 switchexpression语法它让我省去了 if/else或 switchcase代码精练了不少不是非得要用 Rx但 Rx相当于将事件转换为了数据可以让代码精练许多且极大地提高了可扩展性。移动逻辑我们先在脑子里面想想感受一下这款游戏的移动逻辑应该是怎样的。你可以在草稿本上先画画图……我将 2048游戏的逻辑概括如下将所有方块向用户指定的方向遍历找到最近的方块位置如果找到且数字一样则合并删除对面自己加倍如果找到但数字不一样则移动到对面的前一格如果发生过移动则生成一个新方块如果想清楚了这个逻辑就能写出代码如下public void RequestDirection(Direction direction) { if (GameOver) return; var dv Directions[(int)direction]; var tx dv.x 1 ? inorder.Reverse() : inorder; var ty dv.y 1 ? inorder.Reverse() : inorder; bool moved false; foreach (var i in tx.SelectMany(x ty.Select(y (x, y)))) { Cell cell CellTable[i.y, i.x]; if (cell null) continue; var next NextCellInDirection(i, dv); if (WithinBounds(next.target) CellTable[next.target.y, next.target.x].N cell.N) { // 对面有方块且可合并 CellTable[i.y, i.x] null; CellTable[next.target.y, next.target.x] cell; cell.N * 2; moved true; } else if (next.prev ! i) // 对面无方块移动到prev { CellTable[i.y, i.x] null; CellTable[next.prev.y, next.prev.x] cell; moved true; } } if (moved) { var nextPos MatrixPositions .Where(v CellTable[v.y, v.x] null) .ShuffleCopy() .First(); CellTable[nextPos.y, nextPos.x] Cell.CreateRandom(); if (!IsMoveAvailable()) GameOver true; } }其中 dv、 tx与 ty三个变量巧妙地将 Direction枚举转换成了数据避免了过多的 if/else导致代码膨胀。然后通过一行简单的 LINQ再次将两个 for循环联合在一起。注意示例还使用了 (x,y)这样的语法下文将继续大量使用这叫 ValueTuple或者 值元组。 ValueTuple是 C# 7.0的新功能它和 C# 6.0新增的 Tuple的区别有两点ValueTuple可以通过 (x,y)这样的语法内联而 Tuple要使用 Tuple.Create(x,y)来创建ValueTuple故名思义它是 值类型可以无需内存分配和 GC开销但稍稍增长了少许内存复制开销我还定义了另外两个字段 GameOver和 KeepGoing用来表示是否游戏结束和游戏胜利时是否继续public bool GameOver,KeepGoing;其中 NextCellInDirection用来计算方块对面的情况代码如下public ((int x, int y) target, (int x, int y) prev) NextCellInDirection((int x, int y) cell, (int x, int y) dv) { (int x, int y) prevCell; do { prevCell cell; cell (cell.x dv.x, cell.y dv.y); } while (WithinBounds(cell) CellTable[cell.y, cell.x] null); return (cell, prevCell); }IsMoveAvailable函数用来判断游戏是否还能继续如果不能继续将设置 GameOvertrue。它的逻辑是如果方块数不满则显示游戏可以继续然后判断是否有任意相邻方块数字相同有则表示游戏还能继续具体代码如下public bool IsMoveAvailable() GetCells().Count() switch { MatrixSize * MatrixSize MatrixPositions .SelectMany(v Directions.Select(d new { Position v, Next (x: v.x d.x, y: v.y d.y) })) .Where(x WithinBounds(x.Position) WithinBounds(x.Next)) .Any(v CellTable[v.Position.y, v.Position.x]?.N CellTable[v.Next.y, v.Next.x]?.N), _ true, };注意我再次使用了 switchexpression、 ValueTuple和令人拍案叫绝的 LINQ相当于只需一行代码就将这些复杂的逻辑搞定了。最后别忘了在 GameWindow的 OnUpdateLogic重载函数中加入一些弹窗提示显示用于恭喜和失败的信息protected override void OnUpdateLogic(float dt) { base.OnUpdateLogic(dt); if (Matrix.GameOver) { if (MessageBox.Show($总分{Matrix.GetScore()}\r\n重新开始吗, 失败, MessageBoxButtons.YesNo) DialogResult.Yes) { Matrix.ReInitialize(); } else { Matrix.GameOver false; } } else if (!Matrix.KeepGoing Matrix.GetCells().Any(v v.N 2048)) { if (MessageBox.Show(您获得了2048\r\n还想继续升级吗, 恭喜, MessageBoxButtons.YesNo) DialogResult.Yes) { Matrix.KeepGoing true; } else { Matrix.ReInitialize(); } } }这时游戏运行效果显示如下 优化其中到了这一步 2048已经可堪一玩了但总感觉不是那么个味。还有什么可以做的呢动画上文说过动画是灵魂级别的功能。和 CRUD程序员的日常——“功能”实现了就万事大吉不同游戏必须要有动画没有动画简直就相当于游戏白做了。在远古 jQuery中有一个 $(element).animate()方法实现动画挺方便我们可以模仿该方法的调用方式自己实现一个public static GameWindow Instance null; public static Task CreateAnimation(float initialVal, float finalVal, float durationMs, Actionfloat setter) { var tcs new TaskCompletionSourcefloat(); Variable variable Instance.XResource.CreateAnimation(initialVal, finalVal, durationMs / 1000); IDisposable subscription null; subscription Observable .FromEventPatternRenderWindow, float(Instance, nameof(Instance.UpdateLogic)) .Select(x x.EventArgs) .Subscribe(x { setter((float)variable.Value); if (variable.FinalValue variable.Value) { tcs.SetResult(finalVal); variable.Dispose(); subscription.Dispose(); } }); return tcs.Task; } public GameWindow() { Instance this; // ... }注意我实际是将一个动画转换成为了一个 Task这样就可以实际复杂动画、依赖动画、连续动画的效果。使用该函数可以轻易做出这样的效果动画部分代码只需这样写见 animation-demo.linqfloat x 50, y 150, w 50, h 50; float red 0; protected override async void OnLoad(EventArgs e) { var stage1 new[] { CreateAnimation(initialVal: x, finalVal: 340, durationMs: 1000, v x v), CreateAnimation(initialVal: h, finalVal: 100, durationMs: 600, v h v), }; await Task.WhenAll(stage1); await CreateAnimation(initialVal: h, finalVal: 50, durationMs: 1000, v h v); await CreateAnimation(initialVal: x, finalVal: 20, durationMs: 1000, v x v); while (true) { await CreateAnimation(initialVal: red, finalVal: 1.0f, durationMs: 500, v red v); await CreateAnimation(initialVal: red, finalVal: 0.0f, durationMs: 500, v red v); } }运行效果如下请注意最后的黑色-红色闪烁动画其实是一个无限动画各位可以想像下如果手撸状态机这些代码会多么麻烦而 C#支持协程这些代码只需一些 await和一个 while(true)语句即可完美完成有了这个基础开工做动画了首先给 Cell类做一些修改class Cell { public int N; public float DisplayX, DisplayY, DisplaySize 0; const float AnimationDurationMs 120; public bool InAnimation (int)DisplayX ! DisplayX || (int)DisplayY ! DisplayY || (int)DisplaySize ! DisplaySize; public Cell(int x, int y, int n) { DisplayX x; DisplayY y; N n; _ ShowSizeAnimation(); } public async Task ShowSizeAnimation() { await GameWindow.CreateAnimation(DisplaySize, 1.2f, AnimationDurationMs, v DisplaySize v); await GameWindow.CreateAnimation(DisplaySize, 1.0f, AnimationDurationMs, v DisplaySize v); } public void MoveTo(int x, int y, int n default) { _ GameWindow.CreateAnimation(DisplayX, x, AnimationDurationMs, v DisplayX v); _ GameWindow.CreateAnimation(DisplayY, y, AnimationDurationMs, v DisplayY v); if (n ! default) { N n; _ ShowSizeAnimation(); } } public DisplayInfo DisplayInfo N switch // ... static Random r new Random(); public static Cell CreateRandomAt(int x, int y) new Cell(x, y, r.NextDouble() 0.9 ? 2 : 4); }加入了 DisplayX DisplayY、 DisplaySize三个属性用于管理其用于在界面上显示的值。还加入了一个 InAnimation变量用于判断是否处理动画状态。另外构造函数现在也要求传入 x和 y的值如果位置变化了现在必须调用 MoveTo方法它与 Cell建立关联了之前并不会。ShowSizeAnimation函数是演示该动画很好的示例它先将方块放大至 1.2倍然后缩小成原状。有了这个类之后 Matrix和 GameWindow也要做一些相应的调整详情见 2048.linq最终做出来的效果如下注意合并时的动画 撤销功能有一天突然找到了一个带撤销功能的 2048那时我发现 2048带不带撤销其实是两个游戏。撤销就像神器给爱挑( mian)战( zi)的玩( ruo)家( ji)带来了轻松与快乐给予了第二次机会让玩家转危为安。所以不如先加入撤销功能。用户每次撤销的都是最新状态是一个经典的后入先出的模式也就是 栈因此在 .NET中我们可以使用 StackT在 Matrix中可以这样定义Stackint[] CellHistory new Stackint[]();如果要撤销必将调用 Matrix的某个函数这个函数定义如下public void TryPopHistory() { if (CellHistory.TryPop(out int[] history)) { foreach (var pos in MatrixPositions) { CellTable[pos.y, pos.x] history[pos.y * MatrixSize pos.x] switch { default(int) null, _ new Cell(history[pos.y * MatrixSize pos.x]), }; } } }注意这里存在一个 一维数组与 二维数组的转换通过控制下标求值即可轻松将 一维数组转换为 二维数组。然后是创建撤销的时机必须在准备移动前记录当前历史int[] history CellTable.CastCell().Select(v v?.N ?? default).ToArray();注意这其实也是 C#中将 二维数组转换为 一维数组的过程数组继承于 IEnumerable调用其 CastT方法即可转换为 IEnumerableT然后即可愉快地使用 LINQ和 .ToArray()了。然后在确定移动之后将历史 入栈if (moved) { CellHistory.Push(history); // ... }最后当然还需要加入事件支持用户按下 Back键即可撤销keyUp.Where(k k Keys.Back).Subscribe(k Matrix.TryPopHistory());运行效果如下 注意这里又有一个 bug撤销时总分又没变聪明的读者可以试试如何解决。如果使用 MVC和应用程序驱动的实时渲染则这种 bug则不可能发生。手势操作2048可以在平板或手机上玩因此手势操作必不可少虽然电脑上有键盘但多一个功能总比少一个功能好。不知道 C#窗口上有没有做 手势识别这块的开源项目但借助 RX这手撸一个也不难static IObservableDirection DetectMouseGesture(Form form) { var mouseDown Observable.FromEventPatternMouseEventArgs(form, nameof(form.MouseDown)); var mouseUp Observable.FromEventPatternMouseEventArgs(form, nameof(form.MouseUp)); var mouseMove Observable.FromEventPatternMouseEventArgs(form, nameof(form.MouseMove)); const int throhold 6; return mouseDown .SelectMany(x mouseMove .TakeUntil(mouseUp) .Select(x new { X x.EventArgs.X, Y x.EventArgs.Y }) .ToList()) .Select(d { int x 0, y 0; for (var i 0; i d.Count - 1; i) { if (d[i].X d[i 1].X) x; if (d[i].Y d[i 1].Y) y; if (d[i].X d[i 1].X) --x; if (d[i].Y d[i 1].Y) --y; } return (x, y); }) .Select(v new { Max Math.Max(Math.Abs(v.x), Math.Abs(v.y)), Value v}) .Where(x x.Max throhold) .Select(v { if (v.Value.x v.Max) return Direction.Right; if (v.Value.x -v.Max) return Direction.Left; if (v.Value.y v.Max) return Direction.Down; if (v.Value.y -v.Max) return Direction.Up; throw new ArgumentOutOfRangeException(nameof(v)); }); }这个代码非常精练但其本质是 Rx对 MouseDown、 MouseUp和 MouseMove三个窗口事件“拍案叫绝”级别的应用它做了如下操作MouseDown触发时开始记录直到 MouseUp触发为止将 MouseMove的点集合起来生成一个 List记录各个方向坐标递增的次数如果次数大于指定次数 6即认可为一次事件在各个方向中取最大的值以减少误差测试代码及效果如下void Main() { using var form new Form(); DetectMouseGesture(form).Dump(); Application.Run(form); }到了集成到 2048游戏时 Rx的优势又体现出来了如果之前使用事件操作就会出现两个入口。但使用 Rx后触发入口仍然可以保持统一在之前的基础上只需添加一行代码即可解决keyUp.Select(x x switch { Keys.Left (Direction?)Direction.Left, Keys.Right Direction.Right, Keys.Down Direction.Down, Keys.Up Direction.Up, _ null }) .Where(x x ! null !Matrix.IsInAnimation()) .Select(x x.Value) .Merge(DetectMouseGesture(this)) // 只需加入这一行代码 .Subscribe(direction { Matrix.RequestDirection(direction); Text $总分{Matrix.GetScore()}; });简直难以置信有传言说我某个同学使用某知名游戏引擎做小游戏集成手势控制搞三天三夜都没做出来。总结重新来回顾一下最终效果所有这些代码都可以在我的 Github上下载请下载 LINQPad6运行。用 VisualStudio2019/ VSCode也能编译运行只需手动将代码拷贝至项目中并安装 FlysEngine.Desktop和 System.Reactive两个 NuGet包即可。下载地址如下https://github.com/sdcb/blog-data/tree/master/2019/20191030-2048-by-dotnet其中2048.linq是最终版可以完整地看到最终效果最初版是 2048-r4-no-cell.linq可以从该文件开始进行演练演练的顺序是 r4,r3,r2,r1最后最终版因为写这篇文章是先把所有东西做出来然后再慢慢删除做“阉割版”的示例animation-demo.linq、 _mouse-geature.linq是周边示例用于演示动画和鼠标手势我还做了一个 2048-old.linq采用的是 一维数组而非 二维储存 Cell[,]有兴趣的可以看看有少许区别其实除了 C#版我多年前还做了一个 html5/canvas的 js版本 Github地址如下https://github.com/sdcb/2048 其逻辑层和渲染层都有异曲同工之妙事实也是我从 js版本移动到 C#并没花多少心思。这恰恰说明的“小游戏第一原则”—— MVC的重要性。……但完成这篇文章我花了很多、很多心思。微信限制文章可能无法评论喜欢的朋友 可进入我的博客园进行评论(https://www.cnblogs.com/sdflysha/p/20191030-2048-by-dotnet.html)并关注我的微信公众号【DotNet骚操作】
http://www.sadfv.cn/news/152317/

相关文章:

  • 外贸企业网站建设公司价格我国中小企业网站建设
  • ftp上传网站 需要什么文件西部数码网站站点
  • 网站正在维护中 模板网站的整体结构
  • 网站设计的公司价格四川建设网个人证书查询网址
  • 视频网站开发公司有哪些公司天津和平做网站
  • 怎么自己做网站的优化ui设计网站开发
  • 景区宣传网站制作模板海口智能建站模板
  • 表白网站制作系统源码扬中潘杰
  • 建设工程合同备案在什么网站上帮人做非法网站吗
  • 建设部网站信息系统免费创办网站
  • 南京网站建设设计帝国网站管理系统
  • 东莞网站制作及推广价格简单的html网站模板下载
  • 优质的杭州网站优化网站前台模块包括什么软件
  • 成都网站建设私单太原网站排名优化价格
  • 网站后台 请示小程序 wordpress绑定
  • 网站开发报价人天保定官网seo分析
  • 2023新闻大事10条seo学院
  • 昆山网站制作 微博网站建设gon
  • 资讯网站wordpress 手机 插件
  • 洛阳网站建设睿翼网络入驻洛阳在线资源搜索引擎
  • 悠悠我心的个人网站怎么做手机上网网站建设
  • 网站模板凡建站网站怎么做适配
  • 网站取源用iapp做软件主流的net快速开发框架
  • 手机网站 分享公司网站服务器租用
  • 网站在哪备案手机端建站
  • 经典手机网站qq官网登录入口电脑版
  • 做公司网站需要哪些资料网站标题第一个词
  • 厦门php商城网站建设免费申请网站
  • 手表怎么在网站做推广三、网站开发使用软件环境
  • 江苏省建设厅官方网站公式公告衡水企业做网站