概述
重陽已過,中秋將至,想起農村老家,這個季節到了晚上,偶爾會比較涼爽,甚至有些涼意,不禁想吟詞一首:
定風波·湖村晚
湖面蒹葭蕩影重,黃昏漸映水寒清,遠處人家聲影亂,親喚,小童歸去老村驚,
月上枝頭雙戲景,微冷,農家秋月夜燃燈,燈影幢幢人影瘦,濁酒,菜花香入夢回輕,
好吧,這其實是一篇技術文章,
UI布局方面就別吐槽了,讓一個開發來思考這個問題簡直噩夢(😂),里面的色值,樣式,布局換了又換,隨著視覺效果的越來越詭異,我只好戀戀不舍地放棄了 UI 上的修改(🐶),
這個小游戲底部會不停出現一些大小隨機的老鼠,然后過一會后自動消失,自動消失后上方的月餅會被吃掉對應老鼠體積的一部分,點擊老鼠可以增加月餅對應的體積,延長壽命,目前一共設定了 15 關,每一關都設定了 20s 倒計時,在時間內月餅未被吃光則視為勝利!在通關后會有神秘獎品哦~中間的 Banner 廣告是瞎加的,不然看上去 UI (我自己)感覺底部的老鼠區域太大了不協調,
接下來看看游戲實作,原始碼使用 MVVM 架構,github 鏈接在文末,歡迎 star~
吃月餅控制元件
月餅MoonView
首先看看頂部這個月餅控制元件的實作,其實本來一開始是想做“守護月亮”,網上查了查月亮陰晴陽缺的代碼,看到是用xxx曲線,橢圓畫的,畢業到兩年,學霸也無言,我覺得大可不必,還是別去挑戰這些數學問題了吧,已經過了爭狠斗勇的年紀了(Doge),所以把月亮換成了月餅,用倆月餅來實作這個被吃掉的效果,一個是“正常”月餅,另一個是跟背景色一樣的月餅,通過用這個白色的月餅左右移動,來遮擋住正常的月餅,實作視覺上被吃掉的效果,
首先看下月餅 View 的實作:
class MoonView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
// 半徑
private var radius = SizeUtil.dp2px(30f)
// 顏色
private var color = Color.BLUE
// 繪制的文本
private var text = ""
// 圓的畫筆
private val moonPaint = Paint(Paint.ANTI_ALIAS_FLAG)
// 文本畫筆
private val textPaint = TextPaint()
// 省略了初始化和 onMeasure 方法
/**
* 畫圓和文本
*/
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
val padding = (measuredWidth - radius * 2) / 2f
canvas?.drawCircle(padding + radius, padding + radius, radius.toFloat(), moonPaint)
if (text.isNotEmpty()) {
val fontMetrics = textPaint.fontMetricsInt
canvas?.drawText(
text,
radius + padding,
radius + padding - (fontMetrics.bottom + fontMetrics.top) / 2,
textPaint
)
}
}
}
這個自定義 View 其實很簡單,就是畫了個實心圓,然后中心畫上文本,這里其實可以直接用一張月餅的圖片,也可以用月餅的 emoji 等,不過中心寫上“月餅”的文字是不是最直白!
吃月餅MoonEatView
接著就是通過上面的月餅 MoonView 控制元件來實作這個吃月餅控制元件,
class MoonEatView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) {
// 正常月餅
private var moonView: MoonView
// 用來遮擋正常月餅的 mask 月餅
private var maskView: MoonView
// mask 月餅的移動量
private var maskTranX: Float = 0f
}
上面定義了兩個月餅:一個正常顯示的月餅 moonView,另一個是遮擋的背景色月餅 maskView,通過移動 maskView 來實作被吃效果:
fun translateMask(tranX: Float) {
maskTranX += tranX
if (maskTranX < 0) {
maskTranX = 0f
}
if (maskTranX > moonView.width) {
maskTranX = moonView.width.toFloat()
}
maskView.animate().translationX(maskTranX).setDuration(100).setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
if (maskAll()) {
// 如果全部被吃掉了,則回呼監聽器
onMaskListener?.onMaskAll()
}
}
}).start()
// 根據被吃的大小,展示不同的月餅色
modifyMoonColor()
}
上面都有注釋,就不詳細介紹代碼邏輯了,在下面的老鼠控制元件消失后,會計算對應應該吃掉或增加的月餅偏移量,然后設定給 MoonEatView, 來實作這個效果,
老鼠控制元件
老鼠MouseView
考慮單個老鼠控制元件的特點:展示一段時間后消失,如果是自動消失則回呼 onDismiss 監聽方法,如果是點擊后消失,則應該回呼 onClick 監聽方法,注意這些回呼方法都應該在游戲正在進行的時候才執行,
因此 MouseView 需要持有其所在的容器 ViewGroup 的參考,用來添加移除老鼠 View 自身,并用來判斷游戲是否在進行:
class MouseView constructor(
context: Context,
private val goneInterval: Long, // 超時自動消失的時間,用來控制不同關卡的難度
private val container: OperateLayout,
private val listener: onm ouseListener?
) : AppCompatImageView(context, null, 0), Runnable {
init {
setImageDrawable(
AppCompatResources.getDrawable(
context, when (Random.nextInt(3)) {
0 -> R.drawable.mouse1
1 -> R.drawable.mouse2
else -> R.drawable.mouse3
}
)
)
// 設定點擊后移除自身,并移除超時自動消失的任務
setOnClickListener {
removeCallbacks(this)
container.removeView(this)
if (container.isRunning) {
listener?.onClick(size())
}
}
}
/**
* 超時自動消失的任務
*/
override fun run() {
container.removeView(this)
if (container.isRunning) {
listener?.onDismiss(size())
}
}
/**
* 展示自身,并發送一個超時自動消失的任務
*/
fun show() {
val size = mouseSize()
// 隨機大小,隨機位置
val params = FrameLayout.LayoutParams(size, size)
params.leftMargin = Random.nextInt(0, max(container.width - size, 1))
params.topMargin = Random.nextInt(0, max(container.height - size, 1))
container.addView(this, params)
postDelayed(this, goneInterval)
}
}
上面代碼都有注釋,邏輯比較清晰,這里用了一個 goneInterval 屬性來控制不同關卡的難度,這個引數表示老鼠超時多久沒被點擊后會自動消失,
老鼠容器OperateLayout
OperateLayout 就是游戲底部的控制元件,它在開始游戲后用來控制老鼠的出現和消失,同時將老鼠的點擊消失和自動消失回呼給外部,其內有三個屬性引數:
// 同時生成的 View 數
var countOnce = 1
// 生成 View 的間隔速度
var speed = 1000L
// 游戲是否進行中
var isRunning = false
private set
countOnce 和 speed 用來控制關卡游戲難度,isRunning 表示游戲是否在進行,然后就是游戲開始的邏輯了:
fun start(listener: MouseView.OnMouseListener) {
this.onMouseListener = listener
this.isRunning = true
removeAllViews()
// 發送一個任務,會執行下面的 run 方法
post(this)
}
override fun run() {
repeat(countOnce) { // 生成 countOnce 數量的老鼠
if (!isRunning) {
return
}
val mouseView = MouseView(
context,
goneInterval = speed,
container = this,
listener = onm ouseListener
)
// 呼叫老鼠的 show 方法來展示
mouseView.show()
}
// 發送延時任務,一段時間后接著生成老鼠
postDelayed(this, speed)
}
上面注釋比較清晰,這個控制元件主要就是用來生成老鼠,以及將玩家的點擊或者漏過事件通知給外部呼叫者,將老鼠的自動展示和消失邏輯封裝在內部,
游戲Activity
最后就是游戲的主 MainActivity 實作了,在這里會把上面的控制元件都組合起來,實作游戲功能,游戲的布局檔案就不貼了,布局效果如上的 Gif 圖,
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// ... 初始化 View
initData()
initListener()
initObserver()
}
}
上面主要有三個方法,先看 initData 方法:
private fun initData() {
gameViewModel.initData(this)
bannerView.isScrollRepeatable = true
bannerView.highLightColor = Color.GRAY
bannerView.setContentText(bannerViewModel.getBannerText())
bannerView.resume(100)
}
class GameViewModel : ViewModel() {
private val _level: MutableLiveData<Int> = MutableLiveData(1)
val level: LiveData<Int> = _level
fun initData(context: Context) {
_level.value = getLevel(context)
}
}
首先 gameViewModel.initData() 方法用來在 ViewModel 中初始化當前的關卡是第幾關,簡單實作,當前關卡是用 SP 存盤的,然后就是初始化 Banner 控制元件了,
接著在 initListener 方法中初始化監聽器,具體代碼可以看文章末貼出的 GitHub 鏈接,這里在“開始游戲”的點擊事件里,會先判斷當前關卡是不是已經通關了,通關則會提醒是否跳轉神秘獎品頁面,否則會呼叫 startGame() 開始游戲,
至于 initObserver 方法則是監聽 gameViewModel.level 這個 LiveData 的資料,用來展示當前關卡的文案,
最后再看下游戲開始的方法實作:
// startGame()
operateLayout.start(object : MouseView.OnMouseListener {
override fun onClick(size: Int) {
moonEatLayout.translateMask(-maskTranslate(size))
}
override fun onDismiss(size: Int) {
moonEatLayout.translateMask(maskTranslate(size))
}
})
moonEatLayout.onMaskListener = object : MoonEatView.OnMaskListener {
override fun onMaskAll() {
stopGame()
}
}
可以看到游戲開始就是呼叫了 OperateLayout.start() 方法,底部開始老鼠的出現和消失,然后在其回呼方法中呼叫 MoonEatLayout.translateMask() 方法來控制月餅的被吃掉和增多效果,
總結
看了一圈下來,其實游戲實作是比較簡單的,重要的是啊,蹭蹭中秋的熱氣,圖個吉利!
總結一下這個小游戲的邏輯是:開始游戲后,底部老鼠控制元件 OperateLayout.start() 會控制老鼠的出現和消失,老鼠的點擊消失和自動消失會觸發對應的回呼,在回呼方法里通過計算偏移量,然后設定給 MoonEatLayout 吃月餅控制元件來實作吃月餅的效果,
游戲一個設定了 15 關
重九剛過,遠在異鄉的各種飄們是否想家呢?再來一首詞fs一下吧~
漁歌子·重九
陌上閑枝半過秋,重陽當飲酒難休,
杯入曲,晚歸悠,炊煙裊裊正秋收,
文中內容如有錯誤歡迎指出,共同進步!覺得不錯的同學留個贊再走哈~
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/300509.html
標籤:其他
