抖音潜水艇游戏玩之后怎么保存,抖音潜水艇游戏音乐怎么加

首页 > 游戏 > 作者:YD1662024-01-02 09:00:03

作者:fundroid_方卓

链接:https://blog.csdn.net/vitaviva/article/details/105613652

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

抖音潜水艇游戏玩之后怎么保存,抖音潜水艇游戏音乐怎么加(1)

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

需要学习的内容

NDK模块开发

音视频的开发,往往是比较难的,而这个比较难的技术就是NDK里面的技术。音视频/高清大图片/人工智能/直播/抖音等等这年与用户最紧密,与我们生活最相关的技术一直都在寻找最终的技术落地平台,以前是windows系统,而现在则是移动系统了,移动系统中又是以Android占比绝大部分为前提,所以AndroidNDK技术已经是我们必备技能了。要学习好NDK,其中的关于C/C ,jni,Linux基础都是需要学习的,除此之外,音视频的编解码技术,流媒体协议,ffmpeg这些都是音视频开发必备技能,而且OpenCV/OpenGl/这些又是图像处理必备知识,这些都是需要学习的。

需要下面资料视频的可以私信我【进阶】我免费分享给你,希望对大家有帮助。

抖音潜水艇游戏玩之后怎么保存,抖音潜水艇游戏音乐怎么加(2)

学习视频

抖音潜水艇游戏玩之后怎么保存,抖音潜水艇游戏音乐怎么加(3)

基本思路

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

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

<FrameLayoutxmlns: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>

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

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

2、后景(Background)

Bar

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

/** *屏幕下方障碍物 */ classDnBar(context:Context,container:ViewGroup):Bar(context){ overridevalbmp=super.bmp.let{ Bitmap.createBitmap( it,0,0,it.width,it.height, Matrix().apply{postRotate(-180F)},true ) } privateval_srcRectbylazy(LazyThreadSafetyMode.NONE){ Rect(0,0,bmp.width,(bmp.height*(h/container.height)).toInt()) } overridevalsrcRect:Rect get()=_srcRect }

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

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

/** *屏幕下方障碍物 */ classDnBar(context:Context,container:ViewGroup):Bar(context){ overridevalbmp=super.bmp.let{ Bitmap.createBitmap( it,0,0,it.width,it.height, Matrix().apply{postRotate(-180F)},true ) } privateval_srcRectbylazy(LazyThreadSafetyMode.NONE){ Rect(0,0,bmp.width,(bmp.height*(h/container.height)).toInt()) } overridevalsrcRect:Rect get()=_srcRect }

BackgroundView

接下来创建后景的容器BackgroundView,容器用来定时地创建、并移动障碍物。

通过列表barsList管理当前所有的障碍物,onLayout中,将障碍物分别布局到屏幕上方和下方

