疯狂java实例-第15章仿QQ游戏大厅
第15章 仿QQ游戏大厅
第15章 仿QQ游戏大厅
15.1 游戏大厅简介
我们曾经见过许多的游戏大厅,笔者从最开始接触的联众游戏大厅到现在十分多人玩的QQ游戏大厅,这些游戏大厅为我们提供了游戏的平台,可以让我们在网络上进行各种游戏对战。这些游戏大厅提供了各式各样的游戏,例如斗地主、泡泡龙、俄罗斯方块等,我们只要下载一些游戏大厅的客户端,就可以进行网络的游戏对战,并从游戏中得到积分。在玩这些游戏大厅的时候,我们不妨可以考虑一下,使用我们所学的Java知识去实现这些游戏大厅,本章主要介绍如何使用Java去开发一款属于自己的游戏大厅,让大家在开发的过程中,了解这些游戏大厅的实现原理。
15.2 编写游戏大厅框架
在开发游戏大厅前,我们需要先了解下游戏大厅的原理。例如,一个玩家登录进入游戏大厅,那么就需要将该用户进入的信息发送给其他已经进入大厅的玩家,如果玩家坐下到大厅的某个桌子的时
告诉其他玩家,并更新其他玩家的界面组件。在本章中我候,就需要将这些信息(玩家坐下的信息)
们使用Socket来进行服务器端与客户端之间的通信,Socket就是两台机器上的通信端点。
Socket中包含一个输出流对象,一台机器可以通过这个输出流,将信息发送到另外一端的机器中。例如,我们可以使用以下代码得到输出流,并将一些信息打印到输出流中:
PrintStream ps = new PrintStream(socket.getOutputStream());
ps.println("这是打印信息");
使用以上的方法,可以将一些信息从一台机器发送到另外一台机器中。我们在游戏大厅中的每一个操作,都可能会将自己所操作的信息发送到服务器,再由服务器转发给其他玩家,因此在玩家发送信息给服务器或者服务器再转发给其他玩家,都可以使用PrintStream的print方法来将信息打印到Socket的输出流中。既然每个操作都如此,我们可以编写一个游戏大厅的小框架,专门用于中转,框架中的服务器端与客户端的代码都不需要编写任何的业务逻辑,如果编写了新的游戏,可以将该游戏的包放到框架中运行,当然,这些游戏都必须遵守框架的
规则
编码规则下载淘宝规则下载天猫规则下载麻将竞赛规则pdf麻将竞赛规则pdf
。
15.2.1 确定传输格式
我们已经知道了服务器间通过PrintStream来向输出流打印字符串信息,那么我们可以先确定这些字符串的格式。本章中使用XML作为它们之间的转输格式,例如服务器向客户端输出信息的时候,可以将一段XML打印到输出流中,客户端得到这些XML字段串后,就将其转化为特定的对象,再根据这些对象进行相应的处理。
当客户端发送一些信息给服务器的时候,我们就把这些信息封装到一个Request对象中,该对象包括参数列
表
关于同志近三年现实表现材料材料类招标技术评分表图表与交易pdf视力表打印pdf用图表说话 pdf
、服务器处理类、客户端处理类等信息,当服务器端接收到该请求对象后,就将这些信息返回一个Response对象,将Request与Response对象当作参数传递给服务器处理类,当服务器处理类完成了服务器的操作后,就将这些信息通过一个Response对象返回给客户端。
代码清单:code\GameHall-Commons\src\org\crazyit\gamehall\commons\Request.java
public class Request {
//参数列表
?2? 第15章 仿QQ游戏大厅
private Map
parameters;
//服务器处理类
private String serverActionClass;
//客户端处理类
private String clientActionClass;
}
代码清单:code\GameHall-Commons\src\org\crazyit\gamehall\commons\Response.java
public class Response {
//服务器返回的各个数值
private Map datas;
//错误代码
private String errorCode;
//客户端处理类, 该值保存在Request的请求参数的Map中
private String actionClass;
}
当客户端发送一次请求的时候,就封装一个Request对象,告诉服务器由哪个服务器处理类处理这一次的请求。Request对象中的服务器处理类与客户端处理类由发送请求的客户来确定,因此客户端需要清楚的知道服务器处理类的具体类名。
这样就好比我们在开发web应用的时候,客户端发送一个请求的url,服务器就可以根据这个url来确定处理类。确定了请求的对象为Request,服务器响应的对象为Response后,客户端发送请求的字段串时(在将XML打印到Socket输出流),就可以将Request对象转化为一个XML字符串,服务器作为响应时,就可以将一个Response对象转化为一个XML字符串,服务器或者客户端得到该XML字符串后,就可以转化为Request或者Response对象。对象与XML之间的转换我们使用XStream来实现,XStream可以轻松的帮我们在这两者之间进行转换。
编写一个XStreamUtil的类,用于处理对象与XML之间的转换。
代码清单:code\GameHall-Commons\src\org\crazyit\gamehall\util\XStreamUtil.java
public class XStreamUtil {
private static XStream xstream = new XStream();
//将XML转换成对象
public static Object fromXML(String xml) {
return xstream.fromXML(xml);
}
//将对象转换成XML字段串
public static String toXML(Object obj) {
String xml = xstream.toXML(obj);
//去掉换行
String a = xml.replaceAll("\n", "");
String s = a.replaceAll("\r", "");
return s;
}
}
需要注意的是,将对象转换成XML的时候,需要将生成的XML字符串进行处理,去掉换行。 15.2.2 建立处理类接口
当服务器接收到客户端发送的一次请求后,就可以根据Request对象得到服务器处理类,我们先编写服务器处理类的接口。
新建ServerAction接口。
代码清单:code\GameHall-Commons\src\org\crazyit\gamehall\commons\ServerAction.java
第15章 仿QQ游戏大厅 ?3?
//服务器处理请求的接口
public interface ServerAction {
//Action的执行方法
void execute(Request request, Response response, Socket socket);
}
通过Request具体的某个服务器处理类后,就可以使用Java的反射来实现化该类,由于服务器处理类都必须实现ServerAction接口,因此实现化该类后,就可以直接调用execute方法来执行某些具体的行为。
同样的,新建一个ClientClass接口,表示客户端处理类。ClientAction。
代码清单:code\GameHall-Commons\src\org\crazyit\gamehall\commons\ClientAction.java
//客户端处理服务器响应的接口
public interface ClientAction {
//客户端处理类执行
void execute(Response response);
}
服务器端发送响应到客户端时,客户端就根据响应(Response)中的处理类来执行某些操作,例如更新界面组件等。Response对象中包含一个actionClass的处理类,该类就是客户端的处理类。得到这个处理类的字符串后,同样地,我们就可以根据这个类名来得到具体的某个ClientClass的实现类实例,再调用execute方法即可。
15.2.2 建立玩家类与游戏接口
我们为游戏大厅新建一个玩家类,用来代表一个玩家,该类中保存了玩家的大多数信息,包括名称、头像图片、该玩家的标识等。
代码清单:code\GameHall-Commons\src\org\crazyit\gamehall\commons\User.java
public class User {
//玩家的唯一标识
private String id;
//头像图片
private String headImage;
//玩家名称
private String name;
//0男, 1女
private int sex;
//玩家对应的Socket
private Socket socket;
}
User类中除了包含玩家的一些基本信息外,还需要保存一个Socket对象,在服务器中我们需要将这一系列的玩家对象都保存起来,当根据玩家的id得到某个玩家对象后,就可以直接得到该玩家对应的Socket,根据这个Socket就可以向客户端发送相关的信息(Response对象)。在实际的情况中,每个游戏都会有不同的玩家对象,因此当我们编写一个新的游戏并放到这个框架中的时候,可以根据游戏的实际情况来继承User类。
接下来新建一个游戏的接口,该接口只需要提供一个start的方法,表示游戏的开始动作,因此,每一个放在该框架中的游戏,都需要去实现这个游戏接口,并实现start方法。
代码清单:code\GameHall-Commons\src\org\crazyit\gamehall\commons\Game.java
public interface Game {
//开始游戏的方法
void start(User user);
?4? 第15章 仿QQ游戏大厅
}
到这里,我们的这个框架定义了两个规范,第一,客户端与服务器进行信息传输的时候,只能使用Request和Response对象所生成的XML字符串;第二,每个放在该框架中的游戏都必须提供一个实现Game接口的游戏实现类,为游戏提供一个入口。
15.2.3 编写框架服务器
服务器端编写较为简单,只需要创建一个ServerSocket对象,再使用该对象的accept方法来监听和接收连接到服务器的Socket,再以该Socket来启动一条线程来监听客户端所发送的请求。 代码清单:code\GameHall-Server\src\org\crazyit\gamehall\server\Server.java public class Server {
//服务器Socket对象
ServerSocket serverSocket;
public Server() {
//创建ServerSocket对象, 端口为12000
this.serverSocket = new ServerSocket(12000);
while(true) {
//监听Socket的连接
Socket s = this.serverSocket.accept();
//启动服务器线程
new ServerThread(s).start();
}
}
}
该类接收到Socket后,就启动服务器线程,用于处理服务器端Socket接收到的信息。那么服务器线程就可以一直监听建立这个Socket的客户端所发送的信息。
代码清单:code\GameHall-Server\src\org\crazyit\gamehall\server\ServerThread.java
private Socket socket;
public ServerThread(Socket socket) {
this.socket = socket;
}
ServerThread类继承Thread线程类,还需要重写Thread的run方法,在run方法中,我们需要将根据该Socket所得到的信息(客户端发送的信息)转化为具体的某个Request对象,服务器的线程得到Request对象后,就可以实例化Request中所包含的服务器处理类。
ServerThread中的run方法。
代码清单:code\GameHall-Server\src\org\crazyit\gamehall\server\ServerThread.java
this.br = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
while((this.line = br.readLine()) != null) {
//得到请求对象
Request request = getRequest(this.line);
//从request中得到客户端处理类, 并且构造Response对象
Response response = new Response(request.getClientActionClass());
//将请求的参数都设置到Response中
copyParameters(request, response);
//得到Server处理类
ServerAction action = getAction(request.getServerActionClass());
action.execute(request, response, this.socket);
}
以上代码中的getRequest方法,只要通过XStreamUtil中的toObject方法即可以将客户请求的字符
第15章 仿QQ游戏大厅 ?5? 串转换成一个Request对象,XStreamUtil已经在15.2.1中实现。当服务器线程得到Request对象后,就可以新建一个Response对象作为服务器的响应,并根据Request对象中的serverActionClass属性得到具体的某个服务器处理Action类,执行execute方法。需要特别注意的是,通过Request的serverActionClass属性得到某个实现类的时候,可以将得到的实现类实例放到一个Map中进行保存,那么当用户多次请求同一个Action的时候,就可以不再需要重复创建。
到这里,我们框架的服务器端已经全部实现了,在本章的代码中,对应的是GameHall-Server模块,按照以上的代码实现了该模块后,这个服务器模块就可以不再需要作改动,即使加入新的游戏,它也可以不用进行代码改变。GameHall模块只是服务器端负责处理转发的一个中间角色,它不负责处理任何的业务逻辑。由于在ServerThread中,我们需要使用反射来得到某个服务器处理类,因此,当加入一些新的游戏时,我们就需要将这些游戏的模块加入到环境变量中。一些公用的接口或者类,我们可以使用一个GameHall-Commons的模块来存放,例如可以将Request、Response、ServerClass、ClientClass、User和Game等接口与类放到GameHall-Commons模块中,客户端与服务器端的模块都依赖于GameHall-Commons模块。
15.2.4 编写框架客户端
由于我们这个游戏大厅的框架,并不需要编写任何的业务逻辑,因此客户端的实现与服务器端的实现基本类似。当用户编写了一个新的游戏时,就可以直接将游戏的包放置到客户端的目录中,用于给客户端加载相应的类。与服务器端的实现一样,都是负责一个中转的功能,当接收到服务器响应时(得到Response对象的XML),就直接根据Response对象中的actionClass来得到客户端处理类,同样再使用反射来得到某个客户端处理类的实例,再调用execute方法。这样做,就规定了发送请求时,需要声明客户端处理类,也就是设置Request对象中的clientActionClass,而在服务器处理时,就会将这个属性设置为Response的actionClass。每个客户端处理类都必须实现ClientAction接口。
编写客户端线程类ClientThread,该类的主体代码如下。
代码清单:code\GameHall-Client\src\org\crazyit\gamehall\client\ClientThread.java
InputStream is = this.user.getSocket().getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
while ((this.line = br.readLine()) != null) {
Response response = getResponse(this.line);
//得到客户端的处理类
ClientAction action = getClientAction(response.getActionClass());
//执行客户端处理类
action.execute(response);
}
根据服务器返回的响应字符串,将这些字符串转换成一个Response对象,再通过该对象得到具体某个ClientAction的实现类,再调用execute方法。与服务器端的实现一样,客户端也并不需要处理任何的业务,这些业务都交由客户端或者服务器端处理类进行。在本章中客户端模块为GameHall-Client。 15.2.5 建立登录界面
框架的客户端除了提供一个客户端线程类之外,还需要提供一个登录界面,让用户选择进入的游戏大厅(某个游戏的大厅)和输与相关的登录信息。由于登录界面并不与某个特定的游戏相关,只是用户输入信息的一个界面,因此可以将登录界面的相关类放到客户端的模块GameHall-Client中。登录界面如图15.1所示。
?6? 第15章 仿QQ游戏大厅
图15.1 登录界面
在本章中,登录界面是GameHall-Client的LoginFrame类。登录界面需要注意的是头像的下拉框实现,需要到特定的某个目录中读取所有的头像文件,该目录存在于GameHall-Client模块。在读取头像文件的时候,我们需要将读取到的头像文件封装成一个Map对象,该对象里面key为头像图片的相对路径,value就是该图片的ImageIcon对象。为了让下拉框中能显示图片,需要为下拉框JComboBox提供一个ListCellRenderer的实现类。
代码清单:code\GameHall-Client\src\org\crazyit\gamehall\client\HeadComboBoxRenderer.java
public Component getListCellRendererComponent(JList list, Object value,
int index, boolean isSelected, boolean cellHasFocus) {
String selectValue = (String)value;
//设置背景颜色
if (isSelected) {
setBackground(list.getSelectionBackground());
setForeground(list.getSelectionForeground());
} else {
setBackground(list.getBackground());
setForeground(list.getForeground());
}
//从头像的Map中得到当前选择的头像图片
Icon icon = this.heads.get(selectValue);
setIcon(icon);
if (icon != null) setFont(list.getFont());
return this;
}
注意以上代码中,需要得到下拉框所选中的值,这个值必须是Map中的某个key,这样的话,意味着需要将头像文件的相对路径作为值来创建一个JComboBox。以下方法创建一个JComboBox对象。
代码清单:code\GameHall-Client\src\org\crazyit\gamehall\client\LoginFrame.java
//创建头像选择下拉
private void buildHeadSelect() {
this.headSelect = new JComboBox(this.heads.keySet().toArray());
this.headSelect.setMaximumRowCount(5);
this.headSelect.setRenderer(new HeadComboBoxRenderer(this.heads));
第15章 仿QQ游戏大厅 ?7?
}
以上的黑体代码,使用头像Map的key来创建JComboBox的选择项。除了头像的选择外,还需要特别注意的是游戏的选择下拉。在本章中,我们已经确定了游戏大厅只是一个简单的框架,在登录界面需要到某个目录去读取一些相关的包,将这些包所代表的游戏显示到游戏下拉中。因此还需要编写读取包信息的代码。
以下代码读取jar包。
代码清单:code\GameHall-Client\src\org\crazyit\gamehall\client\LoginFrame.java
File folder = new File("game");
for (File file : folder.listFiles()) {
if (isJar(file.getName())) {
//得到jar文件
JarFile jar = new JarFile(file);
//得到元数据文件
Manifest mf = jar.getManifest();
//获得各个属性
Attributes gameClassAttrs = mf.getMainAttributes();
//查找Game-Class属性
String gameClass = gameClassAttrs.getValue("Game-Class");
if (gameClass != null) {
Game game = (Game)Class.forName(gameClass).newInstance();
this.gameSelect.addItem(game);
}
}
}
以上的代码,到GameHall-Client中的game目录读取所有的jar包,再读取各个元数据文件(MANIFEST.MF文件),得到里面声明的Game-Class属性,再通过反射将这个属性值转换成具体的某个Game的实现类。这个Game-Class属性的值就是该游戏的入口类。这样做无形之中为我们框架所加载的包加入了限制,如果是一个游戏客户端的包,就必须在MANIFEST.MF文件中声明Game-Class属性,这个属性定义该游戏的入口类。
到这里,游戏大厅的登录界面已经实现,下面小节实现玩家的登录功能。
15.2.6 实现登录功能
玩家输入了登录的相关信息后,就可以点击进行登录,在进行登录时,需要得游戏选择下拉框的值(某个游戏的入口类),并将当前的玩家信息封装成一个User对象,作为参数调用游戏入口类(Game的实现类)的start方法。
以下为LoginFrame中的login方法。
代码清单:code\GameHall-Client\src\org\crazyit\gamehall\client\LoginFrame.java
//创建Socket并为User对象设置Socket
this.user.setSocket(createSocket(this.connectionField.getText(), 12000));
//得到用户所选择的游戏
Game game = (Game)this.gameSelect.getSelectedItem();
game.start(this.user);
//启动线程
ClientThread thread = new ClientThread(this.user);
thread.start();
this.setVisible(false);
以上的代码实现了登录功能,从界面的游戏下拉框中得到具体的某个游戏,由于游戏选择下拉框
?8? 第15章 仿QQ游戏大厅
是使用具体的某个游戏类来创建的,因此得到该类后,直接强制转换成Game,再执行start方法并启动客户端线程。
到此,游戏大厅的基本框架已经完成,我们可以在这个框架的基础上开发各种游戏,但是前提是必须遵守该框架定义的一些规则。这些规则包括:
, 每个游戏都必须提供一个游戏入口类(Game接口的实现类);
, 每个游戏打成jar包后,都需要在MANIFEST.MF文件中加入Game-Class属性来声明游戏入口
类;
, 客户端向服务器发送信息时,必须是代表一个Request对象的XML字符串;
, 服务器向客户端发送信息时,必须是代表一个Response对象的XML字符串。
编写完最基本的框架后,下面章节编写一个五子棋游戏大厅。
15.3 建立五子棋游戏大厅
我们已经在15.2中编写了游戏大厅的基本框架,那么本小节开始编写一个五子棋的游戏大厅。在编写游戏大厅框架的时候,就定下规则,必须为游戏建立一个入口类。另外,五子棋的游戏大厅中还包括一系列的对象,包括桌子、位置等。我们将这些对象放到一个fivechess-commons的模块中。由于这些对象可能在客户端,也可能在服务器端使用到,因此我们将这些对象提取出来放到一个公共的模块中。
15.3.1 编写游戏大厅的对象
游戏大厅中最基本的单位就是桌子,但是每种游戏的游戏大厅都并不一致,例如斗地主游戏大厅中的桌子可能会有四个座位,而五子棋只需要两个座位,而且每个游戏都会有自己的玩家对象,这些玩家对象都包括不同的内容。新建一个ChessUser类,继承User类,该类代表一个五子棋玩家对象。
ChessUser包括以下属性:
//是否已经准备游戏
private boolean ready;
由于五子棋中每个桌子只有两个座位,因此我们为五子棋游戏新建一个Table对象来表示这个游戏的桌子对象。一个桌子中有两个座位,我们建立Seat对象来表示具体的某个座位。
Seat对象包括如下属性。
代码清单:code\fivechess-commons\src\org\crazyit\gamehall\fivechess\commons\Seat.java
//该座位的玩家
private ChessUser user;
//座位的座标范围
private Rectangle range;
//座位边, 只能为本类的LEFT和RIGHT属性值
private String side;
//座位宽度
public final static int SEAT_WIDTH = 30;
//座位高度
public final static int SEAT_HEIGHT = 30;
public final static String LEFT = "left";
public final static String RIGHT = "right";
Table对象包括如下属性。
代码清单:code\fivechess-commons\src\org\crazyit\gamehall\fivechess\commons\Table.java
//桌子图片在大厅中的开始X座标
第15章 仿QQ游戏大厅 ?9?
private int beginX;
//桌子图片在大厅中的开始Y座标
private int beginY;
//桌子的图片
private String tableImage;
//桌子号
private int tableNumber;
//默认的图片宽
public final static int DEFAULT_IMAGE_WIDTH = 140;
//默认的图片高
public final static int DEFAULT_IMAGE_HEIGHT = 140;
//该Table对应的范围
private Rectangle range;
//左边的座位
private Seat leftSeat;
//右边的座位
private Seat rightSeat;
这样,我们就定义了桌子和位置的对象,这里需要注意的是,位置并不需要知道自己属于哪张桌
子,桌子对象中则提供了两个座位对象:leftSeat和rightSeat,表示一张桌子中只能有两个位置。Table
与Seat对象都提供了range属性,表示桌子或者位置的图片在大厅中具体位置范围。 为Table对象提供一个构造器,在创建Table的时候,就需要同时创建左边与右边的座位对象,并且
设置相应的坐标位置。
Table的构造器。
commons\Table.java 代码清单:code\fivechess-commons\src\org\crazyit\gamehall\fivechess\
//创建桌子对象的时候就创建左右的Seat对象
this.leftSeat = new Seat(null, new Rectangle(getLeftSeatBeginX(), getLeftSeatBeginY(),
Seat.SEAT_WIDTH, Seat.SEAT_HEIGHT), Seat.LEFT);
this.rightSeat = new Seat(null, new Rectangle(getRightSeatBeginX(), getRightSeatBeginY(),
Seat.SEAT_WIDTH, Seat.SEAT_HEIGHT), Seat.RIGHT);
Seat对象的构造器。
代码清单:code\fivechess-commons\src\org\crazyit\gamehall\fivechess\commons\Seat.java public Seat(ChessUser user, Rectangle range, String side)
这里需要注意的是,位置的具体位置由它所在的桌子确定。
15.3.2 服务器创建游戏大厅数组
确定了游戏大厅的几个对象后,我们就可以在服务器中创建游戏大厅的数组,五子棋游戏的服务
器端在本章中使用的是fivechess-server模块,游戏大厅的几个对象都保存在fivechess-commons模
块,因此fivechess-server模块会依赖于fivechess-commons模块。游戏大厅数组保存在服务器端,我
们使用一个ChessContext的类来保存这些信息。
代码清单:code\fivechess-server\src\org\crazyit\gamehall\fivechess\server\ChessContext.java
//保存桌子信息
public static Table[][] tables = new Table[TABLE_COLUMN_SIZE][TABLE_ROW_SIZE];
static {
//初始化桌子信息
tables = new Table[TABLE_COLUMN_SIZE][TABLE_ROW_SIZE];
int tableNumber = 0;
for (int i = 0; i < tables.length; i++) {
for (int j = 0; j < tables[i].length; j++) {
?10? 第15章 仿QQ游戏大厅
Table table = new Table(Table.DEFAULT_IMAGE_WIDTH*i,
Table.DEFAULT_IMAGE_HEIGHT*j, tableNumber);
tables[i][j] = table;
tableNumber++;
}
}
}
建立一个Table的二维数组,并在ChessContext类的初始化块中建立这个数组。就这样,在服务器端就保存了一个桌子的二维数组,当有新的玩家进入五子棋游戏大厅的时候,就将这个桌子的二维数组设置到Response的数据列表中,返回给所有的用户。对象与XML字符串进行互转的时候,尽量避免使用一些大对象,例如Image对象等,否则将对象转换成XML的时候,将生成大量的XML字符串,影响传输性能。
15.3.3 玩家进入游戏大厅
玩家通过登录界面,选择具体进入的某个游戏,就首先调用Game实现类的start方法。我们新建一个fivechess-client的模块,表示五子棋游戏大厅的客户端。按照之前的规则,提供游戏客户端,需要在MANIFEST.MF文件中声明一个Game-Class的属性,表示该游戏的入口类。下面为五子棋的游戏客户端提供一个入口类,实现Game接口。
代码清单:code\fivechess-client\src\org\crazyit\gamehall\fivechess\client\action\ChessGame.java
public void start(User user) {
//得到进入游戏的玩家信息
ChessUser cu = convertUser(user);
ChessClientContext.chessUser = cu;
//构造一次请求, 告诉服务器玩家进入大厅, 服务器响应处理类是LoginAction
Request req = new Request("org.crazyit.gamehall.fivechess.server.action.LoginAction",
"org.crazyit.gamehall.fivechess.client.action.ClientInAction");
req.setParameter("user", cu);
//得到玩家的Socket并发送请求, 告诉服务器玩家进入了大厅
cu.getPrintStream().println(XStreamUtil.toXML(req));
}
ChessGame的start方法,首先将玩家对象(User)对象转换成一个五子棋玩家对象(ChessUser),再将当前的五子棋玩家对象设置到五子棋游戏的上下文对象中,最后,构造一次请求,发送到服务器,告诉服务器,新的玩家进入了五子棋游戏大厅。在以上代码中,声明了服务器处理类是服务器端的LoginAction,客户端处理类是ClientInAction。
当玩家发送了请求到服务器后,最先处理请求的是Game-Server模块,该模块将玩家的请求转发到具体的某个ServerAction实现类中,这里的LoginAction就是我们的服务器处理类。这里需要注意的是LoginAction位置fivechess-server模块中,表示该类是属于服务器执行的类。在编写LoginAction前,我们需要明白这Action需要进行的一些动作,玩家进入游戏大厅,首先必须将玩家的信息保存到服务器中,再将游戏大厅当前所有的信息发送给登录的玩家。
代码清单:
code\fivechess-server\src\org\crazyit\gamehall\fivechess\server\action\LoginAction.java
public void execute(Request request, Response response, Socket socket) {
//从请求参数中得到ChessUser
ChessUser cu = (ChessUser)request.getParameter("user");
cu.setSocket(socket);
//加入到所有玩家中
ChessContext.users.put(cu.getId(), cu);
第15章 仿QQ游戏大厅 ?11?
//将玩家设置到响应中
response.setData("user", cu);
//将所有玩家信息设置到响应中
response.setData("users", ChessContext.users);
//将所有的桌子信息返回到客户端
response.setData("tables", ChessContext.tables);
//将大厅中桌子的列数和行数返回到客户端
response.setData("tableColumnSize", ChessContext.TABLE_COLUMN_SIZE);
response.setData("tableRowSize", ChessContext.TABLE_ROW_SIZE);
//返回给登录玩家, 登录成功
cu.getPrintStream().println(XStreamUtil.toXML(response));
}
我们将进入游戏大厅的玩家保存到服务器上下文中,因此需要在ChessContext中添加一个属性来保存玩家信息:
public static Map users = new HashMap();
在服务器上下文中使用一个Map来保存玩家信息,这个Map的key是玩家对象的id,value是具体的某个五子棋玩家对象。玩家通过ChessGame来发送第一次请求给服务器,LoginAction负责处理这一次请求,然后将Response对象转换成XML字符串返回给客户端,接下来,就是客户端处理类(ClientInAction)负责处理这一次服务器响应。服务器响应首先是发送给fivechess-client模块的ClientThread类进行处理,该类同样地负责转发,去寻找具体的某个客户端处理类(ClientAction)的实现类进行处理。ClientInAction接收到服务器的响应后,就需要为刚登录的玩家创建游戏大厅,服务器的响应中已经包括创建游戏大厅所需的各个信息,包括桌子,玩家等。我们暂时不提供实现,下面创建游戏大厅的各个界面。
15.3.4 创建游戏大厅界面
游戏大厅界面在本章对应的是GameHallFrame类,该类在fivechess-client模块中。我们需要做的效果如图15.2所示。
?12? 第15章 仿QQ游戏大厅
图15.2 游戏大厅界面
五子棋游戏大厅主要包括一个存放桌子的JPanel,存放所有玩家的JTable对象,主要用于聊天的JPanel,玩家列表的JTable对象与聊天对象界面较为简单,复杂的是大厅对象,该对象在本章中对应的HallPanel对象。
当登录进入五子棋游戏大厅的时候,我们首先会调用ChessGame的start方法,该方法会发送一次请求到服务器取全部的桌子信息,再由客户端的ClentInAction负责创建游戏界面。在服务器响应中我们可以得到所有的桌子信息,然后再根据这些桌子的信息去创建HallPanel。
以下是HallPanel的paint方法,主要用于绘画界面中的桌子与玩家。
代码清单:code\fivechess-client\src\org\crazyit\gamehall\fivechess\client\ui\HallPanel.java
public void paint(Graphics g) {
for (int i = 0; i < this.tables.length; i++) {
for (int j = 0; j < this.tables[i].length; j++) {
Table table = this.tables[i][j];
Seat leftSeat = table.getLeftSeat();
Seat rightSeat = table.getRightSeat();
//画桌子的图片
g.drawImage(tableImage, table.getBeginX(),
table.getBeginY(), this);
//画左边座位的玩家
if (leftSeat.getUser() != null) {
Image head = getHeadImage(leftSeat.getUser().getHeadImage());
g.drawImage(head, table.getLeftSeatBeginX(),
table.getLeftSeatBeginY(), this);
第15章 仿QQ游戏大厅 ?13?
}
//画右边座位的玩家
if (rightSeat.getUser() != null) {
Image head = getHeadImage(rightSeat.getUser().getHeadImage());
g.drawImage(head, table.getRightSeatBeginX(),
table.getRightSeatBeginY(), this);
}
}
}
}
HallPanel得到各个桌子的二维数组后,根据这个二维数组去绘画桌子,如果桌子中的座位有玩家
的话,就画上相应的玩家头像。画完桌子后,我们需要处理鼠标事件,当鼠标的指针移动到某个位置上
面的时候,就需要帮这个位置更换图片,让其做到有阴影的效果。
为鼠标移动添加事件。
代码清单:code\fivechess-client\src\org\crazyit\gamehall\fivechess\client\ui\HallPanel.java
this.addMouseMotionListener(new MouseMotionAdapter() {
public void mouseMoved(MouseEvent e) {
moveMouse(e);
}
});
玩家移动鼠标的时候,就触发mouseMove方法,该方法的主体代码如下。 代码清单:code\fivechess-client\src\org\crazyit\gamehall\fivechess\client\ui\HallPanel.java
if (table.getLeftSeat().getRange().contains(x, y)) {
//左边座位
this.setCursor(HAND_CURSOR);
//如果位置上没有人才更换图片
if (table.getLeftSeat().getUser() == null) {
g.drawImage(seatSelectImage, table.getLeftSeatBeginX(),
table.getLeftSeatBeginY(), this);
}
} else if (table.getRightSeat().getRange().contains(x, y)) {
//右边座位
this.setCursor(HAND_CURSOR);
//如果位置上没有人才更换图片
if (table.getRightSeat().getUser() == null) {
g.drawImage(seatSelectImage, table.getRightSeatBeginX(),
table.getRightSeatBeginY(), this);
}
} else {
this.setCursor(DEFAULT_CURSOR);
this.repaint();
}
当鼠标移动到有人的位置时,就不更改阴影图片,如果鼠标移动到没有人的座位时,才更换座位
的阴影图片与鼠标指针,具体的效果如图15.3所示。
?14? 第15章 仿QQ游戏大厅
15.3 鼠标指针移动到空座位上时
15.3.5 创建玩家列表与聊天界面
玩家登录进入游戏大厅的时候,服务器需要将所有玩有的信息发送给登录的玩家,界面得到这些玩这有信息后,就将所有的玩家设置到玩家列表中。在本章,玩家列表使用的是UserTable来表示。UserTable的实现较为简单,只需要得到具体的五子棋玩家列表,就可以根据这些玩家来创建列表。该列表需要注意的是,列表的单元格并不可以编辑。需要将自己放到所有玩家的最前面。
由于玩家列表中涉及图片的显示,同样也是需要提供一个DefaultTableCellRenderer来显示相应列的图片。编写一个DefaultTableCellRenderer的子类来处理显示头像图片:
代码清单:
code\fivechess-client\src\org\crazyit\gamehall\fivechess\client\ui\UserTableCellRenderer.java
public Component getTableCellRendererComponent(JTable table, Object value,
boolean isSelected, boolean hasFocus, int row, int column) {
setHorizontalAlignment(SwingConstants.CENTER);
//设置显示图片
if (value instanceof Icon) this.setIcon((Icon)value);
else this.setText(value.toString());
//设置单元格的北景颜色
if (isSelected) this.setBackground(Color.YELLOW);
else this.setBackground(Color.WHITE);
return this;
}
最后将各个玩家的信息转换成列表显示的数据类型显示到列表中,在UserTable中,我们使用一个List来保存所有的玩家信息,提供一个getDatas的方法来转换玩家集合。
代码清单:code\fivechess-client\src\org\crazyit\gamehall\fivechess\client\ui\UserTable.java
//得到玩家列表的数据格式
private Vector getDatas() {
Vector result = new Vector();
for (ChessUser user : this.users) {
Vector v = new Vector();
v.add(user.getId());
v.add(getHead(user.getHeadImage()));
v.add(user.getName());
v.add(getSex(user.getSex()));
result.add(v);
}
return result;
}
如图15.2所示,除了玩家列表外,还有聊天的界面,聊天界面在本章中使用ChatPanel表示。在ChatPanel中,只需要创建一些基本的界面组件即可,包括一个JTextArea、一个JTextField和一个发送的按钮。需要游戏的是,构建ChatPanel时,也需要将所有的玩家与当前的玩家都作为构造参数传入,这是由于如果进行聊天,就需要让玩家选择聊天的对象,再发送给服务器。
第15章 仿QQ游戏大厅 ?15?
玩家列表与聊天界面已经创建完成,这两个界面组件可以在游戏界面重用,游戏界面的实现将在下面章节讲述。
15.3.6 使用服务器的数据创建游戏大厅
建立游戏大厅的各个界面,都离不开玩家的信息。在15.3.3中,我们还没有实现ClientInAction,这个客户端处理类主要用于得到服务器传过来的大厅信息、玩家信息,我们可以根据这些信息来创建游戏大厅的界面。
代码清单:
code\fivechess-client\src\org\crazyit\gamehall\fivechess\client\action\ClientInAction.java
public void execute(Response response) {
//从服务器中得到大厅信息并封装成一个GameHallInfo对象
GameHallInfo hallInfo = getGameHallInfo(response);
//得到全部玩家的信息
List users = getUsers(response);
//得到进入游戏的玩家信息
ChessUser cu = ChessClientContext.chessUser;
//创建界面GameHallFrame
GameHallFrame mainFrame = new GameHallFrame(hallInfo, cu, users);
mainFrame.sendUserIn();
}
以上代码的getGameHallInfo方法与getUsers方法,从服务器响应中得到桌子的信息与所有用户的信息,由于服务器处理类在接收用户进入游戏大厅信息的时候,就已经将这些信息设置到服务器响应里,因此只需要在客户端处理类中通过getDatas(key)方法就可以得到这些数据,这些数据在15.3.3中的LoginAction中已经设置。
客户端将接收到的数据创建游戏大厅后,还需要调用游戏大厅对象(GameHallFrame)的sendUserIn方法,这个方法主要用于客户端发送请求到服务器,告诉服务器,当前的玩家已经成功进入游戏大厅了。以下是sendUserIn方法的主要代码。
代码清单:code\fivechess-client\src\org\crazyit\gamehall\fivechess\client\ui\GameHallFrame.java
//构造一次请求, 告诉服务器用户进入大厅, 服务器响应处理类是ReceiveInAction
Request req = new Request("org.crazyit.gamehall.fivechess.server.action.NewUserInAction",
"org.crazyit.gamehall.fivechess.client.action.ReceiveInAction");
req.setParameter("userId", this.user.getId());
//得到用户的Socket并发送请求, 告诉服务器用户进入了大厅
this.user.getPrintStream().println(XStreamUtil.toXML(req));
sendUserIn方法重新构造一次请求,并发送给服务器,服务器处理类是NewUserInAction,返回给客户端处理类是ReceiveInAction。NewUserInAction是服务器接收新玩家进入游戏大厅的请求,然后根据这个请求得到相应的玩家id,再告诉这个大厅中的其他玩家,有新的玩家进入了游戏大厅了。
代码清单:code\GameHall-Server\src\org\crazyit\gamehall\server\action\NewUserInAction.java
public void execute(Request request, Response response, Socket socket) {
//得到新登录的玩家
String userId = (String)request.getParameter("userId");
ChessUser user = ChessContext.users.get(userId);
//将新玩家信息放到响应中
response.setData("newUser", user);
//向所有玩家发送信息
for (String id : ChessContext.users.keySet()) {
ChessUser hasLogin = ChessContext.users.get(id);
?16? 第15章 仿QQ游戏大厅
//不必发送给自己
if (id.equals(user.getId())) continue;
hasLogin.getPrintStream().println(XStreamUtil.toXML(response));
}
}
NewUserInAction放在fivechess-server模块中,表示该Action由服务器执行。下面为客户端创建一个ReceiveInAction,用于客户端接收服务器发送“有新玩家进入”的信息。ReceiveInAction是客户端执行的Action。
代码清单:
code\fivechess-client\src\org\crazyit\gamehall\fivechess\client\action\ReceiveInAction.java
public void execute(Response response) {
//得到新进入的玩家
ChessUser newUser = (ChessUser)response.getData("newUser");
//向玩家列表中加入一个新玩家
UserTable userTable = (UserTable)UIContext.modules.get(UIContext.HALL_USER_TABLE);
userTable.addUser(newUser);
//向聊天内容中添加
ChatPanel chatPanel = (ChatPanel)UIContext.modules.get(UIContext.HALL_CHAT_PANEL);
chatPanel.appendContent(newUser.getName() + " 进来了");
chatPanel.refreshJComboBox();
}
以上代码需要注意的是,新建一个UIContext来保存各个界面组件,UIContext中提供一个Map对
,当有新的界面组件被创建时,就需要加入到Map中,并为该组件提供一个唯一的象来保存界面组件
名称。
ReceiveInAction是玩家用玩接收其他玩家进入游戏大厅的消息,一旦有新的玩家进入,服务器就会向所有玩家发送消息,有新的玩家进入,需要更新玩家列表,更新聊天界面组件的下拉框,最后在ChatPanel添加消息提示。具体的效果如图15.4所示。
第15章 仿QQ游戏大厅 ?17?
图15.4 新的玩家进入游戏大厅
在本小节中,我们编写了游戏大厅的各个界面组件与对象,并实现了用户进入游戏大厅的功能,在下面章节,我们将实现游戏大厅的其他功能。
15.4 实现聊天功能
在游戏大厅中,我们提供了一个聊天的界面,玩家可以在上面进行聊天,发送或者接收聊天信息,需要注意的是,聊天界面是可以共用的,除了在游戏大厅中使用聊天界面外,还可以在游戏界面中使用。本小节实现游戏大厅中的聊天功能。
15.4.1 发送聊天信息
在聊天界面(ChatPanel)中,当玩家选择了某个聊天的对象,输入内容再点击发送按钮后,就可以创建一次请求,将该请求发送到服务器,告诉服务器:我对某个人(所有人)发送了聊天内容,请帮我转发。服务器得到这个请求后,就执行某个服务器处理类,该处理类就将聊天内容转发给相应的玩家。
以下是ChatPanel发送聊天内容的方法。
代码清单:code\fivechess-client\src\org\crazyit\gamehall\fivechess\client\ui\ChatPanel.java
//发送信息
public void send() {
?18? 第15章 仿QQ游戏大厅
//得到发送的内容
String content = this.conentField.getText();
//得到接收玩家
ChessUser receiver = (ChessUser)this.target.getSelectedItem();
//构造请求
Request request = new Request(this.serverAction, this.clientAction);
//设置参数
request.setParameter("receiverId", receiver.getId());
request.setParameter("senderId", this.user.getId());
request.setParameter("content", content);
//发送请求
this.user.getPrintStream().println(XStreamUtil.toXML(request));
appendContent("你对 " + receiver.getName() + " 说: " + content);
}
以上的代码中,构造Request对象时,我们使用的是serverAction与clientAction这两个类属性,由于这个ChatPanel是可以重用的,因此serverAction与clientAction也是由使用者来提供。游戏大厅中发送聊天请求的服务器处理类是SendMessageAction,客户端处理类是ReceiveMessageAction。Request中包括了接收人id、发送人id和聊天内容的信息。服务器中的SendMessageAction得到这些信息后,就可以将内容发送给相关的玩家,以下是SendMessageAction的具体实现。玩家发送聊天信息,由服务器的SendMessageAction接收,再由该Action转发,让客户端的ReceiveMessageAction负责处理。
代码清单:
code\GameHall-Server\src\org\crazyit\gamehall\server\action\SendMessageAction.java
public void execute(Request request, Response response, Socket socket) {
String receiverId = (String)request.getParameter("receiverId");
String senderId = (String)request.getParameter("senderId");
ChessUser sender = ChessContext.users.get(senderId);
String content = (String)request.getParameter("content");
if (receiverId == null) {
//向所有人发
for (String id : ChessContext.users.keySet()) {
if (id.equals(senderId)) continue;
ChessUser cu = ChessContext.users.get(id);
response.setData("content", sender.getName() + " 对 所有人 说:" + content);
cu.getPrintStream().println(XStreamUtil.toXML(response));
}
} else {
//得到接收人
ChessUser receiver = ChessContext.users.get(receiverId);
if (receiver.getId().equals(sender.getId())) return;
response.setData("content", sender.getName() + " 对你说:" + content);
receiver.getPrintStream().println(XStreamUtil.toXML(response));
}
}
SendMessageAction中根据请求的参数来发送相应的信息,需要注意的是,如果接收人id为空的话,那么就意味着向所有人发送聊天内容。
第15章 仿QQ游戏大厅 ?19? 15.4.2 接收聊天信息
接收聊天信息由ReceiveMessageAction负责处理,该类属于客户端处理类,在fivechess-client模块中,某个玩家得到聊天内容后,就可以获得界面组件,再将这些聊天内容追加到聊天界面组件的文本域中。
代码清单:
code\fivechess-client\src\org\crazyit\gamehall\fivechess\client\action\ReceiveMessageAction.java
public void execute(Response response) {
//得到聊天的界面组件
ChatPanel chatPanel = (ChatPanel)UIContext.modules.get(UIContext.HALL_CHAT_PANEL);
//从服务器响应中得到内容
String content = (String)response.getData("content");
chatPanel.appendContent(content);
}
聊天的具体效果如图15.5所示。
图15.5 聊天效果
15.5 启动游戏
玩家选择了某一个位置坐下的时候,需要对位置进行判断,看下该位置是否有人,还要对玩家当前的状态进行判断,判断其是否已经坐下了,如果玩家没有坐到任何位置上并且当前所选择的位置没
?20? 第15章 仿QQ游戏大厅
有玩家,就可以坐到位置上并展现游戏界面。
15.5.1 建立游戏界面
游戏界面的窗口在本章中使用ChessFrame类,该类继承于JFrame,ChessFrame包括三个界面组件,一个是游戏区域的GamePanel,另外两个就是我们已经实现的用户列表(UserTable对象)和聊天界面对象(ChatPanel),这里主要实现GamePanel。游戏界面的效果如图15.6所示。
图15.6 游戏界面
游戏区(GamePanel)只需要将棋盘图片、玩家头像、工具栏图片绘画出来即可,以下是GamePanel的paint方法的主体代码。
代码清单:
code\fivechess-client\src\org\crazyit\gamehall\fivechess\client\ui\game\GamePanel.java
g.drawImage(background, 0, 0, this.getWidth(), this.getHeight(), this);
g.drawImage(chessboard, 85, 80, this);
ChessUser lu = this.table.getLeftSeat().getUser();
ChessUser ru = this.table.getRightSeat().getUser();
//设置左边玩家的头像
this.leftUserHead = getUserHead(this.leftUserHead, lu);
//设置右边玩家的头像
this.rightUserHead = getUserHead(this.rightUserHead, ru);
//画头像
g.drawImage(this.leftUserHead, 30, 300, this);
第15章 仿QQ游戏大厅 ?21?
g.drawImage(this.rightUserHead, 645, 300, this);
//画左边的玩家
drawLeftUser(g, lu);
//画右边的玩家
drawRightUser(g, ru);
//画工具栏
g.drawImage(this.currentToolImage, 160, 630, this); 在游戏区需要画的有棋盘图片、左边玩家的头像图片与玩家名称、右边玩家的头像图片与玩家名
称、工具栏图片。绘画这些图片与文件只需要注意各个界面元素的坐标,根据这些坐标值将相应的图片
画到GamePanel中。创建一个GamePanel对象,需要得到具体的一个桌子对象和当前的玩家对象,
以下是GamePanel的构造器的部分代码:
public GamePanel(Table table, ChessUser user) {
this.table = table;
this.user = user;
}
15.5.2 玩家坐下
玩家选择了具体的一个位置点击时,需要进行一系列的判断,判断位置是否有人,判断玩家是否
已经坐在其他位置上,当条件都不成立时,就可以向服务器发送信息,告诉服务器当前的玩家已经坐
到具体的某个位置上,再向游戏大厅中所有的玩家发送信息:该玩家已经坐在某张桌子中了。 以下是HallPanel的sitDown方法。
代码清单:code\fivechess-client\src\org\crazyit\gamehall\fivechess\client\ui\HallPanel.java
//坐下的桌子的方法
private void sitDown(MouseEvent e) {
int x = e.getX();
int y = e.getY();
//获得桌子
Table table = getTable(x, y);
if (table != null) {
//得到座位
Seat seat = getSeat(table, x, y);
if (seat != null) {
//判断玩家是否已经坐下
if (this.user.hasSitDown(this.tables)) {
//可以提示玩家
return;
}
if (seat.getUser() != null) {
//座位上有人,可以提示玩家
return;
} else {
seat.setUser(this.user);
//没人向服务器发送请求
sendServerSitDown(table, seat.getSide());
}
}
}
}
?22? 第15章 仿QQ游戏大厅
做了一系列判断后,玩家就可以坐到位置上,得到该位置的对象(Seat)后,再设置该位置的玩
家,Seat的user属性。最后调用sendServerSitDown方法告诉服务器。
sendServerSitDown方法。
代码清单:code\fivechess-client\src\org\crazyit\gamehall\fivechess\client\ui\HallPanel.java
//向服务器发送请求, 告诉服务器自己已经坐下
private void sendServerSitDown(Table table, String side) {
//创建Request对象并设置相关的参数
Request request = new Request("org.crazyit.gamehall.fivechess.server.action.UserSitDownAction",
"org.crazyit.gamehall.fivechess.client.action.ReceiveUserSitDownAction");
request.setParameter("tableNumber", table.getTableNumber());
request.setParameter("side", side);
request.setParameter("userId", this.user.getId());
//设置启动游戏的类
request.setParameter("beginClass",
"org.crazyit.gamehall.fivechess.client.action.game.EnterGameAction");
this.user.getPrintStream().println(XStreamUtil.toXML(request));
this.repaint();
}
sendServerSitDown方法构造一个Request对象,向服务器发送玩家坐到位置上的信息,Request
需要保存桌子的桌子号、玩家id和座位位置的信息,服务器端处理类是UserSitDownAction。这里需要
注意的是,由于这次请求需要告诉其他玩家,当前的玩家坐下了,还需要告诉当前的玩家去启动游戏
界面,因此需要为Request再设置多一个参数值beginClass,表示当前玩家启动游戏界面的类,这个
beginClass是客户端处理类EnterGameAction。
UserSitDownAction的execute方法。
代码清单:code\GameHall-Server\src\org\crazyit\gamehall\server\action\UserSitDownAction.java
//得到桌子编号
Integer tableNumber = (Integer)request.getParameter("tableNumber");
String side = (String)request.getParameter("side");
Table table = Table.getTable(tableNumber, ChessContext.tables);
//得到刚坐下的玩家
String userId = (String)request.getParameter("userId");
ChessUser user = ChessContext.users.get(userId);
//得到座位
Seat seat = table.getSeat(side);
seat.setUser(user);
//告诉所有的客户端, 刚坐下的玩家在哪张桌子哪个位置坐下了
response.setData("tableNumber", table.getTableNumber());
response.setData("side", side);
response.setData("user", user.getId());
//向所有玩家发送信息, 有新玩家坐下
printResponse(user, response);
//得到启动游戏的客户端类
String beginClass = (String)request.getParameter("beginClass");
response.setActionClass(beginClass);
//告诉客户端, 需要启动游戏界面
user.getPrintStream().println(XStreamUtil.toXML(response));
UserSitDownAction中的execute方法向客户端发送了两次服务器响应,一次是告诉所有的玩家,
当前的玩家坐到某个位置上了,第二次是告诉当前玩家,需要启动游戏界面类。所有玩家接收到当前玩
家坐下的信息后,客户端处理类是ReceiveUserSitDownAction,当前玩家启动游戏界面的客户端处理
第15章 仿QQ游戏大厅 ?23? 类是EnterGameAction。
代码清单:
code\fivechess-client\src\org\crazyit\gamehall\fivechess\client\action\ReceiveUserSitDownAction.java
//得到界面对象
HallPanel hallPanel = (HallPanel)UIContext.modules.get(UIContext.HALL_PANEL);
//有新的玩家坐下, 这里由所有玩家(不包括发送人)执行
int tableNumber = (Integer)response.getData("tableNumber");
String side = (String)response.getData("side");
String userId = (String)response.getData("userId");
hallPanel.newUserSitDown(tableNumber, side, userId);
ReceiveUserSitDownAction是客户端处理类,接收其他玩家坐下了的信息,最后调用游戏大树界面组件类HallPanel的newUserSitDown方法处理玩家坐下的事件。以下是HallPanel的newUserSitDown的具体实现。
代码清单:code\fivechess-client\src\org\crazyit\gamehall\fivechess\client\ui\HallPanel.java
//新玩家坐下, tableNumber为桌子编号, side为左右位置
public void newUserSitDown(int tableNumber, String side, String userId) {
//得到桌子对象
Table table = getTable(tableNumber);
//得到座位
Seat seat = table.getSeat(side);
//得到坐下的玩家
ChessUser newUser = getUser(userId);
seat.setUser(newUser);
this.repaint();
}
只要知道玩家坐在哪张桌子的哪一边,就可以更新当前的Table二维数组,再进行repaint。这是ReceiveSitDownAction中向其他玩家发送的第一次服务器响应,该Action向当前玩家发送第二次服务器响应时,就需要改变Response的actionClass属性,让发送坐下请求的玩家启动游戏界面(EnterGameAction)。在这里需要注意的是,如果当前玩家进入了游戏中,需要判断当前玩家所坐下的桌子是否有人(对手),如果有对手的话,就要告诉对方自己进来了,这时也需要构造一个新的请求发送到服务器中,让服务器去告诉当前玩家的对手。
代码清单:
code\fivechess-client\src\org\crazyit\gamehall\fivechess\client\action\game\EnterGameAction.java
HallPanel hallPanel = (HallPanel)UIContext.modules.get(UIContext.HALL_PANEL);
//从服务器呼应中得到桌子编号
Integer tableNumber = (Integer)response.getData("tableNumber");
String side = (String)response.getData("side");
//根据桌子编号得到桌子信息
Table table = Table.getTable(tableNumber, hallPanel.getTables());
//显示界面
ChessFrame cf = new ChessFrame(table, ChessClientContext.chessUser);
cf.setVisible(true);
//告诉对方进入游戏(如果有对方玩家的话)
Seat seat = table.getSeat(side);
//得到对方座位
Seat otherSeat = table.getAnotherSeat(seat);
if (otherSeat.getUser() != null) {
?24? 第15章 仿QQ游戏大厅
//有对手, 向服务器发送请求
Request request = new Request(
"org.crazyit.gamehall.fivechess.server.action.OpponentEnterAction",
"org.crazyit.gamehall.fivechess.client.action.game.OpponentEnterAction");
//firstUserId是对手的ID(第一个进入游戏的玩家)
request.setParameter("firstUserId", otherSeat.getUser().getId());
//secondUserId是自己的ID(后进入游戏的玩家)
request.setParameter("secondUserId", seat.getUser().getId());
ChessClientContext.chessUser.getPrintStream().println(XStreamUtil.toXML(request));
}
告诉对手自己进入了游戏,服务器处理类是fivechess-server的OpponentEnterAction,对手执行
的客户端处理类是fivechess-client的OpponentEnterAction。
服务器端的OpponentEnterAction。
代码清单:
code\GameHall-Server\src\org\crazyit\gamehall\server\action\OpponentEnterAction.java
//得到第一个玩家的对象(发送请求的人的对手)
String firstUserId = (String)request.getParameter("firstUserId");
ChessUser firstUser = ChessContext.users.get(firstUserId);
//得到第二个玩家的对象
String secondUserId = (String)request.getParameter("secondUserId");
response.setData("opponentId", secondUserId);
//告诉第一个进入游戏的玩家, 有对手进入
firstUser.getPrintStream().println(XStreamUtil.toXML(response)); 服务器端的Action主要根据参数找到发送这次请求的玩家的对手,告诉他当前玩家已经进入了游
戏。因为服务器响应中就需要将当前玩家的信息设置到响应数据中,再找到当前玩家的对手,向其输入
服务器响应。
客户端的OpponentEnterAction。
代码清单:
code\fivechess-client\src\org\crazyit\gamehall\fivechess\client\action\game\OpponentEnterAc
tion.java
//得到大厅对象
HallPanel gameHall = (HallPanel)UIContext.modules.get(UIContext.HALL_PANEL);
//得到对手的ChessUser对象
String opponentId = (String)response.getData("opponentId");
//从大厅中得到对手的信息
ChessUser opponent = gameHall.getUser(opponentId);
ChessFrame cf = (ChessFrame)UIContext.modules.get(UIContext.GAME_FRAME);
cf.newUserIn(opponent);
客户的OpponentEnterAction主要用于处理接收对手进入我所坐的桌子的服务器响应,当我的对
手进入桌子时,服务器就会告诉我,我的对手进来了,最后调用ChessFrame的newUserIn方法。 ChessFrame的newUserIn方法。
代码清单:
code\fivechess-client\src\org\crazyit\gamehall\fivechess\client\ui\game\ChessFrame.java
//向玩家集合中添加一个新玩家, 表示有新玩家进入
public void newUserIn(ChessUser user) {
this.users.add(user);
refreshUI();
}
newUserIn方法向界面中的玩家集合添加一个新的玩家并更新界面组件。具体的效果如图15.7所
第15章 仿QQ游戏大厅 ?25? 示。
图15.7 双方玩家坐到桌子上
15.5.3 实现游戏聊天
我们已经在15.4中实现了游戏大厅的聊天功能,在游戏界面进行聊天时,使用的是同一个界面组件(ChatPanel),因此在游戏中聊天,只需要重新创建一个ChatPanel对象即可,但是在创建该对象时,需要提供不同的聊天服务器处理类和客户端处理类。在游戏中聊天的服务器处理类是GameMessageAction,客户端处理类是ReceiveMessageAction。
代码清单:
code\GameHall-Server\src\org\crazyit\gamehall\server\action\GameMessageAction.java
String senderId = (String)request.getParameter("senderId");
ChessUser sender = ChessContext.users.get(senderId);
String content = (String)request.getParameter("content");
//得到发送人所在的桌子
Table table = ChessContext.getTable(senderId);
if (table != null ) {
//向对手发送
ChessUser receiver = table.getAnotherUser(sender);
if (receiver != null) {
response.setData("content", sender.getName() + " 对你说:" + content);
receiver.getPrintStream().println(XStreamUtil.toXML(response));
?26? 第15章 仿QQ游戏大厅
}
}
GameMessageAction接收到玩家发送的聊天信息后,就向它的对手发送聊天信息,所作的处理与游戏大厅中的聊天类似。ReceiveMessageAction与游戏大厅中所作的处理一样,都是向ChatPanel中的文本域追加文本。但是需要注意的是,由于我们使用了一个UIContext中的一个Map对象来保存界面组件,因此虽然重用了ChatPanel,但是它们是两个实例,因此需要为这两个实例提供不同的名称。才可以使用UIContext来获得相应的实例。
15.6 开始游戏
当双方玩家坐到同一张桌子上,并且都点击了工具栏中的开始时,游戏就正式开始,在本章中,我们确定左边位置的玩家使用黑棋,并可以先下棋,右边的玩家使用白棋。
15.6.1 游戏准备
一方玩家坐到桌子上的时候,工具栏中只可以点击开始,当玩家点击了开始的时候,该玩家就处于准备状态,ChessUser中提供了一个ready的布尔值来表示该玩家是否准备中的状态。玩家点击了开始,除了需要改变他的状态外,还需要告诉对手,他已经准备好了,如果对手都已经准备好了,那么游戏正式开始。为了表示游戏已经开始,我们在游戏区(GamePanel)中保存一个布尔值,表示当前游戏的状态。如果玩家已经处理游戏准备的状态,那么就需要更换工具栏图片,如果已经在游戏状态,同样地也需要更换另外的工具栏图片。
图15.8 是开始游戏的工具栏。
图15.8 开始游戏的工具栏图片
图15.9是准备游戏的工具栏图片
图15.9 准备游戏的工具栏图片
图15.10是游戏中的工具栏图片
图15.10 游戏中的工具栏图片
当玩家点击了开始时,就需要向服务器发送准备的请求,以下是GamePanel中的ready方法。
代码清单:
code\fivechess-client\src\org\crazyit\gamehall\fivechess\client\ui\game\GamePanel.java
//玩家准备游戏
private void ready() {
第15章 仿QQ游戏大厅 ?27?
if (this.user.isReady()) return;
//设置玩家的状态
this.user.setReady(true);
this.currentToolImage = tool_ready;
this.repaint();
//发送信息给服务器, 告诉服务器已经准备好了
Request request = new Request("org.crazyit.gamehall.fivechess.server.action.ReadyAction",
"org.crazyit.gamehall.fivechess.client.action.game.StartGameAction");
request.setParameter("userId", this.user.getId());
request.setParameter("tableNumber", this.table.getTableNumber());
//如果对手没有准备, 则设置对手的处理类
request.setParameter("opponentAction",
"org.crazyit.gamehall.fivechess.client.action.game.OpponentReadyAction");
this.user.getPrintStream().println(XStreamUtil.toXML(request));
}
以上的ready方法向服务器发送了一次请求,告诉服务器自己已经准备游戏了,请求参数包括准备
游戏的玩家id,桌子编号,服务器处理类是ReadyAction,客户端处理类是StartGameAction,这里需
要注意的是,我们向请求参数中加入了一个opponentAction的客户端处理类,自己准备游戏时,如果对
面位置有玩家,并且该玩家同样已经准备开始游戏的话,就使用客户端的StartGameAction处理类,
如果对手没有准备游戏的话,就使用请求参数中的OpponentReadyAction作为客户端处理类,让服务
器告诉对手,自己准备好了。
代码清单:code\GameHall-Server\src\org\crazyit\gamehall\server\action\ReadyAction.java
public void execute(Request request, Response response, Socket socket) {
//得到准备游戏的玩家
String userId = (String)request.getParameter("userId");
//得到桌子编号
Integer tableNumber = (Integer)request.getParameter("tableNumber");
//得到玩家
ChessUser user = ChessContext.users.get(userId);
user.setReady(true);
//判断对方是否已经准备游戏
//得到桌子对象
Table table = Table.getTable(tableNumber, ChessContext.tables);
Seat seat = table.getUserSeat(user);
//得到对手
ChessUser opponent = table.getAnotherSeat(seat).getUser();
if (opponent != null) {
//对面座位有人, 再判断对手是否已经准备好了
if (opponent.isReady()) {
//创建棋盘数组
createChessArray(table);
//向双方玩家发送响应, 游戏开始
opponent.getPrintStream().println(XStreamUtil.toXML(response));
user.getPrintStream().println(XStreamUtil.toXML(response));
}
//告诉对手自己准备好了, 使用对手接收准备的客户端处理类
String opponentAction = (String)request.getParameter("opponentAction");
response.setActionClass(opponentAction);
response.setData("userId", userId);
opponent.getPrintStream().println(XStreamUtil.toXML(response));
?28? 第15章 仿QQ游戏大厅
}
}
ReadyAction中得到桌子与游戏准备的玩家后,再告诉对手,当前的玩家已经准备好了,如果双方都已经准备好了,那么就创建棋盘的二维数组,棋盘二维数组里面存放的是Chess对象,在本章中,一个棋子使用一个Chess对象来表示,Chess对象放在fivechess-commons模块,Chess对象的属性如下:
private int beginX; //棋子的开始X坐标
private int beginY; //棋子的开始Y坐标
private int i; //在二维数组中的一维值
private int j; //在二维数组中的二维值
private String color; //棋子颜色
private Rectangle range; //该棋子的区域
由于Chess对象是客户端与服务器端共同使用的类,因此可以将这个对象放到fivechess-commons模块中。服务器创建了数组后,就可以告诉双方游戏开始。在创建棋盘数组时,我们需要将创建的棋盘数组放到ChessContext中,也就是将棋盘的二维数组放到服务器中进行保存,保存的数据结构是Map,这个Map的key是桌子编号,value是Chess的二维数组:Map。需要注意的是,玩家在下棋的时候,服务器再对这个二维数组进行遍历,判断输赢,因此判断输赢的操作由服务器进行,并不由客户端进行输赢的判断。OpponentReadyAction接收到对方准备游戏的服务器响应后,只是设置对手的状态,再对界面组件进行一次repaint。如果双方都已经准备了游戏,那么服务器就需要发送信息给双方,让两边的客户端去创建棋盘的二维数组,最后调用GamePanel的startGame方法来开始游戏。
以下是GamePanel的startGame方法。
代码清单:
code\fivechess-client\src\org\crazyit\gamehall\fivechess\client\ui\game\GamePanel.java
//设置游戏状态
public void startGame() {
this.gaming = true;
this.currentToolImage = tool_drawAndLost;
//设置开始游戏的提供
if (getUserSide().equals(Seat.LEFT)) {//自己先下棋
this.myTurn = true;
this.gameStartImage = ImageUtil.getImage("images/fivechess/start-game-you-first.gif");
} else {//对手先下棋
this.gameStartImage = ImageUtil.getImage("images/fivechess/start-game-opponent-first.gif");
}
this.selectImage = getSelectImage();
this.startGameTask = new StartGameTask(this);
this.timer = new Timer();
timer.schedule(this.startGameTask, 0, 20);
}
开始游戏需要设置gaming为true,表示当前正在游戏中,在GamePanel中还需要提供一个myTure的布尔值,标识是否轮到当前玩家下棋,在startGame方法的最后,启动一个Timer,将开始游戏的几个字作出动画的效果。开始游戏的具体效果如图15.11所示。
第15章 仿QQ游戏大厅 ?29?
图15.11 游戏开始
图15.11中的“游戏开始,你先下棋”几个字会慢慢向下,最后消失,提示玩家游戏开始,并且说明下棋的顺序。那么白棋一方看到的效果就是“游戏开始,对方先下棋”。在开始游戏的时候,就需要为客户端创建棋盘的二维数组。客户端创建棋盘二维数组的方法与服务器端创建的方式一样。这里需要注意的是,下棋的一方,鼠标移动时,会有一张准备下棋的图片跟随着鼠标的光标,只需要在鼠标移动时加入选择的图片并在paint方法中加入判断即可实现。
15.6.2 玩家下棋
下棋的玩家在棋盘的某个区域点击了下棋后,我们界面组件就需要得到鼠标点击的坐标,再从棋盘二维数组中得到具体的某个Chess对象。我们棋盘中是一个Chess的二维数组,在游戏开始时,就会初始化这个二维数组(创建数组中的所有Chess对象),该数组中所有的Chess对象都没有图片,初始化时只会赋于它们相应的坐标与范围,当玩家进行了下棋的操作后,就将玩家对应的棋(黑棋或者白棋)的颜色设置到Chess对象中,Chess对象中有一个color的属性,那么在GamePanel的paint方法中,就可以将每一个Chess都画到界面中。
以下是玩家下棋时所执行的方法:
代码清单:
code\fivechess-client\src\org\crazyit\gamehall\fivechess\client\ui\game\GamePanel.java
//下棋的方法
private void takeChess(int x, int y) {
Chess chess = getSelectChess(x, y);
if (chess != null) {
?30? 第15章 仿QQ游戏大厅
//当前位置有棋子
if (chess.getColor() != null) {
UIContext.showMessage("该位置已经有棋子");
} else {
//设置颜色
chess.setColor(getChessColor());
//轮到对方下棋
this.myTurn = false;
//设置选择图片为空
this.selectImage = null;
//向服务器发送请求
requestTakeChess(chess);
this.repaint();
}
}
}
玩家下完棋,还需要发送一次请求到服务器,告诉服务器下棋了,服务器需要做的是将玩家下棋的信息保存到服务器的二维数组中并判断是否胜利,如果没有胜利,还需要将玩家的下棋信息发送给对手,让对手去更新他自己的界面组件并且轮到对手下棋。这里需要考虑的是请求中应该存放一些什么样的参数给服务器,由于玩家在某个位置下棋,该位置对应的是某个Chess对象,因此我们可以将这个Chess对象在二维数组中的一维值(i)与二维值(j)作为请求参数,另外还要告诉服务器是哪个玩家在哪张桌子中下的棋,并且下棋的颜色是什么,除了这些信息外,如果玩家的这一步棋影响了游戏的结果(胜利了),就需要让对手去处理失败的动作。
//告诉服务器自己下棋了
private void requestTakeChess(Chess chess) {
Request request = new Request(
"org.crazyit.gamehall.fivechess.server.action.TakeChessAction",
"org.crazyit.gamehall.fivechess.client.action.game.TakeChessAction");
//设置请求的各个参数
request.setParameter("i", chess.getI());
request.setParameter("j", chess.getJ());
request.setParameter("userId", this.user.getId());
request.setParameter("tableNumber", this.table.getTableNumber());
request.setParameter("color", chess.getColor());
//设置处理胜利的Action
request.setParameter("winAction",
"org.crazyit.gamehall.fivechess.client.action.game.WinAction");
//设置处理输的Action
request.setParameter("lostAction",
"org.crazyit.gamehall.fivechess.client.action.game.LostAction");
this.user.getPrintStream().println(XStreamUtil.toXML(request));
}
需要注意的是,以上的代码设置了处理胜利的Action和处理失败的Action,如果当前玩家下的棋导致玩家游戏胜利的话,那么就是当前的玩家执行WinAction,由于自己胜利了,对手自然就是失败,对手执行LostAction。如果没有胜利的话,对手就执行客户端处理类TakeChessAction。
服务器端的TakeChessAction需要将当前玩家下的棋子信息保存到服务器端的二维数组,再对该数组进行遍历,判断是否胜利。
以下是服务器端的TakeChessAction判断是否胜利的代码。
代码清单:code\GameHall-Server\src\org\crazyit\gamehall\server\action\TakeChessAction.java
第15章 仿QQ游戏大厅 ?31?
//判断是否胜利
boolean win = validateWin(chessArray, chessArray[i][j]);
if (opponent == null) win = true;
//告诉双方, 赢了
if (win) {
//告诉赢的一方
tellWin(request, user, response);
//告诉输的一方
tellLost(request, opponent, response);
//设置服务器中双方的状态
opponent.setReady(false);
user.setReady(false);
}
以上的代码中使用了一个win的布尔值,该值取决于validateWin方法,validateWin方法用于遍历
服务器的二维数组,遍历一个二维数组中的每一个Chess对象,判断是否可以横、竖、斜连成五个棋
子。遍历的时候,需要纵向遍历、横向遍历、从上往下斜向遍历、从下往上斜向遍历。以下是纵向遍历
的代码。
代码清单:code\GameHall-Server\src\org\crazyit\gamehall\server\action\TakeChessAction.java
//纵向遍历
private boolean vertical(Chess[][] chessArray, Chess chess) {
//连续棋子的总数
int count = 0;
for (int i = 0; i < chessArray.length; i++) {
if (i == chess.getI()) {
for (int j = 0; j < chessArray[i].length; j++) {
Chess c = chessArray[i][j];
if (c.getColor() != null && c.getColor().equals(chess.getColor())) {
count++;
}
}
}
}
if (count >= 5) return true;
return false;
}
如果Chess对象的颜色与玩家刚才所下的棋子颜色一致并且可以连成五个,那么就返回true,表
示下的该棋子导致游戏结束(下棋的玩家胜利了)。玩家胜利后,就调用tellWin方法告诉下棋的一方:
你赢了,调用tellLost方法告诉对方:你输了。赢的一方就会执行客户端处理类WinAction,输的一方就
会执行客户端处理类LostAction。WinAction与LostAction都是将游戏重新进行初始化,再告诉玩家游
戏结果。游戏初始化需要进行如下操作:
, 将下棋时候的选择图片设置为null
, 将游戏状态(GamePanel的gaming属性)设置为false
, 将轮到自己下棋的标识(GamePanel的myTurn属性)设置为false
, 将客户端双方玩家的准备标识(ChessUser的ready属性)设置为false , 设置工具栏图片为图15.8(只允许点击开始)
玩家下棋的代码已经实现,具体效果如图15.12所示。
?32? 第15章 仿QQ游戏大厅
图15.12 下棋游戏胜利
15.6.3 逃跑与认输
一方玩家由于特殊原因,点击了认输或者直接关掉游戏,那么服务器就直接判定该玩家输,并提示对方胜利,认输与关掉游戏,都可以发送一次请求到服务器,让服务器进行相关的处理。
以下是GamePanel的认输方法。
代码清单:
code\fivechess-client\src\org\crazyit\gamehall\fivechess\client\ui\game\GamePanel.java
//发送认输的请求
public void sendLostRequest() {
Request request = new Request(
"org.crazyit.gamehall.fivechess.server.action.LostAction",
"org.crazyit.gamehall.fivechess.client.action.game.OpponentLostAction");
request.setParameter("userId", this.user.getId());
request.setParameter("tableNumber", this.table.getTableNumber());
this.user.getPrintStream().println(XStreamUtil.toXML(request));
}
以上代码构造一次请求发送到服务器,服务器的处理类是LostAction,客户端处理类是OpponentLostAction,客户端处理类是由赢的一方(认输方的对手)执行的。客户端处理类得到认输的请求后,就将该玩家的所有状态(服务器中的状态)都还原,发送信息给对手:你赢了。请求认输我们需要将认输玩家的id,桌子编写等信息告诉服务器,因此需要添加相关的请求参数。
第15章 仿QQ游戏大厅 ?33?
处理逃跑的Action与认输一样,只是当用户关闭游戏窗口时执行发送请求,服务器端与客户端处理类执行的操作与认输类似。
15.6.4 请求和棋
请求和棋的发送方向服务器发送一次请求,让服务器找到他的对手,向对手询问:是否同意和棋。如果对手同意和棋,那么就发送信息到服务器,告诉服务器他们和棋了,服务再向双方发送和棋的信息,让双方显示和棋的提示。如果对手拒绝和棋,同样地,也会发送一次请求到服务器,让服务器转发,告诉和棋请求人,对方拒绝和和棋。到目前为止,游戏大厅中所有的相关请求都通过Request来发送信息到服务器,告诉服务器应该作如何处理,服务器再找到相关的客户端并发送相应的服务器响应(Response)。
以下是GamePanel中请求和棋的方法。
代码清单:
code\fivechess-client\src\org\crazyit\gamehall\fivechess\client\ui\game\GamePanel.java
//请求求和
private void requestDraw() {
//询问是否求和
int result = UIContext.showConfirm("你确定要求和吗?");
if (result == 0) {
//向服务器发送求和请求
Request request = new Request(
"org.crazyit.gamehall.fivechess.server.action.DrawAction",
"org.crazyit.gamehall.fivechess.client.action.game.DrawAction");
request.setParameter("userId", this.user.getId());
request.setParameter("tableNumber", this.table.getTableNumber());
this.user.getPrintStream().println(XStreamUtil.toXML(request));
} else {
return;
}
}
以上代码构造一个Request对象向服务器发送求和请求,需要将请求人的id(玩家id)与桌子编号设置到参数中,那么服务器就可以知道哪张桌子的哪个玩家请求和棋,就可以根据这些信息得到发送和棋请求的玩家的对手,再向该对手发送和棋询问。和棋的服务器处理类是DrawAction,客户端处理类是DrawAction,这两个Action名字相同,但是存在的模块不一样。本章中所有的服务器处理类都存在于fivechess-server模块,所有的客户端处理类都存在于fivechess-client模块。
以下是GamePanel中同意求和的方法。
代码清单:
code\fivechess-client\src\org\crazyit\gamehall\fivechess\client\ui\game\GamePanel.java
//同意求和
private void agreeDraw() {
draw();
//告诉服务器同意求和
Request request = new Request("org.crazyit.gamehall.fivechess.server.action.AgreeDrawAction",
"org.crazyit.gamehall.fivechess.client.action.game.AgreeDrawAction");
request.setParameter("userId", this.user.getId());
request.setParameter("tableNumber", this.table.getTableNumber());
this.user.getPrintStream().println(XStreamUtil.toXML(request));
}
?34? 第15章 仿QQ游戏大厅
以上的黑体代码调用了draw方法,该方法只是普通的弹出提示与初始化游戏,提示和棋后,再发送一次请求到服务器,告诉服务器同意求和。服务器处理类是AgreeDrawAction,客户端处理类(发送求和请求一方)是AgreeDrawAction。当服务器端接收到同意求和的请求后,就可以初始化游戏双方在服务器中的状态,包括设置玩家(ChessUser)的ready属性,再告诉发送求和信息的一方,对方同意了和棋,发送求和信息的一方得到服务器的响应后,同样的把发送求和信息一方的所有状态都改变成为初始化状态,可以继续下一局的游戏。
以下是GamePanel中拒绝求和的方法。
代码清单:
code\fivechess-client\src\org\crazyit\gamehall\fivechess\client\ui\game\GamePanel.java
//拒绝求和
private void refuseDraw() {
//告诉服务器拒绝和棋
Request request = new Request("org.crazyit.gamehall.fivechess.server.action.RefuseDrawAction",
"org.crazyit.gamehall.fivechess.client.action.game.RefuseDrawAction");
request.setParameter("userId", this.user.getId());
request.setParameter("tableNumber", this.table.getTableNumber());
this.user.getPrintStream().println(XStreamUtil.toXML(request));
}
同样地,拒绝求和也是构造一次请求发送到服务器,让服务器去告诉发送求和信息的一方:对方拒绝了你的求和要求。发送的一方得到服务器响应后,就执行客户端处理类RefuseDrawAction,提示相关的信息。
15.7 五子棋游戏大厅
总结
初级经济法重点总结下载党员个人总结TXt高中句型全总结.doc高中句型全总结.doc理论力学知识点总结pdf
到此,五子棋游戏大厅已经全部完成,可能还有一些细节可以做得更好,例如提示信息不必使用弹出提示等。五子棋游戏大厅使用了我们在15.2中编写的游戏大厅框架来实现,所有的客户端与服务器通信,都是使用Request对象,服务器使用Response对象作出响应,所有的请求参数与响应参数都设置到这两个对象中,我们在开发五子棋游戏大厅的时候,基本上没有接触过任何的Socket对象,只需要构造一次请示,将服务器处理类与客户端处理类放到请求中,让我们的服务器去帮助我们寻找服务器处理类,让客户端去寻找客户端处理类。在本章中,框架的服务器端是fivechess-server模块,框架的客户端是fivechess-client模块。如果我们编写了新的游戏,就可以直接将游戏的服务器包放到fivechess-server中,将游戏的客户端的包放到fivechess-client中,编写游戏的时候,并不需要关心如何进行通信,只需要关心具体的业务实现,前提是必须遵守这个游戏大厅框架所定的一些规则。
在本章的
案例
全员育人导师制案例信息技术应用案例心得信息技术教学案例综合实践活动案例我余额宝案例
中,源文件所存放的是gamehall-src目录,游戏运行的目录是gamehall-run,该目录下存在有crazyit-gamehall-client与crazyit-gamehall-server目录,读者在运行游戏的时候,先打开crazyit-gamehall-server/bin下的startup.bat,就可以启动服务器,运行客户端的话,打开crazyit-gamehall-client/bin下的startup.bat就可以启动客户端。以下是服务器端与客户端的目录说明。
crazyit-gamehall-server:
, bin目录:包含一些启动服务器的命令;
, game目录:存放游戏服务器端的jar包;
, lib目录:存放一些服务器端所必需的包,例如gamehall-server模块的包、xstream的包等。
crazyit-gamehall-client:
, bin目录:包括一些启动客户的命令;
, game目录:存放游戏客户的jar包;
, images目录:各个游戏所需要的图片存放目录;
, lib目录:存放一些客户端所必需的包。
第15章 仿QQ游戏大厅 ?35?
15.8 编写一个测试聊天室
我们编写了一个游戏大厅的框,我们再次编写一个简单的聊天室,并将这个聊天室的客户端与服务器端打成jar包,分别放到框架的服务器目录与客户端目录,测试我们的框架是否支持游戏的扩展。 15.8.1 建立聊天室界面
聊天室界面十分简单,一个JTextArea、JTextField、发送消息的按钮与用户列表即可。具体效果如图15.13所示。
图15.13 聊天室界面
15.8.2 实现聊天室
聊天室所包含的动作有:用户进入聊天室、发送聊天信息、接收聊天信息等。我们在聊天室的服务器端模块中加入一个聊天室上下文,用于保存进入聊天室的全部用户。当有新的用户进入聊天室时,我们就向其他在线的用户发送信息。这样的一个过程,在我们编写的框架中,只需要构造一个Request对象并发送给服务器即可实现,服务器再作出相关的响应,在JTextArea中显示相关的信息。
开发客户端处理类与五子棋一样,都是通过一个界面的上下文类来得到界面组件对象,服务器作出响应时,就会执行这些客户端处理类,然后再对界面组件进行相关的控制。
聊天室的具体效果如图15.14所示。
?36? 第15章 仿QQ游戏大厅
图15.14 聊天室完成效果
基于我们所编写的框架来完成一个聊天室十分简单,只需要简单的界面代码和几个处理类即可,可以看到我们所编写的框架具有扩展性,如果需要编写其他游戏加入到该框架中,只需要编写相关的Action类即可,我们可以不用去关心如何进行信息传输。由于聊天室的实现相对较为简单,只需要遵守框架的几个规则即可,因此具体的编写代码在此不详细描述,可以具体参看源代码中的chat-room-client模块与chat-room-server模块。
15.8.3 将聊天室放置到框架中测试
我们的框架中有一个最基本的规则,就是需要在客户端的包的MANIFEST.MF中加入Game-Class属性,来声明客户端的游戏入口类,因此聊天室的MANIFEST.MF文件内容如下。
代码清单:code\chat-room-client\src\META-INF\MANIFEST.MF
Manifest-Version: 1.0
Game-Class: org.crazyit.gamehall.chatroom.client.ChatIndex
聊天室的MANIFEST.MF文件声明了由ChatIndex类作为聊天室的入口类,当GameHall-Client模块启动登录界面的时候,就会到客户端的game目录去加载所有的游戏客户端的包,并读取这些包中的MANIFEST.MF文件中的Game-Class属性,这样才会向登录界面的游戏下拉框架中加入一个新的游戏,登录界面的具体效果如图15.15所示。
第15章 仿QQ游戏大厅 ?37?
图15.15 登录界面选择游戏
这样,我们的框架就可以支持游戏的加入删除,只需要在crazyit-gamehall-client的game目录下加入一个新的jar包就可以实现加入游戏。
15.9 本章小节
本章开发了一个游戏大厅的框架,该框架主要用于处理服务器与客户端之间的信息传输,并且在些基础上开发了一个五子棋的游戏大厅与一个简单的聊天室。讲解Sokcet编程的相关知识点。在开发五子棋游戏大厅时,主要讲解了五子棋游戏大厅的实现原理。本章的重点是游戏大厅框架的开发,让我们的这个游戏大厅框架可以做到动态的加载游戏,让玩家选择进入的游戏,当加入其他游戏时,该框架并不需要对原来的代码进行修改,按照一定的规则就可以加入新的游戏。本章开发的这个游戏大厅框架,希望能给读者带来一些编程上的启发,开发出更多优秀的基于网络的游戏。