Java Maze

本次开发的是一个基于Socket编程、GUI编程、多线程编程等技术的网络程序,实现的是两个玩家联机比赛走迷宫的功能。

设计说明

本程序采用Java程序设计语言,在Eclipse平台下编辑、编译与调试。通过Socket编程技术实现客户端之间、客户端与服务器之间的通信,通过GUI编程技术实现窗口显示,通过多线程技术实现多用户同时使用。

具体实现的功能如下:

  1. 使用基本的Client-Server模型。用户使用Client端进行连接时,将会跳出弹窗提示输入服务器的IP地址和自定义的用户名与Server端连接。如果服务器未开启,将跳出错误提示。

  2. 当两个用户都完成连接之后,显示完整地图,比赛开始。角色将出现在地图右上角,游戏的终点在地图左下角。窗口的左边显示自己的地图,右边显示对方的游戏状态。

  3. 用户可以通过方向键控制角色移动,角色不能穿越障碍物。

  4. 比赛过程中可以看到对方的当前位置、移动信息、比赛进度等。

  5. 当有一方先到达终点,比赛结束。胜负方可以看到各自的胜负信息。

总体设计

功能模块设计

本程序需实现的主要功能有:

  1. 使用Socket进行通信,服务器开启之后,客户端可以申请连接。
  2. 客户端与服务器之间实现信息交换。
  3. 使用GUI显示对手以及自己的游戏界面。
  4. 多线程操作,服务器可以同时管理多个客户端

程序的总体功能如图1所示:

img

图1 总体功能图

流程图设计

程序总体流程如图2所示:

img

图2 总体流程图

详细设计

GUI图形显示

在本项目中,使用java的图形界面组件来实现地图和角色的显示、角色位置的刷新以及弹窗的显示。要注意的是,在JFrame组件的显示时,在相同位置上进行组件叠加时,只能够显示添加的第一个组件,这给角色的位置刷新带来一定难度和格式限制。

其具体实现如下所示:

地图的显示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static JFrame frame = new JFrame("test");  
static JPanel mainScreen=new JPanel();
static JLabel player, map_label, bound, map2, player2;

static ImageIcon player_img = new ImageIcon("img\\player.gif");
static ImageIcon map_img = new ImageIcon("img\\map.png");
static ImageIcon bound_img = new ImageIcon("img\\bound.png");

frame.setBounds(100,100,GAME_WIDTH, GAME_HEIGHT);
mainScreen.setLayout(null);
map_label = new JLabel();
map_label.setBounds(0, 0, 800, 800);
setIcon(map_img, map_label);
mainScreen.add(map_label);
bound = new JLabel();
bound.setBounds(800, 0, 100, 800);
setIcon(bound_img, bound);
mainScreen.add(bound);

frame.add(mainScreen);
frame.setVisible(true);
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);

角色位置的刷新:

1
2
3
4
5
6
7
8
9
player = new JLabel();  
player.setBounds(player_posi*40, player_posj*40, 40, 40);
setIcon(player_img, player);
mainScreen.add(player);

player.setLocation(posi*40, posj*40);
setIcon(player_img, player);
mainScreen.remove(map_label);
mainScreen.add(map_label);

弹窗显示:

在本次项目中,使用弹窗来输入连接的IP和用户名信息,并在游戏结束之后使用弹窗来提示游戏结果。实现方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
JOptionPane.showMessageDialog(frame, "YOU WIN!!", "Message", JOptionPane.INFORMATION_MESSAGE);  

class ConDialog extends Dialog{
Button b = new Button("connect to server");
TextField serverIP = new TextField("127.0.0.1", 15);//服务器的IP地址
TextField userName = new TextField("", 8);

public ConDialog() {
super(Client.this, true);
this.setLayout(new FlowLayout());
this.add(new Label("server IP:"));
this.add(serverIP);
this.add(new Label("user name:"));
this.add(userName);
this.add(b);
this.setLocation(500, 400);
this.pack();
this.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
setVisible(false);
System.exit(0);
}
});
b.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
String IP = serverIP.getText().trim();
nc.connect(IP);
setVisible(false);
}
});
}
}

游戏逻辑

实现迷宫的游戏逻辑较为简单,将地图信息使用二维数组进行存储。在角色进行移动时,读取角色下一个位置的地图信息,如果存在障碍物,将无法通过,否则将能够继续通过。在角色进行移动之后,在窗口中刷新角色位置。如果先于对方角色移动到终点,则获胜,游戏结束。

Socket连接

网络相关的模块分成两部分,一部分是连接,另一部分是通信。

连接部分,客户端填写要连接的IP地址,作为TCP报文段中的字段,然后通过TCP连接上服务器, 并把自己的UDP端口号发送给服务器。服务器通过TCP和客户端连上后收到客户端的UDP端口号信息, 并将客户端的IP地址和UDP端口号封装成一个Client对象, 保存在容器中。

