[Day25] swift & kotlin 游戏篇!(7) 小鸡BB-游戏制作-API与游戏动画

游戏示意

Swift 汇入图片

swift - 游戏功能

接下来当我们点击按钮
我们来打个API 并告知道有没有猜对
来看看API吧

API说明

请使用POST方式传送资料,API会把结果告诉你

  • URL

    http://pinyi.ami-shake.com/gg_order.php

  • Method:

    POST

  • URL Params

    None

  • Data Params

    Required:

    choose=选择的项目
    balance=目前的结余的点数

  • Success Response:
    end even 代表走了四条横线 odd代表三条

    • Code: 200
      Content: {"error_code":0,"error_msg":"","info":{"balance":"1200","is_win":true,"result":{"end":"even","stairs":"3","start":"left"}}}
  • Error Response:

    • Code: 200
      Content: {"error_code":10001,"error_msg":"Please POST 'choose' and 'balance' property","info":{"balance":"0","is_win":false,"result":{"end":"","stairs":"","start":""}}}

接下来撰写按钮点击後的程序吧
首先产生一个Class来储存分数
Swift 汇入图片
Swift 汇入图片
Swift 汇入图片

此时根目录下会跑出Player.swift
然後撰写成这样

import UIKit

class Player: NSObject {
    var point: Int = 1000
}

同样的做法 我们还需要再一个
OrderResponse.swift

import UIKit

class OrderResponse: Decodable {
    let error_code: Int
    let error_msg: String
    let info: OrderInfoResponse
}

struct OrderInfoResponse: Decodable { // or Decodable
  let balance: String
  let is_win: Bool
  let result: ResultResponse
}
struct ResultResponse: Decodable { // or Decodable
    let end: String
    let stairs: String
    let start: String
}

Decodable 是用来解析API的JSON资料用的

此时回到 ViewController.swift
撰写order方法, 按住 control 从按钮分别拖拉到方法上

 // 按钮
@IBOutlet weak var left_blue: UIButton!
@IBOutlet weak var right_blue: UIButton!
@IBOutlet weak var left_red: UIButton!
@IBOutlet weak var right_red: UIButton!

var player = Player()

@IBAction func choose(_ sender: UIButton) {
    // 使用 URLSession 打api
    let session = URLSession(configuration: .default)
    var request = URLRequest(url: URL(string: "http://pinyi.ami-shake.com/gg_order.php")!)
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    request.httpMethod = "POST"
    let data = ["choose": self.getChoose(sender), "balance": String(self.player.point)]
    
    do{
        request.httpBody = try JSONSerialization.data(withJSONObject: data, options: JSONSerialization.WritingOptions())
    }catch let error{
        print("passer data error")
        print(error)
    }
    
    session.dataTask(with: request) { data, response, error in
        if let data = data {
            do {
                let res = try JSONDecoder().decode(OrderResponse.self, from: data)
                执行动画播放
                判断输赢 增减分数
            } catch let error {
                print("error")
                print(error)
            }
            
        }
    }.resume()
}

fileprivate func getChoose(_ sender: UIButton) -> String {
    if(sender == self.left_red){
        return "left_even"
    }
    if(sender == self.left_blue){
        return "left_odd"
    }
    if(sender == self.right_blue){
        return "right_odd"
    }
    if(sender == self.right_red){
        return "right_even"
    }
    
    return ""
}

接下来撰写播放结果动画的方法
API的 start 来决定打开鸡蛋是左边还是右边
打开鸡蛋後 判断是否要显示第四条线
然後让云朵隐藏起来
鸡蛋依照线条开始跑
跑完之後更新分数
并重新让动画恢复播放前的状态

