吃豆人多人游戏(译文)
By robot-v1.0
本文链接 https://www.kyfws.com/games/pacman-multiplayer-zh/
版权声明 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
- 29 分钟阅读 - 14199 个词 阅读量 0吃豆人多人游戏(译文)
原文地址:https://www.codeproject.com/Articles/520783/Pacman-Multiplayer
原文作者:Mamoun Alghaslan
译文由本站 robot-v1.0 翻译
前言
A basic LAN game in C#
C#中的基本LAN游戏
介绍(Introduction)
每个人都知道传说中的吃豆子游戏,即使玩了几个小时也很有趣.这款游戏的机制一直吸引着我的注意力.非常简单但是,背后有很多想法.(Everyone knows the legendary Pacman game and how fun it is even if played for hours and hours. This game’s mechanics have always grasped my attention. It’s very simple. However, there has been a lot of thoughts put behind it.) 我对原始游戏的大部分理解来自一个非常好的网站,专门用于解释原始游戏.您可以访问该网站(Much of my own understanding of the original game, came from a very nice website dedicated to explaining the original game. You can give that site a visit) 点击这里(by clicking here) .(.) 在本文中,您将找到类似的游戏实现方式,但略有改动.(In this article, you will find a similar implementation of the game, with a small twist on it.) 下载(DOWNLOAD):这是一个(: Here’s a) 保管箱上的链接(link on dropbox) 用于项目文件.(for the files of the project.)
致谢(Acknowledgement)
该游戏是根据法赫德国王石油和矿产大学提供的" SWE344-Internet协议和客户端服务器编程"课程的项目创建的;根据我的老师的建议,(This game has been created as a project for the course “SWE344 - Internet protocols and client-server programming” given at King Fahd University of Petroleum and Minerals; Suggested by my instructor,) 纳西尔`达维什(Nasir Darwish)(Nasir Darwish) .(.)
免责声明(Disclaimer)
游戏的唯一目的是提供使用.NET框架UPD类的工作示例.这可能不是最好的示例,因为许多优秀的程序员可能知道解决该游戏算法的更好技巧.(The sole purpose of the game is giving a working example of using .NET framework UPD classes. This may not be the best example since many good programmers may know better tricks to solve this game’s algorithms.) 游戏的图形只会刮擦原始游戏的实际外观.我不是图形设计专家.您将在游戏中看到的所有图像,都是我使用Microsoft Paint绘制的.(The graphics of the game only scratches the surface of what the original game actually looks like. I’m not a graphics designing expert. All of the images you will see in the game, I have drawn them my self using Microsoft Paint.)
游戏逻辑(Game Logic)
游戏的主要逻辑基于原始游戏的抽象逻辑:吃豆子吃点;鬼魂困扰着吃豆人.原始游戏的变化如下:游戏有两个吃豆子玩家和四个幽灵.最初,每对幽灵都会跟随一个吃豆人.每当一个吃豆人吃了一个超级点(最初会使幽灵惧怕吃豆人的较大的点)时,所有的鬼魂现在都会困扰对手的吃豆人.简单?继续阅读下一节以获得更清晰的理解.(The main logic of the game is based on the abstract logic of the original game: Pacman eats dot; Ghosts haunt Pacman. The twist on the original game is as follows: the game has two Pacman players, and four ghosts. Initially, each pair of ghosts will follow one pacman. Whenever one pacman eats a Super Dot (The bigger dot that originally will make ghosts fear pacman), all of the ghosts will now haunt the opponent pacman. Simple? Continue reading the next sections for a clearer understanding.)
迷宫(The Maze)
下图显示了从原始游戏中汲取灵感的游戏背景:(The following image shows the background of the game, inspired from the original game:)
迷宫中充满了瓷砖,每个瓷砖代表一个可以存在Actor的地方.演员可以是吃豆人或幽灵.一格大小为20x20像素.下图显示以白色突出显示的图块:(The maze is full of tiles, where each tile represents a place where an Actor can exist. An Actor could be either a Pacman, or a Ghost. One tile is of the size 20x20 pixels. The following image shows the tiles highlighted in white:) 笔记:(Notes:) 忽略隧道或背景的左中和右中侧,因为我没有将它们包括在游戏中-检查第一个背景图像-.该图像仅用于显示图块.磁贴实际上彼此相邻.也就是说,如图中黑色所示,它们之间没有分隔符.这只是为了使事情变得清楚.(Ignore the tunnels or the mid-left and mid-right sides of the background as I have not included them in the game - Check first background image -. This image is only for showing the tiles. Tiles are actually right next to each other. That is, there’s no separators between them as shown in the image as black. This is just for making things clear.)
鬼有大脑(我不知道这是否真的适用于现实生活).但是,它们在魔术上仅限于仅在交叉路口存在幻影时才能工作.这实际上是原始Ghost逻辑的快捷方式.您可以阅读有关此内容的更多信息(Ghosts have brains (I don’t know if that is actually applicable in real life). However, they are magically limited to work only when a ghost exists in an intersection. This is a actually a shortcut version of the original Ghost’s logic. You can read more about that) 这里(here) 如果您有兴趣.尽管如此,回到这个游戏.下图显示了他们实际可以思考的地方:(if you were interested. Nevertheless, back to this game. The following image shows where they can actually think:)
如果鬼决定了去向,它的大脑将关闭并一直沿直线前进,直到到达另一个可以神奇地思考的交叉点为止.(If a ghost had made a decision of where to go, its brain will shut-off and it will keep going on a straight line until it reaches another intersection where it can magically think again.) 这是游戏逻辑的最抽象表示.现在让我们详细介绍一下.(That’s the most abstract representation for the logic of the game. Let’s get more into details now.)
守则的类别(Classes of the Code)
我认为在这里值得一提的是,该游戏被设计为主机-客户端架构,其中主机负责所有逻辑,客户端仅显示其从主机接收到的内容.对此的更多细节将在后面进一步解释.(I think it’s worth to mention here that the game was designed as Host-Client architecture, where the host makes all of the logic and client just shows what it receives from the host. More details of this will be explained further ahead.) “(")就在我写这篇文章的那一刻,我正享受着一杯咖啡,苏门答腊,最好的自制咖啡."(At this exact exact moment when I was writing this article, I was enjoying a great cup of coffee, Sumatran, home brewed, at it’s finest.") 下面的列表显示了用于构建游戏的所有类:(The following list shows all the classes that were used to build the game:)
Actor
Pacman
Ghost
Tile
Maze
MainMenu
GameWindow
Logic
HostLogic
ClientLogic
让我们一个一个地解决每个问题,并深入研究它.(Let’s tackle each, one-by-one, and dig deep into it.)小费:(Tip:)假设代码块中的所有代码都是连续的.如图所示不分开.(assume all code in code blocks is continuous. Not separate as illustrated.)
演员(Actor)
这是代表可以在迷宫的瓷砖中移动的演员的抽象类.(This is an abstract class representing an actor which can move in the tiles of the maze.)
public class Actor
{
这些是角色的X和Y位置:(These are the X and Y positions of an actor:)
protected short X;
public short getX() { return this.X; }
public void setX(short newX) { this.X = newX; }
protected short Y;
public short getY() { return this.Y; }
public void setY(short newY) { this.Y = newY; }
该字节保持演员的方向.一个方向只能是以下四种可能性中的一种:0\1\2或3,其中每种分别代表按顺序排列的上,右,下和左.(This byte hold the direction of an actor. A direction, could only be in on of the four possibilities: 0,1,2, or 3 where each represents Up, Right, Down and Left, respectively to order.)
protected byte direction;
public byte getDirection() { return this.direction; }
演员所在的磁贴:(The tiles where the actor is:)
protected Tile currentTile;
public Tile getCurrentTile() { return this.currentTile; }
请注意,设置actor的当前图块会将其位置设置为新图块的位置:(Notice that setting the current tile of an actor, will set it’s position to the new tile’s position:)
public void setCurrentTile(Tile newTile)
{
this.currentTile = newTile;
this.X = newTile.getX();
this.Y = newTile.getY();
}
此方法根据角色的方向将角色移动到下一个相邻的瓦片.如果磁贴与演员的方向没有相邻的瓷砖,则演员将不会移动.我将在后面进一步解释Tile类.(This method moves an actor to the next adjacent tile, based on it’s direction. If the tile doesn’t have a neighboring tile to the direction of the actor, the actor will not move. I will explain the Tile class further ahead.)
public void move()
{
Tile destinationTile = null;
if (this.direction == 0)
{
destinationTile = this.getCurrentTile().getTileAbove();
}
else if (this.direction == 1)
{
destinationTile = this.getCurrentTile().getTileToRight();
}
else if (this.direction == 2)
{
destinationTile = this.getCurrentTile().getTileBelow();
}
else
{
destinationTile = this.getCurrentTile().getTileToLeft();
}
注意是(Notice that is) destinationTile
为空,表示我们处于边缘.我们需要检查是否有可以移动的瓷砖,并且该瓷砖不是墙.(is null, means that we’re at an edge. We need to check the there is a tile that we can move into, and that that tile is not a wall.)
if (destinationTile != null && (!destinationTile.isWall()))
{
setCurrentTile(destinationTile);
}
}
一个构造函数,它将Actors当前的tile设置为作为参数指定的tile,以及另一个空的构造函数.(A constructor that will set the Actors current tile, to a tile given as a parameter, and another empty constructor.)
public Actor(Tile initialTile)
{
this.setCurrentTile(initialTile);
}
protected Actor()
{
}
}
吃豆子(Pacman)
“你喜欢黄(“Do you like Yellow) 或绿色(or green) 更多?我希望我做了一个红色的.这杯咖啡真的很棒!"(more? I wish I had made a red one. This coffee is really great!") 此类代表吃豆人.我们在游戏中的主要参与者.本课延伸(This class represents Pacman. Our main player in the game. This class extends)演员(Actor),因此继承了它的所有方法和属性.(, thus inheriting all of it’s methods and properties.)
public class Pacman : Actor
{
此构造方法比其基本构造方法更详细(This constructor take more details than its base constructor in)演员(Actor)只需要一块瓷砖.因此,我们设置了(that takes only a tile. So, we set our) currentTile
进入(into) initialTile
从(from)演员(Actor)的基本构造函数,我们的(’s base constructor, our) image
到中的图像(to the image in) imagePath
(向前解释一点),我们的((Explain a little bit forward), our) direction
进入(into) initialDirection
然后我们告诉吃豆子的天气他是黄色还是绿色(and we tell our Pacman weather he’s Yellow or Green through) isYellow
. (我向您保证没有歧视).(. (No discrimination I assure you).)
public Pacman(Tile initialTile, string imagePath, byte initialDirection, bool isYellow)
: base(initialTile)
{
this.setImage(imagePath);
this.setDirection(initialDirection);
this.yellow = isYellow;
}
一个布尔变量来查看天气吃豆子是黄色的,否则是绿色的:(A bool variable to see weather Pacman is yellow otherwise green:)
private bool yellow;
public bool isYellow() { return this.yellow; }
这是吃豆人的形象.设置图像将从给定路径的文件中加载图像作为参数.的(This is the image of pacman. Setting the image will load the image from a file given its path as a parameter. The) copyImage
将在下一个方法中使用.设置图像将克隆(will be used in the next method. Setting the image will clone the) otherImage
.(.)
protected Image image;
public Image getImage() { return this.image; }
public void setImage(string imagePath)
{
this.image = Image.FromFile(imagePath);
this.copyImage = (Image)this.image.Clone();
}
public void setImage(Image otherImage)
{
this.image = (Image)otherImage.Clone();
}
protected Image copyImage;
由于我们希望游戏看起来更好一些,因此设置吃豆人的方向将使他(或您愿意的话)旋转以指向该方向.假设在构造Pacman对象时我们输入的原始图像将使Pacman指向右侧.该代码是不言自明的.(Since we want the game to look a little bit nicer, setting the direction of Pacman will rotate him (or her if you would like) to point at that direction. This assumes that the original image we feed when constructing a Pacman object, will have Pacman pointing to the right. The code is self-explanatory.)
public void setDirection(byte newDirection)
{
this.direction = newDirection;
this.image = (Image)this.copyImage.Clone();
if (this.direction == 1)
{
this.image.RotateFlip(RotateFlipType.RotateNoneFlipNone);
}
else if (this.direction == 0)
{
this.image.RotateFlip(RotateFlipType.Rotate270FlipNone);
}
else if (this.direction == 2)
{
this.image.RotateFlip(RotateFlipType.Rotate90FlipNone);
}
else if (this.direction == 3)
{
this.image.RotateFlip(RotateFlipType.RotateNoneFlipX);
}
}
空的构造函数. (我真的不知道为什么我在这里有这些^但是,没关系)(Empty constructor. (I really don’t know why I have these here … but ok it won’t make a difference)) ” * Slurrrrrp 这杯咖啡来自天堂!"("Slurrrrrp This coffee is from heaven!"*)
protected Pacman() : base()
{
}
鬼(Ghost)
"(") 幽灵般的,(Spooky,) 用功,(hard working,) 不安和(restless and) 非常聪明(根据他们自己的智商测度)"(very smart (according to their own IQ measures)").(.) 此类表示一个Ghost.与Pacman一样,此类也扩展了(This class represents a Ghost. This class, alike Pacman, also extends)演员(Actor).这是我所知的部分内容之一,即使我的专利正在申请中,我也可能没有写过最优秀的文章.但这是有目的的.(. This is one of the parts where I know I may not have written the best class even existed to human beings, neither if it is patent pending. But it serves it’s purpose.)
public class Ghost : Actor
{
这个构造函数需要(This constructor takes and) initialTile
并将其设置为我们的(and set it to our) currentTile
,四个图像(每个图像代表每个方向的图像)和一个吃豆人(, four images each representing an image for each direction, and a Pacman) target
出没!(to haunt!)
如果您想知道"四个图像?为什么?为什么吃豆子只有一个?“稍等片刻.将进行说明.(If you were wondering “Four images? Why? Why Pacman had only one?” Wait a moment. It will be explained.)
“它变得越来越有趣;我知道. Slurrrrp "(“It’s getting a little more interesting; I know. Slurrrrp"*)*
public Ghost(Tile initialTile,
string image0Path,string image1Path, string image2Path, string image3Path,
byte initialDirection, Pacman target)
: base(initialTile)
{
this.setImage0(image0Path);
this.setImage1(image1Path);
this.setImage2(image2Path);
this.setImage3(image3Path);
this.setDirection(initialDirection);
this.target = target;
}
一个吃豆人变量,使吃豆人陷入困境.(A Pacman variable holding the pacman to haunt.)
protected Pacman target;
public Pacman getTarget() { return this.target; }
public void setTarget(Pacman newTarget)
{
this.target = newTarget;
}
分别为0\1\2和3的四个图像代表鬼的图像,其眼睛分别指向上,右,下和左.鬼有四个图像而吃豆人只有一个图像的原因是,我们无法翻转鬼的图像.幽灵的下部必须始终指向下方.设置任何图像(类似于Pacman类),都将从给定路径的文件中加载图像.(Four images, numbered 0,1,2 and 3 representing images of the ghost with its eyes pointing Up, Right, Down and Left respectively to order. The reason why a ghost has four images but pacman has only one, is that we cannot flip a ghost’s image around. A ghost’s lower part must always be pointing down. Setting any image, alike Pacman class, will load the image from a file given it’s path.)
protected Image image0;
public Image getImage0() { return this.image0; }
public void setImage0(string path){this.image0 = Image.FromFile(path);}
protected Image image1;
public Image getImage1() { return this.image1; }
public void setImage1(string path){this.image1 = Image.FromFile(path);}
protected Image image2;
public Image getImage2() { return this.image2; }
public void setImage2(string path){this.image2 = Image.FromFile(path);}
protected Image image3;
public Image getImage3() { return this.image3; }
public void setImage3(string path){this.image3 = Image.FromFile(path);}
请注意,此方法名为(Notice that this method is named) getImage()
像以前一样在末尾没有数字.这主要是为了便于我们绘制鬼影时使用.它将根据幽灵的方向返回图像.(without a number appended at the end like the ones before. This is mainly used for ease of used when we want to draw the ghost. It will return an image according to the ghost’s direction.)
public Image getImage()
{
if (this.direction == 0) return image0;
else if (this.direction == 1) return image1;
else if (this.direction == 2) return image2;
else if (this.direction == 3) return image3;
return null;
}
这只是一个简写,而不是调用四个方法.(This is just a shorthand instead of calling four methods.) “程序员.有时候我们自称为聪明.我们真的很懒. Sluuurp 第三杯咖啡."(“Programmers. Sometimes we call ourselves smart. We’re just lazy, really. Sluuurp Third cup of coffee."*)*
public void setImages(string i0, string i1, string i2, string i3)
{
this.setImage0(i0);
this.setImage1(i1);
this.setImage2(i2);
this.setImage3(i3);
}
无需对此做太多解释:(No need to explain much on this one:)
public void setDirection(byte newDirection)
{
this.direction = newDirection;
}
这个方法告诉它的调用者鬼是否已经吞噬了它的目标.基本上,如果鬼的X和Y等于目标的X和Y,则他会死. (Kinda残酷的命名.)(This methods tells its caller if the ghost had eaten it’s target. Basically, if a ghost’s X and Y are equal to its target’s X and Y, then he dies. (Kinda cruel naming.)) 这本可以用另一种方式进行.也就是说,直接检查X和Y是否相等.不计算差异.但是,可以认为这是对下一种方法所见的热身.(This could have been made in another way. That is, directly checking if X and Y are equal; not calculating the difference. However, consider this to be a warm-up to what you’re going to see in the next method.)
public bool targetDies()
{
int xDifference = getX() - target.getX();
int yDifference = getY() - target.getY();
return (xDifference == 0 && yDifference == 0);
}
寻找目标等同于思考下一步的目标.首先,在开始思考之前,我们先检查当前图块是否为鬼图块.什么是鬼砖?这是我们之前讨论的关于幻影能够思考的地方的魔幻瓷砖.有关更多详细信息,请参阅前面解释的迷宫课程.(Seeking the target is equivalent to thinking where to go next. First of all, before initiating an thinking, we check if the current tile is a ghost tile. What is a ghost tile? This is the magical tile we talked earlier about where the ghost is able to think. Refer to Maze class explained ahead for more details.)
public void seekTarget()
{
if (this.getCurrentTile().isGhostTile())
{
因此,我们陷入了困境.接下来要做的是计算我们的幻影和目标之间的X和Y差.(So, we’re in a ghost tile. Next thing to do is calculate the X and Y differences between the our ghost’s and its target’s.)
int xDifference = getX() - target.getX(); //-ve means pacman is to you're right
int yDifference = getY() - target.getY(); //-ve means pacman is below you
抓住这两个变量.我们将需要它们.(Hold you horses on these two variables. We’re gonna need them.)
goCheckY
意味着两件事; X之间的差异小于Y之间的差异,因此继续使用Y(较短的距离).否则,因为没有相邻的图块,所以幻影无法在X轴上移动.(means two things; either the difference between X’s is less that the difference between Y’s, thus go on Y’s (shorter distance). Else, means that the ghost cannot move on on X-axis because there are no adjacent tiles.)
fail
意味着我们既不能在X上也不能在Y上移动.因此,(惰性方法),只需反转幻影方向即可.(means that we couldn’t move either on X neither Y. So, (Lazy approach), just invert the ghost direction.)
bool goCheckY = true;
bool fail = true;
如果X上的差异大于Y上的差异:(If the difference on X is greater than it is on Y:)
if (Math.Abs(xDifference) >= Math.Abs(yDifference))
{
//move on xaxis
if (xDifference < 0) //target is to my right
{
if (!currentTile.getTileToRight().isWall()) // if it's not a wall
{
//I can move to right!
setDirection(1);
goCheckY = false; //No need to go to Y
fail = false; //we did not fail
}
}
else
{
if (!currentTile.getTileToLeft().isWall()) // if it's not a wall
{
//I can move to left!
setDirection(3);
goCheckY = false; //No need to go to Y
fail = false; //we did not fail
}
}
}
如果(If) goCheckY
为真,则Y的差异更大,这意味着我们没有输入前一个if语句,或者我们无法将更多内容移动到x轴上的任何相邻图块.(is true, then either the difference in Y’s is greater, which means we did not enter the previous if statement, or we couldn’t move more to any adjacent tile on the x-axis.)
if (goCheckY)
{
//move on yaxis
if (yDifference < 0) //target is to my right
{
if (!currentTile.getTileBelow().isWall()) // if it's not a wall
{
//I can move to below!
setDirection(2);
fail = false;
}
}
else
{
if (!currentTile.getTileAbove().isWall()) // if it's not a wall
{
//I can move to top!
setDirection(0);
fail = false;
}
}
}
如果(If) fail
仍然是真的,那么我们就无法朝任何方向前进.因此,回去,鬼,你来自哪里.(is still true, then we couldn’t move on any direction. Thus, just go back, ghost, where you came from.)
if (fail)
{
if (direction == 0)
{
direction = 2;
}
else if (direction == 1)
{
direction = 3;
}
else if (direction == 2)
{
direction = 0;
}
else if (direction == 3)
{
direction = 1;
}
}
}
}
}
瓦(Tile)
此类表示一个图块.演员可以存在的地方.有不同类型的瓷砖.以下列表显示了它们:(This class represents a tile. A place where an actor can exists. There are different types of tiles. The following list shows them:)
- 墙砖:没有东西可以存在.(Wall Tile: Nothing can exist in this.)
- 点拼贴:拥有一个点,Actor可以在其中移动.(Dot Tile: Holds a dot, and an Actor can move in it.)
- 超级点图块:也是一个点图块,但包含一个超级点.(Super Dot Tile: Also a dot tile, but holds a Super dot.)
- 鬼点瓷砖:也是点瓷砖,但是鬼可以在这里思考.(Ghost Dot Tile: Also a dot tile, but a ghost can think here.)
- Ghost超级点图块:也是超级点图块,但是鬼可以在这里思考.(Ghost Super Dot Tile: Also a super dot tile, but a ghost can think here.) 因此,您可以认为它们之间存在某种继承关系.但是,这并不表示为继承和代码.只是保存这些信息的一些变量.(So, you can think that there’s some inheritance relation between them. However, this is not represented as inheritance and the code; It’s just some variables holding these information.)
public class Tile
{
以下四个变量将相邻的图块保留到图块.(The following four variables hold the adjacent tiles to a tile.)
protected Tile tileAbove;
public Tile getTileAbove() { return this.tileAbove; }
public void setTileAbove(Tile newTile) { this.tileAbove = newTile; }
protected Tile tileBelow;
public Tile getTileBelow() { return this.tileBelow; }
public void setTileBelow(Tile newTile) { this.tileBelow = newTile; }
protected Tile tileToRight;
public Tile getTileToRight() { return this.tileToRight; }
public void setTileToRight(Tile newTile) { this.tileToRight = newTile; }
protected Tile tileToLeft;
public Tile getTileToLeft() { return this.tileToLeft; }
public void setTileToLeft(Tile newTile) { this.tileToLeft = newTile; }
图块的X和Y位置:(The X and Y position of a tile:)
protected short X;
public short getX() { return this.X; }
public void setX(short newX) { this.X = newX; }
protected short Y;
public short getY() { return this.Y; }
public void setY(short newY) { this.Y = newY; }
这个布尔值告诉我们一个图块是否有一个点,而不管它是否是一个超点.(This bool tells us if a tile had a dot or not, regardless of the fact if it’s a super dot or not.)
protected bool dot;
public bool hasDot() { return dot; }
public void setDot(bool newDot) { this.dot = newDot; }
但是,此布尔值告诉我们该点是否是超级点.(This bool, however, tells us if the dot is a super dot or not.)
protected bool superDot;
public bool isSuperDot() { return superDot; }
public void setSuperDot(bool newSuperDot) { this.superDot = newSuperDot; }
告诉我们是否是墙的布尔值:(A bool that tells us if it’s a wall or not:)
protected bool wall;
public bool isWall() { return wall; }
public void setWall(bool newWall) { this.wall = newWall; }
磁贴的图像.这应该是一个点的图像,或者一个超级点(如果有的话).(An image for the tile. This should be an image of a dot, or a super dot, if any holds.)
protected Image image;
public Image getImage() { return this.image; }
public void setImage(string path)
{
this.image = Image.FromFile(path);
}
public void setImage(Image otherImage)
{
this.image = otherImage;
}
最后,这个布尔告诉我们,瓷砖是鬼魂的神奇瓷砖(他们可以在这里思考)(Lastly, this bool tells us it the tile is a magical tile for the ghosts (Where they can think))
private bool ghostTile;
public bool isGhostTile() { return this.ghostTile; }
public void setGhostTile(bool value) { this.ghostTile = value; }
public Tile(){} //Just a plain constructor...
}
迷宫(Maze)
此类表示迷宫,其中容纳所有瓷砖.一旦创建了自己的瓷砖,该类还将构造这些瓷砖.请注意,构造函数使我们免于手工一一创建拼贴.我认为这是一种明智的实现方式.基本上,我们有一个如下所示的文本文件,其中,瓦片是根据前面提到的类型排序的.(This class represents a maze, which holds all the tiles. This class also constructs the tiles once its self is created. Notice that the constructor saves us from creating tiles one-by-one by hand. I think it’s kind of a smart way this is implemented. Basically, we have a text file as shown below, where the tiles are sorted according to their types, mentioned previously.)
WWWWWWWWWWWWWWWWWWWWWWWWWWWW
WdDDDDdDDDDDsWWsDDDDDdDDDDdW
WDWWWWDWWWWWDWWDWWWWWDWWWWDW
WDWWWWDWWWWWDWWDWWWWWDWWWWDW
WDWWWWDWWWWWDWWDWWWWWDWWWWDW
WdDDDDdDDDDDdDDdDDDDDdDDDDdW
WDWWWWDWWDWWWWWWWWDWWDWWWWDW
WDWWWWDWWDWWWWWWWWDWWDWWWWDW
WsDDDDdWWdDDdWWdDDdWWdDDDDsW
WWWWWWDWWWWWDWWDWWWWWDWWWWWW
WWWWWWDWWWWWDWWDWWWWWDWWWWWW
WWWWWWDWWdDDdDDdDDdWWDWWWWWW
WWWWWWDWWDWWWWWWWWDWWDWWWWWW
WWWWWWDWWDWWWWWWWWDWWDWWWWWW
WWWWWWdDDsWWWWWWWWsDDdWWWWWW
WWWWWWDWWDWWWWWWWWDWWDWWWWWW
WWWWWWDWWDWWWWWWWWDWWDWWWWWW
WWWWWWdWWdDDDDDDDDdWWdWWWWWW
WWWWWWDWWDWWWWWWWWDWWDWWWWWW
WWWWWWDWWDWWWWWWWWDWWDWWWWWW
WdDDDDsDDdDDdWWdDDdDDsDDDDdW
WDWWWWDWWWWWDWWDWWWWWDWWWWDW
WDWWWWDWWWWWDWWDWWWWWDWWWWDW
WdDdWWdDDDDDdDDdDDDDDdWWdDdW
WWWDWWDWWDWWWWWWWWDWWDWWDWWW
WWWDWWDWWDWWWWWWWWDWWDWWDWWW
WsDdDDdWWdDDdWWdDDdWWdDDdDsW
WDWWWWWWWWWWDWWDWWWWWWWWWWDW
WDWWWWWWWWWWDWWDWWWWWWWWWWDW
WdDDDDDDDDDDsDDsDDDDDDDDDDdW
WWWWWWWWWWWWWWWWWWWWWWWWWWWW
如果仔细检查前面的文本,您会注意到它代表了"游戏逻辑"部分第二张图片中显示的背景.(If you carefully examine the previous text, you will notice it represents the background shown in the second picture in the Game Logic section.) 文本中的不同字符表示图块的类型,如下所示:(The different characters in the text represent types of the tiles as follows:)
- 墙砖:W(Wall Tile: W)
- 点瓷砖:D(Dot Tile: D)
- 超点瓷砖:S(Super Dot Tile: S)
- 鬼点瓷砖:d(Ghost Dot Tile: d)
- 幽灵超点瓷砖:s(Ghost Super Dot Tile: s) 解析文本后,我们可以轻松构建图块.(Parsing the text, we can easily construct our tiles.)
public class Maze
{
textualMaze
将包含字符串行,其中包含图块的字符. Tile 2D阵列的宽度为28个图块,高度为31个图块,并将容纳横跨背景的图块.(will hold lines of strings which holds the characters of the tiles. The Tile 2D array is 28 tiles wide and 31 tiles in height and will hold tiles that spans all over the background.)
protected string[] textualMaze;
public Tile[,] tiles = new Tile[28,31]; //The maze must be of 28tiles wide, 31tiles high
totalScore
动态计算(每个点10个,每个超级点100个).这是懒惰的纯粹例子,因为我不想手动计算它.的(is dynamically calculated (10 for each dot and 100 for each super dot). This is a pure example of laziness because I didn’t want to manually calculate it. The) totalScore
使用方法将在后面进一步说明.(use will be explained futher ahead.)
public static int totalScore = 0;
在构造函数中要做的第一件事是读取文本文件中的所有行,并将其保存到外面(First thing to do in the constructor, is to read all the lines in the text file, and save it in out) textualMaze
数组.(array.)
public Maze()
{
textualMaze = System.IO.File.ReadAllLines("maze.txt");
以下for循环将循环28x31次,仅用于初始化所有图块.它可能已与下一个循环合并,但我没有.同样,我不记得为什么.(The following for-loop will loop 28x31 times just to initialize all the tiles. It may have been merged with the next loop, but I didn’t. Again, I don’t recall why.)
for (short i = 0; i < 28; i++) //this will be X
{
for (short j = 0; j < 31; j++) //this will be Y
{
tiles[i, j] = new Tile(); //Just initilize it
}
}
这就是创建迷宫的逻辑所在.首先,我们在索引i处输入一个字符串,并从索引j处获取一个字符.(This is where the logic for creating the maze lays. First of all, we a string at index i, and take a character from it at index j.)
for (short i = 0; i < 28; i++) //this will be X
{
for (short j = 0; j < 31; j++) //this will be Y
{
char readChar = textualMaze[j][i]; //textualMaze[j] <-- up to here it gives a string
//textualMaze[j][i] <-- this is a char in a string
下一步,我们只需将i和j乘以20,即可设置图块的X和Y位置.(Next step, we set the X and Y positions of the tile by simply multiplying i and j by 20.)
//Creating the tile ------------------------------------------------
tiles[i, j].setX((short)(i * 20)); //Set x
tiles[i, j].setY((short)(j * 20)); //Set y
如果读取的字符不是” W”,则将其设置为点图块而不是墙.否则,我们设置反函数.(If the char we read was NOT a ‘W’, we set it as a dot tile and not a wall. Else, we set the inverse.)
if (readChar != 'W') //if it isn't a wall, it's a dot.
{
tiles[i, j].setDot(true);
tiles[i, j].setWall(false);
}
else
{
tiles[i, j].setDot(false);
tiles[i, j].setWall(true);
}
如果读取的字符为” D"或” d”,则为点.我们将其设置为(If the char read was ‘D’ or ’d', then it’s a dot. We set it its) superDot
布尔为假,及其(bool as false, and its) image
到一个点.我们也增加(to that of a dot. We also increment the) totalScore
乘以10(by 10.)
if (readChar == 'D' || readChar == 'd') //this is not a super dot.
{
totalScore += 10;
tiles[i, j].setSuperDot(false);
tiles[i, j].setImage("img/dotTile.png");
}
否则,如果读取的字符是” S"或” s",则将其设置为超级点并加载超级点图像.我们也增加(Else if the read char was ‘S’ or ’s', then we set it as a super dot and load the super dot image. We also increment the) totalScore
100.(by 100.)
else if (readChar == 'S' || readChar == 's') // this is.
{
totalScore += 100;
tiles[i, j].setSuperDot(true);
tiles[i, j].setImage("img/superDotTile.png");
}
在这里,如果读取的字符是’d’或’s',则表示其为鬼块.否则,不是.(Here, if the read char was either ’d' or ’s', it mean itss a ghost tile. Else, it’s not.)
if (readChar == 'd' || readChar == 's')
tiles[i, j].setGhostTile(true);
else
tiles[i, j].setGhostTile(false);
在这里,我们检查(在x轴上)i是否大于0且小于27,因为0的左侧没有图块,而27的右侧没有右图块.此外,这还有一个小缺陷逻辑,因为我们不会将相邻的图块设置为迷宫边缘的图块.实际上,这并不重要,因为它们都是墙,而Actor永远不会在那里存在. y轴的逻辑相同.(Here, we check (on x-axis) if i is greater than 0 and less than 27, because there are no tiles to the left of 0 and no right tiles to the right of 27. Also, there is a small flaw in this logic as we will not set the adjacent tiles to those in the edges of our maze. It actually won’t matter since they all are walls and Actors will never exist there. The same logic goes for the y-axis.)
//--------------------------------------------------------------------
//Setting its adjacent tiles------------------------------------------
if (i > 0 && i < 27) //@0 and @27 are null becuase there's nothing to the left or right there
{
tiles[i, j].setTileToLeft(tiles[i - 1, j]);
tiles[i, j].setTileToRight(tiles[i + 1, j]);
}
if (j > 0 && j < 30) //@0 and @30 are null becuase there's nothing above or below there
{
tiles[i, j].setTileAbove(tiles[i, j - 1]);
tiles[i, j].setTileBelow(tiles[i, j + 1]);
}
}
}
}
一个获取器,将通过tiles数组中的数字获取一个tile(A getter that will get a tile by its number in the tiles array)
public Tile getTile(int i, int j)
{
return tiles[i, j];
}
一个将通过其X和Y位置获得图块的吸气剂.我们只用20除以Voila,就可以得到它在数组中的位置.(A getter that will get a tile by its X and Y position. We just divide by 20 and Voila, we get its position in the array.)
public Tile findTile(int i, int j)
{
return tiles[i / 20, j / 20];
}
public Tile[,] getTiles() //just a getter for all tiles.
{
return this.tiles;
}
}
主菜单(MainMenu)
这是一个扩展的类(This is a class that extends)形成(Form).这将是启动应用程序时用户看到的第一件事.下图显示了它的外观:(. It will be the first thing the user sees when the application is launched. The following image shows how it looks like:)
现在让我们回到代码:(Now let’s get back to the code:)
public partial class MainMenu : Form
{
四个变量:(Four variables:) isHost
判断该游戏是否是主机.(to tell if this game is a host or not.) remoteIP
保存其他播放器的IP地址,在其中具有连接的线程以及一个(to save the IP address of the other player, a thread to have the connection in it, and a) UdpClient
用于发送/接收请求.构造函数只是普通的表单构造函数.(for sending/receiving requests. The constructor is just a normal form constructor.)
private bool isHost;
private string remoteIP;
private Thread connectionThread;
private UdpClient client;
public MainMenu()
{
InitializeComponent();
}
这是客户端单击"主机"按钮时将调用的方法.首先,我们禁用所有控件,向用户显示我们正在等待客户端的文本,然后运行在另一个线程上等待客户端的方法.(This is the method that will be called when a client clicks on the Host button. First of all, we disable all the controls, show the user a text that we’re waiting for a client, and then we run a method that waits for a client on another thread.)
private void hostGameButton_Click(object sender, EventArgs e)
{
hostGameButton.Enabled = false;
joinGameButton.Enabled = false;
hostIPTextBox.Enabled = false;
statusLabel.Text = "Waiting for a player to join...";
try
{
connectionThread = new Thread(waitForClientResponse);
connectionThread.Start();
}
catch (Exception exception)
{
hostGameButton.Enabled = true;
joinGameButton.Enabled = true;
hostIPTextBox.Enabled = true;
statusLabel.Text = "Error awaiting a player to join! Please try again.";
}
}
否则,如果客户端单击加入按钮,则首先我们还将禁用所有控件,但随后我们将使用他输入的IP地址,然后在不同的线程上运行具有其名称的sendConnectionRequest方法.(Otherwise, if the client click on the Join button, first we also disable all the controls, but then we take the IP Address that he entered, and then run the sendConnectionRequest method, which does what it’s named, on a different thread.)
private void joinGameButton_Click(object sender, EventArgs e)
{
hostGameButton.Enabled = false;
joinGameButton.Enabled = false;
hostIPTextBox.Enabled = false;
statusLabel.Text = "Joining game...";
try
{
connectionThread = new Thread(sendConnectionRequest);
connectionThread.Start();
}
catch (Exception exception)
{
hostGameButton.Enabled = true;
joinGameButton.Enabled = true;
hostIPTextBox.Enabled = true;
statusLabel.Text = "Error sending join request! Please try again.";
}
}
此方法的第三行将被阻塞.等待客户发送特定的字符串``JOIN_REQUEST'',然后我们将知道他要加入.因此,我们将向他发送" JOIN_REQUEST_ACEEPTED"消息,并在本地打开游戏窗口.注意我们设置(The third line in this method will block. Waiting for a client to send a specific string “JOIN_REQUEST” which then we will know that he wants to join. Therefore, we will send him a “JOIN_REQUEST_ACEEPTED” message and open the game window locally. Notice that we set) isHost
真实.(to true.)
private void waitForClientResponse()
{
client = new UdpClient(new IPEndPoint(IPAddress.Any, 12345));
IPEndPoint remoteEndPoint = new IPEndPoint(IPAddress.Any, 12345);
byte[] data = client.Receive(ref remoteEndPoint);
remoteIP = remoteEndPoint.Address.ToString();
if (ASCIIEncoding.ASCII.GetString(data).Equals("JOIN_REQUEST"))
{
client.Send(ASCIIEncoding.ASCII.GetBytes("JOIN_REQUEST_ACCEPTED"), 21, remoteEndPoint);
isHost = true;
statusLabel.Invoke((MethodInvoker)delegate() { statusLabel.Text = "A Player has joined the game!"; });
openGameWindow();
}
client.Close();
}
此方法将通过用户在用户界面中提供的IP地址直接向主机发送消息" JOIN_REQUEST".接下来,我们将等待客户端发送回" JOIN_REQUEST_ACCEPTED".如果正确完成,我们将设置(This method will directly send a message “JOIN_REQUEST” to the host at the IP Address supplied by user in the user interface. Next, we will wait for the client to send back “JOIN_REQUEST_ACCEPTED”. If done correctly, we will set) isHost
如果为假,则打开游戏窗口.(to false, the open the game window.)
private void sendConnectionRequest()
{
client = new UdpClient(hostIPTextBox.Text, 12345);
client.Send(ASCIIEncoding.ASCII.GetBytes("JOIN_REQUEST"), 12);
IPEndPoint remoteEndPoint = new IPEndPoint(IPAddress.Parse(hostIPTextBox.Text), 12345);
byte[] data = client.Receive(ref remoteEndPoint);
remoteIP = remoteEndPoint.Address.ToString();
if (ASCIIEncoding.ASCII.GetString(data).Equals("JOIN_REQUEST_ACCEPTED"))
{
isHost = false;
statusLabel.Invoke((MethodInvoker)delegate() { statusLabel.Text = "You have joined the game!"; });
openGameWindow();
}
client.Close();
}
打开一个游戏窗口,创建一个线程,该线程创建一个GameWindow,启动它,然后隐藏此窗体.(Opening a game window, creates a a thread that creates a GameWindow, starts it and then hides this form.)
private void openGameWindow()
{
Thread t = new Thread(ThreadProc);
t.Start();
}
private void ThreadProc()
{
this.Invoke((MethodInvoker)delegate() { this.Hide(); });
Application.Run(new GameWindow(isHost, remoteIP, this));
}
关闭表单后,我们确保连接也已关闭,然后关闭此表单.(When the form is closed, we make sure the connection is also closed and then close this form.)
private void MainMenu_FormClosing(object sender, FormClosingEventArgs e)
{
if (connectionThread != null)
{
if (client != null)
{
client.Close();
}
connectionThread.Abort();
}
}
}
游戏窗口(GameWindow)
这是一个扩展的类(This is a class that extends)形成(Form).这是将吸引游戏的类.(. This is the class that the game will be drawn into.)
public partial class GameWindow : Form
{
一个迷宫,两个吃豆子玩家(主机为黄色,客户端为绿色),以及四个幽灵.的(A Maze, Two pacman players (Yellow for the host and Green for the client), and four ghosts. The) gameLogic
持有控制游戏的逻辑对象.(holds the Logic object that controls the game.) yellowScore
和(and) greenScores
不言自明.我们这里有一个参考(are self explanatory. We have here a reference to a) MainMenu
对象只是为了在游戏结束后取消隐藏它.(object just for un-hiding it after the game finishes.)
public Maze maze;
public Pacman yellowPacman;
public Pacman greenPacman;
public Ghost ghost0;
public Ghost ghost1;
public Ghost ghost2;
public Ghost ghost3;
private Logic gameLogic;
private int yellowScore = 0;
private int greenScore = 0;
private MainMenu mainMenu;
构建游戏后,我们将(After constructing the game, we will) initializeGame
稍后将对其进行解释,然后根据天气情况创建该逻辑对象(which will be explained in a moment, then create the Logic object depending on weather this game) isAHostedGame
或不.之后,我们将与我们的游戏订阅此游戏窗口到Logic类的事件(稍后解释).(or not. After that, we will subscribe this game window to the event in Logic class (Explained in a moment) with our) refreshUI
方法.取消隐藏方法只会取消隐藏主菜单.(method. The unhide method will only unhide the main menu.)
public GameWindow(bool isAHostedGame, string remoteIP, MainMenu mainMenu)
{
this.mainMenu = mainMenu;
InitializeComponent();
initializeGame();
if (isAHostedGame)
this.gameLogic = new HostLogic(remoteIP, this);
else
this.gameLogic = new ClientLogic(remoteIP, this);
gameLogic.gameStateChange += refreshUI;
gameLogic.startGame();
}
public void unhideMainMenu()
{
mainMenu.Invoke((MethodInvoker)delegate() { this.mainMenu.Show(); });
}
为了清楚起见,此方法与构造函数分开进行.使用上面各节中提到的构造函数,将所有Actor在迷宫中的固定位置创建.(This method was made separate from the constructor for clarity. All of the Actors are created in a fixed position in the maze, using their constructors that were mentioned in their sections up above.)
private void initializeGame()
{
maze = new Maze();
yellowPacman = new Pacman(maze.getTile(12, 17), "img/pacman_yellow_1.png", 3, true);
greenPacman = new Pacman(maze.getTile(15, 17), "img/pacman_green_1.png", 1, false);
ghost0 = new Ghost(maze.getTile(1, 1),
"img/ghost_yellow_up.png",
"img/ghost_yellow_right.png",
"img/ghost_yellow_down.png",
"img/ghost_yellow_left.png", 1, greenPacman);
ghost1 = new Ghost(maze.getTile(1, 29),
"img/ghost_yellow_up.png",
"img/ghost_yellow_right.png",
"img/ghost_yellow_down.png",
"img/ghost_yellow_left.png", 0, greenPacman);
ghost2 = new Ghost(maze.getTile(26, 1),
"img/ghost_green_up.png",
"img/ghost_green_right.png",
"img/ghost_green_down.png",
"img/ghost_green_left.png", 2, yellowPacman);
ghost3 = new Ghost(maze.getTile(26, 29),
"img/ghost_green_up.png",
"img/ghost_green_right.png",
"img/ghost_green_down.png",
"img/ghost_green_left.png", 3, yellowPacman);
}
以下方法只会使表格无效.这将触发绘制事件,从而重新绘制所有内容.(The following method will only invalidate the form. Which will trigger the paint event, thus repainting everything.)
private void refreshUI(object sender, EventArgs e)
{
this.Invalidate();
}
每当(This method will be run whenever the) refreshUI
叫做.(is called.)
private void GameWindow_Paint(object sender, PaintEventArgs e)
{
try
{
首先,我们从paint事件args获取图形对象.(First of all, we get the graphics object from the paint event args.)
Graphics g = e.Graphics;
请注意,我们正在克隆拥有的每个图像对象,因此我们永远不会有任何UI-Cross-Thread异常.现在,对于迷宫中的每个瓷砖,我们将在其位置绘制其图像.然后,我们将在其x轴上的位置-4和y轴上的位置-4上绘制所有actor,因为它们的图像为28x28像素,并且背景的壁距其边缘4像素.(Notice that we are cloning every image object we have so that we never have any UI-Cross-Thread exceptions. Now, for each tile in the maze, we will draw its image at its position. Then we will draw all the actors in their position -4 on x-axis and -4 on the y-axis because their images are 28x28 pixels and our background has the walls 4 pixels away from its edges.)
foreach (Tile t in maze.getTiles())
{
if (t.hasDot())
{
if (t.isSuperDot())
g.DrawImage((Image)t.getImage().Clone(), new Point(t.getX(), t.getY()));
else
g.DrawImage((Image)t.getImage().Clone(), new Point(t.getX(), t.getY()));
}
}
g.DrawImage((Image)ghost0.getImage().Clone(), new Point(ghost0.getX() - 4, ghost0.getY() - 4));
g.DrawImage((Image)ghost1.getImage().Clone(), new Point(ghost1.getX() - 4, ghost1.getY() - 4));
g.DrawImage((Image)ghost2.getImage().Clone(), new Point(ghost2.getX() - 4, ghost2.getY() - 4));
g.DrawImage((Image)ghost3.getImage().Clone(), new Point(ghost3.getX() - 4, ghost3.getY() - 4));
g.DrawImage((Image)yellowPacman.getImage().Clone(), new Point(yellowPacman.getX() - 4, yellowPacman.getY() - 4));
g.DrawImage((Image)greenPacman.getImage().Clone(), new Point(greenPacman.getX() - 4, greenPacman.getY() - 4));
yellowScoreLabel.Text = "" + gameLogic.getYellowScore();
greenScoreLabel.Text = "" + gameLogic.getGreenScore();
}
catch (Exception excp)
{
}
}
对于此方法,它是每当玩家按下键盘箭头时的侦听器.我们只是设定方向(For this method, it’s a listener for whenever a player presses the keyboard arrows. We just set the direction of the) gameLogic
如前所述,为0,1,2或3.(to 0,1,2 or 3 as discussed before.)
private void GameWindow_KeyDown(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Up) { this.gameLogic.setDirection(0); }
else if (e.KeyCode == Keys.Right) { this.gameLogic.setDirection(1); }
else if (e.KeyCode == Keys.Down) { this.gameLogic.setDirection(2); }
else if (e.KeyCode == Keys.Left) { this.gameLogic.setDirection(3); }
}
}
逻辑(Logic)
这是一个抽象(实际上并没有使用过),它将包含游戏的某种逻辑.(This is an abstract (Just not used really) that will hold some type of logic for the game.)
public class Logic
{
首先,我们有一个创建事件的委托.此事件将通知其订户游戏状态已更改.(First of all, we have a delegate that we create an event from. This event will notify its subscribers that the game state have been change.)
public delegate void ChangedEventHandler(object sender, EventArgs e);
public event ChangedEventHandler gameStateChange;
protected void OnGameStateChanged(EventArgs e)
{
if (gameStateChange != null)
{
gameStateChange(this, e);
}
}
黄色吃豆人和绿色吃豆人的分数(The Scores for both Yellow Pacman and Green Pacman)
protected int yellowScore = 0;
public int getYellowScore() { return this.yellowScore; }
protected int greenScore = 0;
public int getGreenScore() { return this.greenScore; }
另一个播放器的远程IP地址(The remote IP Address of the other player)
private string remoteIP;
public string getRemoteIP() { return this.remoteIP; }
public void setRemoteIP(string newRemoteIP) { this.remoteIP = newRemoteIP; }
用户输入游戏的方向(The direction that the user inputs into the game)
protected byte direction;
public void setDirection(byte newDirection) { this.direction = newDirection; }
与逻辑相关联的GameWindow实例(The GameWindow instance that the logic is tied with)
protected GameWindow gameWindow;
只是一个构造函数(Just a constructor)
public Logic(string newRemoteIP, GameWindow gameWindow)
{
this.setRemoteIP(newRemoteIP);
this.gameWindow = gameWindow;
}
最后,两个虚拟方法将在Logic的子级中被覆盖(Finally, two virtual methods that will be overridden in children of Logic)
public virtual void recieveDirection(object sender, EventArgs e) { }
public virtual void startGame() { }
}
主机逻辑(HostLogic)
此类表示在主机端运行的逻辑.它延伸(This class represents the logic that runs at the Host side. It extends)逻辑(Logic).(.)
public class HostLogic : Logic
{
该线程将运行逻辑以及与客户端的连接.(This thread will run the logic along with connection to the client.)
private Thread connectionThread;
可以调用此类的方法startGame来通知它应该启动游戏逻辑(The method startGame can be called to this class notifying it that it should start the game logic)
public override void startGame()
{
base.startGame();
connectionThread = new Thread(tick);
connectionThread.Start();
}
这是所有魔术发生的地方.(This is where all the magic happens.) tick()
以一个开始(starts with a) Sleep(2000)
(两秒钟)以确保客户端也已设置好自己的位置.字节((Two Seconds) to make sure that the client has also set its self up. The byte)** directionRecieved
**将保持客户的方向,并将用于更改Green Pacman的方向.(*will hold the direction that the client and will be used to change Green Pacman’s direction.*)
public void tick()
{
byte directionRecieved = 0;
Thread.Sleep(2000);
这种while循环将一直持续下去,直到它从内部出现为止.它将短暂睡眠280毫秒.(This while-loop will keep going forever until it has been from within. It will sleep for a short while of 280 milli second.)
while (true)
{
Thread.Sleep(280); //This is the "Tick"
在这里,我们只是将Yellow Pacman的方向(因为这是宿主)设置为变量中的任意值(Here, we just set the direction of Yellow Pacman (since this is the host) to whatever we have in our variable) direction
.它不断被修改(. It keeps getting modified from the) gameWindow
前面讨论过的对象.(object discussed previously.)
//modify yellowPacman direction before doing any logic
this.gameWindow.yellowPacman.setDirection(this.direction);
this.gameWindow.greenPacman.setDirection(qwe);
在这里,如果吃豆子站在一块有圆点的瓷砖上,就会被吃掉.(Here, if pacman is standing on a tile that has a dot, it get’s eaten.)
//Do some logic.
//if pacman's tile has a dot, eat it
if (this.gameWindow.yellowPacman.getCurrentTile().hasDot())
{
//eat it
this.gameWindow.yellowPacman.getCurrentTile().setDot(false);
this.yellowScore += 10;
现在,如果它是一个超级点,我们可以做一些不同的事情.我们将所有鬼魂的目标设置为绿色吃豆人,并设置它们的图像.自从上次增加10分以来,我们为得分增加了90分.(Now if it’s a super dot, we do something different. We set the target of all the ghosts to the green pacman and set their images too. We add 90 more points to the score since previous we added 10.)
//if it's a super dot, then all ghosts should change their target
if (this.gameWindow.yellowPacman.getCurrentTile().isSuperDot())
{
this.yellowScore += 90;
this.gameWindow.ghost0.setTarget(this.gameWindow.greenPacman);
this.gameWindow.ghost1.setTarget(this.gameWindow.greenPacman);
this.gameWindow.ghost2.setTarget(this.gameWindow.greenPacman);
this.gameWindow.ghost3.setTarget(this.gameWindow.greenPacman);
string i0 = "img/ghost_yellow_up.png";
string i1 = "img/ghost_yellow_right.png";
string i2 = "img/ghost_yellow_down.png";
string i3 = "img/ghost_yellow_left.png";
this.gameWindow.ghost0.setImages(i0, i1, i2, i3);
this.gameWindow.ghost1.setImages(i0, i1, i2, i3);
this.gameWindow.ghost2.setImages(i0, i1, i2, i3);
this.gameWindow.ghost3.setImages(i0, i1, i2, i3);
}
}
与现在讨论的逻辑相同,现在适用于绿色的吃豆人.(Same logic as discussed before, for the green pacman now.)
//if pacman's tile has a dot, remove it
if (this.gameWindow.greenPacman.getCurrentTile().hasDot())
{
//eat it
this.gameWindow.greenPacman.getCurrentTile().setDot(false);
this.greenScore += 10;
//if it's a super dot, then all ghosts should change their target
if (this.gameWindow.greenPacman.getCurrentTile().isSuperDot())
{
this.greenScore += 90;
this.gameWindow.ghost0.setTarget(this.gameWindow.yellowPacman);
this.gameWindow.ghost1.setTarget(this.gameWindow.yellowPacman);
this.gameWindow.ghost2.setTarget(this.gameWindow.yellowPacman);
this.gameWindow.ghost3.setTarget(this.gameWindow.yellowPacman);
string i0 = "img/ghost_green_up.png";
string i1 = "img/ghost_green_right.png";
string i2 = "img/ghost_green_down.png";
string i3 = "img/ghost_green_left.png";
this.gameWindow.ghost0.setImages(i0, i1, i2, i3);
this.gameWindow.ghost1.setImages(i0, i1, i2, i3);
this.gameWindow.ghost2.setImages(i0, i1, i2, i3);
this.gameWindow.ghost3.setImages(i0, i1, i2, i3);
}
}
在这里,我们告诉所有的幽灵设定他们的方向以跟随他们的目标.(Here, we tell all the ghosts to set their directions to follow their targets.)
this.gameWindow.ghost0.seekTarget();
this.gameWindow.ghost1.seekTarget();
this.gameWindow.ghost2.seekTarget();
this.gameWindow.ghost3.seekTarget();
我们移动所有演员:(We move all the actors:)
this.gameWindow.yellowPacman.move();
this.gameWindow.greenPacman.move();
this.gameWindow.ghost0.move();
this.gameWindow.ghost1.move();
this.gameWindow.ghost2.move();
this.gameWindow.ghost3.move();
现在,在介绍一些棘手的部分之前,我想向您解释什么数据将发送到客户端.请参阅下表:(Now before into getting to a bit of a tricky part, I’d like to explain to you what data will be sent to the client. Refer to the following table:)
注意,每个索引代表一个字节.因此,以下内容应易于理解:(Notice that each index represents a byte. Thus, the following should be easy to figure out:)
byte[] data = new byte[40];
byte[] temp = BitConverter.GetBytes(this.gameWindow.yellowPacman.getX());
data[0] = temp[0];
data[1] = temp[1];
temp = BitConverter.GetBytes(this.gameWindow.yellowPacman.getY());
data[2] = temp[0];
data[3] = temp[1];
data[4] = (byte)this.gameWindow.yellowPacman.getDirection();
temp = BitConverter.GetBytes(this.gameWindow.greenPacman.getX());
data[5] = temp[0];
data[6] = temp[1];
temp = BitConverter.GetBytes(this.gameWindow.greenPacman.getY());
data[7] = temp[0];
data[8] = temp[1];
data[9] = (byte)this.gameWindow.greenPacman.getDirection();
temp = BitConverter.GetBytes(this.gameWindow.ghost0.getX());
data[10] = temp[0];
data[11] = temp[1];
temp = BitConverter.GetBytes(this.gameWindow.ghost0.getY());
data[12] = temp[0];
data[13] = temp[1];
data[14] = (byte)this.gameWindow.ghost0.getDirection();
temp = BitConverter.GetBytes(this.gameWindow.ghost1.getX());
data[15] = temp[0];
data[16] = temp[1];
temp = BitConverter.GetBytes(this.gameWindow.ghost1.getY());
data[17] = temp[0];
data[18] = temp[1];
data[19] = (byte)this.gameWindow.ghost1.getDirection();
temp = BitConverter.GetBytes(this.gameWindow.ghost2.getX());
data[20] = temp[0];
data[21] = temp[1];
temp = BitConverter.GetBytes(this.gameWindow.ghost2.getY());
data[22] = temp[0];
data[23] = temp[1];
data[24] = (byte)this.gameWindow.ghost2.getDirection();
temp = BitConverter.GetBytes(this.gameWindow.ghost3.getX());
data[25] = temp[0];
data[26] = temp[1];
temp = BitConverter.GetBytes(this.gameWindow.ghost3.getY());
data[27] = temp[0];
data[28] = temp[1];
data[29] = (byte)this.gameWindow.ghost3.getDirection();
temp = BitConverter.GetBytes(yellowScore);
data[30] = temp[0];
data[31] = temp[1];
data[32] = temp[2];
data[33] = temp[3];
temp = BitConverter.GetBytes(greenScore);
data[34] = temp[0];
data[35] = temp[1];
data[36] = temp[2];
data[37] = temp[3];
data[38] = 0; // 1 if game has fin
data[39] = 0; // 0=yellow, 1=green, 2=draw :(
if (this.gameWindow.ghost0.targetDies())
{
data[38] = 1;
if (this.gameWindow.ghost0.getTarget().isYellow())
{
data[39] = 1;
}
}
else if (this.gameWindow.ghost1.targetDies())
{
data[38] = 1;
if (this.gameWindow.ghost0.getTarget().isYellow())
{
data[39] = 1;
}
}
else if (this.gameWindow.ghost2.targetDies())
{
data[38] = 1;
if (this.gameWindow.ghost0.getTarget().isYellow())
{
data[39] = 1;
}
}
else if (this.gameWindow.ghost3.targetDies())
{
data[38] = 1;
if (this.gameWindow.ghost0.getTarget().isYellow())
{
data[39] = 1;
}
}
else if (this.yellowScore + this.greenScore == Maze.totalScore)
{
data[38] = 1;
if (this.yellowScore < this.greenScore) data[39] = 1;
else if (this.yellowScore == this.greenScore) data[39] = 2;
}
//send data to client
if (data[38] == 1)
{
string textToShow = "";
if (data[39] == 0)
{
textToShow = "Yellow Has won the game! You have Won!";
}
else if(data[39] == 1)
{
textToShow = "Green Has won the game! You have lost, Yellow :-(";
}
else
{
textToShow = "OMG It's a draw!";
}
Thread t = new Thread(new ParameterizedThreadStart(exit)); //check last method
t.Start(textToShow);
this.gameWindow.Invoke((MethodInvoker)delegate() { this.gameWindow.unhideMainMenu(); this.gameWindow.Close(); });
}
这是发送的地方.请注意,当客户端没有在UdpClient时间内发送回任何东西时,我们将其循环放置.但是,在这里的代码中,出于调试目的,我删除了该行.您可以轻松添加它.发送后,我们等待客户发送绿色吃豆人的指示.(This is where the sending takes place. Notice that we’re putting it in a loop while the client hasn’t sent anything back within the time out of the UdpClient. In the code here, however, I’ve removed that line for debugging purposes. You can easily add it. After sending, we wait for the client to send green pacman’s direction.)
bool recieved = false;
while (!recieved)
{
try
{
UdpClient UDPclient = new UdpClient(getRemoteIP(), 12345);
IPEndPoint dummyPoint = new IPEndPoint(IPAddress.Any, 0);
UDPclient.Send(data, data.Length);
UDPclient.Close();
//Recieve state from client
IPEndPoint localEP = new IPEndPoint(IPAddress.Any, 12345);
UDPclient = new UdpClient(localEP);
IPEndPoint remoteEP = new IPEndPoint(IPAddress.Any, 0);
directionRecieved = UDPclient.Receive(ref remoteEP)[0];
UDPclient.Close();
recieved = true;
}
catch (Exception e)
{
recieved = false;
}
}
if (data[38] == 1) break; //if the game is finished.
this.OnGameStateChanged(EventArgs.Empty);
}
}
public void exit(object o)
{
MessageBox.Show((string)o);
}
}
客户端逻辑(ClientLogic)
这是一个扩展的类(This is a class that extends)逻辑(Logic).它代表在客户端运行的逻辑.该课程与之前的课程相似.但是唯一的区别是它只发送Green pacman方向,并接收其他所有内容.因此,我将跳过重要部分.(. It represents the logic running at the client. This class is similar to the one before. But the only difference is that it only sends Green pacman direction, and receives everything else. So, I will skip till the important parts.)
public class ClientLogic : Logic
{
private Thread connectionThread;
public ClientLogic(string newRemoteIP,
GameWindow gameWindow) : base(newRemoteIP, gameWindow)
{
}
public override void startGame()
{
base.startGame();
connectionThread = new Thread(tick);
connectionThread.Start();
}
private void tick()
{
while (true)
{
//DONE: TEST I
UdpClient UDPclient = new UdpClient(new IPEndPoint(IPAddress.Any, 12345));
IPEndPoint remoteEndPoint = new IPEndPoint(IPAddress.Any, 0);
byte[] data = UDPclient.Receive(ref remoteEndPoint); //this will block
UDPclient.Close();
string remoteIPint = remoteEndPoint.Address.ToString();
基本上,就像我们在(Basically, exactly as we have sent the information in the) HostLogic
,我们只是将信息放回去.请注意,有and if语句,检查data [38] ==0(游戏中是否有(, we just put the info back in. Notice that there’s and if statement, checking if data[38] == 0 (game has)不(not)完)(finished))
不幸的是,这是我非常困惑的地方(* Ahem *(*Unfortunately, this is where I got extremely confused (*Ahem**)懒(*lazy*)),只需要再次重做点吃逻辑即可.我们冷已经发送了磁贴的状态,但是随后尺寸会很大,并且UDP将无法发送它.(*), and just had to redo the dot eating logic again. We cold have sent the tiles' states along, but then the size would be large and the UDP won’t be able to send it.*)
//dycrypting the protocol!
if (data[38] == 0)
{
this.gameWindow.yellowPacman.setX(BitConverter.ToInt16(data, 0));
this.gameWindow.yellowPacman.setY(BitConverter.ToInt16(data, 2));
this.gameWindow.yellowPacman.setDirection(data[4]);
Tile yellowTile = this.gameWindow.maze.findTile(BitConverter.ToInt16(data, 0), BitConverter.ToInt16(data, 2));
if (yellowTile.hasDot())
{
//eat it
yellowTile.setDot(false);
//if it's a super dot, then all ghosts should change their target
if (yellowTile.isSuperDot())
{
string i0 = "img/ghost_yellow_up.png";
string i1 = "img/ghost_yellow_right.png";
string i2 = "img/ghost_yellow_down.png";
string i3 = "img/ghost_yellow_left.png";
this.gameWindow.ghost0.setImages(i0, i1, i2, i3);
this.gameWindow.ghost1.setImages(i0, i1, i2, i3);
this.gameWindow.ghost2.setImages(i0, i1, i2, i3);
this.gameWindow.ghost3.setImages(i0, i1, i2, i3);
}
}
this.gameWindow.greenPacman.setX(BitConverter.ToInt16(data, 5));
this.gameWindow.greenPacman.setY(BitConverter.ToInt16(data, 7));
this.gameWindow.greenPacman.setDirection(data[9]);
Tile greenTile = this.gameWindow.maze.findTile(
BitConverter.ToInt16(data, 5), BitConverter.ToInt16(data, 7));
if (greenTile.hasDot())
{
//eat it
greenTile.setDot(false);
//if it's a super dot, then all ghosts should change their target
if (greenTile.isSuperDot())
{
string i0 = "img/ghost_green_up.png";
string i1 = "img/ghost_green_right.png";
string i2 = "img/ghost_green_down.png";
string i3 = "img/ghost_green_left.png";
this.gameWindow.ghost0.setImages(i0, i1, i2, i3);
this.gameWindow.ghost1.setImages(i0, i1, i2, i3);
this.gameWindow.ghost2.setImages(i0, i1, i2, i3);
this.gameWindow.ghost3.setImages(i0, i1, i2, i3);
}
}
this.gameWindow.ghost0.setX(BitConverter.ToInt16(data, 10));
this.gameWindow.ghost0.setY(BitConverter.ToInt16(data, 12));
this.gameWindow.ghost0.setDirection(data[14]);
this.gameWindow.ghost1.setX(BitConverter.ToInt16(data, 15));
this.gameWindow.ghost1.setY(BitConverter.ToInt16(data, 17));
this.gameWindow.ghost1.setDirection(data[19]);
this.gameWindow.ghost2.setX(BitConverter.ToInt16(data, 20));
this.gameWindow.ghost2.setY(BitConverter.ToInt16(data, 22));
this.gameWindow.ghost2.setDirection(data[24]);
this.gameWindow.ghost3.setX(BitConverter.ToInt16(data, 25));
this.gameWindow.ghost3.setY(BitConverter.ToInt16(data, 27));
this.gameWindow.ghost3.setDirection(data[29]);
this.yellowScore = BitConverter.ToInt32(data, 30);
this.greenScore = BitConverter.ToInt32(data, 34);
这是我们将绿色吃豆人位置发回主机的部分.(This is the part where we send the green pacman position back to the host.)
UDPclient = new UdpClient(remoteIPint, 12345);
IPEndPoint dummyPoint = new IPEndPoint(IPAddress.Any, 0);
UDPclient.Send(new byte[] { this.direction }, 1);
UDPclient.Close();
this.OnGameStateChanged(EventArgs.Empty);
}
否则,如果游戏(Else, if the game)有(had)完成:(finished:)
else
{
string textToShow = "";
if (data[39] == 0)
{
textToShow = "Yellow Has won the game! You have lost, Green :-(!";
}
else if (data[39] == 1)
{
textToShow = "Green Has won the game! You have Won!";
}
else
{
textToShow = "OMG It's a draw!";
}
MessageBox.Show(textToShow);
this.gameWindow.Invoke((MethodInvoker)delegate() {
this.gameWindow.unhideMainMenu(); this.gameWindow.Close(); });
break;
}
}
}
}
关闭事情(Closing things up)
同样,我想提一下这个游戏可能没有以最佳方式实现.但是,我真的希望您喜欢它.(Again, I’d like to mention that this game might not have been implemented in the best way. However, I really hope you enjoy it.) 请发表评论,让我知道您的感受.如果您要批评某件事,请记住我确实知道它可以以不同的方式实现.不过,把它放在那里.谁知道有一天我可能会充分发展(Please, do comment and let me know what you feel. If you were to criticize something, remember that I do know that It could have been implemented differently. Nevertheless, put it there. Who knows one day I may develop it fully) " src =" http://www.codeproject.com/script/Forums/Images/smiley_smile.gif"/>(" src=“http://www.codeproject.com/script/Forums/Images/smiley_smile.gif" />) 最好的祝福,(Best Regards,) 马蒙`阿尔加斯兰(Mamoun Alghaslan)
许可
本文以及所有相关的源代码和文件均已获得The Code Project Open License (CPOL)的许可。
C# Windows 新闻 翻译