Android 实现抖音小游戏潜艇大挑战的思路详解

《潜水艇大挑战》是抖音上的一款小游戏,以面部识别来驱动潜艇通过障碍物,最近特别火爆,相信很多人都玩过。

一时兴起自己用Android自定义View也撸了一个,发现只要有好的创意,不用高深的技术照样可以开发出好玩的应用。开发过程现拿出来与大家分享一下。

项目地址:

https://github.com/vitaviva/ugame

基本思路

整个游戏视图可以分成三层:

  • camera(相机):处理相机的preview以及人脸识别
  • background(后景):处理障碍物相关逻辑
  • foreground(前景):处理潜艇相关

代码也是按上面三个层面组织的,游戏界面的布局可以简单理解为三层视图的叠加,然后在各层视图中完成相关工作

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
 xmlns:tools="http://schemas.android.com/tools"
 android:layout_width="match_parent"
 android:layout_height="match_parent">

 <!-- 相机 -->
 <TextureView
 android:layout_width="match_parent"
 android:layout_height="match_parent"/>

 <!-- 后景 -->
 <com.my.ugame.bg.BackgroundView
 android:layout_width="match_parent"
 android:layout_height="match_parent"/>

 <!-- 前景 -->
 <com.my.ugame.fg.ForegroundView
 android:layout_width="match_parent"
 android:layout_height="match_parent"/>

</Framelayout>

开发中会涉及以下技术的使用,没有高精尖、都是大路货:

  • 相机:使用Camera2完成相机的预览和人脸识别
  • 自定义View:定义并控制障碍物和潜艇
  • 属性动画:控制障碍物和潜艇的移动及各种动效

少啰嗦,先看东西!下面介绍各部分代码的实现。

后景(Background)Bar

首先定义障碍物基类Bar,主要负责是将bitmap资源绘制到指定区域。由于障碍物从屏幕右侧定时刷新时的高度随机,所以其绘制区域的x、y、w、h需要动态设置

/**
 * 障碍物基类
 */
sealed class Bar(context: Context) {

 protected open val bmp = context.getDrawable(R.mipmap.bar)!!.toBitmap()

 protected abstract val srcRect: Rect

 private lateinit var dstRect: Rect

 private val paint = Paint()

 var h = 0F
 set(value) {
  field = value
  dstRect = Rect(0, 0, w.toInt(), h.toInt())
 }

 var w = 0F
 set(value) {
  field = value
  dstRect = Rect(0, 0, w.toInt(), h.toInt())
 }

 var x = 0F
 set(value) {
  view.x = value
  field = value
 }

 val y
 get() = view.y

 internal val view by lazy {
 BarView(context) {
  it?.apply {
  drawBitmap(
   bmp,
   srcRect,
   dstRect,
   paint
  )
  }
 }
 }

}

internal class BarView(context: Context?, private val block: (Canvas?) -> Unit) :
 View(context) {

 override fun onDraw(canvas: Canvas?) {
 block((canvas))
 }
}

障碍物分为上方和下方两种,由于使用了同一张资源,所以绘制时要区别对待,因此定义了两个子类:UpBarDnBar

/**
 * 屏幕上方障碍物
 */
class UpBar(context: Context, container: ViewGroup) : Bar(context) {

 private val _srcRect by lazy(LazyThreadSafetyMode.NONE) {
 Rect(0, (bmp.height * (1 - (h / container.height))).toInt(), bmp.width, bmp.height)
 }
 override val srcRect: Rect
 get() = _srcRect
}

下方障碍物的资源旋转180度后绘制

/**
 * 屏幕下方障碍物
 */
class DnBar(context: Context, container: ViewGroup) : Bar(context) {

 override val bmp = super.bmp.let {
 Bitmap.createBitmap(
  it, 0, 0, it.width, it.height,
  Matrix().apply { postRotate(-180F) }, true
 )
 }

 private val _srcRect by lazy(LazyThreadSafetyMode.NONE) {
 Rect(0, 0, bmp.width, (bmp.height * (h / container.height)).toInt())
 }