enum EggWapperDirection {
    case Left
    case Right
}
enum HatColor {
    case Red
    case Blue
}
func playResult(_ eggWapperDirection: EggWapperDirection, _ hatColor: HatColor, _ isWin: Bool, _ newPoint: String)-> Void {
    var hasLastLine = true
    
    if(
        (eggWapperDirection == EggWapperDirection.Right && hatColor == HatColor.Blue) ||
        (eggWapperDirection == EggWapperDirection.Left && hatColor == HatColor.Red)
    ){
        // 这种情况下只有三条线
        hasLastLine = false
    }
    
    let eggshellAni =  self.openEggAni(eggWapperDirection)
    let cloudAni = self.displayCloud(false)
    let playEggAni = self.playEggAniOnLine(eggWapperDirection, hasLastLine)
    
    playEggAni.addCompletion({ _ in
        self.player.point = newPoint
        self.pointLabel.text = "Point: \(self.player.point)"
        self.reSetAni()
    })
    
    cloudAni.addCompletion({ _ in
        playEggAni.startAnimation()
    })
    
    displayLastLine(hasLastLine)
    
    eggshellAni.startAnimation()
    cloudAni.startAnimation()
    
}

func openEggAni(_ eggWapperDirection: EggWapperDirection) -> UIViewPropertyAnimator {
    return UIViewPropertyAnimator(duration: 0.5, curve: .linear, animations: {
        let egg: UIView! = eggWapperDirection == EggWapperDirection.Right ? self.eggshell_right : self.eggshell_left
        egg.transform = CGAffineTransform(translationX: 30, y: -30).rotated(by: 30 *  CGFloat.pi / 180 )
        egg.alpha = 0
    })
}

func displayCloud(_ isShow: Bool) -> UIViewPropertyAnimator{
    let CloudAni = UIViewPropertyAnimator(duration: isShow ? 0 : 1,curve: .linear, animations: {
        self.Cloud.alpha = isShow ? 1 : 0
    })
    
    return CloudAni
}

func playEggAniOnLine(_ eggWapperDirection: EggWapperDirection, _ hasLastLine: Bool) -> UIViewPropertyAnimator {
    let eggWapperAni = UIViewPropertyAnimator(duration: 3, curve: .linear)
    eggWapperAni.addAnimations {
        UIView.animateKeyframes(withDuration: 0, delay: 0, animations: {
            let eggRunLineKeyFrameOptions = self.getEggRunLineKeyFrameOptions(hasLastLine);
            for option in eggRunLineKeyFrameOptions {
                UIView.addKeyframe(
                    withRelativeStartTime: option.startTime,
                    relativeDuration: 0.1,
                    animations: {
                        if(eggWapperDirection == EggWapperDirection.Left){
                            self.eggWapperLeft.transform = CGAffineTransform(translationX: option.translationX, y: option.translationY)
                        } else {
                            self.eggWapperRight.transform = CGAffineTransform(translationX: -option.translationX, y: option.translationY)
                        }
                        
                    })
            }
        })
    }
    
    return eggWapperAni
}

fileprivate func getEggRunLineKeyFrameOptions(_ hasLastLine: Bool) -> Array<KeyFrameOptionItem>{
    var keyFrameOptions: Array<KeyFrameOptionItem> = []
    keyFrameOptions.append(KeyFrameOptionItem(startTime: 0.0, translationX: 0, translationY: self.lineWapperHeight*0.2 + 50, rotated: 0, scaledX: 1.0))
    keyFrameOptions.append(KeyFrameOptionItem(startTime: 0.1, translationX: self.lineWapperWidth, translationY:  self.lineWapperHeight*0.2 + 50, rotated: 0, scaledX: 1.0))
    keyFrameOptions.append(KeyFrameOptionItem(startTime: 0.2, translationX: self.lineWapperWidth, translationY:  self.lineWapperHeight*0.4 + 50, rotated: 0, scaledX: 1.0))

    keyFrameOptions.append(KeyFrameOptionItem(startTime: 0.3, translationX: 0, translationY:  self.lineWapperHeight*0.4 + 50, rotated: 0, scaledX:  1.0))
    keyFrameOptions.append(KeyFrameOptionItem(startTime: 0.4, translationX: 0, translationY: self.lineWapperHeight*0.6 + 50, rotated: 0, scaledX:  1.0))
    keyFrameOptions.append(KeyFrameOptionItem(startTime: 0.5, translationX: self.lineWapperWidth,  translationY: self.lineWapperHeight*0.6 + 50, rotated: 0, scaledX: 1.0))
    keyFrameOptions.append(KeyFrameOptionItem(startTime: 0.6, translationX: self.lineWapperWidth, translationY:  self.lineWapperHeight*0.8 + 50, rotated: 0, scaledX:  1.0))
    if(hasLastLine){
        // 走第四条线
        keyFrameOptions.append(KeyFrameOptionItem(startTime: 0.7, translationX: 0, translationY:  self.lineWapperHeight*0.8 + 50, rotated: 0, scaledX: 1.0))
        keyFrameOptions.append(KeyFrameOptionItem(startTime: 0.8, translationX: 0, translationY:  self.lineWapperHeight*1 + 10, rotated: 0, scaledX:  1.0))
    } else {
        
        keyFrameOptions.append(KeyFrameOptionItem(startTime: 0.7, translationX: self.lineWapperWidth, translationY:  self.lineWapperHeight*1 + 10, rotated: 0, scaledX:  1.0))
    }
    
    return keyFrameOptions
}