/** *后景容器类 */ classBackgroundView(context:Context,attrs:AttributeSet?):FrameLayout(context,attrs){ internalvalbarsList=mutableListOf<Bars>() overridefunonLayout(changed:Boolean,left:Int,top:Int,right:Int,bottom:Int){ barsList.flatMap{listOf(it.up,it.down)}.forEach{ valw=it.view.measuredWidth valh=it.view.measuredHeight when(it){ isUpBar->it.view.layout(0,0,w,h) else->it.view.layout(0,height-h,w,height) } } }

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

/** *游戏结束,停止所有障碍物的移动 */ @UiThread funstop(){ _timer.cancel() _anims.forEach{it.cancel()} _anims.clear() } /** *定时刷新障碍物: *1.创建 *2.添加到视图 *3.移动 */ @UiThread funstart(){ _clearBars() Timer().also{_timer=it}.schedule(object:TimerTask(){ overridefunrun(){ post{ _createBars(context,barsList.lastOrNull()).let{ _addBars(it) _moveBars(it) } } } },FIRST_APPEAR_DELAY_MILLIS,BAR_APPEAR_INTERVAL_MILLIS ) } /** *游戏重启时,清空障碍物 */ privatefun_clearBars(){ barsList.clear() removeAllViews() }

刷新障碍物

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

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

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

/** *创建障碍物(上下两个为一组) */ privatefun_createBars(context:Context,pre:Bars?)=run{ valup=UpBar(context,this).apply{ h=pre?.let{ valstep=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 } valdown=DnBar(context,this).apply{ h=height-up.h-_gap w=_barWidth } Bars(up,down) } /** *添加到屏幕 */ privatefun_addBars(bars:Bars){ barsList.add(bars) bars.asArray().forEach{ addView( it.view, ViewGroup.LayoutParams( it.w.toInt(), it.h.toInt() ) ) } } /** *使用属性动画移动障碍物 */ privatefun_moveBars(bars:Bars){ _anims.add( ValueAnimator.ofFloat(width.toFloat(),-_barWidth) .apply{ addUpdateListener{ bars.asArray().forEach{bar-> bar.x=it.animatedValueasFloat if(bar.x bar.w<=0){ post{removeView(bar.view)} } } } duration=BAR_MOVE_DURATION_MILLIS interpolator=LinearInterpolator() start() }) } }

3、前景(Foreground)

Boat

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

/** *潜艇类 */ classBoat(context:Context){ internalvalviewbylazy{BoatView(context)} valh get()=view.height.toFloat() valw get()=view.width.toFloat() valx get()=view.x valy get()=view.y /** *移动到指定坐标 */ funmoveTo(x:Int,y:Int){ view.smoothMoveTo(x,y) } }

BoatView

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

internalclassBoatView(context:Context?):AppCompatImageView(context){ privateval_scrollerbylazy{OverScroller(context)} privateval_res=arrayOf( R.mipmap.boat_000, R.mipmap.boat_002 ) privatevar_rotationAnimator:ObjectAnimator?=null privatevar_cnt=0 set(value){ field=if(value>1)0elsevalue } init{ scaleType=ScaleType.FIT_CENTER _startFlashing() } privatefun_startFlashing(){ postDelayed({ setImageResource(_res[_cnt ]) _startFlashing() },500) } overridefuncomputeScroll(){ super.computeScroll() if(_scroller.computeScrollOffset()){ x=_scroller.currX.toFloat() y=_scroller.currY.toFloat() //Keepondrawinguntiltheanimationhasfinished. postInvalidateOnAnimation() } } /** *移动更加顺换 */ internalfunsmoothMoveTo(x:Int,y:Int){ if(!_scroller.isFinished)_scroller.abortAnimation() _rotationAnimator?.let{if(it.isRunning)it.cancel()} valcurX=this.x.toInt() valcurY=this.y.toInt() valdx=(x-curX) valdy=(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

/** *游戏开始时通过动画进入 */ @MainThread funstart(){ _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() } } } }

开场动画

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

/** *游戏开始时通过动画进入 */ @MainThread funstart(){ _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() } } } }

4、相机(Camera)

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

适配PreviewSize

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