 override val srcRect: Rect
 get() = _srcRect
}

BackgroundView

接下来创建后景的容器BackgroundView,容器用来定时地创建、并移动障碍物。
通过列表barsList管理当前所有的障碍物,onLayout中,将障碍物分别布局到屏幕上方和下方

/**
 * 后景容器类
 */
class BackgroundView(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs) {

 internal val barsList = mutableListOf<Bars>()

 override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
 barsList.flatMap { listOf(it.up, it.down) }.forEach {
  val w = it.view.measuredWidth
  val h = it.view.measuredHeight
  when (it) {
  is UpBar -> it.view.layout(0, 0, w, h)
  else -> it.view.layout(0, height - h, w, height)
  }
 }
 }

提供两个方法startstop,控制游戏的开始和结束:

  • 游戏结束时,要求所有障碍物停止移动。
  • 游戏开始后会通过Timer,定时刷新障碍物
/**
 * 游戏结束,停止所有障碍物的移动
 */
 @UiThread
 fun stop() {
 _timer.cancel()
 _anims.forEach { it.cancel() }
 _anims.clear()
 }

 /**
 * 定时刷新障碍物:
 * 1. 创建
 * 2. 添加到视图
 * 3. 移动
 */
 @UiThread
 fun start() {
 _clearBars()
 Timer().also { _timer = it }.schedule(object : TimerTask() {
  override fun run() {
  post {
   _createBars(context, barsList.lastOrNull()).let {
   _addBars(it)
   _moveBars(it)
   }
  }
  }

 }, FIRST_APPEAR_DELAY_MILLIS, BAR_APPEAR_INTERVAL_MILLIS
 )
 }

 /**
 * 游戏重启时,清空障碍物
 */
 private fun _clearBars() {
 barsList.clear()
 removeAllViews()
 }

刷新障碍物

障碍物的刷新经历三个步骤:

  • 创建:上下两个为一组创建障碍物
  • 添加:将对象添加到barsList,同时将View添加到容器
  • 移动:通过属性动画从右侧移动到左侧,并在移出屏幕后删除

创建障碍物时会为其设置随机高度,随机不能太过,要以前一个障碍物为基础进行适当调整,保证随机的同时兼具连贯性

 /**
 * 创建障碍物(上下两个为一组)
 */
 private fun _createBars(context: Context, pre: Bars?) = run {
 val up = UpBar(context, this).apply {
  h = pre?.let {
  val step = when {
   it.up.h >= height - _gap - _step -> -_step
   it.up.h <= _step -> _step
   _random.nextBoolean() -> _step
   else -> -_step
  }
  it.up.h + step
  } ?: _barHeight
  w = _barWidth
 }

 val down = DnBar(context, this).apply {
  h = height - up.h - _gap
  w = _barWidth
 }

 Bars(up, down)

 }

 /**
 * 添加到屏幕
 */
 private fun _addBars(bars: Bars) {
 barsList.add(bars)
 bars.asArray().forEach {
  addView(
  it.view,
  ViewGroup.LayoutParams(
   it.w.toInt(),
   it.h.toInt()
  )
  )
 }
 }

 /**
 * 使用属性动画移动障碍物
 */
 private fun _moveBars(bars: Bars) {
 _anims.add(
  ValueAnimator.ofFloat(width.toFloat(), -_barWidth)
  .apply {
   addUpdateListener {
   bars.asArray().forEach { bar ->
    bar.x = it.animatedValue as Float
    if (bar.x + bar.w <= 0) {
    post { removeView(bar.view) }
    }
   }
   }

   duration = BAR_MOVE_DURATION_MILLIS
   interpolator = LinearInterpolator()
   start()
  })
 }

}

前景(Foreground)

Boat

定会潜艇类Boat,创建自定义View,并提供方法移动到指定坐标

/**
 * 潜艇类
 */
class Boat(context: Context) {

 internal val view by lazy { BoatView(context) }

 val h
 get() = view.height.toFloat()

 val w
 get() = view.width.toFloat()

 val x
 get() = view.x

 val y
 get() = view.y