func displayLastLine(_ isShow: Bool) {
    self.lastLineLayer?.isHidden = !isShow
    self.lastLineInLineLayer?.isHidden = !isShow
}

func reSetAni() -> Void {
    self.eggshell_left.transform = .identity
    self.eggshell_left.alpha = 1
    self.eggshell_right.transform = .identity
    self.eggshell_right.alpha = 1
    self.eggWapperLeft.transform = .identity
    self.eggWapperRight.transform = .identity
    
    self.displayCloud(true).startAnimation()
    self.displayLastLine(true)
    // 重新设定初始动画
    self.setChickAnimation()
}

看起来程序码有点多
但实际上就是设置各种动画
让他依序执行~ 完成
接下来为了避免打API时
用的人一直点按钮 再加上一个方法

func enableAllButton(_ isEnable: Bool) -> Void {
    let buttonList = [self.left_red, self.left_blue , self.right_red, self.right_blue]
    let disableAlpha: CGFloat = 0.5
    
    for button in buttonList {
        button?.isEnabled = isEnable
        button?.alpha = isEnable ? 1 : disableAlpha
    }
}

然後当玩家获胜时 我们帮他增加分数
并给他一个赞的图案
输的话扣分, 并给个倒赞的图
我们再加上一些方法

@IBOutlet weak var pointLabel: UILabel!
@IBOutlet weak var winIcon: UIImageView!

func updatePoint(_ isWin: Bool, _ newPoint: String)-> Void {
    self.winIcon.isHidden = false
    if isWin {
        self.winIcon.image = UIImage(systemName: "hands.sparkles.fill")
    } else {
        self.winIcon.image = UIImage(systemName: "hand.thumbsdown")
    }
    
    self.updatePointAndDisplayInUI(Int(newPoint) ?? 0)
    
    self.checkIsGameOver()
}

func updatePointAndDisplayInUI(_ newPoint: Int) {
    self.player.point = newPoint
    self.pointLabel.text = "Point: \(self.player.point)"
}

func checkIsGameOver() -> Void {
    if self.player.point > 0 {
        return
    }
    
    self.alertMessage("游戏结束!", "输了! 游戏即将重启")
    self.updatePointAndDisplayInUI(1000)
}

func alertMessage(_ title: String,_ msg: String) -> Void {
    // 显示提示讯息
    let alert = UIAlertController(title: title, message: msg, preferredStyle: .alert)
    let okBtn = UIAlertAction(title: "OK", style: .default, handler: nil)
    alert.addAction(okBtn)
    self.present(alert, animated: true, completion: nil)
}

整个游戏终於做完了
明天来处里游戏纪录搂!

kotlin - 游戏功能

Kotlin也是要开始打API搂
再来复习一下API文件

API说明

请使用POST方式传送资料,API会把结果告诉你

  • URL

    http://pinyi.ami-shake.com/gg_order.php

  • Method:

    POST

  • URL Params

    None

  • Data Params

    Required:

    choose=选择的项目
    balance=目前的结余的点数

  • Success Response:
    end even 代表走了四条横线 odd代表三条

    • Code: 200
      Content: {"error_code":0,"error_msg":"","info":{"balance":"1200","is_win":true,"result":{"end":"even","stairs":"3","start":"left"}}}
  • Error Response:

    • Code: 200
      Content: {"error_code":10001,"error_msg":"Please POST 'choose' and 'balance' property","info":{"balance":"0","is_win":false,"result":{"end":"","stairs":"","start":""}}}

