欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Android 實現(xiàn)抖音小游戲潛艇大挑戰(zhàn)的思路詳解

 更新時間:2020年04月21日 17:03:48   作者:fundroid_方卓  
《潛水艇大挑戰(zhàn)》是抖音上的一款小游戲,最近特別火爆,很多小伙伴都玩過。接下來通過本文給大家分享Android 手擼抖音小游戲潛艇大挑戰(zhàn)的思路,需要的朋友可以參考下

《潛水艇大挑戰(zhàn)》是抖音上的一款小游戲,以面部識別來驅(qū)動潛艇通過障礙物,最近特別火爆,相信很多人都玩過。

一時興起自己用Android自定義View也擼了一個,發(fā)現(xiàn)只要有好的創(chuàng)意,不用高深的技術照樣可以開發(fā)出好玩的應用。開發(fā)過程現(xiàn)拿出來與大家分享一下。

項目地址:

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>

開發(fā)中會涉及以下技術的使用,沒有高精尖、都是大路貨:

  • 相機:使用Camera2完成相機的預覽和人臉識別
  • 自定義View:定義并控制障礙物和潛艇
  • 屬性動畫:控制障礙物和潛艇的移動及各種動效

少啰嗦,先看東西!下面介紹各部分代碼的實現(xiàn)。

后景(Background)Bar

首先定義障礙物基類Bar,主要負責是將bitmap資源繪制到指定區(qū)域。由于障礙物從屏幕右側(cè)定時刷新時的高度隨機,所以其繪制區(qū)域的x、y、w、h需要動態(tài)設置

/**
 * 障礙物基類
 */
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))
 }
}

障礙物分為上方和下方兩種,由于使用了同一張資源,所以繪制時要區(qū)別對待,因此定義了兩個子類: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
}

下方障礙物的資源旋轉(zhuǎn)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

接下來創(chuàng)建后景的容器BackgroundView,容器用來定時地創(chuàng)建、并移動障礙物。
通過列表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,控制游戲的開始和結(jié)束:

  • 游戲結(jié)束時,要求所有障礙物停止移動。
  • 游戲開始后會通過Timer,定時刷新障礙物
/**
 * 游戲結(jié)束,停止所有障礙物的移動
 */
 @UiThread
 fun stop() {
 _timer.cancel()
 _anims.forEach { it.cancel() }
 _anims.clear()
 }

 /**
 * 定時刷新障礙物:
 * 1. 創(chuàng)建
 * 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()
 }

刷新障礙物

障礙物的刷新經(jīng)歷三個步驟:

  • 創(chuàng)建:上下兩個為一組創(chuàng)建障礙物
  • 添加:將對象添加到barsList,同時將View添加到容器
  • 移動:通過屬性動畫從右側(cè)移動到左側(cè),并在移出屏幕后刪除

創(chuàng)建障礙物時會為其設置隨機高度,隨機不能太過,要以前一個障礙物為基礎進行適當調(diào)整,保證隨機的同時兼具連貫性

 /**
 * 創(chuàng)建障礙物(上下兩個為一組)
 */
 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,創(chuàng)建自定義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中完成以下幾個事情

  • 通過兩個資源定時切換,實現(xiàn)探照燈閃爍的效果
  • 通過OverScroller讓移動過程更加順滑
  • 通過一個Rotation Animation,讓潛艇在移動時可以調(diào)轉(zhuǎn)角度,更加靈動
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成員持有潛艇對象,并對其進行控制
  • 實現(xiàn)CameraHelper.FaceDetectListener根據(jù)人臉識別的回調(diào),移動潛艇到指定位置
  • 游戲開始時,創(chuàng)建潛艇并做開場動畫
/**
 * 前景容器類
 */
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
 }
 
 /**
 * 接受人臉識別的回調(diào),移動位置
 */
 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上發(fā)生畫面拉伸等異常

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()      //人臉檢測坐標轉(zhuǎn)換矩陣
 private var mFacesRect = ArrayList<RectF>()      //保存人臉坐標信息
 private var mFaceDetectListener: FaceDetectListener? = null    //人臉檢測回調(diào)
 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) //預覽尺寸

 // 當屏幕為垂直的時候需要把寬高值進行調(diào)換,保證寬大于高
 mPreviewSize = getBestSize(
  mTextureView.height,
  mTextureView.width,
  previewSize.toList()
 )

 //根據(jù)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初始化,后文介紹

人臉識別

為相機預覽,創(chuàng)建一個CameraCaptureSession對象,會話通過CameraCaptureSession.CaptureCallback返回TotalCaptureResult,通過參數(shù)可以讓其中包括人臉識別的相關信息

 /**
 * 創(chuàng)建預覽會話
 */
 private fun createCaptureSession(cameraDevice: CameraDevice) {

 // 為相機預覽,創(chuàng)建一個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通過回調(diào)傳出:

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

FaceDetectMatrix

mFaceDetectMatrix是在獲取PreviewSize之后創(chuàng)建的

 /**
 * 初始化人臉檢測相關信息
 */
 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)!! //獲取成像區(qū)域
 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等)提供游戲狀態(tài)監(jiān)聽的接口

游戲開始時進行相機的初始化,創(chuàng)建GameHelper類并建立setFaceDetectListener回調(diào)到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)
   }
   }
  })
  }
 }
 }

游戲狀態(tài)

定義GameState,對外提供狀態(tài)的監(jiān)聽。目前支持三種狀態(tài)

  • Start:游戲開始
  • Over:游戲結(jié)束
  • 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的時候,更新狀態(tài)

/**
 * 游戲狀態(tài)
 */
 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上報。
目前的規(guī)則設置很簡單,存活時間即游戲得分

 /**
 * 開始計分
 */
 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根據(jù)潛艇和障礙物當前位置,計算是否發(fā)生了碰撞,發(fā)生碰撞則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的工作簡單:

  • 權限申請:動態(tài)申請Camera權限
  • 監(jiān)聽游戲狀態(tài):創(chuàng)建GameController,并監(jiān)聽GameState狀態(tài)
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("游戲結(jié)束!成功推進 ${it.score / 10f} 米! ")
    .setNegativeButton("結(jié)束游戲") { _: DialogInterface, _: Int ->
    finish()
    }.setCancelable(false)
    .setPositiveButton("再來一把") { _: DialogInterface, _: Int ->
    gameController.start()
    }.show()
  }
  })
 })
 }

最后

在這里插入圖片描述

項目結(jié)構很清晰,用到的大都是常規(guī)技術,即使是新入坑Android的同學看起來也不費力。在現(xiàn)有基礎上還可以通過添加BGM、增加障礙物種類等,進一步提高游戲性。喜歡的話留個star鼓勵一下作者吧 ^^
https://github.com/vitaviva/ugame

到此這篇關于Android 實現(xiàn)抖音小游戲潛艇大挑戰(zhàn)的思路詳解的文章就介紹到這了,更多相關android 抖音游戲潛艇大挑戰(zhàn)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

相關文章

最新評論