疯狂java实例-第15章 仿QQ游戏大厅

更新时间:2023-04-24 09:15:01 阅读量: 实用文档 文档下载

说明:文章内容仅供预览,部分内容可能不全。下载后的文档,内容与下面显示的完全一致。下载之前请确认下面内容是否您想要的,是否完整无缺。

疯狂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的输出流中。既然每个操作都如此,我们可以编写一个游戏大厅的小框架,专门用于中转,框架中的服务器端与客户端的代码都不需要编写任何的业务逻辑,如果编写了新的游戏,可以将该游戏的包放到框架中运行,当然,这些游戏都必须遵守框架的规则。

15.2.1 确定传输格式

我们已经知道了服务器间通过PrintStream来向输出流打印字符串信息,那么我们可以先确定这些字符串的格式。本章中使用XML作为它们之间的转输格式,例如服务器向客户端输出信息的时候,可以将一段XML打印到输出流中,客户端得到这些XML字段串后,就将其转化为特定的对象,再根据这些对象进行相应的处理。

当客户端发送一些信息给服务器的时候,我们就把这些信息封装到一个Request对象中,该对象包括参数列表、服务器处理类、客户端处理类等信息,当服务器端接收到该请求对象后,就将这些信息返回一个Response对象,将Request与Response对象当作参数传递给服务器处理类,当服务器处理类完成了服务器的操作后,就将这些信息通过一个Response对象返回给客户端。

代码清单:code\GameHall-Commons\src\org\crazyit\gamehall\commons\Request.java

public class Request { //参数列表

疯狂java实例-第15章 仿QQ游戏大厅

}

private Map<String, Object> parameters; //服务器处理类

private String serverActionClass; //客户端处理类

private String clientActionClass;

代码清单:code\GameHall-Commons\src\org\crazyit\gamehall\commons\Response.java

public class Response { //服务器返回的各个数值 }

private Map<String, Object> 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

疯狂java实例-第15章 仿QQ游戏大厅

//服务器处理请求的接口 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);

疯狂java实例-第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方法即可以将客户请求的字

疯狂java实例-第15章 仿QQ游戏大厅

符串转换成一个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 = er.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所示。

疯狂java实例-第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));

疯狂java实例-第15章 仿QQ游戏大厅

}

以上的黑体代码,使用头像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

er.setSocket(createSocket(this.connectionField.getText(), 12000)); //得到用户所选择的游戏

Game game = (Game)this.gameSelect.getSelectedItem(); game.start(er); //启动线程

ClientThread thread = new ClientThread(er); thread.start();

this.setVisible(false);

以上的代码实现了登录功能,从界面的游戏下拉框中得到具体的某个游戏,由于游戏选择下拉框是

疯狂java实例-第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座标

疯狂java实例-第15章 仿QQ游戏大厅

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的构造器。

代码清单:code\fivechess-commons\src\org\crazyit\gamehall\fivechess\commons\Table.java

//创建桌子对象的时候就创建左右的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++) {

疯狂java实例-第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); //加入到所有玩家中

ers.put(cu.getId(), cu);

疯狂java实例-第15章 仿QQ游戏大厅

}

//将玩家设置到响应中 response.setData("user", cu); //将所有玩家信息设置到响应中

response.setData("users", ers); //将所有的桌子信息返回到客户端

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<String, ChessUser> users = new HashMap<String, ChessUser>();

在服务器上下文中使用一个Map来保存玩家信息,这个Map的key是玩家对象的id,value是具体的某个五子棋玩家对象。玩家通过ChessGame来发送第一次请求给服务器,LoginAction负责处理这一次请求,然后将Response对象转换成XML字符串返回给客户端,接下来,就是客户端处理类(ClientInAction)负责处理这一次服务器响应。服务器响应首先是发送给fivechess-client模块的ClientThread类进行处理,该类同样地负责转发,去寻找具体的某个客户端处理类(ClientAction)的实现类进行处理。ClientInAction接收到服务器的响应后,就需要为刚登录的玩家创建游戏大厅,服务器的响应中已经包括创建游戏大厅所需的各个信息,包括桌子,玩家等。我们暂时不提供实现,下面创建游戏大厅的各个界面。