接下来撰写按钮点击後的程序吧
kotlin这边在资料储存与传递上
给出了另一种解决方案叫做 ViewModel + LiveData

ViewModel + LiveData

其中的 ViewModel 主要就是用来管理资料并共享使用
而 LiveData 是让资料产生 Lifecycle
从而可达到view与data之间的绑定

这种概念对写前端的工程师来说并不陌生
前端前三大框架 Angular, Vue, React 也都是走这种设计
尤其是Angular 与 android 都是出自Google
所以两个都是在MVVM框架下的系统

想使用 ViewModel的话 没错~
继续去Gradle里面去新增吧

添加依赖 进入build.gradle(Module:chick_bb.app)

dependencies {
    ... 很多东西

    // ViewModel
    implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'

    ... 很多东西
}

在com.test.chickbb底下新增一个package叫player
在com.test.chickbb底下新增一个package叫network
其实就是一个资料夹拉~

Kotlin 汇入图片

然後在player底下新增一个kotlin Class 叫 PlayerViewModel
然後在network底下新增一个kotlin Class 叫 OrderResponse
首先解释一下 OrderResponse
OrderResponse 里面要定义等等打API回来的资料
内容这样

package com.test.chickbb.network

import com.squareup.moshi.Json

data class OrderResponse (
    @Json(name = "error_code") var errorCode: String,
    @Json(name = "error_msg") var errorMsg: String,
    var info: OrderInfoResponse
)

data class OrderInfoResponse(
    var balance: String,
    @Json(name = "is_win") var isWin: Boolean,
    var result: ResultResponse,
)

data class ResultResponse(
    var end: String,
    var stairs: String,
    var start: String
)

PlayerViewModel 是储存玩家点数
与游戏纪录的类别
内容这样

package com.test.chickbb.player

import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.test.chickbb.network.ResultResponse

class PlayerViewModel() :  ViewModel()  {
    private var _currentPoint: Int
    private var _point  = MutableLiveData<Int>()
    val point: LiveData<Int> get() = _point
    val currentPoint: Int get() = _currentPoint

    private var _history = mutableListOf<OrderHistory>()
    val history: MutableList<OrderHistory> get() = _history

    init {
        Log.d("GameFragment", "GameViewModel created!")
        _point.value = 1000
        _currentPoint = 1000
    }

    override fun onCleared() {
        super.onCleared()
        Log.d("GameFragment", "GameViewModel destroyed!")
    }
    fun updatePoint(newPoint: Int) {
        this._point.value = newPoint
        this._currentPoint = newPoint
    }
    fun addHistory(choose: String, is_win: Boolean,newPoint: Int,result: ResultResponse){
        this._history.add(OrderHistory(choose, is_win, this._currentPoint, result, newPoint - this._currentPoint))
    }
}

class OrderHistory(
    var choose: String,
    var is_win: Boolean,
    var point: Int,
    var result: ResultResponse,
    var winPoint: Int)

所谓的 LiveData 其实就是透过观察者模式
去订阅 LiveData 变更的事件
每当资料变更时 就将UI进行更新

而为了方便程序使用 我另外加了 currentPoint
可以直接取得当下的Point
不用通过订阅

此时回到 GameFragment.kt
撰写order方法

override fun onCreateView(
    inflater: LayoutInflater, container: ViewGroup?,
    savedInstanceState: Bundle?
): View? {
    // Inflate the layout for this fragment
    _binding = FragmentGameBinding.inflate(inflater, container, false)
    // 为了资料共用 我们透过ViewModelProvider从Activity 取得实体
    player = ViewModelProvider(requireActivity()).get(PlayerViewModel::class.java)
    setChickAnimation()
    bindingBaseEvent()

    return binding.root
}