 /**
 * 移动到指定坐标
 */
 fun moveTo(x: Int, y: Int) {
 view.smoothMoveTo(x, y)
 }

}

BoatView

自定义View中完成以下几个事情

  • 通过两个资源定时切换,实现探照灯闪烁的效果
  • 通过OverScroller让移动过程更加顺滑
  • 通过一个Rotation Animation,让潜艇在移动时可以调转角度,更加灵动
internal class BoatView(context: Context?) : AppCompatImageView(context) {

 private val _scroller by lazy { OverScroller(context) }

 private val _res = arrayOf(
 R.mipmap.boat_000,
 R.mipmap.boat_002
 )

 private var _rotationAnimator: ObjectAnimator? = null

 private var _cnt = 0
 set(value) {
  field = if (value > 1) 0 else value
 }

 init {
 scaleType = ScaleType.FIT_CENTER
 _startFlashing()
 }

 private fun _startFlashing() {
 postDelayed({
  setImageResource(_res[_cnt++])
  _startFlashing()
 }, 500)
 }

 override fun computeScroll() {
 super.computeScroll()

 if (_scroller.computeScrollOffset()) {

  x = _scroller.currX.toFloat()
  y = _scroller.currY.toFloat()

  // Keep on drawing until the animation has finished.
  postInvalidateOnAnimation()
 }

 }

 /**
 * 移动更加顺换
 */
 internal fun smoothMoveTo(x: Int, y: Int) {
 if (!_scroller.isFinished) _scroller.abortAnimation()
 _rotationAnimator?.let { if (it.isRunning) it.cancel() }

 val curX = this.x.toInt()
 val curY = this.y.toInt()

 val dx = (x - curX)
 val dy = (y - curY)
 _scroller.startScroll(curX, curY, dx, dy, 250)

 _rotationAnimator = ObjectAnimator.ofFloat(
  this,
  "rotation",
  rotation,
  Math.toDegrees(atan((dy / 100.toDouble()))).toFloat()
 ).apply {
  duration = 100
  start()
 }

 postInvalidateOnAnimation()
 }
}

ForegroundView

  • 通过boat成员持有潜艇对象,并对其进行控制
  • 实现CameraHelper.FaceDetectListener根据人脸识别的回调,移动潜艇到指定位置
  • 游戏开始时,创建潜艇并做开场动画
/**
 * 前景容器类
 */
class ForegroundView(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs),
 CameraHelper.FaceDetectListener {

 private var _isStop: Boolean = false

 internal var boat: Boat? = null

 /**
 * 游戏停止,潜艇不再移动
 */
 @MainThread
 fun stop() {
 _isStop = true
 }

 /**
 * 接受人脸识别的回调,移动位置
 */
 override fun onFaceDetect(faces: Array<Face>, facesRect: ArrayList<RectF>) {
 if (_isStop) return
 if (facesRect.isNotEmpty()) {
  boat?.run {
  val face = facesRect.first()
  val x = (face.left - _widthOffset).toInt()
  val y = (face.top + _heightOffset).toInt()
  moveTo(x, y)
  }
  _face = facesRect.first()
 }
 }

}

开场动画

游戏开始时,将潜艇通过动画移动到起始位置,即y轴的二分之一处

 /**
 * 游戏开始时通过动画进入
 */
 @MainThread
 fun start() {
 _isStop = false
 if (boat == null) {
  boat = Boat(context).also {
  post {
   addView(it.view, _width, _width)
   AnimatorSet().apply {
   play(
    ObjectAnimator.ofFloat(
    it.view,
    "y",
    0F,
    this@ForegroundView.height / 2f
    )
   ).with(
    ObjectAnimator.ofFloat(it.view, "rotation", 0F, 360F)
   )
   doOnEnd { _ -> it.view.rotation = 0F }
   duration = 1000
   }.start()
  }
  }
 }
 }

相机(Camera)