因为服务器收到链路层帧后会提取出网络层数据报, 源地址的IP地址在IP数据报的首部字段中, Java对这一提取过程进行了封装, 所以我们能够直接在Java的api中获取源地址的IP。

服务器封装完Client对象后, 为客户端的主机分配一个id号, 这个id号将用于往后游戏的网络传输中进行标识。

同时服务器也会把自己的UDP端口号发送客户端, 因为服务器自身会开启一条UDP线程, 用于接收转发UDP包。

客户端收到坦克id后设置到自己的主战坦克的id字段中. 并保存服务器的UDP端口号。

这部分的实现方式如下:

客户端部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public void connect(String ip){  
serverIP = ip;
Socket s = null;
try {
ds = new DatagramSocket(UDP_PORT);//创建UDP套接字
try {
s = new Socket(ip, Server.TCP_PORT);//创建TCP套接字
}catch (Exception e1){
client.getServerNotStartDialog().setVisible(true);
}
DataOutputStream dos = new DataOutputStream(s.getOutputStream());
dos.writeInt(UDP_PORT);//向服务器发送自己的UDP端口号
DataInputStream dis = new DataInputStream(s.getInputStream());
int id = dis.readInt();//获得自己的id号
this.serverUDPPort = dis.readInt();//获得服务器转发客户端消息的UDP端口号
client.getMe().setId(id);//设置坦克的id号
System.out.println("connect to server successfully...");
} catch (IOException e) {
e.printStackTrace();
}finally {
try{
if(s != null) s.close();
} catch (IOException e) {
e.printStackTrace();
}
}

new Thread(new UDPThread()).start();//开启客户端UDP线程, 向服务器发送或接收游戏数据

connectMsg msg = new connectMsg(client.getMe());
send(msg);
}

