注册

Android Region碰撞检测问题优化

前言


众所周知,Region是android graphics一族中比较低调的工具类,主要原因还是在碰撞检测方面存在一些不足,甚至可以说成事不足败事有余,以至于难以用于2D游戏开发,这也是耽误你们成为2D游戏大师路上的一道坎 。既然Region这么失败的工具为什么要介绍呢,一方面本篇通过路径检测的方式解决了成事不足败事有余的问题,另外一方面我们也要介绍他可用的部分,以及正确的用法。最后,本篇其实主要是通过PathMeasure和Region相互配合,优化了碰撞检测逻辑精确度问题。


预览效果


这是我们最终要达到的效果。


fire_74.gif


异常效果


我们需要重点处理两个问题



  • 没接触到就检测到碰撞
  • 接触已经很多距离了才检测到碰撞

Region 碰撞检测问题



  • Region类能成事的部份主要还是Op布尔操作和矩阵操作,但是这个似乎又和Path的作用重合,不知道是不是因为性能更高呢?本文没有去测试,有机会测试一下。另外一部分containXXX包含关系判断,containXXX能准确的判断点和矩形是不是被包含了,但是其他形状那就没办法了。
  • quickXXX 快速检测方法,返回值true-能确保物体没有碰撞,但false无法确保是不是已经碰撞了,换句话说true是100%没碰撞,但是false还需要你自己进一步确认,不过这点可以作为减少判断的优化方法,但不是判定方法。

学习Region & PathMeasure 的意义


对于一些粒子,我们不太关注大小,这个时候是可以利用中心点去检测的,那对于多边形或者半圆等形状,点是非常多的,显然得找一种更好的方法。实际上看似quickXXX其实用处不大,其实可以减少一部分检测逻辑,quickXXX虽然比不上contains的精确度,但是仍然能检测到没有碰撞,本篇需要了解它的用法,然后配合PathMeasure,实现精确检测。


非Path用法


对于非Path用法,Region还是相当简单的,直接使用set方法即可


mainRegion.set((int) -radius, (int) -radius, (int) radius, (int) radius);

Path方法


这个用法比较奇怪,需要2个参数,最后一个是Region类,弄不好就是鸡生蛋蛋生鸡一样令人迷惑,第二个可以看作被裁剪的区域,如下操作,求并集区域。不过话说回来,这个意义在哪里?


circlePath.reset();
circlePath.addCircle(x- width/2f,y - height/2f,10, Path.Direction.CCW);
circleRegion.setPath(circlePath,mainRegion);

小试一下


实现开头的图片效果,定义一些Path和形状。


定义一些变量


 private float x; //x事件坐标
private float y; //y事件坐标

//所以形状
Path[] objectPaths = new Path[5];
//形状区域检测
Region objectRegion = new Region();

//小圆球区域
Region circleRegion = new Region();
//小圆
Path circlePath = new Path();
//绘制区域
Region mainRegion = new Region();

构建物体


三角形、圆等物体


for (int i = 0; i < objectPaths.length; i++) {
Path path = objectPaths[i];
if (path == null) {
path = new Path();
objectPaths[i] = path;
} else {
path.reset();
}
}

Path path = objectPaths[0];
path.moveTo(radius / 2, -radius / 2);
path.lineTo(0, -radius);
path.lineTo(radius / 2, -radius);
path.close();

path = objectPaths[1];
path.moveTo(-radius / 2, radius / 2);
path.lineTo(-radius / 2 - 100, radius / 2);
path.arcTo(-radius / 2 - 100, radius / 2, -radius / 2, radius / 2 + 100, 0, 180, false);
path.lineTo(-radius / 2, radius / 2);
path.close();

path = objectPaths[2];
path.addCircle(-radius + 200f, -radius + 100f, 50f, Path.Direction.CCW);

path = objectPaths[3];
path.addRoundRect(-radius / 2, -radius / 2, -radius / 2 + 20, 0, 10, 10, Path.Direction.CCW);

path = objectPaths[4];
path.addRect(120, 120, 200, 200, Path.Direction.CCW);

区域检测


检测是否发生了碰撞,准确度不高,但还能凑合


circlePath.reset();
circlePath.addCircle(x- width/2f,y - height/2f,10, Path.Direction.CCW);
circleRegion.setPath(circlePath,mainRegion);