相机部分主要有TextureViewCameraHelper组成。TextureView提供给Camera承载preview;工具类CameraHelper主要完成以下功能:

  • 开启相机:通过CameraManger代开摄像头
  • 摄像头切换:切换前后置摄像头,
  • 预览:获取Camera提供的可预览尺寸,并适配TextureView显示
  • 人脸识别:检测人脸位置,进行TestureView上的坐标变换

相机硬件提供的可预览尺寸与屏幕实际尺寸(即TextureView尺寸)可能不一致,所以需要在相机初始化时,选取最合适的PreviewSize,避免TextureView上发生画面拉伸等异常

class CameraHelper(val mActivity: Activity, private val mTextureView: TextureView) {

 private lateinit var mCameraManager: CameraManager
 private var mCameraDevice: CameraDevice? = null
 private var mCameraCaptureSession: CameraCaptureSession? = null

 private var canExchangeCamera = false      //是否可以切换摄像头
 private var mFaceDetectMatrix = Matrix()      //人脸检测坐标转换矩阵
 private var mFacesRect = ArrayList<RectF>()      //保存人脸坐标信息
 private var mFaceDetectListener: FaceDetectListener? = null    //人脸检测回调
 private lateinit var mPreviewSize: Size

 /**
 * 初始化
 */
 private fun initCameraInfo() {
 mCameraManager = mActivity.getSystemService(Context.CAMERA_SERVICE) as CameraManager
 val cameraIdList = mCameraManager.cameraIdList
 if (cameraIdList.isEmpty()) {
  mActivity.toast("没有可用相机")
  return
 }

 //获取摄像头方向
 mCameraSensorOrientation =
  mCameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION)!!
 //获取StreamConfigurationMap,它是管理摄像头支持的所有输出格式和尺寸
 val configurationMap =
  mCameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!!

 val previewSize = configurationMap.getOutputSizes(SurfaceTexture::class.java) //预览尺寸

 // 当屏幕为垂直的时候需要把宽高值进行调换,保证宽大于高
 mPreviewSize = getBestSize(
  mTextureView.height,
  mTextureView.width,
  previewSize.toList()
 )

 //根据preview的size设置TextureView
 mTextureView.surfaceTexture.setDefaultBufferSize(mPreviewSize.width, mPreviewSize.height)
 mTextureView.setAspectRatio(mPreviewSize.height, mPreviewSize.width)
 }

选取preview尺寸的原则与TextureView的长宽比尽量一致,且面积尽量接近。

private fun getBestSize(
 targetWidth: Int,
 targetHeight: Int,
 sizeList: List<Size>
 ): Size {
 val bigEnough = ArrayList<Size>() //比指定宽高大的Size列表
 val notBigEnough = ArrayList<Size>() //比指定宽高小的Size列表

 for (size in sizeList) {

  //宽高比 == 目标值宽高比
  if (size.width == size.height * targetWidth / targetHeight
  ) {
  if (size.width >= targetWidth && size.height >= targetHeight)
   bigEnough.add(size)
  else
   notBigEnough.add(size)
  }
 }

 //选择bigEnough中最小的值 或 notBigEnough中最大的值
 return when {
  bigEnough.size > 0 -> Collections.min(bigEnough, CompareSizesByArea())
  notBigEnough.size > 0 -> Collections.max(notBigEnough, CompareSizesByArea())
  else -> sizeList[0]
 }

		initFaceDetect()
 }

initFaceDetect()用来进行人脸的Matrix初始化,后文介绍

人脸识别

为相机预览,创建一个CameraCaptureSession对象,会话通过CameraCaptureSession.CaptureCallback返回TotalCaptureResult,通过参数可以让其中包括人脸识别的相关信息

 /**
 * 创建预览会话
 */
 private fun createCaptureSession(cameraDevice: CameraDevice) {

 // 为相机预览,创建一个CameraCaptureSession对象
 cameraDevice.createCaptureSession(
  arrayListOf(surface),
  object : CameraCaptureSession.StateCallback() {

  override fun onConfigured(session: CameraCaptureSession) {
   mCameraCaptureSession = session
   session.setRepeatingRequest(
   captureRequestBuilder.build(),
   mCaptureCallBack,
   mCameraHandler
   )
  }

  },
  mCameraHandler
 )
 }

 private val mCaptureCallBack = object : CameraCaptureSession.CaptureCallback() {
 override fun onCaptureCompleted(
  session: CameraCaptureSession,
  request: CaptureRequest,
  result: TotalCaptureResult
 ) {
  super.onCaptureCompleted(session, request, result)
  if (mFaceDetectMode != CaptureRequest.STATISTICS_FACE_DETECT_MODE_OFF)
  handleFaces(result)

 }
 }