fun bindingBaseEvent() {
    // 执行动画
    binding.root.doOnPreDraw {
        // kotlin 没有生命周期绑定 视图布局完成
        // 所以用这边绑定
        drawGameLine()
    }
    // 绑定按钮事件
    binding.btnLeftBlue.setOnClickListener {
        choose(EggWapperDirection.Left, HatColor.Blue)
    }
    // 绑定按钮事件
    binding.btnLeftRed.setOnClickListener {
        choose(EggWapperDirection.Left, HatColor.Red)
    }
    // 绑定按钮事件
    binding.btnRightBlue.setOnClickListener {
        choose(EggWapperDirection.Right, HatColor.Blue)
    }
    // 绑定按钮事件
    binding.btnRightRed.setOnClickListener {
        choose(EggWapperDirection.Right, HatColor.Red)
    }
    // 订阅 LiveData事件
    player.point.observe(viewLifecycleOwner,
        { newPoint ->
            binding.pointLabel.text = "Point: " + newPoint.toString()
        })
}

LiveData的绑定就这样
很简单吧 ~ 只要值改变 就更新UI
个下来让我们来看看choose 方法做了什麽

fun choose(eggWapperDirection: EggWapperDirection, hatColor: HatColor) {
    // 禁用所有按钮
    this.enableAllButton(false)
    val chooseKey =  this.getChoose(eggWapperDirection, hatColor)
    // 打API与更新UI, 需要丢入後台线程处理  
    GlobalScope.launch {
        try {
            // 打API
            val result = MarsApi.retrofitService.order(chooseKey, player.currentPoint.toString())
            // 更新UI必须回主线程
            Handler(Looper.getMainLooper()).postDelayed({
                // 新增游戏纪录
                player.addHistory(chooseKey, result.info.isWin, result.info.balance.toInt(), result.info.result)
                // 播放动画结果
                playResultFromResponse(result)
            }, 0)

        } catch (e: Exception) {
            println("error:"+e.message)

            // 启动所有按钮
            // 更新UI必须回主线程
            Handler(Looper.getMainLooper()).postDelayed({
                    enableAllButton(true)
            }, 0)
        }
    }
}
fun getChoose(eggWapperDirection: EggWapperDirection, hatColor: HatColor): String {
    if(eggWapperDirection == EggWapperDirection.Left && hatColor == HatColor.Red){
        return "left_even"
    }
    if(eggWapperDirection == EggWapperDirection.Left && hatColor == HatColor.Blue){
        return "left_odd"
    }
    if(eggWapperDirection == EggWapperDirection.Right && hatColor == HatColor.Blue){
        return "right_odd"
    }
    if(eggWapperDirection == EggWapperDirection.Right && hatColor == HatColor.Red){
        return "right_even"
    }

    return ""
}

打API这边牵扯到三个知识点

  1. 打API (透过 Http)
  2. 後台线程
  3. 主线程 UI更新

一个一个来吧

1. 打API (Http Clinet)

Kotlin这边对於Http Clinet 推荐采用第三方库来完成
Http Clinet使用 Retrofit
JSON解析使用 Moshi
首先到gradle

dependencies {
    ... 很多东西

    // Retrofit with Moshi Converter
    implementation 'com.squareup.moshi:moshi-kotlin:1.9.3'
    implementation 'com.squareup.retrofit2:converter-moshi:2.9.0'

    ... 很多东西
}

然後要开通网路权限
AndroidManifest.xml 加上

<uses-permission android:name="android.permission.INTERNET" />

接下来应为打的api是HTTP非HTTPS
所以我们要取消安全连线的限制
在res底下新增xml资料夹
xml下新增 network_security_config
内容如下

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config cleartextTrafficPermitted="true" />
</network-security-config>

然後在AndroidManifest.xml 的 application 内加上

 <application
        ...很多很多
        android:networkSecurityConfig="@xml/network_security_config"
        ...很多很多>
 

设定完长这样

这样才能开始写Api的程序
在刚刚的network的资料夹内新增 kotlin Class
OrderApiService 档案

package com.test.chickbb.network

import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.http.*
// api的 BASE_URL
private const val BASE_URL = "http://pinyi.ami-shake.com"
// 产生moshi JSON解析
private val moshi = Moshi.Builder()
                        .add(KotlinJsonAdapterFactory())
                        .build()