classCameraHelper(valmActivity:Activity,privatevalmTextureView:TextureView){ privatelateinitvarmCameraManager:CameraManager privatevarmCameraDevice:CameraDevice?=null privatevarmCameraCaptureSession:CameraCaptureSession?=null privatevarcanExchangeCamera=false//是否可以切换摄像头 privatevarmFaceDetectMatrix=Matrix()//人脸检测坐标转换矩阵 privatevarmFacesRect=ArrayList<RectF>()//保存人脸坐标信息 privatevarmFaceDetectListener:FaceDetectListener?=null//人脸检测回调 privatelateinitvarmPreviewSize:Size /** *初始化 */ privatefuninitCameraInfo(){ mCameraManager=mActivity.getSystemService(Context.CAMERA_SERVICE)asCameraManager valcameraIdList=mCameraManager.cameraIdList if(cameraIdList.isEmpty()){ mActivity.toast("没有可用相机") return } //获取摄像头方向 mCameraSensorOrientation= mCameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION)!! //获取StreamConfigurationMap,它是管理摄像头支持的所有输出格式和尺寸 valconfigurationMap= mCameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!! valpreviewSize=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的长宽比尽量一致,且面积尽量接近。

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

人脸识别

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

/** *创建预览会话 */ privatefuncreateCaptureSession(cameraDevice:CameraDevice){ //为相机预览,创建一个CameraCaptureSession对象 cameraDevice.createCaptureSession( arrayListOf(surface), object:CameraCaptureSession.StateCallback(){ overridefunonConfigured(session:CameraCaptureSession){ mCameraCaptureSession=session session.setRepeatingRequest( captureRequestBuilder.build(), mCaptureCallBack, mCameraHandler ) } }, mCameraHandler ) } privatevalmCaptureCallBack=object:CameraCaptureSession.CaptureCallback(){ overridefunonCaptureCompleted( 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之后创建的

/** *初始化人脸检测相关信息 */ privatefuninitFaceDetect(){ valfaceDetectModes= 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 } valactiveArraySizeRect= mCameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE)!!//获取成像区域 valscaledWidth=mPreviewSize.width/activeArraySizeRect.width().toFloat() valscaledHeight=mPreviewSize.height/activeArraySizeRect.height().toFloat() valmirror=mCameraFacing==CameraCharacteristics.LENS_FACING_FRONT mFaceDetectMatrix.setRotate(mCameraSensorOrientation.toFloat()) mFaceDetectMatrix.postScale(if(mirror)-scaledHeightelsescaledHeight,scaledWidth)//注意交换width和height的位置! mFaceDetectMatrix.postTranslate( mPreviewSize.height.toFloat(), mPreviewSize.width.toFloat() ) }

5、控制类(GameController)

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

主要完成以下工作:

初始化

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

classGameController( privatevalactivity:AppCompatActivity, privatevaltextureView:AutoFitTextureView, privatevalbg:BackgroundView, privatevalfg:ForegroundView ){ privatevarcamera2HelperFace:CameraHelper?=null /** *相机初始化 */ privatefuninitCamera(){ cameraHelper?:run{ cameraHelper=CameraHelper(activity,textureView).apply{ setFaceDetectListener(object:CameraHelper.FaceDetectListener{ overridefunonFaceDetect(faces:Array<Face>,facesRect:ArrayList<RectF>){ if(facesRect.isNotEmpty()){ fg.onFaceDetect(faces,facesRect) } } }) } } }

游戏状态

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

sealedclassGameState(openvalscore:Long){ objectStart:GameState(0) dataclassOver(overridevalscore:Long):GameState(score) dataclassScore(overridevalscore:Long):GameState(score) }

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

/** *游戏状态 */ privateval_state=MutableLiveData<GameState>() internalvalgameState:LiveData<GameState> get()=_state /** *游戏停止 */ funstop(){ bg.stop() fg.stop() _state.value=GameState.Over(_score) _score=0L } /** *游戏开始 */ funstart(){ initCamera() fg.start() bg.start() _state.value=GameState.Start handler.postDelayed({ startScoring() },FIRST_APPEAR_DELAY_MILLIS) }

计算得分

游戏启动时通过startScoring开始计算得分并通过GameState上报。

目前的规则设置很简单,存活时间即游戏得分

/** *开始计分 */ privatefunstartScoring(){ 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

/** *碰撞检测 */ privatefunisCollision( 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){ returnfalse } returntrue }

6、Activity

Activity的工作简单:

privatefunstartGame(){ PermissionUtils.checkPermission(this,Runnable{ gameController.start() gameController.gameState.observe(this,Observer{ when(it){ isGameState.Start-> score.text="DANGER\nAHEAD" isGameState.Score-> score.text="${it.score/10f}m" isGameState.Over-> AlertDialog.Builder(this) .setMessage("游戏结束!成功推进${it.score/10f}米!") .setNegativeButton("结束游戏"){_:DialogInterface,_:Int-> finish() }.setCancelable(false) .setPositiveButton("再来一把"){_:DialogInterface,_:Int-> gameController.start() }.show() } }) }) }

最后

项目结构很清晰,用到的大都是常规技术,即使是新入坑Android的同学看起来也不费力。在现有基础上还可以通过添加BGM、增加障碍物种类等,进一步提高游戏性。

喜欢的话留个star鼓励一下作者吧 ^^

栏目热文

文档排行

本站推荐

Copyright © 2018 - 2021 www.yd166.com., All Rights Reserved.