Zyan Drench,支持Wifi的Android游戏

时间:2023-12-06 14:29:26

介绍 “雨淋”是一款最初使用Adobe Flash开发的单人游戏(你可以试试谷歌一下“世界上最简单的Flash游戏”)。它相当流行,并且已经被移植到Android平台上。在谷歌Play和亚马逊应用商店有许多克隆游戏,包括Coloroid, PaintIT, Splash!和洪水填充,举几个例子。 虽然确切的游戏规则可能有所不同,但所有这些游戏都是单人游戏。《Zyan Drench》是对双人游戏的一次尝试,并引入了两种新的游戏模式:电脑对战和网络游戏模式。本文描述了使用c#语言Xamarin构建Android版本的游戏。Android平台(独立版)及网络游戏自言通信框架。 游戏概述 《雨》是一款很容易上手,但解释起来有点难的益智游戏。游戏一开始是一个15x15的棋盘,棋盘上随机分布着像素块。从左上角的像素开始,你必须用一种颜色填充(淋)整个面板。您可以通过将左上角的像素设置为一个新颜色来实现这一点。当你将像素的颜色变成与相邻像素相同的颜色时,你将扩展湿透区域: 最初的淋水游戏是单人游戏,有15x15的棋盘和每级30步的限制。一些游戏实现允许选择不同的棋盘大小,限制通常是棋盘大小的两倍,大多数实现使用6种颜色。我们的单人游戏将使用相同的参数来向后兼容。 双人模式 让游戏适合两名玩家是很简单的:如果第一个玩家从左上角的像素开始,那么对手就会从相反的角落开始。游戏依次进行,直到整个棋盘涂上两种颜色。淋湿的像素比对手多的玩家获胜: 被禁止的颜色 在单人游戏模式下,使用相同颜色两次是没有意义的,因为该颜色的所有相邻像素都已经被捕获。双玩家模式又增加了一个限制:你不能吃掉对手的像素,因此你不能使用与对手相同的颜色。在每个回合,有两种颜色不能使用:你当前的颜色和你对手的颜色。我们称它们为禁忌色。 游戏开发 为了给游戏建模,我们需要一个棋盘,它是一个二维的像素数组。每个像素都有一个颜色,我们可以使用从0到5的整数对其进行编码。为了显示板子,我们需要为这些数字分配任何不同的颜色(即。,创建调色板): 隐藏,复制Code

public class DrenchGame
{
public const int BoardSize = 15; public const int ColorCount = 6; private int[,] Board = new int[BoardSize, BoardSize]; private Color[] Palette = new[] { Color.Red, Color.Green, ... }; public void NewGame()
{
// randomize the board
} public void MakeMove(int color)
{
// flood fill the board starting from the top-left pixel
} public void CheckIfStopped()
{
// check if the whole board is drenched
}
}

淋水游戏有很多不同的游戏模式:单人,双人,对战电脑(有几个技能级别)和基于网络的游戏。所有这些游戏都有相同的游戏规则:设置新颜色,扩大淋湿区域,为新游戏随机棋盘,等等。让我们将所有这些细节提取到一个表示板的单独类中。 DrenchBoard 下面是一个类,我们将使用它来表示一个板。像计算机屏幕一样,像素位置由它的X和Y坐标决定,其中(0,0)是左上角。索引器用于访问单个像素的颜色:this[x, y]。为了方便起见,我们将封装坐标(就像perl语言中的数组索引一样),以便这个[-1,-1]的意思与这个[BoardSize -1, BoardSize -1]相同。 委员会将进行扩大受潮地区所需的所有计算。每个玩家都试图从自己的位置开始喷洒棋盘,这就是SetColor(x, y, color)获取x和y坐标的原因。下面讨论SetColor的具体算法: 隐藏,收缩,复制Code

public class DrenchBoard
{
// skipped: BoardSize, ColorCount and Board (same as above) public void Randomize()
{
// assign a random color to every pixel of the board
} public void SetColor(int x, int y, int color)
{
// flood fill algorithm (discussed below)
} public bool CheckAllColors(param int[] allowedColors)
{
// check if all pixels have one of the allowed colors
} public int this[int x, int y]
{
get
{
// wrap coordinate values so that Board[-1, -1]
// means the right-bottom corner
if (x < 0)
x = BoardSize - x;
if (y < 0)
y = BoardSize - y; // return the color of a pixel given its coordinates
return Board[x, y];
}
}
}

为了表示板中的单个位置,我创建了点结构:new Point(x, y)。这个结构被洪水填充算法使用。该算法以一组点进行运算,并优化比较点结构,实现了iequatable>& lt; / point> & lt; point>接口: 隐藏,复制Code

