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

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

项目地址:

基本思路

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

camera(相机):处理相机的preview以及人脸识别

background(后景):处理障碍物相关逻辑

foreground(前景):处理潜艇相关

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

xmlns:tools="http://schemas.android.com/tools"

android:layout_width="match_parent"

android:layout_height="match_parent">

android:layout_width="match_parent"

android:layout_height="match_parent"/>

android:layout_width="match_parent"

android:layout_height="match_parent"/>

android:layout_width="match_parent"

android:layout_height="match_parent"/>

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

相机:使用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))

}

}

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

/**

* 屏幕上方障碍物

*/

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()

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)

}

}

}

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

游戏结束时,要求所有障碍物停止移动。

游戏开始后会通过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, facesRect: ArrayList) {

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)

相机部分主要有TextureView和CameraHelper组成。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() //保存人脸坐标信息

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 {

val bigEnough = ArrayList() //比指定宽高大的Size列表

val notBigEnough = ArrayList() //比指定宽高小的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, facesRect: ArrayList) {

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()

internal val gameState: LiveData

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 抖音游戏潜艇大挑战内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

JAVA抖音潜艇挑战_Android 实现抖音小游戏潜艇大挑战的思路详解相关推荐

  1. java android消息推送_Android中使用socket通信实现消息推送的方法详解

    原理最近用socket写了一个消息推送的demo,在这里和大家分享一下. 主要实现了:一台手机向另外一台手机发送消息,这两台手机可以随时自由发送文本消息进行通信,类似我们常用的QQ. 效果图: 原理: ...

  2. Java编程配置思路详解

    Java编程配置思路详解 SpringBoot虽然提供了很多优秀的starter帮助我们快速开发,可实际生产环境的特殊性,我们依然需要对默认整合配置做自定义操作,提高程序的可控性,虽然你配的不一定比官 ...

  3. java中四种操作(DOM、SAX、JDOM、DOM4J)xml方式详解与比较(转)

    java中四种操作(DOM.SAX.JDOM.DOM4J)xml方式详解与比较(转) http://wishlife.javaeye.com/blog/181865 posted on 2010-12 ...

  4. java excel中重复数据 事务处理_Java导出excel时合并同一列中相同内容的行思路详解...

    一.有时候导出Excel时需要按类别导出,一大类下好几个小类,小类下又有好几个小小类,就像下图: 要实现这个也不难, 思路如下:按照大类来循环,如上就是按照张江校区.徐汇校区.临港校区三个大类循环,然 ...

  5. 即时通讯音视频开发(十):实时语音通讯的回音消除技术详解

    前言 即时通讯应用中的实时音视频技术,几乎是IM开发中的最后一道高墙.原因在于:实时音视频技术 = 音视频处理技术 + 网络传输技术 的横向技术应用集合体,而公共互联网不是为了实时通信设计的.有关实时 ...

  6. java 导出如何合并列_Java导出excel时合并同一列中相同内容的行思路详解

    一.有时候导出Excel时需要按类别导出,一大类下好几个小类,小类下又有好几个小小类,就像下图: 要实现这个也不难, 思路如下:按照大类来循环,如上就是按照张江校区.徐汇校区.临港校区三个大类循环,然 ...

  7. 抖音很火的召唤神龙的小游戏完整代码-召唤神龙

    抖音很火的解压小游戏,完整代码分享.有兴趣的可以试着写一下. 1.  index <!DOCTYPE html> <html> <head><meta cha ...

  8. 小游戏-------------潜艇大战

    <--第一天   --> 潜艇游戏----潜艇大战图片 列表 创建战舰类.侦察潜艇类.鱼雷潜艇类.水雷潜艇类.水雷类.深水炸弹类,设计类中的成员变量和move()方法 创建World类,在 ...

  9. java判断一个对象是否为空_Java中判断对象是否为空的方法的详解

    首先来看一下工具StringUtils的判断方法: 一种是org.apache.commons.lang3包下的: 另一种是org.springframework.util包下的.这两种StringU ...

最新文章

  1. springboot 跨域解决方案
  2. JAVA并发编程8_线程池的使用
  3. 威佐夫博弈(模板题)
  4. [转]sudoers设置
  5. RabbitMQ系列教程之二:工作队列(Work Queues)
  6. ES6中对象新增方法
  7. # Mysql免登录重置root密码
  8. 三维旋转矩阵_第三讲:三维空间的刚体运动
  9. C++ 复制一个文件夹下的所有文件到另一个文件夹
  10. 时间进度表html,网页的设计进度表.doc
  11. win10和win7鲁大师测试软件,实测:老电脑用Win7、Win10哪个流畅?
  12. xposed 入门之修改手机 IMEI
  13. Linux下pip安装教程
  14. selenium自动化测试01
  15. 华三模拟器之OSPF实验
  16. Alibaba内部首发“M8级”微服务架构手册,GitHub上杀疯了
  17. 智能定位系统实验报告
  18. 网页采集工具-免费网页采集工具大全
  19. 数据脱敏(Data Masking)学习
  20. Qt应用程序图标设置任务栏图标设置

热门文章

  1. simulink中的mux与的demux
  2. win7 64bit下硬盘内容显示该文件夹为空,但是显示有7G的占用,解释
  3. 怎么把真人照片转漫画?快来收下这一招
  4. 【SQL】排序与运算、筛选函数
  5. 分数整数计算机在线,百分比计算器
  6. 知识变现的三种主要方式
  7. 【杂谈】从学员到合作伙伴,我与有三AI不得不说的故事
  8. 程序员副业赚钱之2021版30个软件创意 第一期
  9. 对战坦克大战(vc++)
  10. python函数的动态参数之一个星号和两个星号