通过mFaceDetectMatrix对人脸信息进行矩阵变化,确定人脸坐标以使其准确应用到TextureView。

 /**
 * 处理人脸信息
 */
 private fun handleFaces(result: TotalCaptureResult) {
 val faces = result.get(CaptureResult.STATISTICS_FACES)!!
 mFacesRect.clear()

 for (face in faces) {
  val bounds = face.bounds

  val left = bounds.left
  val top = bounds.top
  val right = bounds.right
  val bottom = bounds.bottom

  val rawFaceRect =
  RectF(left.toFloat(), top.toFloat(), right.toFloat(), bottom.toFloat())
  mFaceDetectMatrix.mapRect(rawFaceRect)

  var resultFaceRect = if (mCameraFacing == CaptureRequest.LENS_FACING_FRONT) {
  rawFaceRect
  } else {
  RectF(
   rawFaceRect.left,
   rawFaceRect.top - mPreviewSize.width,
   rawFaceRect.right,
   rawFaceRect.bottom - mPreviewSize.width
  )
  }

  mFacesRect.add(resultFaceRect)

 }

 		mActivity.runOnUiThread {
  mFaceDetectListener?.onFaceDetect(faces, mFacesRect)
 }
 }

最后,在UI线程将包含人脸坐标的Rect通过回调传出:

mActivity.runOnUiThread {
 mFaceDetectListener?.onFaceDetect(faces, mFacesRect)
 }

FaceDetectMatrix

mFaceDetectMatrix是在获取PreviewSize之后创建的

 /**
 * 初始化人脸检测相关信息
 */
 private fun initFaceDetect() {

 val faceDetectModes =
  mCameraCharacteristics.get(CameraCharacteristics.STATISTICS_INFO_AVAILABLE_FACE_DETECT_MODES) //人脸检测的模式

 mFaceDetectMode = when {
  faceDetectModes!!.contains(CaptureRequest.STATISTICS_FACE_DETECT_MODE_FULL) -> CaptureRequest.STATISTICS_FACE_DETECT_MODE_FULL
  faceDetectModes!!.contains(CaptureRequest.STATISTICS_FACE_DETECT_MODE_SIMPLE) -> CaptureRequest.STATISTICS_FACE_DETECT_MODE_FULL
  else -> CaptureRequest.STATISTICS_FACE_DETECT_MODE_OFF
 }

 if (mFaceDetectMode == CaptureRequest.STATISTICS_FACE_DETECT_MODE_OFF) {
  mActivity.toast("相机硬件不支持人脸检测")
  return
 }

 val activeArraySizeRect =
  mCameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE)!! //获取成像区域
 val scaledWidth = mPreviewSize.width / activeArraySizeRect.width().toFloat()
 val scaledHeight = mPreviewSize.height / activeArraySizeRect.height().toFloat()

 val mirror = mCameraFacing == CameraCharacteristics.LENS_FACING_FRONT

 mFaceDetectMatrix.setRotate(mCameraSensorOrientation.toFloat())
 mFaceDetectMatrix.postScale(if (mirror) -scaledHeight else scaledHeight, scaledWidth)// 注意交换width和height的位置!
 mFaceDetectMatrix.postTranslate(
  mPreviewSize.height.toFloat(),
  mPreviewSize.width.toFloat()
 )

 }

控制类(GameController)

三大视图层组装完毕,最后需要一个总控类,对游戏进行逻辑控制

GameController

主要完成以下工作:

  • 控制游戏的开启/停止
  • 计算游戏的当前得分
  • 检测潜艇的碰撞
  • 对外(Activity或者Fragment等)提供游戏状态监听的接口