public struct Point: IEquatable<Point>
{
private int x, y; public Point(int x, int y)
{
this.x = x;
this.y = y;
}
...
}

洪水填充算法 我不是计算机图形学的专家,但是填充看起来并不是一件困难的事情,所以我编造了我自己的算法,如下所示。对于每个像素,我检查它的四个相邻像素,并记住那些具有相同颜色的像素。递归地重复这个过程,我最终得到一个与初始像素颜色相同的有限的相邻像素集。最后,我遍历这些像素,将它们全部设置为新颜色。 在实现这个简单算法时,我将递归转换为迭代,以消耗更少的堆栈内存,并使用计划进行处理的点队列。但事实证明,当处理大量像素时,Queue类的行为相当缓慢。 然后我意识到顺序或处理像素根本不重要,于是用HashSet替换了队列。这神奇地解决了所有的性能问题!哈希et的性能并不依赖于设置的大小,所以它处理数百项的速度和处理少数项的速度一样快。下面是我完成的完整的洪水填充算法: 隐藏,收缩,复制Code

public void SetColor(int x, int y, int newColor)
{
var color = Board[x, y];
if (color == newColor)
{
return 1;
} var points = new HashSet<Point>();
var queue = new HashSet<Point>();
queue.Add(new Point(x, y)); var adjacents = new[] { new Point(-1, 0), new Point(0, -1), new Point(0, 1), new Point(1, 0) };
while (queue.Any())
{
var point = queue.First();
queue.Remove(point);
points.Add(point);
Board[point.X, point.Y] = newColor; // process adjacent points of the same color
foreach (var delta in adjacents)
{
var newX = point.X + delta.X;
var newY = point.Y + delta.Y; // skip invalid point
if (newX < 0 || newX > BoardSize - 1 || newY < 0 || newY > BoardSize - 1)
{
continue;
} // skip pixels of other colors
if (Board[newX, newY] != color)
{
continue;
} // skip already processed point
var newPoint = new Point(newX, newY);
if (points.Contains(newPoint))
{
continue;
} // schedule the point for processing
queue.Add(newPoint);
}
}
}

使用提供的DrenchBoard类游戏编程非常简单: 隐藏,复制Code

class SomeKindOfDrenchGame
{
public void NewGame()
{
Board.Randomize();
} public void MakeMove(int newColor)
{
Board.SetColor(0, 0, newColor);
}
}

IDrenchGame接口 遵循DRY(不要重复自己)原则,我们希望我们的应用程序处理所有游戏模式使用相同的UI,看起来像一个像素板下面有几个彩色按钮: 玩家通过触摸彩色按钮来移动。禁止颜色的按钮被禁用。随着游戏的进行,UI会更新面板上的当前状态文本。这对于所有的游戏模式都是常见的,所以我们可以将它们描述为一个界面。实际的游戏界面可能有点复杂,但我们总是可以添加更多的方法和属性,因为我们需要: 隐藏,复制Code

public interface IDrenchGame
{
DrenchBoard Board { get; } void NewGame(); void MakeMove(int color); bool IsStopped { get; } string CurrentStatus { get; } IEnumerable<int> ForbiddenColors { get; } event EventHandler GameChanged; event EventHandler GameStopped;
}

为了使事情更简单,我们将为所有游戏模式创建一个基本抽象类。子类将根据游戏规则重写MakeMove和CheckIfStopped方法: 隐藏,收缩,复制Code

public abstract class DrenchGameBase
{
public virtual DrenchBoard Board { get; private set; } public virtual void NewGame()
{
Board.Randomize();
} public virtual void SetColor(int x, int y, int color)
{
Board.SetColor(x, y, color);
OnGameChanged();
} public abstract MakeMove(int color); protected abstract CheckIfStopped(); public virtual bool IsStopped { get; protected set; } public virtual string CurrentStatus { get; protected set; } public virtual IEnumerable<int> ForbiddenColors { get; protected set; } public event EvenHandler GameChanged; protected void OnGameChanged()
{
var gameChanged = GameChanged;
if (gameChanged != null)
gameChanged(this, EventArgs.Empty);
} public static IEnumerable<int> Enumerate(params int[] colors)
{
// utility method to return an IEnumerable<int>
return colors;
}
}

SinglePlayerGame和TwoPlayerGame 使用提供的DrenchGameBase类,创建特定的游戏模式非常容易。重写MakeMove和CheckIfStopped方法,我们可以控制游戏如何进行。基类使用Board实例执行所有计算。这里是完整的源代码为单机游戏: 隐藏,收缩,复制Code