// 产生retrofit 并添加JSON解析器
private val retrofit = Retrofit.Builder()
    .addConverterFactory(MoshiConverterFactory.create(moshi))
    .baseUrl(BASE_URL)
    .build()

// 定义API  
// FormUrlEncoded代表解析对body进行 Encoded  
// POST(里面是API路径)
// suspend是限制他只能运作在线程中
interface OrderApiService {
    @FormUrlEncoded
    @POST("/gg_order.php")
    suspend fun order(@Field("choose") choose: String, @Field("balance") balance: String): OrderResponse
}

// 透过这个方法,达到单一实例
object OrderApi {
    val retrofitService : OrderApiService by lazy {
        retrofit.create(OrderApiService::class.java)
    }
}

此时只要 OrderApi.retrofitService.order("left_red", player.currentPoint.toString())
就可以取得资料了!

2. 後台线程

这边要说明的是
应为API是非同步的, 你也不知道他要执行多久
所以必须把他交给子线程执行
GlobalScope.launch {}
可以产生一个後台线程 你不用去管理他
就可以丢出一个任务给他执行
整个程序也不会因为这个任务而卡住

3. 主线程 UI更新

当子线程打完api取得资料了
这是要进行UI的变更渲染
就要把任务交回主线程来更新画面

Handler(Looper.getMainLooper()).postDelayed({
        更新UI任务()
}, 0)

比起swift, 需要了解更多知识点
接下来撰写动画吧

API的 start 来决定打开鸡蛋是左边还是右边
打开鸡蛋後 判断是否要显示第四条线
然後让云朵隐藏起来
鸡蛋依照线条开始跑
跑完之後更新分数
并重新让动画恢复播放前的状态

