Skip to content

Commit f0db190

Browse files
committed
Attempt at fixing wine glasses sync issues.
Fixed some issues in the frequency calculation. Started sending the frequency along with the angle to the other player instead of having it calculate both frequencies through the angles localy, which proved unreliable.
1 parent 7b2f920 commit f0db190

5 files changed

Lines changed: 67 additions & 47 deletions

File tree

common/src/main/java/com/imsproject/common/gameserver/GameAction.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ data class GameAction internal constructor(
2727

2828
fun fromString(message: String): GameAction {
2929
val parts = message.split(";")
30+
if(parts.size != 5){
31+
throw IllegalArgumentException("GameAction string must have 5 parts, but found ${parts.size}")
32+
}
3033
return GameAction(
3134
Type.valueOf(parts[0]),
3235
if (parts[1] == "") null else parts[1],

watch/app/src/main/java/com/imsproject/watch/Properties.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ const val UNDEFINED_ANGLE = 600f
5555
const val ARC_DEFAULT_ALPHA = 0.8f
5656
const val MAX_ANGLE_SKEW = 60f
5757
const val MIN_ANGLE_SKEW = 15f
58-
var WINE_GLASSES_SYNC_FREQUENCY_THRESHOLD = 0.05f
59-
var FREQUENCY_HISTORY_MILLISECONDS = 5000L
58+
var WINE_GLASSES_SYNC_FREQUENCY_THRESHOLD = 0.5f
59+
var FREQUENCY_HISTORY_MILLISECONDS = 1000L
6060
var OUTER_TOUCH_POINT = 0f
6161
var INNER_TOUCH_POINT = 0f
6262

watch/app/src/main/java/com/imsproject/watch/utils/FrequencyTracker.kt

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
package com.imsproject.watch.utils
22

33
import com.imsproject.watch.FREQUENCY_HISTORY_MILLISECONDS
4+
import kotlinx.coroutines.DelicateCoroutinesApi
5+
import kotlinx.coroutines.GlobalScope
6+
import kotlinx.coroutines.launch
7+
import java.util.concurrent.Executors
48

5-
private const val SAMPLES_PER_SECOND = 1000 / 60 // 60 fps
9+
private const val SAMPLES_PER_SECOND = 1000 / 16f // 60 fps
610

711
class FrequencyTracker {
812

9-
private val samplesHistoryCount : Int = (SAMPLES_PER_SECOND * FREQUENCY_HISTORY_MILLISECONDS).toInt()
13+
private val samplesHistoryCount : Int = (SAMPLES_PER_SECOND * FREQUENCY_HISTORY_MILLISECONDS / 1000f).toInt()
1014

1115
val frequency : Float
12-
get() = sum / sampleCount.fastCoerceIn(1,samplesHistoryCount)
16+
get() = (sum / sampleCount.fastCoerceIn(1,samplesHistoryCount)).let { if(it < 0.001) 0f else it }
1317

1418
private val samples = Array(samplesHistoryCount) {0f}
1519
private var sum : Float = 0f
@@ -21,6 +25,7 @@ class FrequencyTracker {
2125
fun addSample(angle: Float) {
2226
val currentTime = System.currentTimeMillis()
2327
val timeDiff = currentTime - lastSampleTime
28+
if(timeDiff == 0L) return
2429
val angleDiff = calculateAngleDiff(lastSampleAngle,angle)
2530
val radiansDiff = Math.toRadians(angleDiff.toDouble())
2631
val omega = radiansDiff / (timeDiff / 1000.0)

watch/app/src/main/java/com/imsproject/watch/utils/PacketTracker.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,16 @@ class PacketTracker() {
2424
}
2525
}
2626

27-
fun receivedOtherPacket(packetNum: Long) {
27+
/**
28+
* @return true if the packet is out of order
29+
*/
30+
fun receivedOtherPacket(packetNum: Long) : Boolean {
2831
val lastReceived = otherLastReceivedPacket
2932
otherLastReceivedPacket = packetNum
3033
if (packetNum != lastReceived + 1) {
3134
onOutOfOrderPacket?.invoke()
35+
return true
3236
}
37+
return false
3338
}
3439
}

watch/app/src/main/java/com/imsproject/watch/viewmodel/WineGlassesViewModel.kt

Lines changed: 48 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import androidx.compose.runtime.setValue
99
import androidx.compose.ui.util.fastCoerceAtLeast
1010
import androidx.compose.ui.util.fastCoerceAtMost
1111
import androidx.compose.ui.util.fastCoerceIn
12+
import androidx.compose.ui.util.fastMapTo
1213
import androidx.lifecycle.viewModelScope
1314
import com.imsproject.common.gameserver.GameAction
1415
import com.imsproject.common.gameserver.GameType
@@ -67,7 +68,8 @@ class WineGlassesViewModel : GameViewModel(GameType.WINE_GLASSES) {
6768
val myArc = Arc()
6869
val opponentArc = Arc()
6970
private lateinit var myFrequencyTracker : FrequencyTracker
70-
private lateinit var opponentFrequencyTracker : FrequencyTracker
71+
@Volatile
72+
private var opponentFrequency = 0f
7173

7274
private var _released = MutableStateFlow(true)
7375
val released : StateFlow<Boolean> = _released
@@ -96,17 +98,23 @@ class WineGlassesViewModel : GameViewModel(GameType.WINE_GLASSES) {
9698
override fun onCreate(intent: Intent, context: Context) {
9799
super.onCreate(intent,context)
98100

101+
setupWavPlayer(context)
102+
myFrequencyTracker = FrequencyTracker()
103+
99104
if(ACTIVITY_DEBUG_MODE){
100105
viewModelScope.launch(Dispatchers.Default) {
106+
val opponentFrequencyTracker = FrequencyTracker()
101107
while(true) {
102-
var angle = 0.0f
103-
while(angle < 360 * 15){
104-
opponentArc.startAngle = angle
105-
opponentFrequencyTracker.addSample(angle)
106-
angle += 4
108+
var rawAngle = 0.0f
109+
while(rawAngle < 360 * 15){
110+
opponentFrequencyTracker.addSample(rawAngle)
111+
opponentFrequency = opponentFrequencyTracker.frequency
112+
updateArc(rawAngle,opponentArc)
113+
rawAngle += 4
107114
delay(16)
108115
}
109116
_opponentReleased.value = true
117+
opponentFrequency = 0f
110118
opponentFrequencyTracker.reset()
111119
delay(2000)
112120
_opponentReleased.value = false
@@ -115,8 +123,6 @@ class WineGlassesViewModel : GameViewModel(GameType.WINE_GLASSES) {
115123
return
116124
}
117125

118-
setupWavPlayer(context)
119-
120126
// set up sync params
121127
val syncTolerance = intent.getLongExtra("$PACKAGE_PREFIX.syncTolerance", -1)
122128
if (syncTolerance <= 0L) {
@@ -133,19 +139,13 @@ class WineGlassesViewModel : GameViewModel(GameType.WINE_GLASSES) {
133139
Log.d(TAG, "syncTolerance: $syncTolerance")
134140
Log.d(TAG, "syncWindowLength: $syncWindowLength")
135141

136-
// set up frequency trackers
137-
myFrequencyTracker = FrequencyTracker()
138-
opponentFrequencyTracker = FrequencyTracker()
139-
140142
// start the frequency tracking loop
141143
viewModelScope.launch(Dispatchers.Default){
142144
while(true){
143145
delay(100) // run this loop roughly 10 times per second
144-
if(inSync){
145-
val timestamp = getCurrentGameTime()
146-
addEvent(SessionEvent.frequency(playerId, timestamp, myFrequencyTracker.frequency.toString()))
147-
addEvent(SessionEvent.opponentFrequency(playerId, timestamp, opponentFrequencyTracker.frequency.toString()))
148-
}
146+
val timestamp = getCurrentGameTime()
147+
addEvent(SessionEvent.frequency(playerId, timestamp, myFrequencyTracker.frequency.toString()))
148+
addEvent(SessionEvent.opponentFrequency(playerId,timestamp,opponentFrequency.toString()))
149149
}
150150
}
151151
}
@@ -159,9 +159,9 @@ class WineGlassesViewModel : GameViewModel(GameType.WINE_GLASSES) {
159159
}
160160

161161
if(inBounds){
162-
updateMyArc(rawAngle)
162+
updateArc(rawAngle,myArc)
163163
_released.value = false
164-
myFrequencyTracker.addSample(myArc.startAngle)
164+
myFrequencyTracker.addSample(rawAngle)
165165
} else {
166166
_released.value = true
167167
myFrequencyTracker.reset()
@@ -172,15 +172,17 @@ class WineGlassesViewModel : GameViewModel(GameType.WINE_GLASSES) {
172172
// send input to server
173173
viewModelScope.launch(Dispatchers.IO) {
174174
val timestamp = getCurrentGameTime()
175-
val data = if(inBounds) myArc.startAngle.toString() else UNDEFINED_ANGLE.toString()
175+
val angle = if(inBounds) rawAngle else UNDEFINED_ANGLE
176+
val frequency = myFrequencyTracker.frequency
177+
val data = "$angle,$frequency"
176178
model.sendUserInput(timestamp, packetTracker.newPacket(),data)
177179
addEvent(SessionEvent.angle(playerId,timestamp,data))
178180
}
179181
}
180182

181183
fun inSync() = (
182184
!released.value && !opponentReleased.value
183-
&& (myFrequencyTracker.frequency - opponentFrequencyTracker.frequency)
185+
&& (myFrequencyTracker.frequency - opponentFrequency)
184186
.absoluteValue < WINE_GLASSES_SYNC_FREQUENCY_THRESHOLD
185187
).also { inSync = it }
186188

@@ -203,7 +205,7 @@ class WineGlassesViewModel : GameViewModel(GameType.WINE_GLASSES) {
203205
Log.e(TAG, "handleGameAction: missing timestamp in user input action")
204206
return
205207
}
206-
val angle = action.data?.toFloat() ?: run{
208+
val data = action.data?: run{
207209
Log.e(TAG, "handleGameAction: missing data in user input action")
208210
return
209211
}
@@ -214,44 +216,49 @@ class WineGlassesViewModel : GameViewModel(GameType.WINE_GLASSES) {
214216

215217
val arrivedTimestamp = getCurrentGameTime()
216218

217-
if(angle == UNDEFINED_ANGLE){
219+
val outOfOrder = packetTracker.receivedOtherPacket(sequenceNumber)
220+
if(outOfOrder) return
221+
222+
val (rawAngle, frequency) = data.split(",").let{
223+
Pair(it[0].toFloat(), it[1].toFloat())
224+
}
225+
if(rawAngle == UNDEFINED_ANGLE){
218226
_opponentReleased.value = true
219-
opponentFrequencyTracker.reset()
227+
opponentFrequency = 0f
220228
} else {
221229
_opponentReleased.value = false
222-
opponentArc.startAngle = angle
223-
opponentFrequencyTracker.addSample(angle)
230+
opponentFrequency = frequency
231+
updateArc(rawAngle,opponentArc)
224232
}
225233

226-
packetTracker.receivedOtherPacket(sequenceNumber)
227-
addEvent(SessionEvent.opponentAngle(playerId,arrivedTimestamp,angle.toString()))
234+
addEvent(SessionEvent.opponentAngle(playerId,arrivedTimestamp,rawAngle.toString()))
228235
}
229236
else -> super.handleGameAction(action)
230237
}
231238
}
232239

233-
private fun updateMyArc(angle: Float){
240+
private fun updateArc(angle: Float, arc: Arc){
234241

235242
// =========== for current iteration =============== |
236243

237244
// calculate the skew angle to show the arc ahead of the finger
238245
// based on the calculations of the previous iteration
239-
val angleSkew = myArc.angleSkew
240-
myArc.startAngle = addToAngle(angle,
241-
myArc.direction.fastCoerceIn(-DIRECTION_MAX_OFFSET, DIRECTION_MAX_OFFSET) * angleSkew
246+
val angleSkew = arc.angleSkew
247+
arc.startAngle = addToAngle(angle,
248+
arc.direction.fastCoerceIn(-DIRECTION_MAX_OFFSET, DIRECTION_MAX_OFFSET) * angleSkew
242249
- MY_SWEEP_ANGLE / 2
243250
)
244251

245252
// ============== for next iteration =============== |
246253

247254
// prepare the skew angle for the next iteration
248-
val previousAngle = myArc.previousAngle
255+
val previousAngle = arc.previousAngle
249256
var angleDiff = 0f
250257
if(previousAngle != UNDEFINED_ANGLE){
251258
angleDiff = calculateAngleDiff(previousAngle, angle)
252-
val previousAngleDiff = myArc.previousAngleDiff
259+
val previousAngleDiff = arc.previousAngleDiff
253260
val angleDiffDiff = angleDiff - previousAngleDiff
254-
myArc.angleSkew = if (angleDiffDiff > 1 && angleDiff > 3){
261+
arc.angleSkew = if (angleDiffDiff > 1 && angleDiff > 3){
255262
(angleSkew + angleDiff * 0.75f).fastCoerceAtMost(MAX_ANGLE_SKEW)
256263
} else if (angleDiffDiff < 1){
257264
(angleSkew - angleDiff * 0.375f).fastCoerceAtLeast(MIN_ANGLE_SKEW)
@@ -264,22 +271,22 @@ class WineGlassesViewModel : GameViewModel(GameType.WINE_GLASSES) {
264271
// we add a bit to the max offset to prevent random jitter in the direction
265272
// we clamp the direction to the max offset when calculating the skewed angle
266273
if (previousAngle != UNDEFINED_ANGLE){
267-
val direction = myArc.direction
268-
myArc.direction = if(isClockwise(previousAngle, angle)){
274+
val direction = arc.direction
275+
arc.direction = if(isClockwise(previousAngle, angle)){
269276
(direction + angleDiff * 0.2f).fastCoerceAtMost(DIRECTION_MAX_OFFSET + 0.5f)
270277
} else if (! isClockwise(previousAngle, angle)){
271278
(direction - angleDiff * 0.2f).fastCoerceAtLeast(-(DIRECTION_MAX_OFFSET + 0.5f))
272279
} else {
273280
direction
274281
}
275-
if(myArc.direction.isBetweenInclusive(-WINE_GLASSES_SYNC_FREQUENCY_THRESHOLD,
282+
if(arc.direction.isBetweenInclusive(-WINE_GLASSES_SYNC_FREQUENCY_THRESHOLD,
276283
WINE_GLASSES_SYNC_FREQUENCY_THRESHOLD
277-
)) myArc.angleSkew = MIN_ANGLE_SKEW
284+
)) arc.angleSkew = MIN_ANGLE_SKEW
278285
}
279286

280287
// current angle becomes previous angle for the next iteration
281-
myArc.previousAngle = angle
282-
myArc.previousAngleDiff = angleDiff
288+
arc.previousAngle = angle
289+
arc.previousAngleDiff = angleDiff
283290
}
284291

285292
private fun setupWavPlayer(context: Context){

0 commit comments

Comments
 (0)