public class SinglePlayerGame : DrenchGameBase
{
public const int MaxMoves = 30; public override void NewGame()
{
base.NewGame(); CurrentMove = 1;
ForbiddenColors = Enumerate(Board[0, 0]);
CurrentStatus = string.Format("{0} moves left. Good luck!", MaxMoves);
OnGameChanged();
} public override void MakeMove(int value)
{
CurrentMove++;
CurrentStatus = string.Format("Move {0} out of {1}", CurrentMove, MaxMoves);
ForbiddenColors = Enumerable.Repeat(value, 1); // set the new color
SetColor(0, 0, value);
} protected override void CheckIfStopped()
{
var allowedColor = Board[0, 0];
var success = Board.CheckAllColors(allowedColor);
if (success || CurrentMove > MaxMoves)
{
var result = success ? "won" : "lost";
OnGameStopped(true, "You have {0} the game!", result);
}
}
}

两个playergame保持对当前玩家的跟踪,以便每次调用MakeMove时交替地在顶层或右下角像素上绘制。CheckIfStopped检查所有像素是否都有这两种颜色中的一种。 组装Android应用程序 让我们使用提供的游戏类来构建一个运行的Android应用程序。典型的应用程序由几个与用户交互的活动(屏幕)组成。每个活动都包含几个合并到层次结构中的视图来创建用户界面。我不会深入探讨Android应用程序结构的细节,因为已经有很多关于这个主题的好文章,所以我只关注一些细节。 我们的主要游戏活动将使用TableLayout创建董事会和一组按钮。按钮是在布局设计器中创建的,而板子是通过编程方式构建的,因此很容易随时更改板子的大小。董事会屏幕的布局如下所示(大部分跳过的细节): 隐藏,复制Code

<LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android">
<!-- Stub view for the board -->
<TableLayoutandroid:id="@+id/boardTable"android:stretchColumns="*">
<TableRowandroid:id="@+id/tableRow0">
</TableRow>
</TableLayout>
<!-- Buttons panel -->
<TableLayoutandroid:id="@+id/buttonsTable"android:stretchColumns="*">
<TableRowandroid:id="@+id/tableRow1">
<Buttonandroid:id="@+id/button0"/>
<Buttonandroid:id="@+id/button1"/>
<Buttonandroid:id="@+id/button2"/>
</TableRow>
<TableRowandroid:id="@+id/tableRow2">
<Buttonandroid:id="@+id/button3"/>
<Buttonandroid:id="@+id/button4"/>
<Buttonandroid:id="@+id/button5"/>
</TableRow>
</TableLayout>
</LinearLayout>

下面是在OnCreate方法中执行的用block填充董事会表的代码: 隐藏,复制Code

// Create board tiles
var colors = Palette;
for (var j = 0; j < BoardSize; j++)
{
tableRow = new TableRow(BaseContext);
tableRow.LayoutParameters = new TableLayout.LayoutParams(
TableLayout.LayoutParams.WrapContent,
TableLayout.LayoutParams.WrapContent, 1f);
table.AddView(tableRow); for (var i = 0; i < BoardSize; i++)
{
var button = new Button(BaseContext);
button.LayoutParameters = new TableRow.LayoutParams(i);
button.LayoutParameters.Width = 1;
button.LayoutParameters.Height = ViewGroup.LayoutParams.MatchParent;
button.SetBackgroundColor(colors[(i + j * 2) % 6]);
tableRow.AddView(button); Tiles[i, j] = button;
}
}

每个块由一个按钮视图表示。创建具有相同重力值的行确保所有的行具有相同的高度,设置android:stretchColumns="*"使列具有相同的宽度,这正是我们需要的像素板。 注意:Android设备支持不同的屏幕大小和宽高比,这就是为什么块不可能总是完美的正方形。 处理设备旋转 在Android中,Activity对象可以在Android想创建和销毁时创建和销毁。例如,当您旋转设备时,将从头重新创建当前活动,您必须加载布局并重新创建板。这意味着您不能简单地将当前游戏实例存储在活动中。当前的游戏实例必须放在其他地方。 自定义应用程序类 看起来安全存储它的最简单方法是创建一个自定义应用程序类。应用程序实例在流程的整个生命周期中都存在,并且通过Application属性对所有活动都可用。唯一需要注意的是application类是从Java代码创建的,所以它必须是一个特殊的构造函数,看起来像这样: 隐藏,复制Code

