我有一个简单的游戏应用程序,它使用Android canvas API绘制整个场景。但是,我的游戏存在以下症状的性能问题:
游戏开始时流畅。
过了一会儿(约30秒),每400~500毫秒就会出现一次明显的帧丢失。随着时间的推移,一帧丢失会变得越来越长。如果我让游戏运行5分钟,那么一帧下降就会长达一秒钟,这使得游戏无法播放。
两个连续帧丢失之间的20~30帧是平滑的。
使用systrace工具,我发现帧丢失是由RenderThread在“flush drawing命令”阶段的“waitForTask”方法中被阻塞引起的。同时,在主线程和渲染线程以外的第三个线程上运行几乎100%CPU时间/壁时间比的“pathPrecache”。
可以访问的所有测试设备上重现。
“pathPrecache”在做什么?我可以在这里找到它的来源:https://android.googlesource.com/platform/frameworks/base/+/0f809f3/libs/hwui/PathCache.cpp。看来这个任务试图将画布drawPath()调用的栅格化结果预先缓存到PathTexture对象中。我的游戏场景确实包含一些drawPath()调用(以及drawRect()调用,它们实际上也是drawPath())。但是每个帧都是完全独立的,整个场景都是重绘的。是什么让pathPrecache的工作随着时间的推移变得越来越重?有没有办法禁用pathPrecache功能?
public class GameScene extends View {
private static final String TAG = GameScene.class.getSimpleName();
// ... Debug flags ...
// ... Color constants ...
// ... Dimension calculation constants ...
private GameState mState;
private Paint mBeltPaint;
private Paint mActiveZonePaint;
private Paint mDividerPaint;
private Paint mBorderLinePaint;
private Paint mBorderLineOuterPaint;
private Paint mBitmapPaint;
private Bitmap mActiveZoneShaderBitmap;
private Bitmap mBorderLightBitmapLeft;
private Bitmap mBorderLightBitmapRight;
private Arm mArm;
private Projector mPrj;
private float mBannerHeight;
private final float mDividerWidth;
private final float mLargeBoxWidth;
private final float mSmallBoxWidth;
private final float mBombWidth;
private final float mArmBottomOffsetUp;
private final float mBorderOffsetInner;
private final float mBorderOffsetOuter;
private final float mEmptyCatchTranslationY;
/**
* Manually maintains two software drawing caches containing the game board for performance.
*/
private DrawingCacheInfo[] mBoardCaches;
private static final int DRAW_COMPONENT_BELT = 0;
private static final int DRAW_COMPONENT_BORDER_LIGHTS = 1;
// ... Pre-allocated objects (Rect, etc.) ...
private long mLastDrawTime;
public GameScene(Context context, AttributeSet attrs) {
super(context, attrs);
Resources res = context.getResources();
mBeltPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mBeltPaint.setStyle(Paint.Style.FILL);
mBeltPaint.setDither(true);
mDividerPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mDividerPaint.setStyle(Paint.Style.FILL);
mDividerPaint.setDither(true);
mActiveZonePaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
mActiveZonePaint.setStyle(Paint.Style.FILL);
mBorderLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mBorderLinePaint.setStyle(Paint.Style.STROKE);
mBorderLinePaint.setStrokeWidth(res.getDimension(R.dimen.lucky_game_border_line_width));
mBorderLinePaint.setColor(BORDER_LINE_COLOR);
mBorderLineOuterPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mBorderLineOuterPaint.setStyle(Paint.Style.STROKE);
mBorderLineOuterPaint.setStrokeWidth(res.getDimension(R.dimen.lucky_game_border_outer_line_width));
mBorderLineOuterPaint.setColor(BORDER_OUTER_LINE_COLOR);
mBitmapPaint = new Paint(Paint.FILTER_BITMAP_FLAG);
mBitmapPaint.setStyle(Paint.Style.FILL);
// ... Dimensions initialization ...
// ... Bitmaps initialization ...
}
public void setState(GameState state) {
mState = state;
}
public void setBannerHeight(int bannerHeight) {
mBannerHeight = bannerHeight;
int viewWidth = getWidth();
int viewHeight = getHeight();
int boardHeight = viewHeight - bannerHeight;
mPrj = new Projector(bannerHeight, viewWidth, boardHeight);
mBoardCaches = new DrawingCacheInfo[] {
new DrawingCacheInfo(viewWidth, boardHeight, DRAW_COMPONENT_BELT),
new DrawingCacheInfo(viewWidth, boardHeight, DRAW_COMPONENT_BORDER_LIGHTS)};
mArm = new Arm(getContext(), bannerHeight * (1f - ARM_TOP_OFFSET_RATIO));
Shader beltShader = new LinearGradient(0f, bannerHeight, viewWidth / 2, viewHeight,
BELT_COLOR_FAR, BELT_COLOR_NEAR, Shader.TileMode.CLAMP);
mBeltPaint.setShader(beltShader);
Shader dividerShader = new LinearGradient(0f, bannerHeight, 0f, viewHeight,
DIVIDER_COLOR_FAR, DIVIDER_COLOR_NEAR, Shader.TileMode.CLAMP);
mDividerPaint.setShader(dividerShader);
Shader activeZoneShader = new BitmapShader(mActiveZoneShaderBitmap,
Shader.TileMode.MIRROR, Shader.TileMode.CLAMP);
mActiveZonePaint.setShader(activeZoneShader);
// Heat up drawing cache in advance to avoid first-frame jitter in animation
buildDrawingCache(mBoardCaches[0]);
buildDrawingCache(mBoardCaches[1]);
}
@Override
protected void onDraw(Canvas canvas) {
if (mPrj == null) {
HSLog.i(TAG, "Projector not initialized, skip drawing");
return;
}
super.onDraw(canvas);
if (PROFILE) {
profile();
}
drawGameBoard(canvas);
// Draw all targets except one in catch animation. The drawing of this target is deferred until arm is drawn.
PendingTarget pendingTarget = drawTargets(canvas);
// Draw the arm. Report pulled-up distance of the target being caught.
float pullUpY = drawArm(canvas, pendingTarget);
if (pendingTarget != null) {
// The the pending target, if there is one.
drawPendingTarget(canvas, pendingTarget, -pullUpY);
}
}
private void drawGameBoard(Canvas canvas) {
for (DrawingCacheInfo cacheInfo : mBoardCaches) {
if (cacheInfo.hasBuilt) {
// Draw from cache
GameState.CatchAnimator catchAnim = mState.getCatchAnimator();
if (cacheInfo.component != DRAW_COMPONENT_BORDER_LIGHTS || catchAnim == null) {
mBitmapPaint.setAlpha(0xff);
} else {
float alpha = catchAnim.getBorderLightAlpha();
mBitmapPaint.setAlpha((int) (0xff * alpha));
}
float revealRatio = 1f;
if (cacheInfo.component == DRAW_COMPONENT_BORDER_LIGHTS) {
revealRatio = mState.getBorderLightsRevealRatio();
}
mRect.set(0, 0, cacheInfo.cache.getWidth(),
(int) (revealRatio * cacheInfo.cache.getHeight()));
mRectF.set(0, mBannerHeight, cacheInfo.cache.getWidth(),
mBannerHeight + revealRatio * cacheInfo.cache.getHeight());
canvas.drawBitmap(cacheInfo.cache, mRect, mRectF, mBitmapPaint);
} else {
buildDrawingCache(cacheInfo);
// Draw directly to on-screen canvas
performDrawBoardComponent(canvas, cacheInfo.component, false);
}
}
}
private void buildDrawingCache(DrawingCacheInfo cacheInfo) {
if (cacheInfo.enabled) {
Canvas cacheCanvas = new Canvas(cacheInfo.cache);
cacheCanvas.translate(0, -mBannerHeight);
// Draw to cache
performDrawBoardComponent(cacheCanvas, cacheInfo.component, true);
cacheInfo.hasBuilt = true;
}
}
/**
* @param cacheDraw {@code true} if we're drawing to cache bitmap, {@code false} if we're drawing to screen.
*/
private void performDrawBoardComponent(Canvas canvas, int which, boolean cacheDraw) {
switch (which) {
case DRAW_COMPONENT_BELT:
performDrawBelt(canvas);
break;
case DRAW_COMPONENT_BORDER_LIGHTS:
performDrawBorderLights(canvas, cacheDraw);
break;
}
}
private void performDrawBelt(Canvas canvas) {
GameConfig config = mState.getConfig();
float activeRowIndex = config.getActiveRowIndex();
int trackCount = config.getTrackCount();
int rowCount = config.getRowCount();
float toleranceV = config.getCatchToleranceVertical();
int viewWidth = getWidth();
int viewHeight = getHeight();
// Draw belt color
mRectF.set(0, 0, viewWidth, viewHeight);
mPrj.drawRect(canvas, mRectF, mBeltPaint);
// Draw track dividers
float perTrackWidth = getWidth() / (float) trackCount;
for (int i = 1; i < trackCount; i++) {
float dividerX = i * perTrackWidth;
mRectF.set(dividerX - mDividerWidth / 2, 0, dividerX + mDividerWidth / 2, viewHeight);
mPrj.drawRect(canvas, mRectF, mDividerPaint);
}
// Draw active zone
float activeZoneTop = ((activeRowIndex - 0.5f - 0.5f * toleranceV) / (float) rowCount) * viewHeight;
float activeZoneBottom = ((activeRowIndex - 0.5f + 0.5f * toleranceV) / (float) rowCount) * viewHeight;
mRectF.set(0, activeZoneTop, viewWidth, activeZoneBottom);
mPrj.drawRect(canvas, mRectF, mActiveZonePaint, mActiveZoneShaderBitmap);
// Draw border lines
mPrj.drawLine(canvas, -mBorderOffsetOuter, 0f, -mBorderOffsetOuter, viewHeight, mBorderLinePaint);
mPrj.drawLine(canvas, -mBorderOffsetInner, 0f, -mBorderOffsetInner, viewHeight, mBorderLinePaint);
mPrj.drawLine(canvas, viewWidth + mBorderOffsetInner, 0f, viewWidth + mBorderOffsetInner, viewHeight, mBorderLinePaint);
mPrj.drawLine(canvas, viewWidth + mBorderOffsetOuter, 0f, viewWidth + mBorderOffsetOuter, viewHeight, mBorderLinePaint);
mRectF.set(-mBorderOffsetOuter - mBorderOffsetInner, 0f, 0f, viewHeight);
mPrj.drawRect(canvas, mRectF, mBorderLineOuterPaint);
mRectF.set(viewWidth, 0f, viewWidth + mBorderOffsetOuter + mBorderOffsetInner, viewHeight);
mPrj.drawRect(canvas, mRectF, mBorderLineOuterPaint);
}
private void performDrawBorderLights(Canvas canvas, boolean cacheDraw) {
int lightCount = mState.getConfig().getBorderLightCount();
int viewWidth = getWidth();
int viewHeight = getHeight();
float lightWidth = 4 * (mBorderOffsetOuter - mBorderOffsetInner);
float lightHeight = lightWidth * ((float) mBorderLightBitmapLeft.getHeight() / mBorderLightBitmapLeft.getWidth());
GameState.CatchAnimator catchAnim = mState.getCatchAnimator();
if (cacheDraw || catchAnim == null) {
mBitmapPaint.setAlpha(0xff);
} else {
float alpha = catchAnim.getBorderLightAlpha();
mBitmapPaint.setAlpha((int) (0xff * alpha));
}
float revealRatio = 1f;
if (!cacheDraw) { // For cache building, draw all lights regardless of current reveal ratio
revealRatio = mState.getBorderLightsRevealRatio();
}
for (int i = 1, n = (int) (lightCount * revealRatio); i <= n; i++) {
float lightX = -(mBorderOffsetInner + mBorderOffsetOuter) / 2f;
float lightY = ((float) i / lightCount) * viewHeight;
mRectF.set(lightX - lightWidth / 2f, lightY - lightHeight / 2f,
lightX + lightWidth / 2f, lightY + lightHeight / 2f);
mPrj.drawBitmap(canvas, mBorderLightBitmapLeft, mRectF, mBitmapPaint);
lightX = viewWidth + (mBorderOffsetInner + mBorderOffsetOuter) / 2f;
mRectF.set(lightX - lightWidth / 2f, lightY - lightHeight / 2f,
lightX + lightWidth / 2f, lightY + lightHeight / 2f);
mPrj.drawBitmap(canvas, mBorderLightBitmapRight, mRectF, mBitmapPaint);
}
}
/**
* Draw all targets of current game state except the one in catch animation.
*
* @return Drawing info of the target currently in catch animation. Or null.
*/
private PendingTarget drawTargets(Canvas canvas) {
List<TargetInfo> targets = mState.getTargets();
PendingTarget pendingTarget = null;
for (TargetInfo target : targets) {
if (target.hasFlag(TargetInfo.FLAG_CAUGHT)) {
continue;
}
mapTargetCoordinates(target, mCoords, mBitmapPaint);
Bitmap bitmap = target.getBitmap();
float halfWidth = 0f, halfHeight, up, down;
switch (target.type) {
case LARGE_BOX:
halfWidth = mLargeBoxWidth / 2f;
break;
case SMALL_BOX:
halfWidth = mSmallBoxWidth / 2f;
break;
case BOMB:
halfWidth = mBombWidth / 2f;
break;
}
// ... Some calculations ...
if (target.hasFlag(TargetInfo.FLAG_IN_CATCH_ANIMATION)) {
if (pendingTarget != null) {
throw new GameException("Multiple targets in catching animation: "
+ target.trackIndex + ", " + target.translation);
}
pendingTarget = new PendingTarget(target, bitmap, new RectF(mRectF));
} else {
mPrj.drawBitmap(canvas, bitmap, mRectF, mBitmapPaint);
}
}
return pendingTarget;
}
private void drawPendingTarget(Canvas canvas, PendingTarget target, float offsetY) {
mBitmapPaint.setAlpha(0xff);
mPrj.drawBitmap(canvas, target.bitmap, target.bounds, offsetY, mBitmapPaint);
}
private static class PendingTarget {
TargetInfo target;
Bitmap bitmap;
RectF bounds;
PendingTarget(TargetInfo target, Bitmap bitmap, RectF bounds) {
this.target = target;
this.bitmap = bitmap;
this.bounds = bounds;
}
}
private float drawArm(Canvas canvas, PendingTarget preyTarget) {
GameConfig config = mState.getConfig();
float activeRowIndex = config.getActiveRowIndex();
int trackCount = config.getTrackCount();
int rowCount = config.getRowCount();
float toleranceV = config.getCatchToleranceVertical();
// ... Some calculations ...
mInCoord[0] = armX;
mInCoord[1] = activeZoneCenterY;
mPrj.projectPoint(mInCoord, mOutCoord);
float prjCenterX = mOutCoord[0];
mInCoord[1] = activeZoneTopY;
mPrj.projectPoint(mInCoord, mOutCoord);
float prjActiveZoneTopY = mOutCoord[1];
GameState.CatchAnimator catchAnim = mState.getCatchAnimator();
float closeAngle = handleCatchAnimation(prjCenterX, prjActiveZoneTopY + mArmBottomOffsetUp, preyTarget,
catchAnim, mOutCoord, mOutVal);
mArm.draw(canvas, mOutCoord[0], mOutCoord[1], closeAngle);
return mOutVal[0];
}
/**
* @param paint Paint used to draw this {@code target}. It's alpha would be set in this method based on distance.
*/
private void mapTargetCoordinates(TargetInfo target, int[] outCoord, Paint paint) {
GameConfig config = mState.getConfig();
int trackCount = config.getTrackCount();
int rowCount = config.getRowCount();
float trackIndex = extendHorizontalPosition(target.trackIndex);
float translation = target.translation;
outCoord[0] = (int) (((trackIndex + 0.5f) / (float) trackCount) * getWidth());
outCoord[1] = (int) (((translation - 0.5f) / (float) rowCount) * getHeight());
float alphaUnclamped = TARGET_ALPHA_FAR +
(1f - TARGET_ALPHA_FAR) * (translation / rowCount) / TARGET_OPAQUE_POSITION_RATIO;
float alpha = Math.max(0f, Math.min(alphaUnclamped, 1f));
paint.setAlpha((int) (0xff * alpha));
}
private float extendHorizontalPosition(float rawPosition) {
int span = mState.getConfig().getTrackCount() - 1;
return rawPosition * (span + 2 * HORIZONTAL_POSITION_EXTENSION) / span
- HORIZONTAL_POSITION_EXTENSION;
}
public void release() {
if (mActiveZoneShaderBitmap != null) {
mActiveZoneShaderBitmap = null;
}
if (mBorderLightBitmapLeft != null) {
mBorderLightBitmapLeft = null;
}
if (mBorderLightBitmapRight != null) {
mBorderLightBitmapRight = null;
}
}
private static class Arm {
private static final int HAND_LEFT = 0;
private static final int HAND_RIGHT = 1;
/** Hands' angle to vertical position at normal open status, in radians (about 16 degrees) */
private static final float HAND_OPEN_ANGLE_RADIANS = 0.281f;
private Drawable mBody;
private Drawable mLeftHand;
private Drawable mRightHand;
private final float mTop;
private final float mHandPivotY;
private final float mLeftHandInstallX;
private final float mRightHandInstallX;
private final float mHandsInstallY;
Arm(Context context, float top) {
Resources res = context.getResources();
mHandPivotY = res.getDimension(R.dimen.lucky_game_arm_hand_pivot_y);
mLeftHandInstallX = res.getDimension(R.dimen.lucky_game_arm_left_hand_install_x);
mRightHandInstallX = res.getDimension(R.dimen.lucky_game_arm_right_hand_install_x);
mHandsInstallY = res.getDimension(R.dimen.lucky_game_arm_hands_install_y);
mBody = ContextCompat.getDrawable(context, R.drawable.lucky_arm_body);
Bitmap rightHandBitmap = BitmapFactory.decodeResource(res, R.drawable.lucky_arm_hand);
mRightHand = new BitmapDrawable(res, rightHandBitmap);
Matrix m = new Matrix();
m.setScale(-1f, 1f);
Bitmap leftHandBitmap = Bitmap.createBitmap(rightHandBitmap, 0, 0,
rightHandBitmap.getWidth(), rightHandBitmap.getHeight(), m, false);
mLeftHand = new BitmapDrawable(res, leftHandBitmap);
mTop = top;
}
void draw(Canvas canvas, float centerX, float bottom, float closeAngleRadians) {
int width = mBody.getIntrinsicWidth();
mBody.setBounds((int) centerX - width / 2, (int) mTop, (int) centerX + width / 2, (int) bottom);
mBody.draw(canvas);
drawHand(canvas, HAND_LEFT, closeAngleRadians);
drawHand(canvas, HAND_RIGHT, closeAngleRadians);
}
private void drawHand(Canvas canvas, int which, float closeAngleRadians) {
float isLeft = (which == HAND_LEFT ? 1f : -1f);
float drawAngle = isLeft * (HAND_OPEN_ANGLE_RADIANS - closeAngleRadians);
Rect bodyBounds = mBody.getBounds();
float pivotX = bodyBounds.left + (which == HAND_LEFT ? mLeftHandInstallX : mRightHandInstallX);
float pivotY = bodyBounds.bottom - mHandsInstallY;
canvas.save();
canvas.rotate((float) Math.toDegrees(drawAngle), pivotX, pivotY);
Drawable drawable = (which == HAND_LEFT ? mLeftHand : mRightHand);
drawable.setBounds(
(int) (pivotX - mRightHand.getIntrinsicWidth() / 2f),
(int) (pivotY - mHandPivotY),
(int) (pivotX + mRightHand.getIntrinsicWidth() / 2f),
(int) (pivotY + (mRightHand.getIntrinsicHeight() - mHandPivotY)));
drawable.draw(canvas);
canvas.restore();
}
}
/**
* Takes drawing commands and projects objects in [0, 0 - width, height] to a perspective position.
*/
private class Projector {
private static final float PROJECTION_RATIO_FAR = 0.426f;
private static final float PROJECTION_RATIO_NEAR = 1.095f;
private int mTopBanner;
private int mWidth;
private int mHeight;
private Path mPath = new Path();
private Matrix mMatrix = new Matrix();
Projector(int topBanner, int width, int height) {
mTopBanner = topBanner;
mWidth = width;
mHeight = height;
}
void drawRect(Canvas canvas, RectF rect, @NonNull Paint paint) {
drawRect(canvas, rect, paint, null);
}
void drawRect(Canvas canvas, RectF rect, @NonNull Paint paint, @Nullable Bitmap shaderBitmap) {
// Outer RECTANGLE bounds AFTER projection, used to process shader on the paint
float left, top, right, bottom;
mPath.reset();
mInCoord[0] = rect.left;
mInCoord[1] = rect.top;
projectPoint(mInCoord, mOutCoord);
mPath.moveTo(mOutCoord[0], mOutCoord[1]);
top = mOutCoord[1];
mInCoord[0] = rect.left;
mInCoord[1] = rect.bottom;
projectPoint(mInCoord, mOutCoord);
mPath.lineTo(mOutCoord[0], mOutCoord[1]);
left = mOutCoord[0];
bottom = mOutCoord[1];
mInCoord[0] = rect.right;
mInCoord[1] = rect.bottom;
projectPoint(mInCoord, mOutCoord);
mPath.lineTo(mOutCoord[0], mOutCoord[1]);
right = mOutCoord[0];
mInCoord[0] = rect.right;
mInCoord[1] = rect.top;
projectPoint(mInCoord, mOutCoord);
mPath.lineTo(mOutCoord[0], mOutCoord[1]);
mPath.close();
// Translate and scale bitmap shader on the paint (if set) to proper position
if (shaderBitmap != null && paint.getShader() instanceof BitmapShader) {
BitmapShader shader = (BitmapShader) paint.getShader();
mMatrix.setTranslate(left, top);
float scaleX = (right - left) / shaderBitmap.getWidth();
float scaleY = (bottom - top) / shaderBitmap.getHeight();
mMatrix.preScale(scaleX, scaleY);
shader.setLocalMatrix(mMatrix);
}
canvas.drawPath(mPath, paint);
}
void drawLine(Canvas canvas, float startX, float startY, float stopX, float stopY, @NonNull Paint paint) {
mInCoord[0] = startX;
mInCoord[1] = startY;
projectPoint(mInCoord, mOutCoord);
startX = mOutCoord[0];
startY = mOutCoord[1];
mInCoord[0] = stopX;
mInCoord[1] = stopY;
projectPoint(mInCoord, mOutCoord);
stopX = mOutCoord[0];
stopY = mOutCoord[1];
canvas.drawLine(startX, startY, stopX, stopY, paint);
}
/**
* Draw with position projection and scale only. Aspect ratio is not altered. Bitmap is considered to have a
* perspective look itself and hence is not twisted.
*/
void drawBitmap(Canvas canvas, @NonNull Bitmap bitmap, @NonNull RectF dst, @Nullable Paint paint) {
drawBitmap(canvas, bitmap, dst, 0f, paint);
}
/**
* Draw with position projection and scale only. Aspect ratio is not altered. Bitmap is considered to have a
* perspective look itself and hence is not twisted.
*
* @param offsetY Offset Y AFTER projection.
*/
void drawBitmap(Canvas canvas, @NonNull Bitmap bitmap, @NonNull RectF dst, float offsetY, @Nullable Paint paint) {
// Project rect center
mInCoord[0] = (dst.left + dst.right) / 2f;
mInCoord[1] = (dst.top + dst.bottom) / 2f;
float halfWidth = mInCoord[0] - dst.left;
float halfHeight = mInCoord[1] - dst.top;
float ratio = projectPoint(mInCoord, mOutCoord);
float centerX = mOutCoord[0];
float centerY = mOutCoord[1] + offsetY;
mRectF.set(centerX - halfWidth * ratio, centerY - halfHeight * ratio,
centerX + halfWidth * ratio, centerY + halfHeight * ratio);
canvas.drawBitmap(bitmap, null, mRectF, paint);
}
float projectPoint(float[] inCoord, float[] outCoord) {
if (DEBUG_PROJECTION) {
outCoord[0] = inCoord[0];
outCoord[1] = inCoord[1];
return 1f;
}
float x = inCoord[0];
float y = inCoord[1];
float yRelative = perspective(y / (mTopBanner + mHeight));
float projectionRatio = PROJECTION_RATIO_FAR
+ yRelative * (PROJECTION_RATIO_NEAR - PROJECTION_RATIO_FAR);
float centerX = mWidth / 2f;
outCoord[0] = centerX + projectionRatio * (x - centerX);
outCoord[1] = mTopBanner + yRelative * mHeight;
return projectionRatio;
}
/**
* [0, 1] -> [0, 1] mapping function that adjust Y value to make difference in distance looks real.
*/
private float perspective(float p) {
final float coefSum = PROJECTION_RATIO_NEAR + PROJECTION_RATIO_FAR;
return ((PROJECTION_RATIO_NEAR - PROJECTION_RATIO_FAR) / coefSum) * p * p
+ (2 * PROJECTION_RATIO_FAR / coefSum) * p;
}
}
private static class DrawingCacheInfo {
/** Whether the cache is enabled. Cache is disabled if OutOfMemory error occurs with memory allocation. */
boolean enabled = true;
boolean hasBuilt = false;
Bitmap cache;
/**
* Which component of game board this drawing cache is holding.
* Must be one of {@link #DRAW_COMPONENT_BELT}, {@link #DRAW_COMPONENT_BORDER_LIGHTS}.
* */
int component;
DrawingCacheInfo(int width, int height, int whichComponent) {
try {
cache = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
} catch (OutOfMemoryError e) {
HSLog.w(TAG, "Out of memory while trying to allocate drawing cache, disable cache");
enabled = false;
}
component = whichComponent;
}
}
private static class GameException extends RuntimeException {
GameException(String message) {
super(message);
}
}
}