欢迎光临
我们一直在努力

Java实现Socket网络对战游戏(一)

本节目标:

实现多界面操控功能,且不同用户端能够看到彼此的运动轨迹

目前我们处于初步阶段,所以暂时使用小球图形代替玩家对象(完整代码放在绑定资源里了)

一、思路

在写代码之前,我们先构思一下实现这些功能需要哪些类

让特定的类去实现特定的功能,可以让结构更清晰,便于后续修改、优化以及增添一些功能

首先,客户端Client和服务端Server肯定是少不了的,服务端我们就用于处理小球的移动,服务端处理完之后发送帧动画坐标到客户端,然后在客户端界面渲染出来

为了方便管理,小球的相关参数(颜色、速度……)以及运动逻辑函数统一封装到Ball球类里,这个球类是用于处理运动指令的,主要在服务端跑,后面我们客户端还需要渲染,所以客户端也需要一个球类ball,为了便于区分,我们给它命名为小球类ball(客户端单独建一个包,这样名字不会冲突结构也更清晰)

由于每个小球都需要绑定一个socket,所以我们可以单独创建一个用户信息类ClientDO来存储这些信息(当然也可以直接存在ball类里,只不过是每次创建对象的时候需要多传一个参数而已)

最后还需要一个监听类ClientListener用于监听键盘操作

结构如下

具体功能我们下面来一步步实现

二、代码实现

首先我们先设置球类ball相关参数方法,初步定义其含有坐标x,y、速度speedx,speedy,尺寸size以及颜色color

构造初始化函数,颜色随机,其他参数由外部传入,然后定义一些想移动逻辑一类的基本方法

public class Ball {
int x, y, size, speedx, speedy;
Color color;
Random ran = new Random();

public Ball(int x, int y, int size, int speedx, int speedy) {
this.x = x;
this.y = y;
this.size = size;
this.speedx = speedx;
this.speedy = speedy;
color = new Color(ran.nextInt(Integer.MAX_VALUE));
}

public int getX() {
return x;
}

public void setX(int x) {
this.x = x;
}

public int getY() {
return y;
}

public void setY(int y) {
this.y = y;
}

public int getSize() {
return size;
}

public void setSize(int size) {
this.size = size;
}

public int getSpeedx() {
return speedx;
}

public void setSpeedx(int speedx) {
this.speedx = speedx;
}

public int getSpeedy() {
return speedy;
}

public void setSpeedy(int speedy) {
this.speedy = speedy;
}

// 功能方法
public void move() {
x += speedx;
y += speedy;
}

// 上下左右移动控制
public void up() {
speedy = -5;
}

public void down() {
speedy = 5;
}

public void left() {
speedx = -5;
}

public void right() {
speedx = 5;
}

public void stop() {
speedx = 0;
speedy = 0;
}
}

然后补全用户列表类ClientDO

这里在构造函数内设置小球的随机位置

public class ClientDO {
Socket socket;
Ball ball;
Random ran = new Random();

public ClientDO(Socket socket) {
this.socket=socket;
ball = new Ball(ran.nextInt(800), ran.nextInt(700), 50, 0, 0);
}
}

接着我们先在服务端创建一个服务器套接字serversocket(定义为全局变量),设置好端口号并封装到初始化函数init()中

ServerSocket serverSocket = null;
public void init() {
try {
serverSocket = new ServerSocket(8888);
} catch (Exception e) {
throw new RuntimeException(e);
}

}

接着封装监听函数listen(),如果有新的连接就调用accept()函数分配socket,并把其存入新创建的ClientDO对象中

这里我们设置了两个全局变量,一个哈希表clientList(ClientDO类型),一个动态数组balllist(ball类型)(也可以只设置一个ClientList,但为了方便遍历调用所以定义两个)

然后把CDO以及其自动生成的ball存入两个数组中即可

最后调用read(ClientDO cdo)函数启动客户端操作读取

因为要一直随时监听可能接入的客户端连接,所以需要单开一个线程,循环持续监听

