基于MVC模式的设计

本代码采用数据 - 视图 - 控制器的框架设计,以使代码更简洁易读、易维护使用。

MVC 模式代表 Model-View-Controller(模型-视图-控制器) 模式。这种模式用于应用程序的分层开发。

  • Model(模型) - 模型代表一个存取数据的对象或 JAVA POJO。它也可以带有逻辑,在数据变化时更新控制器。
  • View(视图) - 视图代表模型包含的数据的可视化。
  • Controller(控制器) - 控制器作用于模型和视图上。它控制数据流向模型对象,并在数据变化时更新视图。它使视图与模型分离开。
  • img

控制器层

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import java.awt.*;

//MVC中的控制层
public class AlgoVisualizer {
private CircleAnimationTest[] circles;//数据信息
private AlgoFrame frame;//视图层

public AlgoVisualizer(int sceneWidth, int sceneHeight,int N){
//CircleAnimationTest[]
circles = new CircleAnimationTest[N];
int R=50;
for (int i = 0;i<N;i++){
int x = (int)(Math.random()*(sceneWidth-2*R))+R;
int y=(int)(Math.random()*(sceneHeight-2*R))+R;;
int vx=(int)(Math.random()*11) - 5;
int vy=(int)(Math.random()*11) - 5;
circles[i] = new CircleAnimationTest(x,y,R,vx,vy);
//rand(0,sceneWidth-2R)+R
}
EventQueue.invokeLater(() -> {
//AlgoFrame frame = new AlgoFrame("Welcome");
frame = new AlgoFrame("Welcome",sceneWidth,sceneHeight);
//AlgoFrame
//不能直接放进事件队列里 以防事件阻塞 new一个thread
new Thread(() -> {
run();
}).start();
});



}

private void run(){
while(true){
//绘制数据
frame.render(circles);
AlgoVisHelper.pause(20);

//更新数据
for(CircleAnimationTest circle: circles){
circle.move(0,0,frame.getCanvasWitdh(),frame.getCanvasHeight());
}
for(int p =0;p<circles.length;p++){
for(int i = 0; i<p; i++){
circles[p].addjustMove(circles[i]);
}
for(int i = p+1; i<circles.length; i++){
circles[p].addjustMove(circles[i]);
}
}
}
}
}

视图层

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
import javax.swing.*;
import java.awt.*;
import java.awt.geom.Ellipse2D;
//import java.awt.geom.Ellipse2D;