mCommonPaint.setColor(Color.CYAN);
for (int i = 0; i < objectPaths.length; i++) {
objectRegion.setPath(objectPaths[i],mainRegion);
if(!objectRegion.quickReject(circleRegion)){
Log.d("RegionView"," 可能发生了碰撞");
mCommonPaint.setColor(Color.YELLOW);
}else{
mCommonPaint.setColor(Color.CYAN);
}
canvas.drawPath(objectPaths[i], mCommonPaint);
}

到这里我们完成了简单的检测,但其实它的精确度很差,这个效果显然不是我们想要的,尤其没有实际接触的情况就染色了。这样会产生很多争议,比如游戏中刘备不可能超出攻击范围去打你一样。


fire_81.gif


精准区域检测优化


在我们做推箱子游戏和珠珠碰撞的时候,我们都是用圆心之间的距离去检测,显然这里是不行的,不光障碍物本身有形状且不规则,而且中心区域正中可能是空白区域,显然圆心之间的距离是不合适的。我们之前学过PathMeasure很多用法《心跳效果》,其中之一是使用粒子描线,下图是我们的效果,在这篇中我们利用PathMeasure对获取路径坐标,并对线周围布置粒子。


fire_49.gif


那么,使用PathMeasure方式获取线条边缘的点不就更准确了么 ?好的,我们开干。


优化逻辑



  • 获取障碍物和圆的Bounds,计算面积,这样把检测物体和被检测物体中最小的设置给PathMeasure
  • 利用PathMeasure的getPosTan获取点
  • 使用Region的contain进行判断点是不是在区域内

下面是优化逻辑


circlePath.reset();
circlePath.addCircle(x- width/2f,y - height/2f,20, Path.Direction.CCW);
circleRegion.setPath(circlePath,mainRegion);


mCommonPaint.setColor(Color.CYAN);
for (int i = 0; i < objectPaths.length; i++) {
objectRegion.setPath(objectPaths[i],mainRegion);
if(!objectRegion.quickReject(circleRegion)){
mCommonPaint.setColor(Color.YELLOW);
if (circleRegion.getBounds(circleRect)
&& objectRegion.getBounds(objectRect)) {

Region regionChecker = null;
if (circleRect.width() * circleRect.height() > objectRect.width() * objectRect.height()) {
pathMeasure.setPath(objectPaths[i], false);
regionChecker = circleRegion;
} else {
pathMeasure.setPath(circlePath, false);
regionChecker = objectRegion;
}

for (int len = 0; len < pathMeasure.getLength(); len++) {
pathMeasure.getPosTan(len, pos, tan);
if(regionChecker.contains((int) pos[0], (int) pos[1])){
Log.d("RegionView"," 可能发生了碰撞");
mCommonPaint.setColor(Color.YELLOW);
}
}

}

}else{
mCommonPaint.setColor(Color.CYAN);
}
canvas.drawPath(objectPaths[i], mCommonPaint);
}

我们再来看效果,就是文章开头的效果


fire_76.gif


总结


到这里结束了,对于Region类,对点的检测是非常精准的,但是在数学中,所有图形都是点构成线、线构成面,我们本篇利用PathMeasure和Region配合实现了精准检测逻辑,扫平了2D游戏开发过程中的一道门槛。希望看过本篇之后,你能成为游戏大师。


全部代码


有个小插曲,演示精确度低的时候导致代码被还原了,所以重新画了一些东西。


public class RegionView extends View {
private final DisplayMetrics mDM;
private TextPaint mCommonPaint;

public RegionView(Context context) {
this(context, null);
}

public RegionView(Context context, AttributeSet attrs) {
super(context, attrs);
mDM = getResources().getDisplayMetrics();
initPaint();
setClickable(true); //触发hotspot
}

private void initPaint() {
//否则提供给外部纹理绘制
mCommonPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
mCommonPaint.setAntiAlias(true);
mCommonPaint.setStyle(Paint.Style.FILL);
mCommonPaint.setStrokeCap(Paint.Cap.ROUND);
mCommonPaint.setFilterBitmap(true);
mCommonPaint.setDither(true);
mCommonPaint.setStrokeWidth(dp2px(20));

}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
if (widthMode != MeasureSpec.EXACTLY) {
widthSize = mDM.widthPixels / 2;
}
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);

if (heightMode != MeasureSpec.EXACTLY) {
heightSize = widthSize / 2;
}
setMeasuredDimension(widthSize, heightSize);

}

private float x;
private float y;

//所以形状
Path[] objectPaths = new Path[7];
//形状区域检测
Region objectRegion = new Region();