public void listen() {
new Thread(() -> {
while (true) {
try {
Socket socket = serverSocket.accept();
System.out.println("有新的连接:" + socket.getInetAddress() + "(" + socket.getPort() + ")");
ClientDO cdo = new ClientDO(socket);
clientList.put("" + socket.getPort(), cdo);//转化成字符串
ballList.add(cdo.ball);
read(cdo);//启动客户端读取
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}).start();
}

接下来补全read()函数

首先先获取操作对象的输入流,用来读取用户操作信息

InputStream is = null;
try {
is = cdo.socket.getInputStream();
} catch (IOException e) {
throw new RuntimeException(e);
}

然后循环读取操作

这里需要注意,当用户断开连接时,要及时将两个数组内存储的相关信息移除,否则会有幽灵球一直存在

while (true) {
try {
int key = is.read();
switch (key) {
case 1:
cdo.ball.up();
break;
case 2:
cdo.ball.down();
break;
case 3:
cdo.ball.left();
break;
case 4:
cdo.ball.right();
break;
default:
cdo.ball.stop();
break;
}
} catch (IOException e) {
//throw new RuntimeException(e);
// 客户端断开了
// 1. 从 clientList 移除
clientList.remove("" + cdo.socket.getPort());

// 2. 从 ballList 移除这个球
ballList.remove(cdo.ball);

System.out.println("客户端断开:" + cdo.socket.getPort());
break; // 结束线程
}
}

补充一点,因为read函数里面有while死循环,所以需要套个新线程,否则会一直卡死在第一个客户端连接后,无法进行下一步操作也无法有新的连接

tips.

由于删球删除的是本身存储的地址,是指向球的引用,所以两个数组都需要删除相关信息,需要注意的是改球改的是地址指向的对象本身,因为如果我添加的是同一个对象,那么二者指向的地址就相同,所以修改其中一个数组时另外一方所指向的对象也会同样改变

接下来写游戏服务端的的主进程gameLoop()

它主要是负责让小球动起来 + 把所有小球的位置广播给所有客户端,让大家看到同一个画面

首先是帧率设置

try {
Thread.sleep(30);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}

然后控制小球移动,并把帧动画相关信息发送到客户端(消息协议在注释里)

for (int i = 0; i < ballList.size(); i++) {
Ball ball = ballList.get(i);
ball.move();
}
//发送当前帧数据包
// 1#x#y#colorRGB:2#x#y#colorRGB:3#x#y#colorRGB
String data = "";
for (int i = 0; i < ballList.size(); i++) {
Ball ball = ballList.get(i);
data += "1#" + ball.getX() + "#" + ball.getY() + "#" + ball.color.getRGB() + ":";
}
send(data);// 发送数据包 广播给所有的客户端

同样,有while所以开新线程(后面都一样,就不多说了)

然后我们补全发送send(String data)函数

先从clientList函数里把clientDO一个个取出来,调用里面的输出流,然后把data信息转成字节传出去(消息协议是 长度+信息)

public void send(String data) {
//从一个叫 clientList 的 Map 里,把所有的 key(键)取出来,放到一个 Set<String> 集合里,赋值给变量 keys。
Set<String> keys = clientList.keySet();
for (String key : keys) {
ClientDO clientDO = clientList.get(key);
try {
OutputStream os = clientDO.socket.getOutputStream();
os.write(data.getBytes().length);
os.write(data.getBytes());
os.flush();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

最后补全主函数

public static void main(String[] args) {
Server server = new Server();
server.init();
server.listen();//连接
server.gameLoop();
}

这样server服务端就写完了

接着先把客户端的小球ball类写好,这里和上面球类没什么太大区别,只不过是函数不同,这里只需要一个渲染函数draw,将小球画出来即可

public class Ball {
int x,y,size,speedx,speedy,rgb;

public Ball(int x, int y, int size, int rgb){
this.x = x;
this.y = y;
this.size = size;
this.speedx = speedx;
this.speedy = speedy;
this.rgb = rgb;
}

public void setSpeedx(int speedx) {
this.speedx = speedx;
}
public void setSpeedy(int speedy) {
this.speedy = speedy;
}
public void draw(Graphics g){
g.setColor(new Color(rgb));
g.fillOval(x,y,size,size);
}
}

然后我们去处理客户端

首先需要先和服务端创建连接,在初始化函数init内封装

public void init() {
try {
socket = new Socket("127.0.0.1", 8888);
//login(socket.getOutputStream());
showUI();
} catch (Exception e) {
throw new RuntimeException(e);
}
}

然后创建可视化面板,并为其添加监听器,最后调用showGame(InputStream is, Graphics g)渲染函数

public void showUI() {
JFrame jf = new JFrame("网络游戏");
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jf.setSize(800, 800);
jf.setLocationRelativeTo(null);

jf.setVisible(true);

ClientListener cl = new ClientListener();
jf.addKeyListener(cl);//给面板添加键盘监听器,只要监听到动作就启用cl里的监听规则
try {
cl.os = socket.getOutputStream();
showGame(socket.getInputStream(),jf.getGraphics());// 游戏画面渲染 客户端显示画面
} catch (IOException e) {
throw new RuntimeException(e);
}
}

下面补全showGame渲染函数

将读取到的数据切分储存读取,传入ball并绘画

public void showGame(InputStream is, Graphics g) {
new Thread(() -> {
while (true) {
try {
int len = is.read();
byte[] data = new byte[len];
is.read(data);
String msg = new String(data);
//解析数据包
String[] balls = msg.split(":");

int dataNum = balls.length;
// 背景板
g.setColor(Color.WHITE);
g.fillRect(0, 0, 800, 800);

for (int i = 0; i < dataNum; i++) {

String ballStrs = balls[i];
String[] ballStr = ballStrs.split("#");
int x = Integer.parseInt(ballStr[1]);
int y = Integer.parseInt(ballStr[2]);
int rgb = Integer.parseInt(ballStr[3]);
Ball ball = new Ball(x, y, 60, rgb);
ball.draw(g);
}

} catch (Exception e) {
throw new RuntimeException(e);
}
}
}).start();
}

最后写主函数

public static void main(String[] args) {
Client client = new Client();
client.init();
}

做完这些,还剩一个监听类ClientListener,下面我们把它补全

监听类需要继承键盘监听器接口KeyListener,主要作用是将键盘操作转化为便于判断操作的数字信息,然后发送给服务端

public class ClientListener implements KeyListener {

//Socket socket;
OutputStream os;

@Override
public void keyTyped(KeyEvent e) {

}

@Override
public void keyPressed(KeyEvent e) {
int keyCode=e.getKeyCode();
System.out.println("动了");
int code=0;
switch (keyCode){
case KeyEvent.VK_UP:
code=1;
break;
case KeyEvent.VK_DOWN:
code=2;
break;
case KeyEvent.VK_LEFT:
code=3;
break;
case KeyEvent.VK_RIGHT:
code=4;
break;
}
try {
os.write(code);
os.flush();
System.out.println("发送"+keyCode+" : "+code);
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}

@Override
public void keyReleased(KeyEvent e) {

}
}

到此,我们便实现了本节所有任务

下章节继续拓展更多内容……

赞(0)
未经允许不得转载:171主机测评 » Java实现Socket网络对战游戏(一)
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址