基于MVC模式的设计 本代码采用数据 - 视图 - 控制器的框架设计,以使代码更简洁易读、易维护使用。
MVC 模式代表 Model-View-Controller(模型-视图-控制器) 模式。这种模式用于应用程序的分层开发。
Model(模型) - 模型代表一个存取数据的对象或 JAVA POJO。它也可以带有逻辑,在数据变化时更新控制器。
View(视图) - 视图代表模型包含的数据的可视化。
Controller(控制器) - 控制器作用于模型和视图上。它控制数据流向模型对象,并在数据变化时更新视图。它使视图与模型分离开。
控制器层 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碰撞的物理引擎