一、前言
2021级的软件体系结构作业是单机版和联机版坦克大战各一个,最后大实验是分布式多车探索地图仿真实验平台,软工和软件体系结构联合验收,坦克大战代码在406mmSKC · GitHub,想要的自己看,大实验代码雏形取自学长,不过学长的比较粗糙,存在很多BUG和性能问题,课程要求也有变化,我进行了大幅修改优化,公开的话不太尊重学长们的知识产权,如果你想要可以私信我(但我不一定看)。注:本实验的代码和设计除部分参考了学长之外,完全由本人独立完成。
下面主要讲一讲需要的知识,面向一点东西都不会的新手,如果你有JAVA编程经验,可以认为本文章对你无用。
1.开发环境及构建工具
我用的是IDEA,构建工具是Maven,其实一直到我写完坦克大战才知道有这么个好用的东西,之前添加依赖项和构建jar包都是自己一个一个文件下载然后手动添加的,具体方法如下。
假如我想添加一个FastJson的依赖,我得先去官网下一个我需要的FastJson的jar包,文件我已经放在github了,在IDEA中,我们点击左上角的“文件”,“项目结构”,“库”,点击上面的加号,如下图所示:
点这个Java,然后选中你下载下来的依赖的jar包就行了,打包jar包也是在这里,点“工件”,把自己需要的依赖拖进去,然后在上方工具栏里的“构建”,构建工件,就可以得到jar包,但是这样做仅限于小程序,稍微大一点的就不建议这么干了,接下来介绍Maven。
使用Maven构建项目之前需要下载,我建议你首先安装一个AI,有不会的直接问AI就行了,我当时就是看AI看会的,在左上角“文件”,“设置”,“插件”,搜索,我使用的是通义灵码,下载之后登录就行了,我这里说的不清楚的部分都可以问AI。
Maven请自己到官网去下,配置完环境变量后,然后在这里
选中你的Maven Home,我这里装了汉化包,好像是Maven主路径来着,时间有点久,我忘了(不会就问AI),配置成功之后你就可以创建Maven项目了。
Maven项目文件夹下有一个叫pom.xml的文件,里面是依赖信息,如果你想装依赖,直接在里面输入就可以,然后在左边的管理器里右键它,重新加载,会自动给你下载,我建议你换成国内源,要不然容易卡住。我这里展示一下我的pom.xml,你如果不会写可以直接让AI给你写一个。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>origin_control</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.7.0</version>
</dependency>
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>3.6.5</version>
</dependency>
<!-- Jackson Core -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.12.5</version>
</dependency>
<!-- Jackson Databind -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.12.5</version>
</dependency>
<!-- Jackson Annotations -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.12.5</version>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20210307</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.8</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version> <!-- 使用最新版本 -->
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>origin_control.printControl</mainClass> <!-- 指定主类 -->
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
我已经安装了打包的依赖,所以在下面的终端里输入 mvn package就可以得到jar包,运行可以放在命令行下输入 java -jar 文件名 参数
代码请看我上面提到的Github链接,运行需要的注意事项已经在仓库里写了,这里不多说了。
二、单机坦克大战简单讲解
2024-12-01:更新一下对单机版坦克大战的讲解,这里仅面向编程新手,做一些入门级的讲解,很多大学完全不在乎计算机系学生代码能力的培养,之前在绿群见到同济的大二课程设计是要求学生组队写一个类似原神的游戏,我深感震惊,意识到自己和985高校学生的代码能力差距已经如天堑一般了,不希望学弟学妹重蹈我的覆辙,我也知道,对于完全没基础的同学来说,就算他有雄心壮志,突然让他写一个小游戏出来玩那也是无比迷茫的,并不是他能力不行,而是无从下手,完全不知道自己该干什么,所以我这里简单讲一讲这个入门级小游戏。
(一)架构
使用MVC架构,分为模型层、视图层、控制层(Model,View,Control)三层结构,视图层负责将游戏画面显示给玩家,并提供交互按钮,不负责处理这些交互得到的数据,控制器负责处理这些交互得到的信息,并将它们转交给模型层,模型层包含游戏逻辑,单位的定义,游戏计算主要在这里进行。
使用这种架构的好处在于结构清晰,便于开发和维护,负责人只需要关注自己层的功能即可,便于复用模型层的一些模型;但是应当注意层与层直接的通信,我写的就不是很好,每次通信都把整个游戏的数据对象传来传去。
(2)游戏单位及所需功能
坦克大战所需要的单位很简单,我方坦克,敌方坦克,炮弹,地图中的草丛和河流。
功能:游戏开始页面:开始游戏,读取存档,退出。
进入游戏:创建敌我坦克,随机生成地图,开炮,草丛隐身,碰撞检测,坦克击毁判定,河流禁止通行,数据统计实时显示,游戏结束判定,中途退出存档。
游戏结束页面:开始新游戏,退出。
(3)写代码
既然已经知道我们需要什么东西了,那就开始入手代码,我们首先从视图层开始(这里建议一边打开AI代码助手一边看着现成的代码理解)
Wardata来自模型层,坦克和炮弹都是调用的自身的绘制方法,我一开始是想在视图层编写绘制坦克的方法的,但是因为模型层那边已经写好了,就懒得改了,这里只是展示思路,如果你有自己的方法可以自行修改。
package cn.edu.ncepu.sa.GameView;
import cn.edu.ncepu.sa.Control.GameSave_and_Load;
import cn.edu.ncepu.sa.Control.GameStart_and_End;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.border.Border;
import javax.swing.BorderFactory;
public class GameStartPanel extends JFrame {
private JButton newGameButton;
private JButton loadGameButton;
private JButton onlineRedButton;
private JButton onlineBlueButton;
private boolean gameStart = false;
public GameStartPanel() {
setTitle("坦克大战 - 开始界面");
setSize(400, 400); // 调整高度以适应新增按钮
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLocationRelativeTo(null);
JPanel contentPane = new JPanel(new BorderLayout());
contentPane.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20));
JLabel titleLabel = new JLabel("Tank War", JLabel.CENTER);
titleLabel.setFont(new Font("Arial", Font.BOLD, 24));
titleLabel.setForeground(new Color(0, 153, 76));
contentPane.add(titleLabel, BorderLayout.NORTH);
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 20, 20));
buttonPanel.setBackground(Color.LIGHT_GRAY);
//ImageIcon newGameIcon = new ImageIcon("path_to_new_game_icon.png");
newGameButton = new JButton("新游戏");
configureButton(newGameButton, new Color(51, 204, 51));
buttonPanel.add(newGameButton);
//ImageIcon loadGameIcon = new ImageIcon("path_to_load_game_icon.png");
loadGameButton = new JButton("读取存档");
configureButton(loadGameButton, new Color(0, 153, 204));
buttonPanel.add(loadGameButton);
// 新增联机对战按钮
//ImageIcon onlineRedIcon = new ImageIcon("path_to_online_red_icon.png"); // 替换为红色联机图标路径
onlineRedButton = new JButton("联机对战(红)");
configureButton(onlineRedButton, new Color(255, 0, 0)); // 红色背景
buttonPanel.add(onlineRedButton);
// ImageIcon onlineBlueIcon = new ImageIcon("path_to_online_blue_icon.png"); // 替换为蓝色联机图标路径
onlineBlueButton = new JButton("联机对战(蓝)");
configureButton(onlineBlueButton, new Color(0, 0, 255)); // 蓝色背景
buttonPanel.add(onlineBlueButton);
contentPane.add(buttonPanel, BorderLayout.CENTER);
setContentPane(contentPane);
setVisible(true);
setupActionListeners();
}
private void configureButton(JButton button, Color bgColor) {
button.setOpaque(true);
button.setBackground(bgColor);
button.setForeground(Color.WHITE);
Border lineBorder = BorderFactory.createLineBorder(Color.BLACK);
Border emptyBorder = BorderFactory.createEmptyBorder(10, 20, 10, 20);
button.setBorder(BorderFactory.createCompoundBorder(lineBorder, emptyBorder));
}
// 示例动作监听器,具体逻辑需您实现
private void setupActionListeners() {
newGameButton.addActionListener(e -> {
System.out.println("开始新游戏...");
gameStart = true;
GameStart_and_End.newStart();
setVisible(false);
});
loadGameButton.addActionListener(e -> {
System.out.println("读取存档...");
GameSave_and_Load.clickLoad();
gameStart = true;
setVisible(false);
});
onlineRedButton.addActionListener(e -> {
System.out.println("联机对战(红) 功能待实现...");
// 实现联机对战(红)逻辑
});
onlineBlueButton.addActionListener(e -> {
System.out.println("联机对战(蓝) 功能待实现...");
// 实现联机对战(蓝)逻辑
});
}
}
package cn.edu.ncepu.sa.GameView;
import cn.edu.ncepu.sa.Model.*;
import javax.swing.*;
import java.awt.*;
/**
* 游戏画板
*/
public class GamePanel extends JPanel {
/**
* 数据区引用,
* 放到参数区也可以
*/
private WarData warData;
//private Mapc mapc=new Mapc(70,50);
private boolean flag=false;
/**
* 游戏帧率
*/
private double frameRate = 0.0;
public long gameStartTime=System.currentTimeMillis();
/**
* 初始化数据引用
*
* @param warData 注意是引用传递
*/
public void setWarData(WarData warData) {
this.warData = warData;
// 单例类的用法
// this.warData = WarDataSingleton.getInstance();
}
public void setFrameRate(double frameRate) {
this.frameRate = frameRate;
}
public void paint(Graphics g) {
synchronized (warData)
{
super.paint(g);//保留原来的paint,g相当于画笔
final int tile = 64;
Graphics2D g2 = (Graphics2D) g;
//绘制边框
int boardWidth = 0;
g.setColor(Color.black);
g.drawRect(boardWidth, boardWidth, 860, 640);
//if(!flag){
MapDraw.draw(g2, warData.mapc);
//flag=true;
//}
// 绘制每一个游戏元素
if (warData != null && warData.elements.size() > 0) {
for (Element element : warData.elements) {
//element.draw(g2);//让每个节点都自我绘制
if (element instanceof Shot) {
EleDraw.draw(g2, (Shot) element);
} else if (element instanceof Tank) {
EleDraw.draw(g2, (Tank) element);
} else if (element instanceof Shot_High_Speed) {
EleDraw.draw(g2, (Shot_High_Speed) element);
}
}
}
/*for (int y = 0; y <640; y++) {
for (int x = 0; x < 860; x++) {
System.out.println(x+" "+y);
Terrain terrain = mapc.terrains[y][x];
int tileX = x * tile;
int tileY = y * tile;
switch (terrain) {
case PLAINS:
// 默认情况下,不需要绘制平地(可以用背景色代替)
break;
case GRASS:
g.drawImage(ImageCache.get("grass"), tileX, tileY, tile, tile, this);
break;
case RIVER:
g.drawImage(ImageCache.get("water"), tileX, tileY, tile, tile, this);
break;
}
}*/
// 显示帧率
String str = String.format("fps:%.2f", frameRate);
g.drawString(str, 10, 15);
//显示击杀数
String strKill = String.format("kill:%d", warData.getUserKillNum());
g.drawString(strKill, 10, 30);
//显示游戏时间
long currTime = System.currentTimeMillis();
int interval = (int) (currTime - gameStartTime);
String gameTime = String.format("%2d:%2d:%2d", interval / 3600000, (interval / 60000) % 60, (interval / 1000) % 60);
g.drawString(gameTime, 10, 45);
}
}
}
这个是部分废案,也在github里,感兴趣的话可以自己看看。
package cn.edu.ncepu.sa.GameView;
import cn.edu.ncepu.sa.Model.ImageCache;
import cn.edu.ncepu.sa.Model.Tank;
import cn.edu.ncepu.sa.Model.TankTeam;
import java.awt.*;
//废案,如果按策略模式应当采用这种方法,但是我懒
public class TankDraw implements Idraw{
private Tank tank;
public TankDraw(Tank tank){
this.tank=tank;
}
public void draw(Graphics2D g2){
//System.out.println("画:"+x+","+y);
Image img1 = null;
Image img2 = null;
if (tank.getTeam() == TankTeam.RED.ordinal()) {
img1 = ImageCache.get("tank_red");
img2 = ImageCache.get("turret_red");
}
if (tank.getTeam() == TankTeam.BLUE.ordinal()) {
img1 = ImageCache.get("tank_blue");
img2 = ImageCache.get("turret_blue");
}
Graphics2D g = (Graphics2D) g2.create();//复制画笔
g.translate(tank.x, tank.y);
//绘制坦克身体
g.rotate(Math.toRadians(tank.dir));
g.drawImage(img1, -18, -19, null);
g.rotate(Math.toRadians(-tank.dir));
// 绘制血条
g.drawRect(-22, -34, 44, 8);
//g.setColor(Color.RED);此处添加了绿色黄色红色
if(tank.hp/tank.hpmax>0.7){
g.setColor((Color.GREEN));
}
/*else if(hp/hpmax>0.3){//黄色看不清
g.setColor((Color.yellow));
}*/
else{
g.setColor((Color.red));
}
int whp = (int) (43.08 * (tank.hp / tank.hpmax));
//此处添加了血量数字
Font font=new Font("Arial",Font.BOLD,14);
g.setFont(font);
// g.setColor(Color.BLACK);
//本来想把血量数字作为黑色显示的,但是不知道为什么会连同血条一起改变颜色,还是算了
//以下是数字本身内容
if (tank.hp<0)
tank.hp=0;
String booldString=new String((int)tank.hp+"/"+(int)tank.hpmax);
FontMetrics metrics=g.getFontMetrics();
int textWidth=metrics.stringWidth(booldString);
int textHeigth=metrics.getHeight();
int textX=-21+(whp-textWidth)/2;
int textY=-33+(7-textHeigth)/2;
g.drawString(booldString,textX,textY);
g.fillRect(-21, -33, whp, 7);
g.rotate(Math.toRadians(tank.turretDir));
g.drawImage(img2, -32, -32, null);
}
}
这种代码我的意见是交给AI去写,纯粹的重复劳动,没必要让这种低级工作浪费我们的精力。
控制层:这部分主要是处理玩家的交互输入以及游戏本身的刷新节奏。
package cn.edu.ncepu.sa.Control;
import cn.edu.ncepu.sa.GameView.GameStartPanel;
import cn.edu.ncepu.sa.GameView.GameView;
import cn.edu.ncepu.sa.Model.*;
/**
* 主程序入口
*/
public class Main {
public static void main(String[] args) {
System.out.println("Hello world!");
GameStartPanel gameStartPanel=new GameStartPanel();
//构造系统资源
// 构造数据组件
/*WarData warData = new WarData();
//构造显示组件,并告知显示组件要显示的数据,非单例类
GameView win = new GameView(warData);
// 构造控制器组件
WarControl warControl = new WarControl();
// 依据用户输入刷新显示,关联View层和数据层
warControl.StartWar(win, warData);*/
}
}
package cn.edu.ncepu.sa.Control;
import cn.edu.ncepu.sa.GameView.GameEndPanel;
import cn.edu.ncepu.sa.GameView.GameView;
import cn.edu.ncepu.sa.Model.*;
import cn.edu.ncepu.sa.utils.Utils;
import javax.swing.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
/**
* 游戏控制器
* 读取键盘、鼠标信息,控制主坦克
* 线程后台刷新子弹、坦克位置
* 线程后台计算碰撞检测
*/
public class WarControl extends Thread {
/**
* 显示组件引用
*/
GameView win;
/**
* 数据组件引用
*/
WarData warData;
/**
* 默认构造函数
*/
ThreadPool executor=new ThreadPool();
private final int maxAmmoNum=3;
private final int ShotSpeed=200;
private final int HighShotSpeed=400;
private final int SpawningTime=50000;
public WarControl() {
}
public WarControl(GameView win,WarData warData){
this.win=win;
this.warData=warData;
}
/**
* 初始化控制器
*
* @param win 显示组件引用
* @param warData 数据组件引用
*/
public void StartWar(GameView win, WarData warData) {
this.win = win;
this.warData = warData;
Tank tank = warData.userTank;
MouseAdapter adapter = new MouseAdapter() {
@Override
public void mouseMoved(MouseEvent e) {
if (warData.userTank.Destroyed) {
return;
}
// System.out.println(e.getX()+","+e.getY());
double x = e.getX() - 9;
double y = e.getY() - 38;
tank.turretDir = Utils.ppDir(tank.x, tank.y, x, y) + 90;
}
@Override
public void mousePressed(MouseEvent e) {
if (tank.Destroyed) {
tank.moving = false;
return;
}
int button=e.getButton();
//这里应要求改成鼠标右键发射炮弹,鼠标左键移动
switch(button){
case MouseEvent.BUTTON1:
tank.dir = tank.angle(e.getX(),e.getY());
tank.moving = true;
//这里最好改成常量
if (e.getX() < 0||e.getY() < 0||e.getY() >= win.height||e.getX() >= win.width) {
tank.moving = false;
}
tank.tx=e.getX();
tank.ty=e.getY();
break;
case MouseEvent.BUTTON3:
if(tank.shotNum<maxAmmoNum){
Shot_High_Speed shot_h=new Shot_High_Speed(tank,HighShotSpeed);
tank.addShotNum();
warData.elements.add(shot_h);
}
break;
default:
break;
}
}
};
win.addMouseListener(adapter); //点击事件
win.addMouseMotionListener(adapter); //移动事件
win.addMouseWheelListener(adapter); //滚轮事件
win.addKeyListener(new KeyAdapter() {
@Override //键盘按下
public void keyPressed(KeyEvent e) {
System.out.println(e.getKeyChar());
if (tank.Destroyed) {
tank.moving = false;
return;
}
switch (e.getKeyChar()) {
case 'w':
case 'W':
tank.dir = Directions.UP.getAngleValue();
tank.moving = true;
if (tank.y < 0) {
//tank.dir = Directions.DOWN.getAngleValue();
tank.moving = false;
}
break;
case 'a':
case 'A':
tank.dir = Directions.LEFT.getAngleValue();
tank.moving = true;
if (tank.x < 0) {
//tank.dir = Directions.RIGHT.getAngleValue();
tank.moving = false;
}
break;
case 's':
case 'S':
tank.dir = Directions.DOWN.getAngleValue();
tank.moving = true;
if (tank.y >= win.height) {
// tank.dir = Directions.UP.getAngleValue();
tank.moving = false;
}
break;
case 'd':
case 'D':
tank.dir = Directions.RIGHT.getAngleValue();
tank.moving = true;
if (tank.x >= win.width) {
//tank.dir = Directions.LEFT.getAngleValue();
tank.moving = false;
}
break;
case ' ':
if(tank.shotNum<maxAmmoNum){
Shot shot = new Shot(tank, ShotSpeed);
tank.addShotNum();
warData.elements.add(shot);
}
break;
}
}
@Override //键盘抬起
public void keyReleased(KeyEvent e) {
switch (e.getKeyChar()) {
case 'w':
case 'W':
case 'a':
case 'A':
case 's':
case 'S':
case 'd':
case 'D':
tank.moving = false;
break;
}
}
});
this.start();
}
/**
* 线程任务
*/
@Override
public synchronized void run() {
super.run();
long lastUpdate = System.currentTimeMillis();//当前系统时间
long lastSpawn=lastUpdate;
int fps = 60;//理论帧数
while (true) {
long interval = 1000 / fps;//理论间隔
long curr = System.currentTimeMillis();
long _time = curr - lastUpdate;
if (_time < interval) {
// 不到刷新时间,休眠
try {
Thread.sleep(1);
} catch (Exception e) {
}
} else {
// 更新游戏状态
lastUpdate = curr;
// 流逝时间
float dt = _time * 0.001f;
//调度任务,如果有些任务计算量大,可以开线程池
// warData.runEnemyTank(win.width, win.height,100);
//把运行敌方坦克放入线程池
executor.executorService.submit(new T_RunEnemyTank(warData,win.width,win.height,100));
warData.updatePositions(dt);
//自动刷怪
if(curr-lastSpawn>SpawningTime){
warData.regularSpawning();
lastSpawn=curr;
}
// 碰撞检测
//warData.CollisionDetection();
executor.executorService.submit(new T_Collision(warData));
// 依据元素的状态,更新数据区
//warData.updateDataSet();
executor.executorService.submit(new T_UpdateDataSet(warData));
// 刷新界面
// win.update(dt);
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
// 更新UI组件
// 例如:gameView.repaint();
win.update(dt);
//System.out.println("UI updated in Event Dispatch Thread.");
}
});
GameStart_and_End.settleGame(warData,win,executor);
//如果游戏已经结束,终止循环
if(warData.isEnd()!=-1){
return;
}
}
}
}
}
线程池是老师要求的,其实一开始我没打算用。
这里解释一下,当单位的moving为true时,允许其移动,每次更新状态时检查该标志位,如果允许则根据本身的速度和方向对位置进行更新,此事在模型层中亦有记载。
package cn.edu.ncepu.sa.Control;
import cn.edu.ncepu.sa.GameView.GameView;
import cn.edu.ncepu.sa.Model.GameLoad;
import cn.edu.ncepu.sa.Model.GameSave;
import cn.edu.ncepu.sa.Model.GameState;
import cn.edu.ncepu.sa.Model.WarData;
import java.nio.file.Path;
import java.nio.file.Paths;
public class GameSave_and_Load {
public static void clickLoad(){
GameState gameState = GameLoad.loadGame("C:/Users/Lenovo/Desktop/gsave/game_save.json");
WarData warData=gameState.warData;
//System.out.println(warData.userTank.hp);
GameView win = new GameView(warData);
System.out.println("Gameload over i will enter warcontrol");
// 构造控制器组件
WarControl warControl = new WarControl(win,warData);
// 依据用户输入刷新显示,关联View层和数据层
warControl.StartWar(win,warData);
}
public static void clickSave(WarData warData){
GameState gameState=new GameState(warData);
String filep=new String("C:\\Users\\Lenovo\\Desktop\\gsave\\game_save.json");
GameSave.saveGame(gameState,filep); // 保存游戏状态
}
}
存档通过将游戏数据保存为json格式存在文件夹里,代码里用了绝对路径,我建议你改成相对路径,避免游戏文件夹一变就找不到路径了。这里有一个问题,我在存档时经常出现序列化丢失信息,所以在这里做了一些意义不明的包装,然后就解决了,我推测根本原因可能是后面模型层的shot和tank少写了几个无参构造方法,导致序列化识别出了问题,请你自己写的时候注意。
模型层:
这里是最关键的部分,因为代码较多,我只放几个。
package cn.edu.ncepu.sa.Model;
import com.alibaba.fastjson.annotation.JSONType;
import java.awt.*;
/**
* 地图元素类
*/
public class Element implements IElement {
/**
* 位置x
*/
public double x;
/**
* 位置 y
*/
public double y;
/**
* 宽度
*/
public int width = 10;
/**
* 高度
*/
public int height = 10;
/**
* 是否需要销毁,False:显示; True:删除
*/
public boolean Destroyed = false;
//增设“可见”属性,不可见不一定被摧毁
public boolean Viewed=true;
public double tx=-1;
public double ty=-1;
public Element() {
}
public boolean isDestroyed(){
return Destroyed;
}
/**
* 使用子类的位置更新方法
*
* @param timeFlaps 流逝时间间隔
*/
public void update(double timeFlaps) {
}
/**
* 在地图上绘制该元素
* 使用子类的绘制方法
*
* @param g
*/
@Override
public void draw(Graphics2D g) {
}
/**
* 该元素生命周期结束,可以回收资源
*/
public void destroy() {
Destroyed = true; Viewed=false; // 这会触发数据区中删除,该工作在控制器中完成
}
/**
* 向某方向移动一段距离
*
* @param dir 方向
* @param len 距离
*/
public void move(double dir, double len) {
//此处添加限制,使鼠标点击后不会停不下来
if(Math.abs(x-tx)<2&&Math.abs(y-ty)<2){
return;
}
x = x + len * Math.cos((dir - 90) * Math.PI / 180);
y = y + len * Math.sin((dir - 90) * Math.PI / 180);
}
/**
* 计算两个结点的距离
*
* @param target 目标元素
* @return 距离
*/
public double distance(Element target) {
// this,target
double a = this.x - target.x;
double b = this.y - target.y;
return Math.sqrt(a * a + b * b);
}
/**
* 与另一个元素的夹角
*
* @param target 目标元素
* @return
*/
public double angle(Element target) {
double len_x = target.x - x;
double len_y = target.y - y;
double radian = Math.atan2(len_y, len_x);//弧度
return radian * 180 / Math.PI;//角度
}
//此处重载了一个获取角度的函数
public double angle(int tx,int ty) {
double len_x = tx - x;
double len_y = ty - y;
double radian = Math.atan2(len_y, len_x);//弧度
System.out.println(90+Math.toDegrees(radian));
// return radian * 180 / Math.PI;//角度
return 90+Math.toDegrees(radian);
}
/*
* 指定中心点获取矩形,弃用,碰撞检测使用了距离法
*/
public Rectangle getRect() {
return new Rectangle((int) (x - (width / 2)), (int) y - (height / 2), (int) width, (int) height);
}
}
这是元素类,里面是不论坦克还是炮弹都要用到的一些方法,我觉得我命名用的还是很通俗易懂的。
package cn.edu.ncepu.sa.Model;
import com.alibaba.fastjson.annotation.JSONType;
import java.awt.*;
import java.lang.*;
/**
* 坦克类
*/
//@JSONType(typeName = "Tank")
public class Tank extends Element {
/**
* 坦克方向
*/
public double dir = 90;
//public int tx,ty;
/**
* 炮筒方向
*/
public double turretDir;
/**
* 是否在移动
*/
public boolean moving = false;
/**
* 移动步数
*/
public long moveSteps = 0;
/**
* 每秒移动速度,注意要比子弹慢一些
*/
public double speed = 200;
/**
* 生命数,装甲
*/
public double hp = 600;
public double hpmax = 200;
/**
* 每秒回复生命
*/
public double hp_recovery_per_sec = 0.1;
/**
* 队伍,1红,2蓝
*/
public int team = 1;
public int killNum=0;
public int shotNum=0;
public String name;
public double lastX=0;
public double lastY=0;
public Tank() {
}
/**
* 构造坦克
*
* @param x x坐标
* @param y y坐标
* @param dir 方向
* @param hp 初始血量
* @param hp_recovery_per_sec 每秒恢复血量
* @param team 组别
*/
public Tank(int x, int y, double dir, double hp, double hp_recovery_per_sec, int team,int killNum,String name) {
this.x = x;
this.y = y;
this.dir = dir;
this.speed = speed;
this.hp = hp;
this.hp_recovery_per_sec = hp_recovery_per_sec;
this.team = team;
this.killNum=killNum;
this.name=name;
}
/*public Tank(int x, int y, double dir, double hp, double hp_recovery_per_sec, int team,int killNum,String name) {
this.x = x;
this.y = y;
this.dir = dir;
this.speed = speed;
this.hp = hp;
this.hp_recovery_per_sec = hp_recovery_per_sec;
this.team = team;
this.killNum=killNum;
this.name=name;
}*/
/**
* 此坦克受到伤害
*/
public boolean damage(double val) {
this.hp -= val;
if (this.hp <= 0) {
System.out.println(team+"方坦克销毁");
this.destroy();
return true;
}
return false;
}
//修改击杀数和获取击杀数
public synchronized void addKillNum(){
killNum++;
}
public int getKillNum(){
return killNum;
}
//控制炮弹数量
public void addShotNum(){shotNum++;}
public void subShotNum(){shotNum--;}
public int getShotNum(){return shotNum;}
public int getTeam(){return team;}
/**
* 更新坦克位置
*
* @param timeFlaps 流逝时间间隔
*/
public void update(double timeFlaps) {
// 若已死亡,则不再动作
if (Destroyed) {
return;
}
//生命回复
recoverLife();
//更新坦克位置
if (moving){
double len = speed * timeFlaps;
moveSteps++;
this.move(dir, len);
}
}
//重写了移动方法
public void move(double dir, double len) {
//此处添加限制,使鼠标点击后不会停不下来
if(Math.abs(x-tx)<2&&Math.abs(y-ty)<2){
return;
}
lastX=x;
lastY=y;
x = x + len * Math.cos((dir - 90) * Math.PI / 180);
y = y + len * Math.sin((dir - 90) * Math.PI / 180);
}
//返回上一步移动的位置
public void cancelMove(){
x=lastX;
y=lastY;
}
/**
* 定时自动回血
*/
public void recoverLife() {
hp += hp_recovery_per_sec;
if (hp > hpmax) {
hp = hpmax;
}
}
//本绘图方法暂时作废
@Override
public void draw(Graphics2D g2) {
//System.out.println("画:"+x+","+y);
Image img1 = null;
Image img2 = null;
if (team == TankTeam.RED.ordinal()) {
img1 = ImageCache.get("tank_red");
img2 = ImageCache.get("turret_red");
}
if (team == TankTeam.BLUE.ordinal()) {
img1 = ImageCache.get("tank_blue");
img2 = ImageCache.get("turret_blue");
}
Graphics2D g = (Graphics2D) g2.create();//复制画笔
g.translate(x, y);
//绘制坦克身体
g.rotate(Math.toRadians(dir));
g.drawImage(img1, -18, -19, null);
g.rotate(Math.toRadians(-dir));
// 绘制血条
g.drawRect(-22, -34, 44, 8);
//g.setColor(Color.RED);此处添加了绿色黄色红色
if(hp/hpmax>0.7){
g.setColor((Color.GREEN));
}
/*else if(hp/hpmax>0.3){//黄色看不清
g.setColor((Color.yellow));
}*/
else{
g.setColor((Color.red));
}
int whp = (int) (43.08 * (hp / hpmax));
//此处添加了血量数字
Font font=new Font("Arial",Font.BOLD,14);
g.setFont(font);
// g.setColor(Color.BLACK);
//本来想把血量数字作为黑色显示的,但是不知道为什么会连同血条一起改变颜色,还是算了
//以下是数字本身内容
if (hp<0)
hp=0;
String booldString=new String((int)hp+"/"+(int)hpmax);
FontMetrics metrics=g.getFontMetrics();
int textWidth=metrics.stringWidth(booldString);
int textHeigth=metrics.getHeight();
int textX=-21+(whp-textWidth)/2;
int textY=-33+(7-textHeigth)/2;
g.drawString(booldString,textX,textY);
g.fillRect(-21, -33, whp, 7);
g.rotate(Math.toRadians(this.turretDir));
g.drawImage(img2, -32, -32, null);
}
}
请不要学我把属性都用public,我是因为懒,请你不要这样做!
shotnum这种属性作用是记录当前场地内该坦克炮弹数量,防止同时存在过多炮弹。
lastx,y是上一次移动更新时的位置,tx,ty指的是目标位置,我们是鼠标点击进行移动,所以移动到指定位置 |当前位置-目标位置 |<2就算到位了,不再移动,遇到边界或河流则拒绝移动,返回上一帧位置lastx lasty,在玩家视角是看不出坦克倒退了的。如果在边界单纯的把moving标志位置为false,可能会导致坦克卡住。
绘图那里看一看就行,建议让AI写,没有技术含量,一些数学运算是为了让屏幕上的上下左右和我编程时的方向相对应进行换算的,不用管。有兴趣的可以了解一下双缓冲技术。
package cn.edu.ncepu.sa.Model;
import cn.edu.ncepu.sa.GameView.MapDraw;
import java.util.Random;
public class Mapc {
public Terrain [][]terrains;
public static int width=43;//65
public static final int height=32;
//private final int MAX_GRASS_COUNT = 100;
//private final int MAX_RIVER_COUNT = 50;
public int getHeight() {
return height;
}
public int getWidth() {
return width;
}
public boolean isGrass(int x,int y){
if(terrains[y/MapDraw.tile][x/MapDraw.tile]==Terrain.GRASS){
return true;
}
else {
return false;
}
}
public boolean isRiver(int x,int y){
if(terrains[y/ MapDraw.tile][x/MapDraw.tile]==Terrain.RIVER){
return true;
}
else {
return false;
}
}
public Mapc(){}
public Mapc(int MAX_GRASS_COUNT,int MAX_RIVER_COUNT){
terrains = new Terrain[height][width];
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
terrains[y][x] = Terrain.PLAINS;
}
}
Random rand = new Random();
int grassCount = 0;
while (grassCount < MAX_GRASS_COUNT) {
int x = rand.nextInt(width);
int y = rand.nextInt(height);
if (terrains[y][x] == Terrain.PLAINS) {
terrains[y][x] = Terrain.GRASS;
grassCount++;
}
}
// 生成河流(这里简单起见,使用固定河流形状或算法,而不是完全随机)
// 你可以实现更复杂的河流生成算法,比如使用Perlin Noise等
// 这里只是简单示例,随机放置一些河流单元格
int riverCount = 0;
while (riverCount < MAX_RIVER_COUNT) {
int x = rand.nextInt(width);
int y = rand.nextInt(height);
if (terrains[y][x] == Terrain.PLAINS) {
// 假设河流是一个3x3的矩形区域(简单起见)
for (int dx = -1; dx <= 1; dx++) {
for (int dy = -1; dy <= 1; dy++) {
int nx = x + dx;
int ny = y + dy;
if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
terrains[ny][nx] = Terrain.RIVER;
}
}
riverCount++;
// 注意:由于我们一次放置了一个3x3的矩形区域,所以需要适当减少循环次数
if (riverCount >= MAX_RIVER_COUNT) {
break;
}
}
if (riverCount >= MAX_RIVER_COUNT) {
break;
}
}
}
//防止玩家出生点是河流
if(isRiver(600,200)){
//terrains[200/MapDraw.tile][600/MapDraw.tile]=Terrain.PLAINS;
for (int dx = -1; dx <= 1; dx++) {
for (int dy = -1; dy <= 1; dy++) {
int nx = 600 + dx;
int ny = 200 + dy;
if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
terrains[ny][nx] = Terrain.PLAINS;
}
}
}
/*for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
System.out.print(terrains[y][x]+" ");
}
System.out.println();
}*/
}
}
public Mapc(int ty){
terrains = new Terrain[height][width];
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
terrains[y][x] = Terrain.PLAINS;
}
}
//草丛
for (int y = 180; y < 500; y++) {
for (int x = 200; x < 510; x++) {
terrains[y][x] = Terrain.GRASS;
}
}
//河流
for (int y = 300; y < 310; y++) {
for (int x = 320; x < 325; x++) {
terrains[y][x] = Terrain.RIVER;
}
}
}
}
Terrian是一个枚举类,里面包括GRASS,RIVER,PLAINS三种地形,地图随机生成,有兴趣的同学可以上网搜一下现代游戏常用的地形生成算法,比如柏林噪声;我当时写到这里想要效仿星际争霸II那样的高低坡,以及它优秀的寻路算法,但是我水平太次,看都看不懂,只好作罢。
package cn.edu.ncepu.sa.Model;
import cn.edu.ncepu.sa.utils.Utils;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Random;
/**
* 数据组件,除了引用传递还可以使用单例类
*/
public class WarData {
public HashSet<Element> elements = new HashSet<>();
public Tank userTank ;//= new Tank(600, 200, 0, 110, 0.5, TankTeam.RED.ordinal(),0);
public final int enemyMoveGap=100;
public final int enemyAttackRange=800;
public final int maxAmmoNum=3;
public final int MaxGrass=70;
public final int MaxRiver=50;
public Mapc mapc;
public void setUserTank() {
this.userTank.x=600;
this.userTank.y=200;
this.userTank.dir=0;
this.userTank.hp=110;
this.userTank.hp_recovery_per_sec=0.5;
this.userTank.team=TankTeam.RED.ordinal();
this.userTank.killNum=0;
//this.userTank.name="Player";
}
public WarData() {
// 构造我方坦克
//setUserTank();
userTank= new Tank(600, 200, 0, 110, 0.5, TankTeam.RED.ordinal(),0,"Player");
userTank.moving=false;
elements.add(userTank);
mapc=new Mapc(MaxGrass,MaxRiver);
// 构造敌方坦克
AddSomeEnemyTanks();
}
/**
* 增加一辆敌方坦克
*
* @param x x
* @param y y
* @param dir 方向
* @param hp 初始血量
* @param hp_recovery_per_sec 修复血量
* @param team 分组
*/
public void AddAEnemyTank(int x, int y, double dir, double hp, double hp_recovery_per_sec, int team,int killNum,String name) {
if(mapc.isRiver(x,y)){
AddEnemyTankAgain();
return;
}
Tank t = new Tank(x, y, dir, hp, hp_recovery_per_sec, team,killNum,name);
t.moving = true; // 默认是移动状态
elements.add(t);
}
/**
* 构造敌方坦克,之后要依据配置/地图来构造tank
*/
public void AddSomeEnemyTanks() {
// 构造一些敌方坦克
Random r=new Random();
AddAEnemyTank(r.nextInt(600), r.nextInt(600), 0, 200, 0.1, TankTeam.BLUE.ordinal(),0,"enemy");
AddAEnemyTank(r.nextInt(600), r.nextInt(600), 0, 200, 0.1, TankTeam.BLUE.ordinal(),0,"enemy");
AddAEnemyTank(r.nextInt(600), r.nextInt(600), 0, 200, 0.1, TankTeam.BLUE.ordinal(),0,"enemy");
AddAEnemyTank(r.nextInt(600),r.nextInt(600), 0, 200, 0.1, TankTeam.BLUE.ordinal(),0,"enemy");
AddAEnemyTank(r.nextInt(600), r.nextInt(600), 0, 200, 0.1, TankTeam.BLUE.ordinal(),0,"enemy");
}
public void AddEnemyTankAgain() {
// 构造一些敌方坦克
Random r=new Random();
AddAEnemyTank(r.nextInt(600), r.nextInt(600), 0, 200, 0.1, TankTeam.BLUE.ordinal(),0,"enemy");
}
/**
* 敌方坦克动起来,请尝试修改为每个坦克独立线程控制,自主活动
*/
public synchronized void runEnemyTank(int viewWidth, int viewHeight,int shot_Speed) {
if (userTank.Destroyed) {
return;
}
for (Element elemnet : elements) {
// 找坦克
if (elemnet instanceof Tank) {
// 找敌方坦克
if (((Tank) elemnet).getTeam() == TankTeam.BLUE.ordinal()) {
Tank t = (Tank) elemnet;
// 防止跑出地图
if (t.x < 0) {
t.dir = Directions.RIGHT.getAngleValue();
}
else if (t.y < 0) {
t.dir = Directions.DOWN.getAngleValue();
}
else if (t.x >= viewWidth) {
t.dir = Directions.LEFT.getAngleValue();
}
else if (t.y >= viewHeight) {
t.dir = Directions.UP.getAngleValue();
}
// 运动几步随机开炮,50应该设置为参数或者常量
if (t.moveSteps > enemyMoveGap) {
// 方向随机
double random = Math.random() * 360;
t.dir = random;
t.turretDir = random;
t.moving = true;
t.moveSteps = 0;
// 如果我方坦克进入射程,800应该设置为常量
if (t.distance(userTank) < enemyAttackRange&&userTank.Viewed==true) {
// 自动瞄准
t.turretDir = Utils.ppDir(t.x, t.y, userTank.x, userTank.y) + 90;
// 开炮,同一时间一个坦克存在的炮弹不得超过三发
//本来想在shot类中做限制,但是考虑到让shot类直接修改tank类不太好,作罢
if (t.shotNum < maxAmmoNum) {
Shot shot = new Shot(t, shot_Speed);
//t.addKillNum();
elements.add(shot);
}
return;
}
}
}
}
//子弹越界销毁
else if(elemnet instanceof Shot){
if (elemnet.x < 0||elemnet.y < 0||elemnet.x >= viewWidth||elemnet.y >= viewHeight) {
elemnet.destroy();
Shot sh=(Shot)elemnet;
sh.tank.subShotNum();
}
}
else if(elemnet instanceof Shot_High_Speed){
if (elemnet.x < 0||elemnet.y < 0||elemnet.x >= viewWidth||elemnet.y >= viewHeight) {
elemnet.destroy();
Shot_High_Speed sh=(Shot_High_Speed) elemnet;
sh.tank.subShotNum();
}
}
}
}
/**
* 更新坦克的位置
*
* @param timeFlaps 运行时间间隔
*/
public void updatePositions(double timeFlaps) {
// 所有元素依据流逝时间更新状态
for (Element elemnet : elements) {
elemnet.update(timeFlaps);
}
}
//每隔一段时间检查一下坦克数量,如果不足五辆就再添加一辆
public void regularSpawning(){
int tankCnt=0;
for(Element elt :elements){
if(elt instanceof Tank){
tankCnt++;
}
}
if(tankCnt<6){
Random r=new Random();
AddAEnemyTank(r.nextInt(600),r.nextInt(600),0,200,0.1,TankTeam.BLUE.ordinal(),0,"enemy");
}
}
//如果集合中的所有坦克team都相同,就返回team,否则返回-1,平局返回-2
public int isEnd(){
synchronized(this){
if(elements.isEmpty()){
return -2;//平局
}
Tank firstTank=null;
for (Element element : elements) {
if (element instanceof Tank) {
if (firstTank == null) {
firstTank = (Tank) element;
} else {
if (((Tank) element).getTeam() != firstTank.getTeam()) {
return -1;
}
}
}
}
return firstTank.getTeam();
}}
/**
* 碰撞检测
*/
public int getUserKillNum(){
return userTank.killNum;
}
//其实应当新建一个碰撞管理器类,但是这里逻辑并不复杂,就不添加了
public synchronized void CollisionDetection() {
//遍历每一个子弹
boolean isKilled=false;
for (Element shot : elements) {
if (shot instanceof Shot||shot instanceof Shot_High_Speed) {
// 寻找每一辆坦克
for (Element tank : elements) {
//进行敌我识别
if(shot instanceof Shot){
if ((tank instanceof Tank) && (tank != ((Shot) shot).tank)) {
// 距离过近,则认为打中,20应该设置为常量
if (shot.distance(tank) < 20) {
//添加了击杀判定
isKilled=((Tank) tank).damage(((Shot) shot).damage); //使坦克受到伤害
if(isKilled==true){
((Shot) shot).tank.addKillNum();
}
shot.destroy(); //销毁当前子弹
((Shot) shot).tank.subShotNum();
}
}
}//这里添加了另一种子弹,由右键发射
else if(shot instanceof Shot_High_Speed){
if ((tank instanceof Tank) && (tank != ((Shot_High_Speed) shot).tank)) {
// 距离过近,则认为打中,20应该设置为常量
if (shot.distance(tank) < 20) {
isKilled=((Tank) tank).damage(((Shot_High_Speed) shot).damage); //使坦克受到伤害
if(isKilled==true){
((Shot_High_Speed) shot).tank.addKillNum();
}
shot.destroy(); //销毁当前子弹
((Shot_High_Speed) shot).tank.subShotNum();
}
}
}
}
}
}
//检测草丛和河流碰撞
for(Element etank:elements){
if(etank instanceof Tank){
Tank tank_t=(Tank)etank;
if(mapc.isGrass((int)tank_t.x,(int)tank_t.y)){
//System.out.println("enter grass");
tank_t.Viewed=false;
}
else {
tank_t.Viewed=true;
}
if(mapc.isRiver((int)tank_t.x,(int)tank_t.y)){
//tank_t.dir=-tank_t.dir;
tank_t.cancelMove();
}
}
}
}
/**
* 依据元素的状态,处理是否还保留在数据区中
*/
//public synchronized void updateDataSet()
public synchronized void updateDataSet() {
// 删除中间的元素会有问题
/*for (Element element : elements) {
if (element.Destroyed) {
elements.remove(element);
} else {
}
}*/
// 请大家思考,为什么要采取这种方法
// 因为在for循环时改变了遍历目标的大小,所以采用更安全的迭代器,但是仍然解决不了线程访问
Iterator<Element> it = elements.iterator();
while (it.hasNext()) {
Element tmp = it.next();
if (tmp.isDestroyed()) {
if (tmp != userTank) {
it.remove();
}
}
}
// 上文方法依然存在数据并行操作问题,如何加锁?
}
}
臃肿的wardata,迫于当时赶作业和自身水平,把wardata弄成了这样,我建议把里面的功能都拆出单独的文件,不要像我一样都堆在一起。
到这里我的讲解就结束了(好像光贴代码了),建议大家常备AI助手,这种入门级小游戏AI写的比我好多了。
强烈建议看完这篇文章之后想想自己的坦克大战要实现什么功能,怎么分层,然后把我的代码全删掉,只留一个开始游戏,自行修改成一个相对完善的游戏,或者是B站找成品,看懂它,然后你就算入门了,可以尝试自己从零开始写一个小游戏。
——其实最好办法是找教这门课的王老师要一个简易版,然后自己一周之内改成一个成品,遗憾的是老师把代码发布的git关闭了,所以只能线下找他要,老师人挺好的,基本有求必应。