public class AlgoFrame extends JFrame {
private int canvasWidth;
private int canvasHeight;

//告诉框架需要绘制什么
private CircleAnimationTest[] _circles;
public void render(CircleAnimationTest[] crils)
{
this._circles = crils;
this.repaint();//重新绘制 会重新调用paintComponent方法
}

public AlgoFrame(String title,int canvasWidth,int canvasHeight){
//调用父类构造函数
super(title);

this.canvasWidth = canvasWidth;
this.canvasHeight = canvasHeight;

AlgoCanvas canvas = new AlgoCanvas();
//canvas.setPreferredSize(new Dimension(canvasWidth,canvasHeight));
//setSize(canvasWidth,canvasHeight);
setContentPane(canvas);
pack();//根据加载内容自动调整画布大小

setResizable(false);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

setVisible(true);
}

public AlgoFrame(String title){
this(title, 1024,768);
}

public int getCanvasWitdh(){return canvasWidth;}
public int getCanvasHeight(){return canvasHeight;}




//内部类 为了帮助实现面板绘制
private class AlgoCanvas extends JPanel{

//(默认)支持双缓冲
// public AlgoCanvas(){
// super(true);//打开双缓冲
// }

//g连接了上下文 绘制函数都定义在g中
//JPanel override 减少bug出现概率
@Override
public void paintComponent(Graphics g){
super.paintComponent(g);

Graphics2D g2d = (Graphics2D)g;

//抗锯齿
RenderingHints hints = new RenderingHints(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
g2d.addRenderingHints(hints);

//具体绘制
/*
//内部参数定义出一个包围盒 正方形就是圆形 长方形就是椭圆
//左上角坐标 长宽
//屏幕坐标系在屏幕左上角 x轴正方向一致 y相反
//g.drawOval(50,50,300,300);

//int strokeWidth = 10;
//g2d.setStroke(new BasicStroke(strokeWidth));
AlgoVisHelper.setStrokeWidth(g2d,5);

g2d.setColor(Color.BLUE);
//Ellipse2D circle2 = new Ellipse2D.Double(60,60,280,280);
Ellipse2D circle2 = new Ellipse2D.Double(50,50,300,300);
g2d.fill(circle2);

g2d.setColor(Color.RED);
Ellipse2D circle= new Ellipse2D.Double(50,50,300,300);
g2d.draw(circle);
*/
//具体绘制
AlgoVisHelper.setStrokeWidth(g2d,1);
AlgoVisHelper.setColor(g2d,Color.red);
for(CircleAnimationTest circle:_circles)
AlgoVisHelper.strokeCircle(g2d,circle.x,circle.y,circle.getR());

}

@Override
public Dimension getPreferredSize(){
return new Dimension(canvasWidth,canvasHeight);
}

}

}

数据层

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
import java.awt.*;

public class CircleAnimationTest {
public int x,y;//圆的圆心位置
private int r;//初始化指定半径 不能动态修改
public int vx,vy;//圆的速度 可动态修改

public CircleAnimationTest(int x, int y, int r, int vx, int vy){
this.x = x;
this.y = y;
this.r = r;
this.vx = vx;
this.vy = vy;
}

public int getR(){return r;}

public void move(int minx,int miny,int maxx,int maxy){
x+=vx;
y+=vy;

checkCollision(minx, miny, maxx, maxy);
//if(isCollided(cir)){
// vx = -vx;
// vy = -vy;
// }
}
public void addjustMove(CircleAnimationTest cir){
CollisionRejust(cir);
// if(isCollided(cir)){
// vx = -vx;
// vy = -vy;
// }
//抽搐效果是因为没有逃逸 以至于在边界来回碰撞了
//解决方法1:计算嵌入深度 然后推离
}

private void checkCollision(int minx,int miny,int maxx,int maxy){
if(x - r<minx){x = r; vx= -vx;}
if(x+r>=maxx){x = maxx - r; vx = -vx;}
if(y -r<miny){y = r;vy=-vy;}
if(y+r >=maxy){y = maxy - r;vy=-vy;}
}

private boolean isCollided(CircleAnimationTest cir){
double _centerDIs = centerDistance(new Point(this.x,this.y),new Point(cir.x,cir.y));
if(_centerDIs<(this.r+cir.r)){
return true;
}
return false;
}
private double centerDistance(Point p1, Point p2){
return Math.hypot((p2.x)-(p1.x),(p2.y)-(p1.y));
}
private Point normalizeVector(Point pVector){
//精度丢失警告
int length = (int)centerDistance(new Point(0,0),pVector);
if(length!=0){
return new Point(pVector.x/length,pVector.y/length);
}
else{
return new Point(pVector.x,pVector.y);
}

}

//解决碰撞抽搐问题
//因为我们知道它们的速度 所以可以计算出嵌入深度
// private double qianrushendu(CircleAnimationTest cir){
// if(isCollided(cir)){
//
// }
// return 0;
// }

//解决碰撞抽搐问题
private void CollisionRejust(CircleAnimationTest cir){
double _centerDIs = centerDistance(new Point(this.x,this.y),new Point(cir.x,cir.y));
if(_centerDIs<(this.r+cir.r)){
vx=-vx;
vy=-vy;

//弹出嵌入深度
double _innerDIs = this.r + cir.r - _centerDIs;//嵌入深度
//方向
Point directionP = normalizeVector(new Point(this.x - cir.x,this.y - cir.y));
this.x += (int)((directionP.x*_innerDIs+1));
this.y += (int)((directionP.y*_innerDIs+1));


}
else{

}
}

}

总结

项目地址:https://github.com/Perhacept/AlgorithmApplication/tree/main/test01

总之,写起来还是很简单的,从了解api到上手写完还不到2小时罢。但中间也遇到了一些难点。

难点并不在于怎么判断盒体是否碰撞,难点在于怎么处理碰撞。处理不好很容易出现盒体抽搐的情况。而且java.awt的内置Point,x和y都是int型的,在拿来当作向量用并且手动normalize的时候精度不够。

目前在处理碰撞的时候仍有一些小瑕疵需要修改。

真正的游戏引擎在计算碰撞的时候要比目前所写的要复杂得多。比如,目前我的写法是遍历其他对象,再计算距离。一旦数据量变大、运算变复杂,那么这耗费的算力是非常可观的。之后我自己写引擎的时候,可以考虑使用四叉树这样一种数据结构来进行空间索引,来筛选需要计算的数据,缩小检测规模。

空间索引的算法很多,大体上归为三类:

基于图

基于树

使用空间填充曲线

筛选出需要计算的对象后,还需要计算碰撞。

采用包围盒

写出计算碰撞的逻辑(目前想法是,计算包围盒在平面上投影是否重叠,在有多个碰撞对象的情况下还需要进行遍历与排序)

优化碰撞逻辑

TODO: 写一个基于3D碰撞的物理引擎