//小圆球区域
Region circleRegion = new Region();
//小圆
Path circlePath = new Path();
//绘制区域
Region mainRegion = new Region();

Rect circleRect = new Rect();
Rect objectRect = new Rect();

float[] pos = new float[2];
float[] tan = new float[2];

PathMeasure pathMeasure = new PathMeasure();

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
if (width < 1 || height < 1) {
return;
}

int save = canvas.save();
canvas.translate(width / 2f, height / 2f);
float radius = Math.min(width / 2f, height / 2f);

mainRegion.set((int) -radius, (int) -radius, (int) radius, (int) radius);

for (int i = 0; i < objectPaths.length; i++) {
Path path = objectPaths[i];
if (path == null) {
path = new Path();
objectPaths[i] = path;
} else {
path.reset();
}
}

Path path = objectPaths[0];
path.moveTo(radius / 2, -radius / 2);
path.lineTo(0, -radius);
path.lineTo(radius / 2, -radius);
path.close();

path = objectPaths[1];
path.moveTo(-radius / 2, radius / 2);
path.lineTo(-radius / 2 - 100, radius / 2);
path.arcTo(-radius / 2 - 100, radius / 2, -radius / 2, radius / 2 + 100, 0, 180, false);
path.lineTo(-radius / 2, radius / 2);
path.close();

path = objectPaths[2];
path.addCircle(-radius + 200f, -radius + 200f, 50f, Path.Direction.CCW);

path = objectPaths[3];
path.addRoundRect(-radius + 50, -radius / 2, -radius + 90, 0, 10, 10, Path.Direction.CCW);

path = objectPaths[4];
path.addRect(120, 120, 200, 200, Path.Direction.CCW);

path = objectPaths[5];
path.addCircle(250, 0, 100, Path.Direction.CCW);

Path tmp = new Path();
tmp.addCircle(250,-80,80,Path.Direction.CCW);
path.op(tmp, Path.Op.DIFFERENCE);

tmp.reset();
path = objectPaths[6];
path.addCircle(0, 0, 100, Path.Direction.CCW);
tmp.addCircle(0, 0, 80, Path.Direction.CCW);
path.op(tmp, Path.Op.DIFFERENCE);


circlePath.reset();
circlePath.addCircle(x- width/2f,y - height/2f,20, Path.Direction.CCW);
circleRegion.setPath(circlePath,mainRegion);


mCommonPaint.setColor(Color.CYAN);
for (int i = 0; i < objectPaths.length; i++) {
objectRegion.setPath(objectPaths[i],mainRegion);
if(!objectRegion.quickReject(circleRegion)){
if (circleRegion.getBounds(circleRect)
&& objectRegion.getBounds(objectRect)) {

Region regionChecker = null;
if (circleRect.width() * circleRect.height() > objectRect.width() * objectRect.height()) {
pathMeasure.setPath(objectPaths[i], false);
regionChecker = circleRegion;
} else {
pathMeasure.setPath(circlePath, false);
regionChecker = objectRegion;
}

for (int len = 0; len < pathMeasure.getLength(); len++) {
pathMeasure.getPosTan(len, pos, tan);
if(regionChecker.contains((int) pos[0], (int) pos[1])){
Log.d("RegionView"," 可能发生了碰撞");
mCommonPaint.setColor(Color.YELLOW);
}
}

}

}else{
mCommonPaint.setColor(Color.CYAN);
}
canvas.drawPath(objectPaths[i], mCommonPaint);
}

mCommonPaint.setColor(Color.WHITE);
canvas.drawPath(circlePath,mCommonPaint);
canvas.restoreToCount(save);
}

@Override
public void dispatchDrawableHotspotChanged(float x, float y) {
super.dispatchDrawableHotspotChanged(x, y);
this.x = x;
this.y = y;
postInvalidate();
}

@Override
protected void dispatchSetPressed(boolean pressed) {
super.dispatchSetPressed(pressed);
postInvalidate();
}

public float dp2px(float dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, mDM);
}

public static int argb(float red, float green, float blue) {
return ((int) (1 * 255.0f + 0.5f) << 24) |
((int) (red * 255.0f + 0.5f) << 16) |
((int) (green * 255.0f + 0.5f) << 8) |
(int) (blue * 255.0f + 0.5f);
}


}

作者:时光少年
来源:juejin.cn/post/7310412252552085513

0 个评论

要回复文章请先登录注册