enum class EggWapperDirection {
    Left,
    Right
}
enum class HatColor {
    Red,
    Blue
}
fun playResultFromResponse(res: OrderResponse) {
    if(res.info.result.start == "left" && res.info.result.end == "odd" ){
        this.playResult(EggWapperDirection.Left, HatColor.Blue, res.info.isWin, res.info.balance)
    }
    if(res.info.result.start == "left" && res.info.result.end == "even" ){
        this.playResult(EggWapperDirection.Left, HatColor.Red, res.info.isWin, res.info.balance)
    }
    if(res.info.result.start == "right"  && res.info.result.end == "odd" ){
        this.playResult(EggWapperDirection.Right, HatColor.Blue, res.info.isWin, res.info.balance)
    }
    if(res.info.result.start == "right"  && res.info.result.end == "even" ){
        this.playResult(EggWapperDirection.Right, HatColor.Red, res.info.isWin, res.info.balance)
    }
}
fun playResult(eggWapperDirection: EggWapperDirection, hatColor: HatColor, isWin: Boolean, newPoint: String) {

    var hasLastLine = true

    if(
        (eggWapperDirection == EggWapperDirection.Right && hatColor == HatColor.Blue) ||
        (eggWapperDirection == EggWapperDirection.Left && hatColor == HatColor.Red)
    ){
        hasLastLine = false
    }

    val eggshellAni  = this.openEggAni(eggWapperDirection)
    val cloudAni = this.displayCloud(false)
    val playEggAni = this.playEggAniOnLine(eggWapperDirection, hasLastLine)

    playEggAni.doOnEnd {
        this.updatePoint(isWin, newPoint)
        this.reSetAni()
    }

    cloudAni.doOnEnd {
        playEggAni.apply {
            duration = 4000 // 动画持续四秒
            start()  // 开始播放
        }
    }

    this.displayLastLine(hasLastLine)

    eggshellAni.apply {
        duration = 500
        start()
    }
    cloudAni.apply {
        duration = 1000
        start()
    }
}
private fun openEggAni(eggWapperDirection: EggWapperDirection): ObjectAnimator {
    val eggShell = if(eggWapperDirection == EggWapperDirection.Left)  binding.eggshellLeft else binding.eggshellRight

    // translationX
    val pvhtranslationX = PropertyValuesHolder.ofKeyframe("translationX",
        Keyframe.ofFloat(0f, 0f),
        Keyframe.ofFloat(1f, eggShell.width.toFloat()/2 )
    )

    // translationY
    val pvhtranslationY = PropertyValuesHolder.ofKeyframe("translationY",
        Keyframe.ofFloat(0f, 0f),
        Keyframe.ofFloat(1f, -(eggShell.width.toFloat())/2)
    )
    // rotation
    val pvhRotation = PropertyValuesHolder.ofKeyframe("rotation",
        Keyframe.ofFloat(0f, 10f),
        Keyframe.ofFloat(1f, 60f)
    )
    // rotation
    val pvhAlpha = PropertyValuesHolder.ofKeyframe("alpha",
        Keyframe.ofFloat(0f, 1f),
        Keyframe.ofFloat(1f, 0f)
    )
    // 设定 ggView 关键影格
    val ani = ObjectAnimator.ofPropertyValuesHolder(eggShell,
        pvhtranslationY,
        pvhtranslationX,
        pvhRotation,
        pvhAlpha)

    return ani

}
private fun displayCloud(isShow: Boolean): ObjectAnimator{
    return ObjectAnimator.ofPropertyValuesHolder(binding.cloud, PropertyValuesHolder.ofKeyframe("alpha",
        Keyframe.ofFloat(0f, binding.cloud.alpha),
        Keyframe.ofFloat(1f, if(isShow) 1f else 0f)
    ))
}
fun playEggAniOnLine(eggWapperDirection: EggWapperDirection, hasLastLine: Boolean): ObjectAnimator {
    val eggRunLineKeyFrameOptions = getEggRunLineKeyFrameOptions(hasLastLine, eggWapperDirection)
    val eggView = if (eggWapperDirection === EggWapperDirection.Left) binding.eggWapperLeft else binding.eggWapperRight

    return ObjectAnimator.ofPropertyValuesHolder(eggView, *eggRunLineKeyFrameOptions)
}
private fun  getEggRunLineKeyFrameOptions(hasLastLine: Boolean,direction: EggWapperDirection ): Array<PropertyValuesHolder>{
    val eggXOffset = if (direction === EggWapperDirection.Left) this.lineWapperWidth else -this.lineWapperWidth
    // translationX
    val pvhtranslationX = PropertyValuesHolder.ofKeyframe("translationX",
        Keyframe.ofFloat(0f, 0f),
        Keyframe.ofFloat(.1f, 0f),
        Keyframe.ofFloat(.2f, eggXOffset),
        Keyframe.ofFloat(.3f, eggXOffset),
        Keyframe.ofFloat(.4f, 0f),
        Keyframe.ofFloat(.5f, 0f),
        Keyframe.ofFloat(.6f, eggXOffset),
        Keyframe.ofFloat(.7f, eggXOffset),
        Keyframe.ofFloat(.8f, if(hasLastLine) 0f else eggXOffset),
        Keyframe.ofFloat(.9f, if(hasLastLine) 0f else eggXOffset),
        Keyframe.ofFloat(1f, if(hasLastLine) 0f else eggXOffset)
    )
    val eggYOffset = (binding.eggLeft.height/3)
    val finalYOffset = (binding.eggLeft.height/4)

    val pvhtranslationY = PropertyValuesHolder.ofKeyframe("translationY",
        Keyframe.ofFloat(0f, 0f),
        Keyframe.ofFloat(.1f, this.lineWapperHeight*0.2f + eggYOffset),
        Keyframe.ofFloat(.2f, this.lineWapperHeight*0.2f + eggYOffset),
        Keyframe.ofFloat(.3f, this.lineWapperHeight*0.4f + eggYOffset),
        Keyframe.ofFloat(.4f, this.lineWapperHeight*0.4f + eggYOffset),
        Keyframe.ofFloat(.5f, this.lineWapperHeight*0.6f + eggYOffset),
        Keyframe.ofFloat(.6f, this.lineWapperHeight*0.6f + eggYOffset),
        Keyframe.ofFloat(.7f, this.lineWapperHeight*0.8f + eggYOffset),
        Keyframe.ofFloat(.8f, if(hasLastLine) this.lineWapperHeight*0.8f + eggYOffset else this.lineWapperHeight*1f + finalYOffset),
        Keyframe.ofFloat(.9f, this.lineWapperHeight*1f + finalYOffset),
        Keyframe.ofFloat(1f, this.lineWapperHeight*1f + finalYOffset)
    )

    return arrayOf(pvhtranslationX,pvhtranslationY)

}
fun displayLastLine(isShow: Boolean) {
    this.lastLineImageView?.alpha = if (isShow)  1f else 0f
}
fun reSetAni() {
    val resetViews = listOf<View>(binding.eggshellLeft, binding.eggshellRight, binding.eggWapperRight, binding.eggWapperLeft)
    for (vi in resetViews) {
        vi.translationX = 0f
        vi.translationY = 0f
        vi.rotation = 0f
        vi.alpha = 1f
    }

    this.displayCloud(true).apply {
        duration = 0
        start()
    }
    this.displayLastLine(true)
    this.enableAllButton(true)
}

