Skip to content

Commit

Permalink
feat: 在线匹配与联机对战
Browse files Browse the repository at this point in the history
  • Loading branch information
Jim-shop committed May 31, 2023
1 parent 1e21061 commit 5fc786a
Show file tree
Hide file tree
Showing 9 changed files with 372 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package net.imshit.aircraftwar.data.fighting

data class CommunicateInfo(
val userId: Int,
val score: Int,
val life: Int
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package net.imshit.aircraftwar.data.fighting

import android.content.Context
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import net.imshit.aircraftwar.data.account.LoginManager
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.WebSocket
import okhttp3.WebSocketListener
import org.json.JSONObject
import java.util.concurrent.TimeUnit

class FightingClient(
private val context: Context,
private val roomId: Int,
) : WebSocketListener() {

companion object Const {
const val SOCKET_URL = "wss://haxiaoshen.top/game/fighting"
}

private val httpClient = OkHttpClient.Builder().pingInterval(30, TimeUnit.SECONDS).build()

private var webSocket: WebSocket? = null
private val loginManager = LoginManager(context)

fun run() {
CoroutineScope(Dispatchers.Default).launch {
if (loginManager.requireLogin()) {
val request =
Request.Builder().url("$SOCKET_URL/$roomId?token=${loginManager.token}")
.build()
webSocket = httpClient.newWebSocket(request, this@FightingClient)
}
}
}

var onData: ((CommunicateInfo) -> Unit)? = null
var onQuit: (() -> Unit)? = null

override fun onMessage(webSocket: WebSocket, text: String) {
val jsonObject = JSONObject(text)
val type = jsonObject.getString("type")
if (type == "comm") {
val jsonInfo = jsonObject.getJSONObject("msg")
val userId = jsonInfo.getInt("user")
val score = jsonInfo.getInt("score")
val life = jsonInfo.getInt("life")
onData?.invoke(
CommunicateInfo(
userId, score, life
)
)
} else if (type == "quit") {
onQuit?.invoke()
}
}

fun send(info: CommunicateInfo) {
webSocket?.send("""{"user":${info.userId}, "score":${info.score}, "life":${info.life}}""")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package net.imshit.aircraftwar.data.pairing

import android.content.Context
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import net.imshit.aircraftwar.data.account.LoginManager
import net.imshit.aircraftwar.logic.game.Difficulty
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.WebSocket
import okhttp3.WebSocketListener
import org.json.JSONObject
import java.util.concurrent.TimeUnit

class PairingClient(
private val context: Context,
private val mode: Difficulty,
) : WebSocketListener() {

companion object Const {
const val SOCKET_URL = "wss://haxiaoshen.top/game/pairing"
}

private val httpClient = OkHttpClient.Builder().pingInterval(30, TimeUnit.SECONDS).build()

private var webSocket: WebSocket? = null
private val loginManager = LoginManager(context)

fun run() {
CoroutineScope(Dispatchers.Default).launch {
if (loginManager.requireLogin()) {
val request = Request.Builder()
.url("$SOCKET_URL?token=${loginManager.token}&mode=${mode.name.lowercase()}")
.build()
webSocket = httpClient.newWebSocket(request, this@PairingClient)
}
}
}

var onChange: ((List<PairingInfo>) -> Unit)? = null
var onSucceed: ((Int) -> Unit)? = null

override fun onMessage(webSocket: WebSocket, text: String) {
val jsonObject = JSONObject(text)
val type = jsonObject.getString("type")
if (type == "player") {
val jsonArray = jsonObject.getJSONArray("msg")
val players = mutableListOf<PairingInfo>()
for (i in 0 until jsonArray.length()) {
val jsonPlayer = jsonArray.getJSONObject(i)
players.add(
PairingInfo(
jsonPlayer.getInt("ID"), jsonPlayer.getString("name"),
jsonPlayer.getBoolean("requesting")
)
)
}
onChange?.invoke(players)
} else if (type == "room") {
val room = jsonObject.getInt("msg")
onSucceed?.invoke(room)
}
}

fun select(userId: Int) {
webSocket?.send("$userId")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package net.imshit.aircraftwar.data.scoreboard

import androidx.appcompat.app.AppCompatDialogFragment

class SaveScoreDialog(onlineMode:Boolean):AppCompatDialogFragment() {

}
134 changes: 134 additions & 0 deletions app/src/main/java/net/imshit/aircraftwar/gui/PairingActivity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package net.imshit.aircraftwar.gui

import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.button.MaterialButton
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import net.imshit.aircraftwar.R
import net.imshit.aircraftwar.data.account.LoginManager
import net.imshit.aircraftwar.data.app.AppInfoDialog
import net.imshit.aircraftwar.data.pairing.PairingClient
import net.imshit.aircraftwar.data.pairing.PairingInfo
import net.imshit.aircraftwar.databinding.ActivityPairingBinding
import net.imshit.aircraftwar.logic.game.Difficulty

class PairingActivity : AppCompatActivity() {
companion object Api {
fun actionStart(context: Context, gameMode: Difficulty, soundMode: Boolean) {
CoroutineScope(Dispatchers.Default).launch {
if (LoginManager(context).requireLogin()) {
context.startActivity(Intent(context, PairingActivity::class.java).apply {
putExtra("gameMode", gameMode)
putExtra("soundMode", soundMode)
})
}
}
}
}

class PairingInfoAdapter(
private val activity: AppCompatActivity,
private val client: PairingClient
) : RecyclerView.Adapter<PairingInfoAdapter.PairingInfoViewHolder>() {
class PairingInfoViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val avatar: ImageView = itemView.findViewById(R.id.pvi_iv)
private val accountView: TextView = itemView.findViewById(R.id.pvi_tv)
private val button: MaterialButton = itemView.findViewById(R.id.pvi_btn)
fun bind(pairingInfo: PairingInfo, context: Context, client: PairingClient) {
this.accountView.text = pairingInfo.userAccount
if (pairingInfo.isWilling) {
this.avatar.setImageResource(R.drawable.avatar)
}
this.button.setOnClickListener {
client.select(pairingInfo.userId)
this.button.text = context.getString(R.string.button_pairing_sent)
this.button.setIconResource(R.drawable.ic_check_24)
}
}
}

init {
client.onChange = ::onDataChange
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PairingInfoViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.pairing_view_item, parent, false)
return PairingInfoViewHolder(view)
}

private val buffer = mutableListOf<PairingInfo>()

override fun getItemCount(): Int = buffer.size

override fun onBindViewHolder(holder: PairingInfoViewHolder, position: Int) {
holder.bind(buffer[position], activity, client)
}

private fun onDataChange(data: List<PairingInfo>) {
// 清除已下线
buffer.removeIf {
data.all { newItem -> newItem.userId != it.userId }
}
// 更新现有
buffer.forEach { oldItem ->
val newItem = data.find { newItem -> newItem.userId == oldItem.userId }
newItem?.let {
oldItem.isWilling = it.isWilling
}
}
// 新增新上线
buffer.addAll(data.filter { buffer.all { oldItem -> oldItem.userId != it.userId } })
activity.runOnUiThread {
notifyDataSetChanged()
}
}
}

var gameMode: Difficulty = Difficulty.EASY
var soundMode: Boolean = true

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 获取配置
intent.apply {
gameMode = getSerializableExtra("gameMode", Difficulty::class.java) ?: Difficulty.EASY
soundMode = getBooleanExtra("soundMode", true)
}
with(ActivityPairingBinding.inflate(layoutInflater)) {
setContentView(root)

apTb.setNavigationOnClickListener {
this@PairingActivity.finish()
}

apTb.setOnMenuItemClickListener { item ->
when (item.itemId) {
R.id.item_about -> AppInfoDialog().show(supportFragmentManager, "about")
}
return@setOnMenuItemClickListener true
}

val client = PairingClient(this@PairingActivity, gameMode)
apRv.adapter = PairingInfoAdapter(this@PairingActivity, client)
apRv.setHasFixedSize(true)
client.onSucceed = ::onSucceed
client.run()
}
}

private fun onSucceed(room: Int) {
GameActivity.actionStart(this, gameMode, soundMode, true, room)
this.finish()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,7 @@ class ScoreboardActivity : AppCompatActivity() {
}

class ScoreInfoAdapter(
private val activity: AppCompatActivity,
private val scoreInfoList: MutableList<ScoreInfo>
private val activity: AppCompatActivity, private val scoreInfoList: MutableList<ScoreInfo>
) : RecyclerView.Adapter<ScoreInfoAdapter.ScoreInfoViewHolder>() {
class ScoreInfoViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val nameView: TextView = itemView.findViewById(R.id.sbvi_name)
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/res/drawable/ic_contact_mail_24.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M21,8L21,7l-3,2 -3,-2v1l3,2 3,-2zM22,3L2,3C0.9,3 0,3.9 0,5v14c0,1.1 0.9,2 2,2h20c1.1,0 1.99,-0.9 1.99,-2L24,5c0,-1.1 -0.9,-2 -2,-2zM8,6c1.66,0 3,1.34 3,3s-1.34,3 -3,3 -3,-1.34 -3,-3 1.34,-3 3,-3zM14,18L2,18v-1c0,-2 4,-3.1 6,-3.1s6,1.1 6,3.1v1zM22,12h-8L14,6h8v6z"/>
</vector>
35 changes: 35 additions & 0 deletions app/src/main/res/layout/activity_pairing.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">

<com.google.android.material.appbar.AppBarLayout
android:id="@+id/ap_abl"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true">

<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/ap_tb"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true"
app:menu="@menu/main"
app:navigationContentDescription="@string/navback"
app:navigationIcon="@drawable/ic_arrow_back_24"
app:title="@string/pairing_label"
app:titleCentered="true" />
</com.google.android.material.appbar.AppBarLayout>

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/ap_rv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
app:layoutManager="androidx.recyclerview.widget.StaggeredGridLayoutManager"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
app:spanCount="2" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>
50 changes: 50 additions & 0 deletions app/src/main/res/layout/pairing_view_item.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/pvi_card"
style="?attr/materialCardViewOutlinedStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginVertical="8dp"
android:clickable="true"
android:focusable="true">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp">

<ImageView
android:id="@+id/pvi_iv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:contentDescription="@string/avatar" />

<TextView
android:id="@+id/pvi_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:fontFamily="sans-serif"
android:layout_gravity="left"
android:textFontWeight="450"
android:textSize="24sp"
tools:text="哈小深" />

<Button
android:id="@+id/pvi_btn"
style="@style/Widget.Material3.Button.IconButton.Filled.Tonal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:text="@string/button_pairing"
android:layout_gravity="end"
app:icon="@drawable/ic_contact_mail_24" />

</LinearLayout>

</com.google.android.material.card.MaterialCardView>

0 comments on commit 5fc786a

Please sign in to comment.