服务器部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public void start(){  
new Thread(new UDPThread()).start();
ServerSocket ss = null;
try {
ss = new ServerSocket(TCP_PORT);//在TCP欢迎套接字上监听客户端连接
System.out.println("Server has started...");
} catch (IOException e) {
e.printStackTrace();
}

while(true){
Socket s = null;
try {
s = ss.accept();//给客户但分配专属TCP套接字
System.out.println("A client has connected...");
DataInputStream dis = new DataInputStream(s.getInputStream());
int UDP_PORT = dis.readInt();//记录客户端UDP端口
Client client = new Client(s.getInetAddress().getHostAddress(), UDP_PORT, ID);//创建Client对象
clients.add(client);//添加进客户端容器

DataOutputStream dos = new DataOutputStream(s.getOutputStream());
dos.writeInt(ID++);//向客户端分配id号
dos.writeInt(Server.UDP_PORT);//告诉客户端自己的UDP端口号
}catch (IOException e) {
e.printStackTrace();
}finally {
try {
if(s != null) s.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

Socket通信

Socket通信通过自定义应用层的协议实现,每个应用层协议有消息类型和消息数据两个部分组成,不同的协议数据格式不同。

在本项目中,总共有四种传递协议,分别对应游戏逻辑中的连接、移动、获胜(其中连接部分需要两个协议,新加入的玩家通知已存在的玩家,已存在的玩家给新玩家提供响应,从而分别在两者的地图上添加对方的信息)。

在本项目中,设计Msg接口,定义应用层协议的格式,每个协议在此基础上定义具体的实现类,通过多态进行实现。

Msg接口的定义如下:

1
2
3
4
5
6
7
8
9
public interface Msg {  
public static final int CONNECT_MSG = 1;
public static final int MOVE_MSG = 2;
public static final int WIN_MSG = 3;
public static final int CONNECT_TO_ORI_MSG = 4;

public void send(DatagramSocket ds, String IP, int UDP_Port);
public void parse(DataInputStream dis);
}

连接部分的协议,新用户发送的连接信息用connectMSG类进行实现,消息数据包含用户id、用户名字段。在新用户连接成功后进行发送

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public void send(DatagramSocket ds, String IP, int UDP_Port){  
System.out.println("connect_msg_sent");
ByteArrayOutputStream baos = new ByteArrayOutputStream(88);
DataOutputStream dos = new DataOutputStream(baos);
try {
dos.writeInt(msgType);
dos.writeInt(me.getId());
dos.writeUTF(me.getName());
} catch (IOException e) {
e.printStackTrace();
}
byte[] buf = baos.toByteArray();
try{
DatagramPacket dp = new DatagramPacket(buf, buf.length, new InetSocketAddress(IP, UDP_Port));
ds.send(dp);
} catch (IOException e) {
e.printStackTrace();
}
}

public void parse(DataInputStream dis){
try{
int id = dis.readInt();
if(id == this.client.getMe().getId()){
return;
}
System.out.println("connect_msg_received");
String Name = dis.readUTF();
client.getMe().generatePlayer2(18, 1);
connectToOriMsg msg = new connectToOriMsg(client.getMe());
client.getNc().send(msg);
} catch (IOException e) {
e.printStackTrace();
}
}

已有用户的响应信息用connectToOriMsg进行实现,协议格式实现方式与connectMsg类似,包含用户id和用户名两个字段,在已有用户收到新用户的连接信息之后发送。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public void send(DatagramSocket ds, String IP, int UDP_Port){  
System.out.println("connect_ori_sent");
ByteArrayOutputStream baos = new ByteArrayOutputStream(88);
DataOutputStream dos = new DataOutputStream(baos);
try {
dos.writeInt(msgType);
dos.writeInt(me.getId());
dos.writeUTF(me.getName());
} catch (IOException e) {
e.printStackTrace();
}

byte[] buf = baos.toByteArray();
try{
DatagramPacket dp = new DatagramPacket(buf, buf.length, new InetSocketAddress(IP, UDP_Port));
ds.send(dp);
} catch (IOException e) {
e.printStackTrace();
}
}

public void parse(DataInputStream dis){
try{
int id = dis.readInt();
if(id == this.client.getMe().getId()){
return;
}
String Name = dis.readUTF();
System.out.println("connect_ori_received");
client.getMe().generatePlayer2(18, 1);
} catch (IOException e) {
e.printStackTrace();
}
}

移动的信息通过MoveMsg协议进行传输,每个用户在操纵角色进行移动之后,要像对手发送信息报告角色的新位置,并在对方的地图上刷新角色位置。该协议数据包含用户id、角色新位置的水平、垂直坐标。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@Override  
public void send(DatagramSocket ds, String IP, int UDP_Port) {
System.out.println("["+id+"]"+"move_msg_sent: "+posi+ " " + posj);
ByteArrayOutputStream baos = new ByteArrayOutputStream(30);
DataOutputStream dos = new DataOutputStream(baos);
try {
dos.writeInt(msgType);
dos.writeInt(id);
dos.writeInt(posi);
dos.writeInt(posj);
} catch (IOException e) {
e.printStackTrace();
}
byte[] buf = baos.toByteArray();
try{
DatagramPacket dp = new DatagramPacket(buf, buf.length, new InetSocketAddress(IP, UDP_Port));
ds.send(dp);
} catch (IOException e) {
e.printStackTrace();
}
}

@Override
public void parse(DataInputStream dis) {
try{
int id = dis.readInt();
if(id == this.client.getMe().getId()){
return;
}

int posi = dis.readInt();
int posj = dis.readInt();
System.out.println("["+id+"]"+"move_msg_received: "+posi+" "+posj);
client.getMe().reGeneratePlayer2(posi, posj);
} catch (IOException e) {
e.printStackTrace();
}
}

传递获胜信息的协议在WinMsg中定义。当一方角色移动到终点时,发送获胜信息,并在当前窗口中弹窗提示获胜。另一方的客户端接收到对方的获胜信息后,弹窗提示失败信息。游戏结束。该协议中的数据仅包含获胜者的用户id。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Override  
public void send(DatagramSocket ds, String IP, int UDP_Port) {
System.out.println("move_win_sent");
ByteArrayOutputStream baos = new ByteArrayOutputStream(88);
DataOutputStream dos = new DataOutputStream(baos);
try {
dos.writeInt(msgType);
dos.writeInt(id);
} catch (IOException e) {
e.printStackTrace();
}

byte[] buf = baos.toByteArray();
try{
DatagramPacket dp = new DatagramPacket(buf, buf.length, new InetSocketAddress(IP, UDP_Port));
ds.send(dp);
} catch (IOException e) {
e.printStackTrace();
}
}

@Override
public void parse(DataInputStream dis) {

try{
int id = dis.readInt();
if(id == this.client.getMe().getId()){
return;
}
System.out.println("win_msg_received");
client.getMe().lose();
} catch (IOException e) {
e.printStackTrace();
}
}

测试与运行

程序运行

在程序代码基本完成后,经过不断的调试与修改,能够完成上述功能。

运行客户端会跳出连接提示输入服务器IP和用户名进行连接:

img

连接成功后客户端和服务器的命令行提示如下所示:

客户端1:

img

客户端2:

img

服务器:

img

服务器端可视化情况如下:

img

程序测试

游戏过程的测试如下所示:

如果服务器未开启将提示连接错误:

img

连接成功后将等待另一个用户连接,此时的游戏界面如下:

img

左边是自己的地图,在对方用户连接成功后,将在右边显示对方的游戏界面,游戏开始的初始状态如下所示:

img

游戏进行一段时间后,左边是自己的游戏状态,右边是对方的游戏状态:

img

率先到达终点的用户将获得获胜提示:

img

与此同时在对方的游戏界面将提示失败信息:

img



完整代码请见github,欢迎star :)