public CustomApplication(IntPtr javaReference, JniHandleOwnership transfer)
: base(javaReference, transfer)
{
}

我需要跨不同活动共享的所有实例都可以作为application类的属性发布: 隐藏,复制Code

public IDrenchGame DrenchGame { get; set; }

从活动中访问应用程序实例如下所示: 隐藏,复制Code

private CustomApplication App { get { return (CustomApplication)Application; } }

...
var currentGame = App.DrenchGame;

Game实例使用GameChanged和GameStopped等事件与棋盘活动交互。Activity订阅这些事件在OnResume和unsubsribes从他们在OnPause方法: 隐藏,复制Code

protected override void OnResume()
{
base.OnResume(); DrenchGame.GameChanged += UpdateTiles;
DrenchGame.GameStopped += StopGame;
} protected override void OnPause()
{
base.OnPause(); DrenchGame.GameChanged -= UpdateTiles;
DrenchGame.GameStopped -= StopGame;
}

取消游戏事件订阅是非常重要的:活动事件处理程序将防止活动实例被垃圾收集,并将造成内存泄漏。 开始一个游戏和显示一个棋盘活动 当一个游戏被创建的时候,唯一要做的事情就是开始与用户交互的活动。 隐藏,复制Code

App.DrenchGame = new SinglePlayerGame(); // or any other game class!
StartActivity(typeof(DrenchBoardActivity));

我们可以创建带有不同选项的主菜单:单人游戏,双人游戏,对战Android等等。菜单项的每个处理程序都将以相同的方式工作,唯一的区别是创建游戏类。 添加及rk支持 多人网络游戏模式需要远程玩家之间的特殊同步。例如,两个玩家应该有同一个棋盘来玩。除非双方都准备好了,否则游戏不能开始。如果其中一名玩家退出游戏,游戏将无法继续进行,依此类推。我们的IDrenchGame接口不足以处理所有这些:我们需要额外的方法和事件。 为了节省网络带宽,我们不会在每个回合发送整个游戏状态。相反,每个团队将维护自己的淋板实例,我们只会为每个移动和游戏状态更新发送轻量级事件。 IDrenchGameServer接口 在IDrenchGame界面添加新成员是没有意义的。特定于网络的方法和事件对当地的比赛没有意义。相反,让我们引入一个新的界面来与游戏服务器互操作,扩展IDrenchGame: 隐藏,复制Code

public interface IDrenchGameServer : IDrenchGame
{
void Join(); // join the remote game void Leave(); // leave the remote game bool IsReady { get; } // both players are ready event EventHandler GameStarted; event EventHandler<MoveEventArgs> Moved; // other player made a move
}

使用此接口假定采用以下协议: 建立连接与idrenchgameserver订阅事件GameStarted和移动电话连接方法开始一个新游戏调用移动方法,让你的移动处理移动事件应对对手的移动处理gamestop事件(从IDrenchGame继承)停止当前游戏叫NewGame(也继承了IDrehchGame)开始一个新游戏,如果你想中止当前的游戏,所以服务器可以停止调用离开方法从服务器断开事件服务器退订 DrenchGameServer和DrenchGameClient 让我们创建两个特殊的游戏类来实现上面列出的协议。对于这些类,我决定重用我的TwoPlayerGame类,它已经实现了双人游戏模式所需的一切。 我的两个类都使用了TwoPlayerGame的私有实例来本地管理游戏状态。例如,IsStopped和CurrentStatus属性直接从InnerGame实例中获取: 隐藏,复制Code

public class DrenchGameServer : DrenchGameBase, IDrenchGameServer
{
public DrenchGameServer()
{
InnerGame = new TwoPlayerGame();
} private TwoPlayerGame InnerGame { get; private set; } public override bool IsStopped
{
get { return InnerGame.IsStopped; }
protected set { ... }
} public override string CurrentStatus
{
get { return InnerGame.CurrentStatus; }
protected set { ... }
}
}

实现服务器特定的方法Join和Leave非常容易。所有我们需要做的是确保Join方法只能被调用一次(我们总是对单一对手发挥): 隐藏,复制Code

public void Join()
{
if (IsReady)
{
// this exception will travel across the wire to the client
throw new InvalidOperationException("Second player " +
"already joined the game. Try another server.");
} IsReady = true;
OnGameStarted();
} public void Leave()
{
IsReady = false;
IsStopped = true;
OnGameStopped(false, "Second player has left the game.");
}

除了本地的内部游戏实例外,DrenchGameClient类还持有对远程IDrenchGameServer的引用。它连接到服务器,复制板数据,订阅服务器事件,并调用Join方法: 隐藏,复制Code