游戏开始时进行相机的初始化,创建GameHelper类并建立setFaceDetectListener回调到ForegroundView

class GameController(
 private val activity: AppCompatActivity,
 private val textureView: AutoFitTextureView,
 private val bg: BackgroundView,
 private val fg: ForegroundView
) {

 private var camera2HelperFace: CameraHelper? = null
 /**
 * 相机初始化
 */
 private fun initCamera() {
 cameraHelper ?: run {
  cameraHelper = CameraHelper(activity, textureView).apply {
  setFaceDetectListener(object : CameraHelper.FaceDetectListener {
   override fun onFaceDetect(faces: Array<Face>, facesRect: ArrayList<RectF>) {
   if (facesRect.isNotEmpty()) {
    fg.onFaceDetect(faces, facesRect)
   }
   }
  })
  }
 }
 }

游戏状态

定义GameState,对外提供状态的监听。目前支持三种状态

  • Start:游戏开始
  • Over:游戏结束
  • Score:游戏得分
sealed class GameState(open val score: Long) {
 object Start : GameState(0)
 data class Over(override val score: Long) : GameState(score)
 data class Score(override val score: Long) : GameState(score)
}

可以在stop、start的时候,更新状态

/**
 * 游戏状态
 */
 private val _state = MutableLiveData<GameState>()
 internal val gameState: LiveData<GameState>
 get() = _state

 /**
 * 游戏停止
 */
 fun stop() {
 bg.stop()
 fg.stop()
 _state.value = GameState.Over(_score)
 _score = 0L
 }

 /**
 * 游戏再开
 */
 fun start() {
 initCamera()
 fg.start()
 bg.start()
 _state.value = GameState.Start
 handler.postDelayed({
  startScoring()
 }, FIRST_APPEAR_DELAY_MILLIS)
 }

计算得分

游戏启动时通过startScoring开始计算得分并通过GameState上报。
目前的规则设置很简单,存活时间即游戏得分

 /**
 * 开始计分
 */
 private fun startScoring() {
 handler.postDelayed(
  {
  fg.boat?.run {
   bg.barsList.flatMap { listOf(it.up, it.down) }
   .forEach { bar ->
    if (isCollision(
     bar.x, bar.y, bar.w, bar.h,
     this.x, this.y, this.w, this.h
    )
    ) {
    stop()
    return@postDelayed
    }
   }
  }
  _score++
  _state.value = GameState.Score(_score)
  startScoring()
  }, 100
 )
 }

检测碰撞

isCollision根据潜艇和障碍物当前位置,计算是否发生了碰撞,发生碰撞则GameOver

/**
 * 碰撞检测
 */
 private fun isCollision(
 x1: Float,
 y1: Float,
 w1: Float,
 h1: Float,
 x2: Float,
 y2: Float,
 w2: Float,
 h2: Float
 ): Boolean {
 if (x1 > x2 + w2 || x1 + w1 < x2 || y1 > y2 + h2 || y1 + h1 < y2) {
  return false
 }
 return true
 }

Activity

Activity的工作简单:

  • 权限申请:动态申请Camera权限
  • 监听游戏状态:创建GameController,并监听GameState状态
private fun startGame() {
 PermissionUtils.checkPermission(this, Runnable {
  gameController.start()
  gameController.gameState.observe(this, Observer {
  when (it) {
   is GameState.Start ->
   score.text = "DANGER\nAHEAD"
   is GameState.Score ->
   score.text = "${it.score / 10f} m"
   is GameState.Over ->
   AlertDialog.Builder(this)
    .setMessage("游戏结束!成功推进 ${it.score / 10f} 米! ")
    .setNegativeButton("结束游戏") { _: DialogInterface, _: Int ->
    finish()
    }.setCancelable(false)
    .setPositiveButton("再来一把") { _: DialogInterface, _: Int ->
    gameController.start()
    }.show()
  }
  })
 })
 }

最后

项目结构很清晰,用到的大都是常规技术,即使是新入坑Android的同学看起来也不费力。在现有基础上还可以通过添加BGM、增加障碍物种类等,进一步提高游戏性。喜欢的话留个star鼓励一下作者吧 ^^
https://github.com/vitaviva/ugame

到此这篇关于Android 实现抖音小游戏潜艇大挑战的思路详解的文章就介绍到这了,更多相关android 抖音游戏潜艇大挑战内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Android 实现扫雷小游戏实例代码

    Android 实现扫雷小游戏实例 最近学习Android 应用编程,抽空做个小应用,大家熟悉的扫雷应用,练手用, 以下是实现代码: MainActivity 类 public class MainActivity extends Activity implements OnClickListener, OnLongClickListener { // 最外层布局 LinearLayout textviews; LinearLayout buttons; int[][] map = new in

  • 最常见的猜拳小游戏Android代码实现

    本文实例为大家分享了Android猜拳小游戏,供大家参考,具体内容如下 简单的 页面跳转 和 点击事件 的实现... --> AndroidManifest.xml <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.drag

  • Android实现2048小游戏

    本文实例介绍了Android实现2048小游戏的相关代码,分享给大家供大家参考,具体内容如下 根据界面,主要实现4*4的格子方块比较麻烦,其他的都挺简单的.总体为实现4*4的格子,自定义GridLayout,并在其中添加触摸监听事件,进行一系列的操作,从而实现游戏的逻辑,最后再添加动画效果即可完成. 下面是设计思路: 一.GameView的设计 首先自定义一个类,继承GridLayout,添加两个构造方法 public class GameView extends GridLayout { //

  • Android高仿2048小游戏实现代码

    刚开始进入Splash界面: 1.SplashActivity.Java(两秒后进入开始界面,Splash界面的布局只有一个图片,在博客后,会展示给大家看) public class SplashActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.a

  • Android 实现抖音小游戏潜艇大挑战的思路详解

    <潜水艇大挑战>是抖音上的一款小游戏,以面部识别来驱动潜艇通过障碍物,最近特别火爆,相信很多人都玩过. 一时兴起自己用Android自定义View也撸了一个,发现只要有好的创意,不用高深的技术照样可以开发出好玩的应用.开发过程现拿出来与大家分享一下. 项目地址: https://github.com/vitaviva/ugame 基本思路 整个游戏视图可以分成三层: camera(相机):处理相机的preview以及人脸识别 background(后景):处理障碍物相关逻辑 foregroun

  • 基于Matlab实现抖音小游戏苹果蛇

    最近抖音上苹果蛇小游戏大火,为了证明MATLAB无所不能,咋能不跟风做一个?于是就有了这一篇. 还是提一句这个小游戏开始玩的心路历程: 第1-5关:?这不就是有重力的贪吃蛇,有任何难度嘛?? 第6关:嗯...这个小技巧还是有点意思的 第30关:???这绕来绕去的是人出的题目?? 第90关:… …疯了疯了已经疯了 废话不多说看一下MATLAB版的完整代码: 为了方便修改,关卡地图就摆在m文件里,总共写了八关,但是只有代码没有素材包还是运行不了,因此请从文末获取完整压缩包. function app

  • Vue 幸运大转盘实现思路详解

    转盘抽奖主要有两种,指针转动和转盘转动,个人觉得转盘转动比较好看点,指针转动看着头晕,转盘转动时指针是在转盘的中间位置,这里要用到css的transform属性和transition属性,这两个因为不常用最好是上网查查,用法和功能.   在html部分 <div id="wheel_surf"> <div class="wheel_surf_title">幸运大转盘</div> <div class="lucky-

  • Java跳跃游戏实例真题解决思路详解

    目录 变式题—跳跃游戏 I 一.题目描述 二.思路 三.代码 变式题—跳跃游戏 II 一.题目描述 二.思路 三.代码 变式题—跳跃游戏 I 一.题目描述 给定一个非负整数数组 nums ,你最初位于数组的 第一个下标 .数组中的每个元素代表你在该位置可以跳跃的最大长度.判断你是否能够到达最后一个下标. 来源:https://leetcode.cn/problems/jump-game/ 示例: 二.思路 本题可以使用贪心法解决,对每个能到达的位置(可覆盖到的位置),计算其每次能覆盖的最大长度,

  • JS小游戏之宇宙战机源码详解

    本文实例讲述了JS小游戏的宇宙战机源码,分享给大家供大家参考.具体介绍如下: 一.游戏介绍: 这是一款飞行射击游戏,纵向,共六关. 二.游戏需求: 1.战机可发射子弹,子弹可通过获取道具升级. 2.战机可放bomb,可获取道具增加数量. 3.战机可蓄力攻击. 4.道具有三种,分别是升级子弹,增加bomb数量,增加战机数量. 5.每关音乐不同. 6.战机被击落后再进入战场,有保护状态. 7.敌机AI设计. 游戏运行如下图所示: 完整实例代码点击此处本站下载. 三.Javascript源码部分: /

  • Android Activity通用悬浮可拖拽View封装的思路详解

    目录 1,背景 2,思路 2.1,封装通用的基础悬浮View 2.1,继承通用View 2.3,设计view的控制器 2.4,view的添加和使用 1,背景 在开发中总会遇到一个可拖拽的悬浮View,不管是在开发中,还是在线上,都时长有这样的控件,我们通常遇到这种情况,经常需要自己封装,需要耗费时间,我这边封装了一个可以通用的悬浮可拖拽View,这样使用的时候,只需要传入自己要设计的样式和位置既可 2,思路 2.1,封装通用的基础悬浮View 设计通用的父View 1,传入的childView是

  • Android 仿抖音的评论列表的UI和效果的实现代码

    抖音是一款音乐创意短视频社交软件,是一个专注年轻人的15秒音乐短视频社区.用户可以通过这款软件选择歌曲,拍摄15秒的音乐短视频,形成自己的作品.此App已在Android各大应用商店和APP Store均有上线. 在design包里面 有一个 BottomSheetDialogFragment 这个Fragment,他已经帮我们处理好了手势,所以实现起来很简单.下面是代码: public class ItemListDialogFragment extends BottomSheetDialog

  • Android仿抖音右滑清屏左滑列表功能的实现代码

    概述 ​ 项目中要实现仿抖音直播间滑动清屏,侧滑列表的功能,在此记录下实现过程和踩坑记录希望避免大家走些弯路,也当作自己的一个总结 ​ 首先看下Demo中的效果 ​ 阅读文章需要提前熟悉些事件分发的内容,相信大家都已经了解过了,网上也有很多优秀的文章,这里推荐两篇自己读过印象较深的文章 https://www.jb51.net/article/124249.htm https://www.jb51.net/article/124861.htm 关于这方面的知识,在Android中是再重要不过的了

  • Android仿抖音主页效果实现代码

    写在前面 各位老铁,我又来啦!既然来了,那肯定又来搞事情啦,话不多说,先上图! "抖音"都玩过吧,是不是很好玩,我反正是天天刷,作为一个非著名的Android低级攻城狮,虽然技术菜的一匹,但是也经常刷着刷着会思考:咦?这玩意是用哪个控件做的?这个效果是咋实现的啊?由于本人技术水平有限,所以今天咱就先挑个比较简单的来看看是如何实现的,思考再三,我们就拿抖音首页的这个效果来练练手吧,话不多说,开搞! 一.准备工作 我们先不急着写代码,先对抖音的这种效果做一个简单的分析,首先需要明确的是它是

  • 使用Python下载抖音各大V视频的思路详解

    前言 本文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,如有问题请及时联系我们以作处理. 以下文章来源于Python七号 ,作者 somenzz Python爬虫.数据分析.网站开发等案例教程视频免费在线观看 https://space.bilibili.com/523606542 上次写了用 Python 批量下载知乎视频的方式,这次分享用 Python 批量下载抖音个人主页的全部无水印视频,本文重点不是提供一个好用的脚本,而是讲述如何写出这样的脚本,正所谓授人以鱼,不如授人

随机推荐