15.3.4 创建游戏大厅界面

游戏大厅界面在本章对应的是GameHallFrame类,该类在fivechess-client模块中。我们需要做的效果如图15.2所示。

疯狂java实例-第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);

疯狂java实例-第15章 仿QQ游戏大厅

}

}

}

}

//画右边座位的玩家

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所示。

疯狂java实例-第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<Vector> getDatas() { Vector<Vector> result = new Vector<Vector>(); for (ChessUser user : ers) { 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时,也需要将所有的玩家与当前的玩家都作为构造参数传入,这是由于如果进行聊天,就需要让玩家选择聊天的对象,再发送给服务器。

疯狂java实例-第15章 仿QQ游戏大厅

玩家列表与聊天界面已经创建完成,这两个界面组件可以在游戏界面重用,游戏界面的实现将在下面章节讲述。

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<ChessUser> 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", er.getId());

//得到用户的Socket并发送请求, 告诉服务器用户进入了大厅 er.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 = ers.get(userId); //将新玩家信息放到响应中

response.setData("newUser", user); //向所有玩家发送信息

for (String id : ers.keySet()) { ChessUser hasLogin = ers.get(id); //不必发送给自己

疯狂java实例-第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所示。

疯狂java实例-第15章 仿QQ游戏大厅

图15.4 新的玩家进入游戏大厅

在本小节中,我们编写了游戏大厅的各个界面组件与对象,并实现了用户进入游戏大厅的功能,在下面章节,我们将实现游戏大厅的其他功能。

15.4 实现聊天功能

在游戏大厅中,我们提供了一个聊天的界面,玩家可以在上面进行聊天,发送或者接收聊天信息,需要注意的是,聊天界面是可以共用的,除了在游戏大厅中使用聊天界面外,还可以在游戏界面中使用。本小节实现游戏大厅中的聊天功能。

15.4.1 发送聊天信息

在聊天界面(ChatPanel)中,当玩家选择了某个聊天的对象,输入内容再点击发送按钮后,就可以创建一次请求,将该请求发送到服务器,告诉服务器:我对某个人(所有人)发送了聊天内容,请帮我转发。服务器得到这个请求后,就执行某个服务器处理类,该处理类就将聊天内容转发给相应的玩家。

以下是ChatPanel发送聊天内容的方法。

代码清单:code\fivechess-client\src\org\crazyit\gamehall\fivechess\client\ui\ChatPanel.java

//发送信息

public void send() { //得到发送的内容

疯狂java实例-第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", er.getId()); request.setParameter("content", content); //发送请求

er.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 = ers.get(senderId); String content = (String)request.getParameter("content"); if (receiverId == null) { //向所有人发 }

for (String id : ers.keySet()) { if (id.equals(senderId)) continue; ChessUser cu = ers.get(id); response.setData("content", sender.getName() + " 对 所有人 说:" + content);

cu.getPrintStream().println(XStreamUtil.toXML(response)); } } else { //得到接收人 }

ChessUser receiver = ers.get(receiverId); if (receiver.getId().equals(sender.getId())) return;

response.setData("content", sender.getName() + " 对你说:" + content); receiver.getPrintStream().println(XStreamUtil.toXML(response));

SendMessageAction中根据请求的参数来发送相应的信息,需要注意的是,如果接收人id为空的话,那么就意味着向所有人发送聊天内容。

15.4.2 接收聊天信息

接收聊天信息由ReceiveMessageAction负责处理,该类属于客户端处理类,在fivechess-client

疯狂java实例-第15章 仿QQ游戏大厅

模块中,某个玩家得到聊天内容后,就可以获得界面组件,再将这些聊天内容追加到聊天界面组件的文本域中。 代码清单:

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 启动游戏

玩家选择了某一个位置坐下的时候,需要对位置进行判断,看下该位置是否有人,还要对玩家当前的状态进行判断,判断其是否已经坐下了,如果玩家没有坐到任何位置上并且当前所选择的位置没有玩家,就可以坐到位置上并展现游戏界面。

本文来源:https://www.bwwdw.com/article/kmxq.html

Top