public class DrenchGameClient : DrenchGameBase
{
public DrenchGameClient(IDrenchGameServer server)
{
Server = server;
InnerGame.Board.CopyFromFlipped(Server.Board);
InnerGame.SkipMove();
UpdateStatus();
JoinServer();
} public async void JoinServer()
{
await Task.Factory.StartNew(() =>
{
Server.GameStarted += ServerGameStarted;
Server.GameStopped += ServerGameStopped;
Server.Moved += ServerMoved;
Server.Join();
});
}
...
}

注意,JoinServer方法是异步的。由于网络延迟,远程调用比本地调用慢1000倍。为了确保我们的游戏不会冻结UI,我们需要异步执行远程调用。请注意Xamarin的stable branch。Android仍然不支持异步/等待模式,所以你需要框架的最新beta版本来编译这段代码。 另一个有趣的地方是,DrenchGameClient是如何采取行动的。它所做的唯一事情是调用服务器的方法并处理服务器的事件。游戏客户端不改变其内部游戏的状态:它完全由远程服务器控制。注意,MakeMove方法也是异步的,因为它涉及一个远程调用: 隐藏,复制Code

public override async void MakeMove(int value)
{
await Task.Factory.StartNew(() => Server.MakeMove(value));
} private void ServerMoved(object sender, MoveEventArgs e)
{
InnerGame.MakeMove(e.Color);
UpdateStatus();
}

托管DrenchGameServer 为了实现游戏服务器的网络共享,我们采用了Zyan通信框架。这个库不需要对我们的类进行任何额外处理,因此我们可以按原样发布DrenchGameServer实例。下面的图表概述了Zyan应用程序的典型架构(注意,除了ZyanComponentHost和ZyanConnection类之外,这些内部内容不会出现在我们的应用程序代码中)。青色框表示Zyan库类,黄色框表示应用程序代码: 要启动服务器,我们需要创建一个带有TCP协议的ZyanComponentHost实例。游戏服务器和zyan主机实例将存储在我们自定义应用类的属性中,就像其他共享实例一样: 隐藏,复制Code

public void StartServer()
{
if (ZyanHost == null)
{
// set up duplex tcp protocol, no security required
var portNumber = Settings.PortNumber;
var authProvider = new NullAuthenticationProvider();
var useEncryption = false;
var protocol = new TcpDuplexServerProtocolSetup(portNumber, authProvider, useEncryption); // start the server
ZyanHost = new ZyanComponentHost(Settings.ZyanHostName, protocol);
} // create server game
var server = new DrenchGameServer();
DrenchGame = server; // register game component, so a client can connect to the server
ZyanHost.RegisterComponent<IDrenchGameServer, DrenchGameServer>(server);
}

注:目前Zyan library Android版本中唯一可用的传输协议是双工TCP协议。 连接到DrenchGameServer 要连接到Zyan component host发布的游戏服务器,需要完成以下步骤: 通过创建ZyanConnection类建立连接为远程idrenchgameserver创建代理创建DrenchGameClient,将服务器代理作为构造函数参数传递 让我们在自定义应用程序类中添加一个方法: 隐藏,复制Code

public IDrenchGameServer ConnectToServer(string hostName)
{
var protocol = new TcpDuplexClientProtocolSetup(encryption: false);
var url = protocol.FormatUrl(host, Settings.PortNumber, Settings.ZyanHostName); var zyanConnection = new ZyanConnection(url, protocol);
return zyanConnection.CreateProxy<IDrenchGameServer>();
}
... // this method is used as follows (note the non-blocking asynchronous call):
var server = await Task.Factory.StartNew(() => App.ConnectToServer(Settings.ServerAddress)); // and now -- already familiar code to start a game:
App.DrenchGame = new DrenchGameClient(server);
StartActivity(typeof(DrenchBoardActivity));

所以,我们刚刚为Android创建了一个支持Wifi的简单多人游戏。尽管本文没有涵盖应用程序的每个小方面,但我希望它展示了所有重要的方面。任何反馈将非常感谢! 另外,游戏已经上传至谷歌游戏应用商店,最新的源代码可以在Codeplex和github上找到。 参考文献 Zyan Drench在谷歌游戏:https://play.google.com/store/apps/detailsallie.Zyan。在CodePlex上下载最新的源代码:https://drench.codeplex.com/ xamarin。Android平台:http://xamarin.com/monoforandroid Zyan通信框架:http://zyan.com.de/ 历史 05.08.2013最初的帖子 本文转载于:http://www.diyabc.com/frontweb/news30452.html