前言
客户端连接服务器,服务器新建客户端坦克添加到坦克ArrayList列表,地图为障碍物的位置,服务器启动时新建初始默认地图,服务器启动时创建一个线程每隔0.5s发送一次地图,坦克列表,子弹列表给客户端同时还创建线程接收客户端发送的指令并进行处理,另有线程对所有游戏对象进行移动碰撞检测
客户端接收地图,坦克,子弹进行渲染并且发送键盘监听,服务器接受并控制客户端对应的坦克进行移动和发送子弹,服务器进行冲突检测,子弹打到非己方坦克,非己方坦克掉血,打到障碍物,障碍物消失
一、坦克类
实现启动客户端显示地图及坦克,第二个客户端登录两个客户端显示两个坦克并通过键盘上下左右和空格控制坦克移动和发送子弹
坦克类属性,x,y表示坦克位置,speed表示tank速度,director为移动方向,width和height为坦克图片显示的宽高,alive为坦克的生命值,子弹列表为当前坦克发出的子弹。move方法为坦克根据方向移动的方法,addBullet方法添加子弹
Tank.java
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
public class Tank implements Serializable {
private static final long serialVersionUID = 3L;
transient List<Bullet> bulletList = new ArrayList<>();
//坦克的位置 速度 及方向
int x ;
int y ;
int speed;
int director ;
int alive ;
int width = 40 ;
int height = 40 ;
public Tank(int x, int y) {
this.x = x;
this.y = y;
this.director = 1 ;
this.alive = 100;
}
public List<Bullet> getBulletList() {
return bulletList;
}
public int getAlive() {
return alive;
}
public void setAlive(int alive) {
this.alive = alive;
}
public int getWidth() {
return width;
}
public void setWidth(int width) {
this.width = width;
}
public int getHeight() {
return height;
}
public void setHeight(int height) {
this.height = height;
}
public Tank(){
x = 100 ;
y = 100 ;
speed = 5;
this.director = 1 ;
this.alive = 100;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public int getSpeed() {
return speed;
}
public int getDirector() {
return director;
}
public void setDirector(int director) {
this.director = director;
}
public void move(int director){
this.director = director ;
switch(director){
//1为上
case 1:
y -= speed;
break;
//2为下
case 2:
y+=speed ;
break;
//3为左
case 3:
x-= speed;
break;
//4为右
case 4:
x+=speed;
break;
}
}
public void addBullet(Bullet bullet){
bulletList.add(bullet);
}
二、客户端类
发送请求连接服务器,每200ms接收服务器发送过来的地图、坦克及子弹并进行渲染
发送监听到的键盘事件给服务器,服务器同步坦克的移动
import javax.swing.*;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket;
import java.util.List;
public class GameFrame extends JFrame {
private static final String ADDRESS = "localhost";
private static final int PORT = 6789;
GamePanel panel ;
public GameFrame(){
panel = new GamePanel();
setContentPane(panel);
setTitle("坦克大战");
setSize(800,600);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setVisible(true);
}
public GamePanel getPanel() {
return panel;
}
//客户端渲染坦克 地图 子弹
public void show(List<Tank> tanks , List<Bullet> bullets,Map map ){
System.out.println("showTank");
panel.setTanks(tanks);
panel.setBullets(bullets);
panel.setMap(map);
panel.paintComponent(panel.getGraphics());
}
public static void main(String[] args) {
GameFrame gf = new GameFrame();
gf.setFocusableWindowState(true);
try {
Socket s = new Socket(ADDRESS,PORT);
ObjectInputStream ois = new ObjectInputStream(s.getInputStream());
ObjectOutputStream oos = new ObjectOutputStream(s.getOutputStream());
gf.panel.addKeyListener(new KeyListener() {
@Override
public void keyTyped(KeyEvent e) {
// 处理键入事件
}
@Override
public void keyPressed(KeyEvent e) {
int code = e.getKeyCode();
try{
System.out.println("客户端code:"+code);
switch (code) {
case KeyEvent.VK_UP:
oos.writeObject(new Integer(1));
break;
case KeyEvent.VK_LEFT:
oos.writeObject(new Integer(3));
break;
case KeyEvent.VK_DOWN:
oos.writeObject(new Integer(2));
break;
case KeyEvent.VK_RIGHT:
oos.writeObject(new Integer(4));
break;
case KeyEvent.VK_SPACE:
oos.writeObject(new Integer(0));
break;
}
}catch(IOException ioException ){
ioException.printStackTrace();
}
}
@Override
public void keyReleased(KeyEvent e) {
}
});
while(true){
Map map = (Map) ois.readObject();
List<Tank> tanks = (List<Tank>)ois.readObject();
List<Bullet> bullets = (List<Bullet>) ois.readObject();
System.out.println(tanks);
gf.show(tanks,bullets,map);
Thread.sleep(200);
}
} catch (IOException ioException) {
ioException.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
三、服务器
接受客户端的连接,发送最新坦克、地图、子弹对象给客户端
接受客户端的键盘事件,移动坦克、发送子弹
进行冲突检测,子弹打到除自己外的其他坦克,其他坦克掉血,子弹打到非空白处,地图移除该元素
上述三个功能分别由三个线程进行处理
1.Server.java
包含接受客户端发送数据和发送服务器数据线程
package V3;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class Server {
//服务器监听端口
private static final int PORT =6789;
volatile static List<Tank> tanks = Collections.synchronizedList(new ArrayList<>());
volatile static List<Bullet> bullets = Collections.synchronizedList(new ArrayList<>());
volatile static Map map = new Map(80,80);
public static void main(String[] args) {
try {
ServerSocket ss = new ServerSocket(PORT);
while(true){
Socket s = ss.accept();
System.out.println(s);
Tank tank = new Tank();
tanks.add(tank);
new Thread(new HandleTankTh(s,tanks,bullets,map)).start();
new Thread(new HandleKeyListener(s,tank,bullets)).start();
new Thread(new ConflictTh(tanks,bullets,map)).start();
}
} catch (IOException ioException) {
ioException.printStackTrace();
}
}
}
class HandleTankTh implements Runnable{
private Socket s;
private volatile List<Tank> tanks ;
private volatile List<Bullet> bullets;
private Map map;
public HandleTankTh(Socket s , List<Tank> tanks , List<Bullet> bullets,Map map ) {
this.s = s ;
this.tanks = tanks ;
this.bullets = bullets;
this.map = map ;
}
public Map getMap() {
return map;
}
public void setMap(Map map) {
this.map = map;
}
@Override
public void run() {
System.out.println("tanks="+tanks);
ObjectOutputStream os = null;
try {
os = new ObjectOutputStream(s.getOutputStream());
} catch (IOException ioException) {
ioException.printStackTrace();
}
while(true){
try {
// System.out.println("x="+tank.x+"y="+tank.y);
// System.out.println("服务器向客户端:"+s.getInetAddress().getHostName()+":"+s.getPort()+"发送tanks"+tanks);
if(bullets.size() != 0 ){
for (int i = 0; i < bullets.size(); i++) {
bullets.get(i).move();
}
}
List<Tank> tankList = new ArrayList<>();
List<Bullet> bulletList = new ArrayList<>();
tankList.addAll(tanks);
bulletList.addAll(bullets);
Thread.sleep(500);
os.reset();
os.writeObject(map);
os.writeObject(tankList);
os.writeObject(bulletList);
os.flush();
} catch (IOException | InterruptedException ioException) {
ioException.printStackTrace();
}
}
}
}
class HandleKeyListener implements Runnable{
private Socket s;
private Tank tank ;
private volatile List<Bullet> bullets ;
public HandleKeyListener(Socket s, Tank tank , List<Bullet> bullets ) {
this.s = s;
this.tank = tank;
this.bullets = bullets ;
}
@Override
public void run() {
ObjectInputStream oi = null;
try {
oi = new ObjectInputStream(s.getInputStream());
} catch (IOException ioException) {
ioException.printStackTrace();
}
while(true){
try {
int key = (Integer)oi.readObject();
// System.out.println("服务器接收到的键盘code为:"+director);
if(key != -1){
if(key == 0){
System.out.println("发送子弹!");
//子弹初始化的位置根据坦克的方向决定
int director = tank.getDirector();
int width = tank.getWidth();
int height = tank.getHeight();
int x = tank.getX();
int y = tank.getY();
switch(director){
//向上
case 1 :
x += width/2;
break;
//向下
case 2:
y += height;
x += width/2;
break;
//向左
case 3:
y += height/2;
break;
case 4:
x += width ;
y += height/2;
break;
}
//发送子弹
Bullet bullet = new Bullet(x,y,tank.getDirector(),tank.getSpeed());
tank.addBullet(bullet);
bullets.add(bullet);
}else{
//坦克移动
tank.move(key);
}
}
} catch (IOException | ClassNotFoundException ioException) {
ioException.printStackTrace();
}
}
}
}
2.ConflictTh.java
冲突检测线程类
Map地图中存储一个int类型二维数组loc,0表示无障碍物,冲突检测时获取地图障碍物,判断子弹是否进入障碍物范围,进入则将该位置的值设置为0。注意二维数组横向坐标为j,,即障碍物x的范围根据j来计算,不要把i,j弄反了。本人正好就踩了这个坑。
package V3;
import java.util.List;
public class ConflictTh implements Runnable{
//冲突检测
volatile List<Bullet> bullets ;
volatile List<Tank> tanks ;
Map map ;
public ConflictTh( List<Tank> tanks ,List<Bullet> bullets ,Map map){
this.bullets = bullets ;
this.tanks = tanks ;
this.map = map ;
}
@Override
public void run() {
while(true){
//子弹打到tank,tank生命值下降
for (int i = 0; i < tanks.size(); i++) {
Tank t = tanks.get(i) ;
int x = t.getX();
int y = t.getY();
List<Bullet> mybullets = t.getBulletList();
for (int j = 0; j < bullets.size(); j++) {
Bullet b = bullets.get(j);
if(mybullets.contains(b) || t.getAlive() == 0) continue;
if(b.getX() <= x+t.getWidth() && b.getX() >= x && b.getY() >= y && b.getY() <= y+t.getHeight()){
t.setAlive(t.getAlive()-10);
System.out.println("tanki"+"的生命值为"+t.getAlive());
}
}
if(t.getAlive() == 0){
tanks.remove(t);
}
}
//更新map
int[][] loc = map.getLoc();
int width = map.getWidth();
int height = map.getHeight();
for (int i = 0; i <loc.length ; i++) {
for (int j = 0; j < loc[0].length; j++) {
if(loc[i][j] == 0 ) continue;
for (int k = 0; k < bullets.size(); k++) {
Bullet b = bullets.get(k);
if(b.getX() < (j+1) * width && b.getX() > j * width && b.getY() < (i+1) * height && b.getY() > i * height){
loc[i][j] = 0 ;
}
}
}
}
map.setLoc(loc);
}
}
}
问题总结
- 使用对象流发送tank列表对象时,tank对象位置数据已更新,但客户端接收到的还是最初的tank对象,表现为客户端的tank是静止的不能移动
ObjectOutputStream的writeObject只会发送最初的对象
解决:在调用writeObject方法时,每次发送一个新对象,两种方案,创建一个新list,将tankList中的tank放入新list中;创建一个新list,新建一个tank对象,将原来tank中属性赋值给新tank对象,将新tank添加到新list中。本次采用第一种方法,由于采用了第一种,引发了第三个问题。 - 若把给panel添加监听放在while循环中,则每按一次按键,坦克会不断移动。实现不了理想的按一下走一步的效果
若把panel添加监听放在循环外,服务器在一个线程中同时处理接收键盘事件信息和发送信息给客户端,由于键盘事件不是一直都有,因此发送信息给客户端无法执行,会阻塞读取客户端键盘监听。现象为客户端不能实时同步坦克的移动,第二个客户端登录时,只有按下键盘后才能同步第一个客户端坦克的移动。
解决:客户端将添加监听放在循环外,服务器起两个线程分别处理读和写,将读取键盘监听单独放在一个线程中处理。 - 连续发送子弹时,服务器writeObject(tankList)行报错java.util.ConcurrentModificationException。这个运行时异常一般是在对遍历list的同时,对list进行了修改等操作导致的。ObjectOutputStream的writeObject方法会调用ArrayList的writeObject方法,ArrayList中的writeObejct会先获取modCount,判断是否需要扩容,然后遍历列表,对列表中的元素进行序列化。遍历完后再获取modCount,,若与遍历前的不一致,则抛出ConcurrentModificationException
java.util.ArrayList.writeObject(ArrayList.java:766) s.writeObject(elementData[i]);
由于TankList是在线程中new的,且对tank调用addBullet方法不会导致tankList中的modCount增加,因此不是tanklist报的错
tank中有一个bulletList,将处理键盘监听线程中的tank.addBullet()方法去掉后发射子弹不再报错,因此定位为bulletList的错误
当调用bulletList中的writeObject方法时,处理键盘监听的线程还在不断的向bulletlList中添加子弹,因此报错
Exception in thread "Thread-3" java.util.ConcurrentModificationException
at java.util.ArrayList.writeObject(ArrayList.java:770)
at sun.reflect.GeneratedMethodAccessor1.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at java.io.ObjectStreamClass.invokeWriteObject(ObjectStreamClass.java:1128)
at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1496)
at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1432)
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1178)
at java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1548)
at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1509)
at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1432)
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1178)
at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
at java.util.ArrayList.writeObject(ArrayList.java:766)
at sun.reflect.GeneratedMethodAccessor1.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at java.io.ObjectStreamClass.invokeWriteObject(ObjectStreamClass.java:1128)
at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1496)
at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1432)
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1178)
at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
at V3.HandleTankTh.run(Server.java:85)
at java.lang.Thread.run(Thread.java:748)
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject();
// Write out size as capacity for behavioural compatibility with clone()
s.writeInt(size);
// Write out all elements in the proper order.
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
解决:
1.tank中的子弹列表作用:冲突检测时判断是否为自己的子弹,因此不需要发送给客户端,可给tank中的子弹列表添加transient关键字避免序列化
transient List<Bullet> bulletList = new ArrayList<>();
2.给tank.addBullet() 和writeObject(tankList)两个操作添加锁,但是加锁会出现发射子弹稍微有点慢的问题
synchronized (Tank.class){
tank.addBullet(bullet);
}
synchronized (Tank.class){
os.writeObject(tankList);
}