看起来程序码有点多
但实际上就是设置各种动画
让他依序执行~ 完成
接下来为了避免打API时
用的人一直点按钮 再加上一个方法

fun enableAllButton(isEnable: Boolean) {
    val buttonList = listOf(binding.btnLeftRed, binding.btnLeftBlue,  binding.btnRightRed, binding.btnRightBlue)
    buttonList.forEach {
        it.isEnabled = isEnable
        it.alpha = if (isEnable) 1f else 0.5f
    }

}

然後当玩家获胜时 我们帮他增加分数
并给他一个赞的图案
输的话扣分, 并给个倒赞的图
我们再加上一些方法

fun updatePoint(isWin: Boolean, newPoint: String) {
    binding.winIcon.alpha = 1f
    if (isWin) {
        binding.winIcon.setImageResource(android.R.drawable.stat_sys_upload)
    } else {
        binding.winIcon.setImageResource(android.R.drawable.stat_sys_download)
    }

    this.updatePointAndDisplayInUI(newPoint.toInt())

}

fun updatePointAndDisplayInUI(newPoint: Int) {
    if(newPoint > 0){
        player.updatePoint(newPoint)
    } else {
        this.isGameOver()
    }
}

fun isGameOver() {
    this.alertMessage("游戏结束!", "输了! 游戏即将重启")
    this.player.updatePoint(1000);
}

fun alertMessage (title: String,msg: String) {
    AlertDialog.Builder(binding.root.context)
        .setMessage(msg)
        .setTitle(title)
        .setPositiveButton("OK", null)
        .show()
}

整个游戏终於做完了

差异

Kotlin 在这个章节里面
需要知道非常多的知识点
尤其打HTTP连线 还要选择使用的第三方库

这点在Swift就比较统一
你也不用特别选 就一个方法

但Kotlin好处就是 可以选择你比较习惯的用法
不同第三方库 使用起来的方便度也是有差的
这边采用的是官方教学文件的用法

给大家参考看看搂

小碎嘴时间 ヽ(゚´Д`)ノ゚

哇赛~今天资讯大爆炸
内容超多的

剩下最後五天~还有程序收尾与APP上架
加油加油~终点快到搂~


<<:  Flutter体验 Day 25-SharedPreferences

>>:  【Day20】导航元件 - Select

Day 16: 利用Portainer方便管理Docker

Portainer介绍 有没有觉得每次在玩转Docke的时候都在用指令很不方便,当container...

开 api 日常心得笔记

开 api 规格是个有趣的事情,从栏位命名、资料阶层设计、易读性、合理性、方便性都是需要考量的点,对...

GCP IAP

GCP IAP 今天再来了解一下什麽事IAP?他的全名即是dentity-Aware Proxy简称...

[Day 15]呐呐,还有一半别想跑(前端篇)

挑战目标: MockNative Camp 今天继续来制作我们的Footer, 目标 前两天我们已经...

django入门(四) — 简单范例(2)-范本与范本继承

范本(Template) 范本是放HTML档案的资料夹,Template engine(范本引擎)会...