Compare commits

...

10 Commits

Author SHA1 Message Date
genxium
da1204dc63 Minor update. 2023-03-10 16:20:40 +08:00
genxium
ea14ced958 Improved stability. 2023-03-10 15:40:45 +08:00
genxium
b9beee549f Simplified resolv_tailored. 2023-03-02 10:22:27 +08:00
genxium
04d8013cbb Preparing for go2cs transpiling. 2023-03-01 18:20:54 +08:00
genxium
6b503ec95d Updated README. 2023-03-01 10:53:22 +08:00
genxium
71f2a1ecdf Updated documentation. 2023-03-01 07:03:49 +08:00
genxium
de9f3c9090 Minor update. 2023-02-27 14:58:21 +08:00
genxium
96e355eab3 Fixed frontend rollback upon UpdateOnDynamics. 2023-02-27 14:35:19 +08:00
genxium
16e1d8a913 Minor fix. 2023-02-27 12:02:01 +08:00
genxium
04b033be7e Fixed frame data logging. 2023-02-27 11:09:22 +08:00
19 changed files with 473 additions and 590 deletions

View File

@@ -10,6 +10,10 @@ This project is a demo for a websocket-based rollback netcode inspired by [GGPO]
![Phone4g_battle_spedup](./charts/Phone4g_battle_spedup.gif) ![Phone4g_battle_spedup](./charts/Phone4g_battle_spedup.gif)
**Since v1.0.13, smoothness in worst cases (e.g. turn-around on ground, in air and after dashing) is drastically improved due to update of prediction approach. The gifs and corresponding screenrecordings above are not updated because there's no big difference when network is good -- however, `input delay` is now set to `4 frames` -- while `input delay = 6 frames` was used in the screenrecordings -- and smoothness is even better now (well there's [a new screenrecording for PcWifi vs Android4g here](https://pan.baidu.com/s/1iNrQ2l_wqbWkURMIfyG88w?pwd=fe2f)).** Key changes are listed below.
- [change#1](https://github.com/genxium/DelayNoMore/blob/ea14ced958415dbf4ec1bef89088a3c7607fd8b3/jsexport/battle/battle.go#L647)
- [change#2](https://github.com/genxium/DelayNoMore/blob/ea14ced958415dbf4ec1bef89088a3c7607fd8b3/frontend/assets/scripts/Map.js#L1446)
As lots of feedbacks ask for a discussion on using UDP instead, I tried to summarize my personal opinion about it in [ConcerningEdgeCases](./ConcerningEdgeCases.md) -- **since v0.9.25, the project is actually equipped with UDP capabilities as follows**. As lots of feedbacks ask for a discussion on using UDP instead, I tried to summarize my personal opinion about it in [ConcerningEdgeCases](./ConcerningEdgeCases.md) -- **since v0.9.25, the project is actually equipped with UDP capabilities as follows**.
- When using the so called `native apps` on `Android` and `Windows` (I'm working casually hard to support `iOS` next), the frontends will try to use UDP hole-punching w/ the help of backend as a registry. If UDP hole-punching is working, the rollback is often less than `turn-around frames to recover` and thus not noticeable, being much better than using websocket alone. This video shows how the UDP holepunched p2p performs for [Phone-Wifi v.s. PC-Wifi (viewed by PC side)](https://pan.baidu.com/s/1K6704bJKlrSBTVqGcXhajA?pwd=l7ok). - When using the so called `native apps` on `Android` and `Windows` (I'm working casually hard to support `iOS` next), the frontends will try to use UDP hole-punching w/ the help of backend as a registry. If UDP hole-punching is working, the rollback is often less than `turn-around frames to recover` and thus not noticeable, being much better than using websocket alone. This video shows how the UDP holepunched p2p performs for [Phone-Wifi v.s. PC-Wifi (viewed by PC side)](https://pan.baidu.com/s/1K6704bJKlrSBTVqGcXhajA?pwd=l7ok).
- If UDP hole-punching is not working, e.g. for Symmetric NAT like in 4G/5G cellular network, the frontends will use backend as a UDP tunnel (or relay, whatever you like to call it). This video shows how the UDP tunnel performs for [Phone-4G v.s. PC-Wifi (merged view@v0.9.34, excellent synchronization)](https://pan.baidu.com/s/1yeIrN5TSf6_av_8-N3vdVg?pwd=7tzw). - If UDP hole-punching is not working, e.g. for Symmetric NAT like in 4G/5G cellular network, the frontends will use backend as a UDP tunnel (or relay, whatever you like to call it). This video shows how the UDP tunnel performs for [Phone-4G v.s. PC-Wifi (merged view@v0.9.34, excellent synchronization)](https://pan.baidu.com/s/1yeIrN5TSf6_av_8-N3vdVg?pwd=7tzw).

View File

@@ -156,10 +156,9 @@ type Room struct {
TmxPointsMap StrToVec2DListMap TmxPointsMap StrToVec2DListMap
TmxPolygonsMap StrToPolygon2DListMap TmxPolygonsMap StrToPolygon2DListMap
rdfIdToActuallyUsedInput map[int32]*pb.InputFrameDownsync rdfIdToActuallyUsedInput map[int32]*pb.InputFrameDownsync
allowUpdateInputFrameInPlaceUponDynamics bool LastIndividuallyConfirmedInputFrameId []int32
LastIndividuallyConfirmedInputFrameId []int32 LastIndividuallyConfirmedInputList []uint64
LastIndividuallyConfirmedInputList []uint64
BattleUdpTunnelLock sync.Mutex BattleUdpTunnelLock sync.Mutex
BattleUdpTunnelAddr *pb.PeerUdpAddr BattleUdpTunnelAddr *pb.PeerUdpAddr
@@ -407,6 +406,9 @@ func (pR *Room) rdfIdToActuallyUsedInputString() string {
} }
fireballsStrBldr := make([]string, 0, len(rdf.FireballBullets)) fireballsStrBldr := make([]string, 0, len(rdf.FireballBullets))
for _, fireball := range rdf.FireballBullets { for _, fireball := range rdf.FireballBullets {
if 0 > fireball.BattleAttr.BulletLocalId {
break
}
fireballsStrBldr = append(fireballsStrBldr, pR.fireballDownsyncStr(fireball)) fireballsStrBldr = append(fireballsStrBldr, pR.fireballDownsyncStr(fireball))
} }
s = append(s, fmt.Sprintf("rdfId:%d\nplayers:[%v]\nfireballs:[%v]\nactuallyUsedinputList:{%v}", rdfId, strings.Join(playersStrBldr, ","), strings.Join(fireballsStrBldr, ","), pR.inputFrameDownsyncStr(pR.rdfIdToActuallyUsedInput[rdfId]))) s = append(s, fmt.Sprintf("rdfId:%d\nplayers:[%v]\nfireballs:[%v]\nactuallyUsedinputList:{%v}", rdfId, strings.Join(playersStrBldr, ","), strings.Join(fireballsStrBldr, ","), pR.inputFrameDownsyncStr(pR.rdfIdToActuallyUsedInput[rdfId])))
@@ -805,7 +807,6 @@ func (pR *Room) OnDismissed() {
pR.RenderFrameBuffer = resolv.NewRingBuffer(pR.RenderCacheSize) pR.RenderFrameBuffer = resolv.NewRingBuffer(pR.RenderCacheSize)
pR.InputsBuffer = resolv.NewRingBuffer((pR.RenderCacheSize >> 1) + 1) pR.InputsBuffer = resolv.NewRingBuffer((pR.RenderCacheSize >> 1) + 1)
pR.rdfIdToActuallyUsedInput = make(map[int32]*pb.InputFrameDownsync) pR.rdfIdToActuallyUsedInput = make(map[int32]*pb.InputFrameDownsync)
pR.allowUpdateInputFrameInPlaceUponDynamics = true
pR.LastIndividuallyConfirmedInputFrameId = make([]int32, pR.Capacity) pR.LastIndividuallyConfirmedInputFrameId = make([]int32, pR.Capacity)
for i := 0; i < pR.Capacity; i++ { for i := 0; i < pR.Capacity; i++ {
pR.LastIndividuallyConfirmedInputFrameId[i] = MAGIC_LAST_SENT_INPUT_FRAME_ID_NORMAL_ADDED pR.LastIndividuallyConfirmedInputFrameId[i] = MAGIC_LAST_SENT_INPUT_FRAME_ID_NORMAL_ADDED
@@ -1198,7 +1199,8 @@ func (pR *Room) markConfirmationIfApplicable(inputFrameUpsyncBatch []*pb.InputFr
} }
if clientInputFrameId < player.LastConsecutiveRecvInputFrameId { if clientInputFrameId < player.LastConsecutiveRecvInputFrameId {
// [WARNING] It's important for correctness that we use "player.LastConsecutiveRecvInputFrameId" instead of "pR.LastIndividuallyConfirmedInputFrameId[player.JoinIndex-1]" here! // [WARNING] It's important for correctness that we use "player.LastConsecutiveRecvInputFrameId" instead of "pR.LastIndividuallyConfirmedInputFrameId[player.JoinIndex-1]" here!
Logger.Debug(fmt.Sprintf("Omitting obsolete inputFrameUpsync#2: roomId=%v, playerId=%v, clientInputFrameId=%v, playerLastConsecutiveRecvInputFrameId=%v, InputsBuffer=%v", pR.Id, playerId, clientInputFrameId, player.LastConsecutiveRecvInputFrameId, pR.InputsBufferString(false))) //Logger.Debug(fmt.Sprintf("Omitting obsolete inputFrameUpsync#2: roomId=%v, playerId=%v, clientInputFrameId=%v, playerLastConsecutiveRecvInputFrameId=%v, InputsBuffer=%v", pR.Id, playerId, clientInputFrameId, player.LastConsecutiveRecvInputFrameId, pR.InputsBufferString(false)))
Logger.Debug(fmt.Sprintf("Omitting obsolete inputFrameUpsync#2: roomId=%v, playerId=%v, clientInputFrameId=%v, playerLastConsecutiveRecvInputFrameId=%v", pR.Id, playerId, clientInputFrameId, player.LastConsecutiveRecvInputFrameId))
continue continue
} }
if clientInputFrameId > pR.InputsBuffer.EdFrameId { if clientInputFrameId > pR.InputsBuffer.EdFrameId {
@@ -1256,7 +1258,7 @@ func (pR *Room) markConfirmationIfApplicable(inputFrameUpsyncBatch []*pb.InputFr
shouldBreakConfirmation = true // Could be an `ACTIVE SLOW TICKER` here, but no action needed for now shouldBreakConfirmation = true // Could be an `ACTIVE SLOW TICKER` here, but no action needed for now
break break
} }
Logger.Debug(fmt.Sprintf("markConfirmationIfApplicable for roomId=%v, skipping UNCONFIRMED BUT INACTIVE player(id:%v, joinIndex:%v) while checking inputFrameId=[%v, %v): InputsBuffer=%v", pR.Id, player.Id, player.JoinIndex, inputFrameId1, pR.InputsBuffer.EdFrameId, pR.InputsBufferString(false))) //Logger.Debug(fmt.Sprintf("markConfirmationIfApplicable for roomId=%v, skipping UNCONFIRMED BUT INACTIVE player(id:%v, joinIndex:%v) while checking inputFrameId=[%v, %v): InputsBuffer=%v", pR.Id, player.Id, player.JoinIndex, inputFrameId1, pR.InputsBuffer.EdFrameId, pR.InputsBufferString(false)))
} }
} }
@@ -1305,15 +1307,12 @@ func (pR *Room) forceConfirmationIfApplicable(prevRenderFrameId int32) uint64 {
panic(fmt.Sprintf("inputFrameId=%v doesn't exist for roomId=%v! InputsBuffer=%v", j, pR.Id, pR.InputsBufferString(false))) panic(fmt.Sprintf("inputFrameId=%v doesn't exist for roomId=%v! InputsBuffer=%v", j, pR.Id, pR.InputsBufferString(false)))
} }
inputFrameDownsync := tmp.(*battle.InputFrameDownsync) inputFrameDownsync := tmp.(*battle.InputFrameDownsync)
if pR.allowUpdateInputFrameInPlaceUponDynamics {
battle.UpdateInputFrameInPlaceUponDynamics(j, pR.Capacity, inputFrameDownsync.ConfirmedList, inputFrameDownsync.InputList, pR.LastIndividuallyConfirmedInputFrameId, pR.LastIndividuallyConfirmedInputList, int32(MAGIC_JOIN_INDEX_INVALID))
}
unconfirmedMask |= (allConfirmedMask ^ inputFrameDownsync.ConfirmedList) unconfirmedMask |= (allConfirmedMask ^ inputFrameDownsync.ConfirmedList)
inputFrameDownsync.ConfirmedList = allConfirmedMask inputFrameDownsync.ConfirmedList = allConfirmedMask
pR.onInputFrameDownsyncAllConfirmed(inputFrameDownsync, -1) pR.onInputFrameDownsyncAllConfirmed(inputFrameDownsync, -1)
} }
if 0 < unconfirmedMask { if 0 < unconfirmedMask {
Logger.Info(fmt.Sprintf("[type#1 forceConfirmation] For roomId=%d@renderFrameId=%d, curDynamicsRenderFrameId=%d, LatestPlayerUpsyncedInputFrameId:%d, oldLastAllConfirmedInputFrameId:%d, newLastAllConfirmedInputFrameId:%d, InputFrameUpsyncDelayTolerance:%d, unconfirmedMask=%d; there's a slow ticker suspect, forcing all-confirmation", pR.Id, pR.RenderFrameId, pR.CurDynamicsRenderFrameId, pR.LatestPlayerUpsyncedInputFrameId, oldLastAllConfirmedInputFrameId, pR.LastAllConfirmedInputFrameId, pR.InputFrameUpsyncDelayTolerance, unconfirmedMask)) Logger.Info(fmt.Sprintf("[type#1 forceConfirmation] For roomId=%d@renderFrameId=%d, curDynamicsRenderFrameId=%d, LatestPlayerUpsyncedInputFrameId:%d, LastAllConfirmedInputFrameId:%d -> %d, InputFrameUpsyncDelayTolerance:%d, unconfirmedMask=%d; there's a slow ticker suspect, forcing all-confirmation", pR.Id, pR.RenderFrameId, pR.CurDynamicsRenderFrameId, pR.LatestPlayerUpsyncedInputFrameId, oldLastAllConfirmedInputFrameId, pR.LastAllConfirmedInputFrameId, pR.InputFrameUpsyncDelayTolerance, unconfirmedMask))
} }
} else { } else {
// Type#2 helps resolve the edge case when all players are disconnected temporarily // Type#2 helps resolve the edge case when all players are disconnected temporarily
@@ -1388,7 +1387,7 @@ func (pR *Room) applyInputFrameDownsyncDynamics(fromRenderFrameId int32, toRende
} }
} }
battle.ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(pR.InputsBuffer, currRenderFrame.Id, pR.Space, pR.CollisionSysMap, pR.SpaceOffsetX, pR.SpaceOffsetY, pR.CharacterConfigsArr, pR.RenderFrameBuffer, pR.collisionHolder, pR.effPushbacks, pR.hardPushbackNormsArr, pR.jumpedOrNotList, pR.dynamicRectangleColliders, pR.LastIndividuallyConfirmedInputFrameId, pR.LastIndividuallyConfirmedInputList, false, MAGIC_JOIN_INDEX_INVALID) // "allowUpdateInputFrameInPlaceUponDynamics" is instead used when "forceConfirmationIfApplicable" battle.ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(pR.InputsBuffer, currRenderFrame.Id, pR.Space, pR.CollisionSysMap, pR.SpaceOffsetX, pR.SpaceOffsetY, pR.CharacterConfigsArr, pR.RenderFrameBuffer, pR.collisionHolder, pR.effPushbacks, pR.hardPushbackNormsArr, pR.jumpedOrNotList, pR.dynamicRectangleColliders, pR.LastIndividuallyConfirmedInputFrameId, pR.LastIndividuallyConfirmedInputList, false, MAGIC_JOIN_INDEX_INVALID) // DON'T mutate inputs upon dynamics on backend to avoid complicating the edge cases
pR.CurDynamicsRenderFrameId++ pR.CurDynamicsRenderFrameId++
} }
} }
@@ -1539,6 +1538,7 @@ func (pR *Room) downsyncToAllPlayers(inputsBufferSnapshot *pb.InputsBufferSnapsh
break break
} }
} }
} }
for _, player := range pR.PlayersArr { for _, player := range pR.PlayersArr {
@@ -1555,11 +1555,16 @@ func (pR *Room) downsyncToAllPlayers(inputsBufferSnapshot *pb.InputsBufferSnapsh
if playerDownsyncChan, existent := pR.PlayerDownsyncChanDict[player.Id]; existent { if playerDownsyncChan, existent := pR.PlayerDownsyncChanDict[player.Id]; existent {
playerDownsyncChan <- (*inputsBufferSnapshot) playerDownsyncChan <- (*inputsBufferSnapshot)
//Logger.Info(fmt.Sprintf("Sent inputsBufferSnapshot(refRenderFrameId:%d, unconfirmedMask:%v) to for (roomId: %d, playerId:%d, playerDownsyncChan:%p)#1", inputsBufferSnapshot.RefRenderFrameId, inputsBufferSnapshot.UnconfirmedMask, pR.Id, player.Id, playerDownsyncChan)) //Logger.Info(fmt.Sprintf("Sent inputsBufferSnapshot{refRenderFrameId:%d, unconfirmedMask:%v} to {roomId: %d, playerId:%d, playerDownsyncChan:%p}#1", inputsBufferSnapshot.RefRenderFrameId, inputsBufferSnapshot.UnconfirmedMask, pR.Id, player.Id, playerDownsyncChan))
} else { } else {
Logger.Warn(fmt.Sprintf("playerDownsyncChan for (roomId: %d, playerId:%d) is gone", pR.Id, player.Id)) Logger.Warn(fmt.Sprintf("playerDownsyncChan for (roomId: %d, playerId:%d) is gone", pR.Id, player.Id))
} }
} }
/*
toSendInputFrameDownsyncs := inputsBufferSnapshot.ToSendInputFrameDownsyncs
toSendInputFrameIdSt, toSendInputFrameIdEd := toSendInputFrameDownsyncs[0].InputFrameId, toSendInputFrameDownsyncs[len(toSendInputFrameDownsyncs)-1].InputFrameId+1
Logger.Info(fmt.Sprintf("Sent inputsBufferSnapshot{refRenderFrameId:%d, unconfirmedMask:%v, inputFrameIdRange:[%d, %d)} to {roomId: %d}", inputsBufferSnapshot.RefRenderFrameId, inputsBufferSnapshot.UnconfirmedMask, toSendInputFrameIdSt, toSendInputFrameIdEd, pR.Id))
*/
} }
func (pR *Room) downsyncToSinglePlayer(playerId int32, player *Player, refRenderFrameId int32, unconfirmedMask uint64, toSendInputFrameDownsyncsSnapshot []*pb.InputFrameDownsync, shouldForceResync bool) { func (pR *Room) downsyncToSinglePlayer(playerId int32, player *Player, refRenderFrameId int32, unconfirmedMask uint64, toSendInputFrameDownsyncsSnapshot []*pb.InputFrameDownsync, shouldForceResync bool) {
@@ -1602,9 +1607,10 @@ func (pR *Room) downsyncToSinglePlayer(playerId int32, player *Player, refRender
pbRefRenderFrame := toPbRoomDownsyncFrame(refRenderFrame) pbRefRenderFrame := toPbRoomDownsyncFrame(refRenderFrame)
pbRefRenderFrame.SpeciesIdList = pR.SpeciesIdList pbRefRenderFrame.SpeciesIdList = pR.SpeciesIdList
pR.sendSafely(pbRefRenderFrame, toSendInputFrameDownsyncsSnapshot, DOWNSYNC_MSG_ACT_FORCED_RESYNC, playerId, false, MAGIC_JOIN_INDEX_DEFAULT) pR.sendSafely(pbRefRenderFrame, toSendInputFrameDownsyncsSnapshot, DOWNSYNC_MSG_ACT_FORCED_RESYNC, playerId, false, MAGIC_JOIN_INDEX_DEFAULT)
//Logger.Warn(fmt.Sprintf("Sent refRenderFrameId=%v & inputFrameIds [%d, %d), for roomId=%v, playerId=%d, playerJoinIndex=%d, renderFrameId=%d, curDynamicsRenderFrameId=%d, playerLastSentInputFrameId=%d: InputsBuffer=%v", refRenderFrameId, toSendInputFrameIdSt, toSendInputFrameIdEd, pR.Id, playerId, player.JoinIndex, pR.RenderFrameId, pR.CurDynamicsRenderFrameId, player.LastSentInputFrameId, pR.InputsBufferString(false)))
if shouldResync1 || shouldResync3 { if shouldResync1 || shouldResync3 {
Logger.Debug(fmt.Sprintf("Sent refRenderFrameId=%v & inputFrameIds [%d, %d), for roomId=%v, playerId=%d, playerJoinIndex=%d, renderFrameId=%d, curDynamicsRenderFrameId=%d, playerLastSentInputFrameId=%d: shouldResync1=%v, shouldResync2=%v, shouldResync3=%v, playerBattleState=%d", refRenderFrameId, toSendInputFrameIdSt, toSendInputFrameIdEd, pR.Id, playerId, player.JoinIndex, pR.RenderFrameId, pR.CurDynamicsRenderFrameId, player.LastSentInputFrameId, shouldResync1, shouldResync2, shouldResync3, playerBattleState)) Logger.Debug(fmt.Sprintf("Sent refRenderFrameId=%v & inputFrameIds [%d, %d), for roomId=%v, playerId=%d, playerJoinIndex=%d, renderFrameId=%d, curDynamicsRenderFrameId=%d, playerLastSentInputFrameId=%d: shouldResync1=%v, shouldResync2=%v, shouldResync3=%v, playerBattleState=%d", refRenderFrameId, toSendInputFrameIdSt, toSendInputFrameIdEd, pR.Id, playerId, player.JoinIndex, pR.RenderFrameId, pR.CurDynamicsRenderFrameId, player.LastSentInputFrameId, shouldResync1, shouldResync2, shouldResync3, playerBattleState))
} else {
//Logger.Debug(fmt.Sprintf("Sent refRenderFrameId=%v & inputFrameIds [%d, %d), for roomId=%v, playerId=%d, playerJoinIndex=%d, renderFrameId=%d, curDynamicsRenderFrameId=%d, playerLastSentInputFrameId=%d: InputsBuffer=%v", refRenderFrameId, toSendInputFrameIdSt, toSendInputFrameIdEd, pR.Id, playerId, player.JoinIndex, pR.RenderFrameId, pR.CurDynamicsRenderFrameId, player.LastSentInputFrameId, pR.InputsBufferString(false)))
} }
} else { } else {
pR.sendSafely(nil, toSendInputFrameDownsyncsSnapshot, DOWNSYNC_MSG_ACT_INPUT_BATCH, playerId, false, MAGIC_JOIN_INDEX_DEFAULT) pR.sendSafely(nil, toSendInputFrameDownsyncsSnapshot, DOWNSYNC_MSG_ACT_INPUT_BATCH, playerId, false, MAGIC_JOIN_INDEX_DEFAULT)

View File

@@ -518,7 +518,7 @@
"array": [ "array": [
0, 0,
0, 0,
217.5376534000385, 210.4441731196186,
0, 0,
0, 0,
0, 0,

View File

@@ -713,7 +713,12 @@ cc.Class({
if (notSelfUnconfirmed) { if (notSelfUnconfirmed) {
shouldForceDumping2 = false; shouldForceDumping2 = false;
shouldForceResync = false; shouldForceResync = false;
self.othersForcedDownsyncRenderFrameDict.set(rdfId, rdf); self.othersForcedDownsyncRenderFrameDict.set(rdfId, [pbRdf, rdf]);
if (CC_DEBUG) {
console.warn(`Someone else is forced to resync! renderFrameId=${rdfId}
backendUnconfirmedMask=${pbRdf.backendUnconfirmedMask}
accompaniedInputFrameDownsyncBatchRange=[${null == accompaniedInputFrameDownsyncBatch ? null : accompaniedInputFrameDownsyncBatch[0].inputFrameId}, ${null == accompaniedInputFrameDownsyncBatch ? null : accompaniedInputFrameDownsyncBatch[accompaniedInputFrameDownsyncBatch.length - 1].inputFrameId}]`);
}
} }
/* /*
TODO TODO
@@ -744,10 +749,15 @@ cc.Class({
// In fact, not having "window.RING_BUFF_CONSECUTIVE_SET == dumpRenderCacheRet" should already imply that "self.renderFrameId <= rdfId", but here we double check and log the anomaly // In fact, not having "window.RING_BUFF_CONSECUTIVE_SET == dumpRenderCacheRet" should already imply that "self.renderFrameId <= rdfId", but here we double check and log the anomaly
if (window.MAGIC_ROOM_DOWNSYNC_FRAME_ID.BATTLE_START == rdfId) { if (window.MAGIC_ROOM_DOWNSYNC_FRAME_ID.BATTLE_START == rdfId) {
console.log('On battle started! renderFrameId=', rdfId); console.log(`On battle started! renderFrameId=${rdfId}`);
} else { } else {
self.hideFindingPlayersGUI(); self.hideFindingPlayersGUI();
console.warn('On battle resynced! renderFrameId=', rdf.GetId()); if (CC_DEBUG) {
console.warn(`On battle resynced! renderFrameId=${rdf.GetId()}
accompaniedInputFrameDownsyncBatchRange=[${accompaniedInputFrameDownsyncBatch[0].inputFrameId}, ${accompaniedInputFrameDownsyncBatch[accompaniedInputFrameDownsyncBatch.length - 1].inputFrameId}]`);
} else {
console.warn(`On battle resynced! renderFrameId=${rdf.GetId()}`);
}
} }
self.renderFrameId = rdfId; self.renderFrameId = rdfId;
@@ -872,12 +882,12 @@ cc.Class({
while (self.recentInputCache.GetStFrameId() <= candidateInputFrameId && candidateInputFrameId < self.recentInputCache.GetEdFrameId()) { while (self.recentInputCache.GetStFrameId() <= candidateInputFrameId && candidateInputFrameId < self.recentInputCache.GetEdFrameId()) {
const inputFrameDownsync = gopkgs.GetInputFrameDownsync(self.recentInputCache, candidateInputFrameId); const inputFrameDownsync = gopkgs.GetInputFrameDownsync(self.recentInputCache, candidateInputFrameId);
if (null == inputFrameDownsync) break; if (null == inputFrameDownsync) break;
if (self._allConfirmed(inputFrameDownsync.GetConfirmedList())) break; if (false == self._allConfirmed(inputFrameDownsync.GetConfirmedList())) break;
++candidateInputFrameId; ++candidateInputFrameId;
++newAllConfirmedCnt; ++newAllConfirmedCnt;
} }
if (0 < newAllConfirmedCnt) { if (0 < newAllConfirmedCnt) {
self.lastAllConfirmedInputFrameId = candidateInputFrameId; self.lastAllConfirmedInputFrameId = candidateInputFrameId - 1;
} }
return newAllConfirmedCnt; return newAllConfirmedCnt;
}, },
@@ -904,6 +914,7 @@ cc.Class({
continue; continue;
} }
// [WARNING] Now that "inputFrameDownsyncId > self.lastAllConfirmedInputFrameId", we should make an update immediately because unlike its backend counterpart "Room.LastAllConfirmedInputFrameId", the frontend "mapIns.lastAllConfirmedInputFrameId" might inevitably get gaps among discrete values due to "either type#1 or type#2 forceConfirmation" -- and only "onInputFrameDownsyncBatch" can catch this! // [WARNING] Now that "inputFrameDownsyncId > self.lastAllConfirmedInputFrameId", we should make an update immediately because unlike its backend counterpart "Room.LastAllConfirmedInputFrameId", the frontend "mapIns.lastAllConfirmedInputFrameId" might inevitably get gaps among discrete values due to "either type#1 or type#2 forceConfirmation" -- and only "onInputFrameDownsyncBatch" can catch this!
self.lastAllConfirmedInputFrameId = inputFrameDownsyncId;
const localInputFrame = gopkgs.GetInputFrameDownsync(self.recentInputCache, inputFrameDownsyncId); const localInputFrame = gopkgs.GetInputFrameDownsync(self.recentInputCache, inputFrameDownsyncId);
if (null != localInputFrame if (null != localInputFrame
&& &&
@@ -936,7 +947,7 @@ cc.Class({
_handleIncorrectlyRenderedPrediction(firstPredictedYetIncorrectInputFrameId, batch, fromUDP) { _handleIncorrectlyRenderedPrediction(firstPredictedYetIncorrectInputFrameId, batch, fromUDP) {
if (null == firstPredictedYetIncorrectInputFrameId) return; if (null == firstPredictedYetIncorrectInputFrameId) return;
const self = this; const self = this;
const renderFrameId1 = gopkgs.ConvertToFirstUsedRenderFrameId(firstPredictedYetIncorrectInputFrameId) - 1; const renderFrameId1 = gopkgs.ConvertToFirstUsedRenderFrameId(firstPredictedYetIncorrectInputFrameId);
if (renderFrameId1 >= self.chaserRenderFrameId) return; if (renderFrameId1 >= self.chaserRenderFrameId) return;
/* /*
@@ -957,7 +968,7 @@ cc.Class({
firstPredictedYetIncorrectInputFrameId: ${firstPredictedYetIncorrectInputFrameId} firstPredictedYetIncorrectInputFrameId: ${firstPredictedYetIncorrectInputFrameId}
lastAllConfirmedInputFrameId=${self.lastAllConfirmedInputFrameId} lastAllConfirmedInputFrameId=${self.lastAllConfirmedInputFrameId}
recentInputCache=${self._stringifyRecentInputCache(false)} recentInputCache=${self._stringifyRecentInputCache(false)}
batchInputFrameIdRange=[${batch[0].inputFrameId}, ${batch[batch.length - 1].inputFrameId}] batchInputFrameIdRange=[${null == batch ? null : batch[0].inputFrameId}, ${null == batch ? null : batch[batch.length - 1].inputFrameId}]
fromUDP=${fromUDP}`); fromUDP=${fromUDP}`);
} }
self.chaserRenderFrameId = renderFrameId1; self.chaserRenderFrameId = renderFrameId1;
@@ -1000,8 +1011,9 @@ fromUDP=${fromUDP}`);
} }
const peerJoinIndexMask = (1 << (peerJoinIndex - 1)); const peerJoinIndexMask = (1 << (peerJoinIndex - 1));
self.getOrPrefabInputFrameUpsync(inputFrameId, false); // Make sure that inputFrame exists locally self.getOrPrefabInputFrameUpsync(inputFrameId, false); // Make sure that inputFrame exists locally
const existingInputFrame = self.recentInputCache.GetByFrameId(inputFrameId); const existingInputFrame = gopkgs.GetInputFrameDownsync(self.recentInputCache, inputFrameId);
if (0 < (existingInputFrame.ConfirmedList & peerJoinIndexMask)) { const existingConfirmedList = existingInputFrame.GetConfirmedList();
if (0 < (existingConfirmedList & peerJoinIndexMask)) {
continue; continue;
} }
if (inputFrameId > self.lastIndividuallyConfirmedInputFrameId[peerJoinIndex - 1]) { if (inputFrameId > self.lastIndividuallyConfirmedInputFrameId[peerJoinIndex - 1]) {
@@ -1010,9 +1022,10 @@ fromUDP=${fromUDP}`);
} }
effCnt += 1; effCnt += 1;
// the returned "gopkgs.NewInputFrameDownsync.InputList" is immutable, thus we can only modify the values in "newInputList" and "newConfirmedList"! // the returned "gopkgs.NewInputFrameDownsync.InputList" is immutable, thus we can only modify the values in "newInputList" and "newConfirmedList"!
let newInputList = existingInputFrame.InputList.slice(); const existingInputList = existingInputFrame.GetInputList();
let newInputList = existingInputFrame.GetInputList().slice();
newInputList[peerJoinIndex - 1] = peerEncodedInput; newInputList[peerJoinIndex - 1] = peerEncodedInput;
let newConfirmedList = (existingInputFrame.ConfirmedList | peerJoinIndexMask); let newConfirmedList = (existingConfirmedList | peerJoinIndexMask);
const newInputFrameDownsyncLocal = gopkgs.NewInputFrameDownsync(inputFrameId, newInputList, newConfirmedList); const newInputFrameDownsyncLocal = gopkgs.NewInputFrameDownsync(inputFrameId, newInputList, newConfirmedList);
//console.log(`Updated encoded input of peerJoinIndex=${peerJoinIndex} to ${peerEncodedInput} for inputFrameId=${inputFrameId}/renderedInputFrameIdUpper=${renderedInputFrameIdUpper} from ${JSON.stringify(inputFrame)}; newInputFrameDownsyncLocal=${self.gopkgsInputFrameDownsyncStr(newInputFrameDownsyncLocal)}; existingInputFrame=${self.gopkgsInputFrameDownsyncStr(existingInputFrame)}`); //console.log(`Updated encoded input of peerJoinIndex=${peerJoinIndex} to ${peerEncodedInput} for inputFrameId=${inputFrameId}/renderedInputFrameIdUpper=${renderedInputFrameIdUpper} from ${JSON.stringify(inputFrame)}; newInputFrameDownsyncLocal=${self.gopkgsInputFrameDownsyncStr(newInputFrameDownsyncLocal)}; existingInputFrame=${self.gopkgsInputFrameDownsyncStr(existingInputFrame)}`);
self.recentInputCache.SetByFrameId(newInputFrameDownsyncLocal, inputFrameId); self.recentInputCache.SetByFrameId(newInputFrameDownsyncLocal, inputFrameId);
@@ -1023,7 +1036,7 @@ fromUDP=${fromUDP}`);
if ( if (
null == firstPredictedYetIncorrectInputFrameId null == firstPredictedYetIncorrectInputFrameId
&& &&
existingInputFrame.InputList[peerJoinIndex - 1] != peerEncodedInput existingInputList[peerJoinIndex - 1] != peerEncodedInput
) { ) {
firstPredictedYetIncorrectInputFrameId = inputFrameId; firstPredictedYetIncorrectInputFrameId = inputFrameId;
} }
@@ -1151,23 +1164,25 @@ fromUDP=${fromUDP}`);
rollbackFrames = 0; rollbackFrames = 0;
} }
self.networkDoctor.logRollbackFrames(rollbackFrames); self.networkDoctor.logRollbackFrames(rollbackFrames);
let prevRdf = latestRdfResults[0], let prevRdf = latestRdfResults[0], // Having "prevRdf.Id == self.renderFrameId"
rdf = latestRdfResults[1]; rdf = latestRdfResults[1]; // Having "rdf.Id == self.renderFrameId+1"
/* /*
const nonTrivialChaseEnded = (prevChaserRenderFrameId < nextChaserRenderFrameId && nextChaserRenderFrameId == self.renderFrameId); const nonTrivialChaseEnded = (prevChaserRenderFrameId < nextChaserRenderFrameId && nextChaserRenderFrameId == self.renderFrameId);
if (nonTrivialChaseEnded) { if (nonTrivialChaseEnded) {
console.debug("Non-trivial chase ended, prevChaserRenderFrameId=" + prevChaserRenderFrameId + ", nextChaserRenderFrameId=" + nextChaserRenderFrameId); console.debug("Non-trivial chase ended, prevChaserRenderFrameId=" + prevChaserRenderFrameId + ", nextChaserRenderFrameId=" + nextChaserRenderFrameId);
} }
*/ */
// [WARNING] Don't try to get "prevRdf(i.e. renderFrameId == latest-1)" by "self.recentRenderCache.GetByFrameId(...)" here, as the cache might have been updated by asynchronous "onRoomDownsyncFrame(...)" calls!
if (self.othersForcedDownsyncRenderFrameDict.has(rdf.GetId())) { if (self.othersForcedDownsyncRenderFrameDict.has(rdf.GetId())) {
const delayedInputFrameId = gopkgs.ConvertToDelayedInputFrameId(rdf.GetId()); const [pbOthersForcedDownsyncRenderFrame, othersForcedDownsyncRenderFrame] = self.othersForcedDownsyncRenderFrameDict.get(rdf.GetId());
const othersForcedDownsyncRenderFrame = self.othersForcedDownsyncRenderFrameDict.get(rdf.GetId());
if (self.lastAllConfirmedInputFrameId >= delayedInputFrameId && !self.equalRoomDownsyncFrames(othersForcedDownsyncRenderFrame, rdf)) { if (self.lastAllConfirmedInputFrameId >= delayedInputFrameId && !self.equalRoomDownsyncFrames(othersForcedDownsyncRenderFrame, rdf)) {
console.warn(`Mismatched render frame@rdf.id=${rdf.GetId()} w/ inputFrameId=${delayedInputFrameId}: if (CC_DEBUG) {
rdf=${JSON.stringify(rdf)} console.warn(`Mismatched render frame@rdf.id=${rdf.GetId()} w/ inputFrameId=${delayedInputFrameId}:
othersForcedDownsyncRenderFrame=${JSON.stringify(othersForcedDownsyncRenderFrame)}`); rdf=${self._stringifyGopkgRdfForFrameDataLogging(rdf)}
rdf = othersForcedDownsyncRenderFrame; othersForcedDownsyncRenderFrame=${self._stringifyGopkgRdfForFrameDataLogging(othersForcedDownsyncRenderFrame)}`);
}
// [WARNING] When this happens, something is intrinsically wrong -- to avoid having an inconsistent history in the "recentRenderCache", thus a wrong prediction all the way from here on, clear the history!
pbOthersForcedDownsyncRenderFrame.backendUnconfirmedMask = ((1 << window.boundRoomCapacity) - 1);
self.onRoomDownsyncFrame(pbOthersForcedDownsyncRenderFrame, null);
self.othersForcedDownsyncRenderFrameDict.delete(rdf.GetId()); self.othersForcedDownsyncRenderFrameDict.delete(rdf.GetId());
} }
} }
@@ -1423,8 +1438,17 @@ othersForcedDownsyncRenderFrame=${JSON.stringify(othersForcedDownsyncRenderFrame
const j = gopkgs.ConvertToDelayedInputFrameId(i); const j = gopkgs.ConvertToDelayedInputFrameId(i);
const delayedInputFrame = gopkgs.GetInputFrameDownsync(self.recentInputCache, j); const delayedInputFrame = gopkgs.GetInputFrameDownsync(self.recentInputCache, j);
const allowUpdateInputFrameInPlaceUponDynamics = (!isChasing); // [WARNING] Input mutation could trigger chasing on frontend, thus don't trigger mutation when chasing to avoid confusing recursion.
const hasInputFrameUpdatedOnDynamics = gopkgs.ApplyInputFrameDownsyncDynamicsOnSingleRenderFrameJs(self.recentInputCache, i, collisionSys, collisionSysMap, self.spaceOffsetX, self.spaceOffsetY, self.chConfigsOrderedByJoinIndex, self.recentRenderCache, self.collisionHolder, self.effPushbacks, self.hardPushbackNormsArr, self.jumpedOrNotList, self.dynamicRectangleColliders, self.lastIndividuallyConfirmedInputFrameId, self.lastIndividuallyConfirmedInputList, allowUpdateInputFrameInPlaceUponDynamics, self.selfPlayerInfo.joinIndex);
if (hasInputFrameUpdatedOnDynamics) {
const ii = gopkgs.ConvertToFirstUsedRenderFrameId(j);
if (ii < i) {
self._handleIncorrectlyRenderedPrediction(j, null, false);
}
}
if (self.frameDataLoggingEnabled) { if (self.frameDataLoggingEnabled) {
const actuallyUsedInputClone = delayedInputFrame.GetInputList(); // [WARNING] The "inputList" of "delayedInputFrame" could be mutated in "ApplyInputFrameDownsyncDynamicsOnSingleRenderFrameJs", thus should clone after dynamics is applied!
const actuallyUsedInputClone = delayedInputFrame.GetInputList().slice();
const inputFrameDownsyncClone = { const inputFrameDownsyncClone = {
inputFrameId: j, inputFrameId: j,
inputList: actuallyUsedInputClone, inputList: actuallyUsedInputClone,
@@ -1432,8 +1456,6 @@ othersForcedDownsyncRenderFrame=${JSON.stringify(othersForcedDownsyncRenderFrame
}; };
self.rdfIdToActuallyUsedInput.set(i, inputFrameDownsyncClone); self.rdfIdToActuallyUsedInput.set(i, inputFrameDownsyncClone);
} }
const allowUpdateInputFrameInPlaceUponDynamics = (!isChasing);
const renderRes = gopkgs.ApplyInputFrameDownsyncDynamicsOnSingleRenderFrameJs(self.recentInputCache, i, collisionSys, collisionSysMap, self.spaceOffsetX, self.spaceOffsetY, self.chConfigsOrderedByJoinIndex, self.recentRenderCache, self.collisionHolder, self.effPushbacks, self.hardPushbackNormsArr, self.jumpedOrNotList, self.dynamicRectangleColliders, self.lastIndividuallyConfirmedInputFrameId, self.lastIndividuallyConfirmedInputList, allowUpdateInputFrameInPlaceUponDynamics, self.selfPlayerInfo.joinIndex);
const nextRdf = gopkgs.GetRoomDownsyncFrame(self.recentRenderCache, i + 1); const nextRdf = gopkgs.GetRoomDownsyncFrame(self.recentRenderCache, i + 1);
if (true == isChasing) { if (true == isChasing) {
@@ -1549,34 +1571,41 @@ othersForcedDownsyncRenderFrame=${JSON.stringify(othersForcedDownsyncRenderFrame
if (null == inputFrameDownsync) return "{}"; if (null == inputFrameDownsync) return "{}";
const self = this; const self = this;
let s = []; let s = [];
s.push(`InputFrameId:${inputFrameDownsync.InputFrameId}`); s.push(`InputFrameId:${inputFrameDownsync.GetInputFrameId()}`);
let ss = []; let ss = [];
for (let k = 0; k < window.boundRoomCapacity; ++k) { for (let k = 0; k < window.boundRoomCapacity; ++k) {
ss.push(`"${inputFrameDownsync.InputList[k]}"`); ss.push(`"${inputFrameDownsync.GetInputList()[k]}"`);
} }
s.push(`InputList:[${ss.join(',')}]`); s.push(`InputList:[${ss.join(',')}]`);
s.push(`ConfirmedList:${inputFrameDownsync.ConfirmedList}`); s.push(`ConfirmedList:${inputFrameDownsync.GetConfirmedList()}`);
return `{${s.join(',')}}`; return `{${s.join(',')}}`;
}, },
_stringifyGopkgRdfForFrameDataLogging(rdf) {
const playersStrBldr = [];
for (let k = 0; k < window.boundRoomCapacity; k++) {
playersStrBldr.push(this.playerDownsyncStr(gopkgs.GetPlayer(rdf, k)));
}
const fireballsStrBldr = [];
for (let k = 0;; k++) {
const fireball = gopkgs.GetFireballBullet(rdf, k);
if (null == fireball) break;
fireballsStrBldr.push(this.fireballDownsyncStr(fireball));
}
return `rdfId:${rdf.GetId()}
players:[${playersStrBldr.join(',')}]
fireballs:[${fireballsStrBldr.join(',')}]`;
},
_stringifyRdfIdToActuallyUsedInput() { _stringifyRdfIdToActuallyUsedInput() {
const self = this; const self = this;
let s = []; let s = [];
for (let i = self.recentRenderCache.GetStFrameId(); i < self.recentRenderCache.GetEdFrameId(); i++) { for (let i = self.recentRenderCache.GetStFrameId(); i < self.recentRenderCache.GetEdFrameId(); i++) {
const actuallyUsedInputClone = self.rdfIdToActuallyUsedInput.get(i); const actuallyUsedInputClone = self.rdfIdToActuallyUsedInput.get(i);
const rdf = self.recentRenderCache.GetByFrameId(i); const rdf = gopkgs.GetRoomDownsyncFrame(self.recentRenderCache, i);
const playersStrBldr = []; const rdfStr = self._stringifyGopkgRdfForFrameDataLogging(rdf);
for (let k in rdf.GetPlayersArr()) { s.push(`${rdfStr}
playersStrBldr.push(self.playerDownsyncStr(rdf.GetPlayersArr()[k]));
}
const fireballsStrBldr = [];
for (let k in rdf.FireballBullets) {
fireballsStrBldr.push(self.fireballDownsyncStr(rdf.FireballBullets[k]));
}
s.push(`rdfId:${i}
players:[${playersStrBldr.join(',')}]
fireballs:[${fireballsStrBldr.join(',')}]
actuallyUsedinputList:{${self.inputFrameDownsyncStr(actuallyUsedInputClone)}}`); actuallyUsedinputList:{${self.inputFrameDownsyncStr(actuallyUsedInputClone)}}`);
} }

File diff suppressed because one or more lines are too long

View File

@@ -372,7 +372,7 @@ bool DelayNoMore::UdpSession::pollUdpRecvRingBuff() {
// This function is called by GameThread 60 fps. // This function is called by GameThread 60 fps.
//uv_mutex_lock(&recvRingBuffLock); //uv_mutex_lock(&recvRingBuffLock);
while (true) { while (true && NULL != recvLoop) {
RecvWork f; RecvWork f;
bool res = recvRingBuff->pop(&f); bool res = recvRingBuff->pop(&f);
if (!res) { if (!res) {

View File

@@ -76,7 +76,7 @@
"shelter_z_reducer", "shelter_z_reducer",
"shelter" "shelter"
], ],
"last-module-event-record-time": 1677337364473, "last-module-event-record-time": 1678432182471,
"simulator-orientation": false, "simulator-orientation": false,
"simulator-resolution": { "simulator-resolution": {
"height": 640, "height": 640,

View File

@@ -7,7 +7,7 @@ import (
) )
const ( const (
MAX_FLOAT64 = 1.7e+308 MAX_FLOAT64 = resolv.MaxFloat64
MAX_INT32 = int32(999999999) MAX_INT32 = int32(999999999)
COLLISION_PLAYER_INDEX_PREFIX = (1 << 17) COLLISION_PLAYER_INDEX_PREFIX = (1 << 17)
COLLISION_BARRIER_INDEX_PREFIX = (1 << 16) COLLISION_BARRIER_INDEX_PREFIX = (1 << 16)
@@ -490,7 +490,8 @@ func calcHardPushbacksNorms(joinIndex int32, currPlayerDownsync, thatPlayerInNex
return retCnt return retCnt
} }
func UpdateInputFrameInPlaceUponDynamics(inputFrameId int32, roomCapacity int, confirmedList uint64, inputList []uint64, lastIndividuallyConfirmedInputFrameId []int32, lastIndividuallyConfirmedInputList []uint64, toExcludeJoinIndexUpdateInputFrameInPlaceUponDynamics int32) { func UpdateInputFrameInPlaceUponDynamics(inputFrameId int32, roomCapacity int, confirmedList uint64, inputList []uint64, lastIndividuallyConfirmedInputFrameId []int32, lastIndividuallyConfirmedInputList []uint64, toExcludeJoinIndexUpdateInputFrameInPlaceUponDynamics int32) bool {
hasInputFrameUpdatedOnDynamics := false
for i := 0; i < roomCapacity; i++ { for i := 0; i < roomCapacity; i++ {
if int32(i+1) == toExcludeJoinIndexUpdateInputFrameInPlaceUponDynamics { if int32(i+1) == toExcludeJoinIndexUpdateInputFrameInPlaceUponDynamics {
// On frontend, a "self input" is only confirmed by websocket downsync, which is quite late and might get the "self input" incorrectly overwritten if not excluded here // On frontend, a "self input" is only confirmed by websocket downsync, which is quite late and might get the "self input" incorrectly overwritten if not excluded here
@@ -503,8 +504,13 @@ func UpdateInputFrameInPlaceUponDynamics(inputFrameId int32, roomCapacity int, c
if lastIndividuallyConfirmedInputFrameId[i] >= inputFrameId { if lastIndividuallyConfirmedInputFrameId[i] >= inputFrameId {
continue continue
} }
inputList[i] = (lastIndividuallyConfirmedInputList[i] & uint64(15)) newVal := (lastIndividuallyConfirmedInputList[i] & uint64(15))
if newVal != inputList[i] {
inputList[i] = newVal
hasInputFrameUpdatedOnDynamics = true
}
} }
return hasInputFrameUpdatedOnDynamics
} }
func deriveOpPattern(currPlayerDownsync, thatPlayerInNextFrame *PlayerDownsync, currRenderFrame *RoomDownsyncFrame, chConfig *CharacterConfig, inputsBuffer *resolv.RingBuffer) (int, bool, int32, int32) { func deriveOpPattern(currPlayerDownsync, thatPlayerInNextFrame *PlayerDownsync, currRenderFrame *RoomDownsyncFrame, chConfig *CharacterConfig, inputsBuffer *resolv.RingBuffer) (int, bool, int32, int32) {
@@ -585,6 +591,7 @@ func deriveOpPattern(currPlayerDownsync, thatPlayerInNextFrame *PlayerDownsync,
The function "ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame" is creating new heap-memory blocks at 60fps, e.g. nextRenderFramePlayers & nextRenderFrameMeleeBullets & nextRenderFrameFireballBullets & effPushbacks & hardPushbackNorms & jumpedOrNotList & dynamicRectangleColliders("player" & "bullet"), which would induce "possibly performance impacting garbage collections" when many rooms are running simultaneously. The function "ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame" is creating new heap-memory blocks at 60fps, e.g. nextRenderFramePlayers & nextRenderFrameMeleeBullets & nextRenderFrameFireballBullets & effPushbacks & hardPushbackNorms & jumpedOrNotList & dynamicRectangleColliders("player" & "bullet"), which would induce "possibly performance impacting garbage collections" when many rooms are running simultaneously.
*/ */
func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *resolv.RingBuffer, currRenderFrameId int32, collisionSys *resolv.Space, collisionSysMap map[int32]*resolv.Object, collisionSpaceOffsetX, collisionSpaceOffsetY float64, chConfigsOrderedByJoinIndex []*CharacterConfig, renderFrameBuffer *resolv.RingBuffer, collision *resolv.Collision, effPushbacks []*Vec2D, hardPushbackNormsArr [][]*Vec2D, jumpedOrNotList []bool, dynamicRectangleColliders []*resolv.Object, lastIndividuallyConfirmedInputFrameId []int32, lastIndividuallyConfirmedInputList []uint64, allowUpdateInputFrameInPlaceUponDynamics bool, toExcludeJoinIndexUpdateInputFrameInPlaceUponDynamics int32) bool { func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *resolv.RingBuffer, currRenderFrameId int32, collisionSys *resolv.Space, collisionSysMap map[int32]*resolv.Object, collisionSpaceOffsetX, collisionSpaceOffsetY float64, chConfigsOrderedByJoinIndex []*CharacterConfig, renderFrameBuffer *resolv.RingBuffer, collision *resolv.Collision, effPushbacks []*Vec2D, hardPushbackNormsArr [][]*Vec2D, jumpedOrNotList []bool, dynamicRectangleColliders []*resolv.Object, lastIndividuallyConfirmedInputFrameId []int32, lastIndividuallyConfirmedInputList []uint64, allowUpdateInputFrameInPlaceUponDynamics bool, toExcludeJoinIndexUpdateInputFrameInPlaceUponDynamics int32) bool {
hasInputFrameUpdatedOnDynamics := false
currRenderFrame := renderFrameBuffer.GetByFrameId(currRenderFrameId).(*RoomDownsyncFrame) currRenderFrame := renderFrameBuffer.GetByFrameId(currRenderFrameId).(*RoomDownsyncFrame)
nextRenderFrameId := currRenderFrameId + 1 nextRenderFrameId := currRenderFrameId + 1
roomCapacity := len(currRenderFrame.PlayersArr) roomCapacity := len(currRenderFrame.PlayersArr)
@@ -637,7 +644,7 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *resolv.Rin
delayedInputList := delayedInputFrameDownsync.InputList delayedInputList := delayedInputFrameDownsync.InputList
roomCapacity := len(delayedInputList) roomCapacity := len(delayedInputList)
if allowUpdateInputFrameInPlaceUponDynamics { if allowUpdateInputFrameInPlaceUponDynamics {
UpdateInputFrameInPlaceUponDynamics(delayedInputFrameId, roomCapacity, delayedInputFrameDownsync.ConfirmedList, delayedInputList, lastIndividuallyConfirmedInputFrameId, lastIndividuallyConfirmedInputList, toExcludeJoinIndexUpdateInputFrameInPlaceUponDynamics) hasInputFrameUpdatedOnDynamics = UpdateInputFrameInPlaceUponDynamics(delayedInputFrameId, roomCapacity, delayedInputFrameDownsync.ConfirmedList, delayedInputList, lastIndividuallyConfirmedInputFrameId, lastIndividuallyConfirmedInputList, toExcludeJoinIndexUpdateInputFrameInPlaceUponDynamics)
} }
} }
@@ -648,7 +655,7 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *resolv.Rin
jumpedOrNotList[i] = jumpedOrNot jumpedOrNotList[i] = jumpedOrNot
joinIndex := currPlayerDownsync.JoinIndex joinIndex := currPlayerDownsync.JoinIndex
skillId := chConfig.SkillMapper(patternId, currPlayerDownsync) skillId := chConfig.SkillMapper(patternId, currPlayerDownsync, chConfig.SpeciesId)
if skillConfig, existent := skills[skillId]; existent { if skillConfig, existent := skills[skillId]; existent {
thatPlayerInNextFrame.ActiveSkillId = int32(skillId) thatPlayerInNextFrame.ActiveSkillId = int32(skillId)
thatPlayerInNextFrame.ActiveSkillHit = 0 thatPlayerInNextFrame.ActiveSkillHit = 0
@@ -1210,7 +1217,7 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *resolv.Rin
ret.Id = nextRenderFrameId ret.Id = nextRenderFrameId
ret.BulletLocalIdCounter = bulletLocalId ret.BulletLocalIdCounter = bulletLocalId
return true return hasInputFrameUpdatedOnDynamics
} }
func GenerateRectCollider(wx, wy, w, h, topPadding, bottomPadding, leftPadding, rightPadding, spaceOffsetX, spaceOffsetY float64, data interface{}, tag string) *resolv.Object { func GenerateRectCollider(wx, wy, w, h, topPadding, bottomPadding, leftPadding, rightPadding, spaceOffsetX, spaceOffsetY float64, data interface{}, tag string) *resolv.Object {

View File

@@ -1,6 +1,6 @@
package battle package battle
type SkillMapperType func(patternId int, currPlayerDownsync *PlayerDownsync) int type SkillMapperType = func(patternId int, currPlayerDownsync *PlayerDownsync, speciesId int) int
type CharacterConfig struct { type CharacterConfig struct {
SpeciesId int SpeciesId int
@@ -31,6 +31,118 @@ type CharacterConfig struct {
SkillMapper SkillMapperType SkillMapper SkillMapperType
} }
func defaultSkillMapper(patternId int, currPlayerDownsync *PlayerDownsync, speciesId int) int {
switch speciesId {
case 0:
if 1 == patternId {
if 0 == currPlayerDownsync.FramesToRecover {
if currPlayerDownsync.InAir {
return 255
} else {
return 1
}
} else {
// Now that "0 < FramesToRecover", we're only able to fire any skill if it's a cancellation
if skillConfig, existent1 := skills[int(currPlayerDownsync.ActiveSkillId)]; existent1 {
switch v := skillConfig.Hits[currPlayerDownsync.ActiveSkillHit].(type) {
case *MeleeBullet:
if v.Bullet.CancellableStFrame <= currPlayerDownsync.FramesInChState && currPlayerDownsync.FramesInChState < v.Bullet.CancellableEdFrame {
if nextSkillId, existent2 := v.Bullet.CancelTransit[patternId]; existent2 {
return nextSkillId
}
}
}
}
}
} else if 3 == patternId {
if 0 == currPlayerDownsync.FramesToRecover && !currPlayerDownsync.InAir {
return 15
}
} else if 5 == patternId {
// Dashing is already constrained by "FramesToRecover & CapturedByInertia" in "deriveOpPattern"
if !currPlayerDownsync.InAir {
return 12
}
}
// By default no skill can be fired
return NO_SKILL
case 1:
if 1 == patternId {
if 0 == currPlayerDownsync.FramesToRecover {
if currPlayerDownsync.InAir {
return 256
} else {
return 4
}
} else {
// Now that "0 < FramesToRecover", we're only able to fire any skill if it's a cancellation
if skillConfig, existent1 := skills[int(currPlayerDownsync.ActiveSkillId)]; existent1 {
switch v := skillConfig.Hits[currPlayerDownsync.ActiveSkillHit].(type) {
case *MeleeBullet:
if v.Bullet.CancellableStFrame <= currPlayerDownsync.FramesInChState && currPlayerDownsync.FramesInChState < v.Bullet.CancellableEdFrame {
if nextSkillId, existent2 := v.Bullet.CancelTransit[patternId]; existent2 {
return nextSkillId
}
}
}
}
}
} else if 3 == patternId {
if 0 == currPlayerDownsync.FramesToRecover && !currPlayerDownsync.InAir {
return 16
}
} else if 5 == patternId {
// Air dash allowed for this character
// Dashing is already constrained by "FramesToRecover & CapturedByInertia" in "deriveOpPattern"
return 13
}
// By default no skill can be fired
return NO_SKILL
case 4096:
if 1 == patternId {
if 0 == currPlayerDownsync.FramesToRecover {
if currPlayerDownsync.InAir {
return 257
} else {
return 7
}
} else {
// Now that "0 < FramesToRecover", we're only able to fire any skill if it's a cancellation
if skillConfig, existent1 := skills[int(currPlayerDownsync.ActiveSkillId)]; existent1 {
switch v := skillConfig.Hits[currPlayerDownsync.ActiveSkillHit].(type) {
case *MeleeBullet:
if v.Bullet.CancellableStFrame <= currPlayerDownsync.FramesInChState && currPlayerDownsync.FramesInChState < v.Bullet.CancellableEdFrame {
if nextSkillId, existent2 := v.Bullet.CancelTransit[patternId]; existent2 {
return nextSkillId
}
}
}
}
}
} else if 2 == patternId {
if 0 == currPlayerDownsync.FramesToRecover && !currPlayerDownsync.InAir {
return 11
}
} else if 3 == patternId {
if 0 == currPlayerDownsync.FramesToRecover && !currPlayerDownsync.InAir {
return 10
}
} else if 5 == patternId {
// Dashing is already constrained by "FramesToRecover & CapturedByInertia" in "deriveOpPattern"
if !currPlayerDownsync.InAir {
return 14
}
}
// By default no skill can be fired
return NO_SKILL
}
return NO_SKILL
}
var Characters = map[int]*CharacterConfig{ var Characters = map[int]*CharacterConfig{
0: &CharacterConfig{ 0: &CharacterConfig{
SpeciesId: 0, SpeciesId: 0,
@@ -58,41 +170,7 @@ var Characters = map[int]*CharacterConfig{
WallJumpingInitVelY: int32(float64(7) * WORLD_TO_VIRTUAL_GRID_RATIO), WallJumpingInitVelY: int32(float64(7) * WORLD_TO_VIRTUAL_GRID_RATIO),
WallSlidingVelY: int32(float64(-1) * WORLD_TO_VIRTUAL_GRID_RATIO), WallSlidingVelY: int32(float64(-1) * WORLD_TO_VIRTUAL_GRID_RATIO),
SkillMapper: func(patternId int, currPlayerDownsync *PlayerDownsync) int { SkillMapper: defaultSkillMapper,
if 1 == patternId {
if 0 == currPlayerDownsync.FramesToRecover {
if currPlayerDownsync.InAir {
return 255
} else {
return 1
}
} else {
// Now that "0 < FramesToRecover", we're only able to fire any skill if it's a cancellation
if skillConfig, existent1 := skills[int(currPlayerDownsync.ActiveSkillId)]; existent1 {
switch v := skillConfig.Hits[currPlayerDownsync.ActiveSkillHit].(type) {
case *MeleeBullet:
if v.Bullet.CancellableStFrame <= currPlayerDownsync.FramesInChState && currPlayerDownsync.FramesInChState < v.Bullet.CancellableEdFrame {
if nextSkillId, existent2 := v.Bullet.CancelTransit[patternId]; existent2 {
return nextSkillId
}
}
}
}
}
} else if 3 == patternId {
if 0 == currPlayerDownsync.FramesToRecover && !currPlayerDownsync.InAir {
return 15
}
} else if 5 == patternId {
// Dashing is already constrained by "FramesToRecover & CapturedByInertia" in "deriveOpPattern"
if !currPlayerDownsync.InAir {
return 12
}
}
// By default no skill can be fired
return NO_SKILL
},
}, },
1: &CharacterConfig{ 1: &CharacterConfig{
SpeciesId: 1, SpeciesId: 1,
@@ -120,40 +198,7 @@ var Characters = map[int]*CharacterConfig{
WallJumpingInitVelY: int32(float64(7) * WORLD_TO_VIRTUAL_GRID_RATIO), WallJumpingInitVelY: int32(float64(7) * WORLD_TO_VIRTUAL_GRID_RATIO),
WallSlidingVelY: int32(float64(-1) * WORLD_TO_VIRTUAL_GRID_RATIO), WallSlidingVelY: int32(float64(-1) * WORLD_TO_VIRTUAL_GRID_RATIO),
SkillMapper: func(patternId int, currPlayerDownsync *PlayerDownsync) int { SkillMapper: defaultSkillMapper,
if 1 == patternId {
if 0 == currPlayerDownsync.FramesToRecover {
if currPlayerDownsync.InAir {
return 256
} else {
return 4
}
} else {
// Now that "0 < FramesToRecover", we're only able to fire any skill if it's a cancellation
if skillConfig, existent1 := skills[int(currPlayerDownsync.ActiveSkillId)]; existent1 {
switch v := skillConfig.Hits[currPlayerDownsync.ActiveSkillHit].(type) {
case *MeleeBullet:
if v.Bullet.CancellableStFrame <= currPlayerDownsync.FramesInChState && currPlayerDownsync.FramesInChState < v.Bullet.CancellableEdFrame {
if nextSkillId, existent2 := v.Bullet.CancelTransit[patternId]; existent2 {
return nextSkillId
}
}
}
}
}
} else if 3 == patternId {
if 0 == currPlayerDownsync.FramesToRecover && !currPlayerDownsync.InAir {
return 16
}
} else if 5 == patternId {
// Air dash allowed for this character
// Dashing is already constrained by "FramesToRecover & CapturedByInertia" in "deriveOpPattern"
return 13
}
// By default no skill can be fired
return NO_SKILL
},
}, },
4096: &CharacterConfig{ 4096: &CharacterConfig{
SpeciesId: 4096, SpeciesId: 4096,
@@ -177,45 +222,7 @@ var Characters = map[int]*CharacterConfig{
DashingEnabled: true, DashingEnabled: true,
OnWallEnabled: false, OnWallEnabled: false,
SkillMapper: func(patternId int, currPlayerDownsync *PlayerDownsync) int { SkillMapper: defaultSkillMapper,
if 1 == patternId {
if 0 == currPlayerDownsync.FramesToRecover {
if currPlayerDownsync.InAir {
return 257
} else {
return 7
}
} else {
// Now that "0 < FramesToRecover", we're only able to fire any skill if it's a cancellation
if skillConfig, existent1 := skills[int(currPlayerDownsync.ActiveSkillId)]; existent1 {
switch v := skillConfig.Hits[currPlayerDownsync.ActiveSkillHit].(type) {
case *MeleeBullet:
if v.Bullet.CancellableStFrame <= currPlayerDownsync.FramesInChState && currPlayerDownsync.FramesInChState < v.Bullet.CancellableEdFrame {
if nextSkillId, existent2 := v.Bullet.CancelTransit[patternId]; existent2 {
return nextSkillId
}
}
}
}
}
} else if 2 == patternId {
if 0 == currPlayerDownsync.FramesToRecover && !currPlayerDownsync.InAir {
return 11
}
} else if 3 == patternId {
if 0 == currPlayerDownsync.FramesToRecover && !currPlayerDownsync.InAir {
return 10
}
} else if 5 == patternId {
// Dashing is already constrained by "FramesToRecover & CapturedByInertia" in "deriveOpPattern"
if !currPlayerDownsync.InAir {
return 14
}
}
// By default no skill can be fired
return NO_SKILL
},
}, },
} }
@@ -226,7 +233,7 @@ var skills = map[int]*Skill{
RecoveryFramesOnHit: int32(30), RecoveryFramesOnHit: int32(30),
ReleaseTriggerType: int32(1), ReleaseTriggerType: int32(1),
BoundChState: ATK_CHARACTER_STATE_ATK1, BoundChState: ATK_CHARACTER_STATE_ATK1,
Hits: []interface{}{ Hits: []AnyBullet{
&MeleeBullet{ &MeleeBullet{
Bullet: &BulletConfig{ Bullet: &BulletConfig{
StartupFrames: int32(7), StartupFrames: int32(7),
@@ -261,7 +268,7 @@ var skills = map[int]*Skill{
RecoveryFramesOnHit: int32(36), RecoveryFramesOnHit: int32(36),
ReleaseTriggerType: int32(1), ReleaseTriggerType: int32(1),
BoundChState: ATK_CHARACTER_STATE_ATK2, BoundChState: ATK_CHARACTER_STATE_ATK2,
Hits: []interface{}{ Hits: []AnyBullet{
&MeleeBullet{ &MeleeBullet{
Bullet: &BulletConfig{ Bullet: &BulletConfig{
StartupFrames: int32(18), StartupFrames: int32(18),
@@ -295,7 +302,7 @@ var skills = map[int]*Skill{
RecoveryFramesOnHit: int32(50), RecoveryFramesOnHit: int32(50),
ReleaseTriggerType: int32(1), ReleaseTriggerType: int32(1),
BoundChState: ATK_CHARACTER_STATE_ATK3, BoundChState: ATK_CHARACTER_STATE_ATK3,
Hits: []interface{}{ Hits: []AnyBullet{
&MeleeBullet{ &MeleeBullet{
Bullet: &BulletConfig{ Bullet: &BulletConfig{
StartupFrames: int32(8), StartupFrames: int32(8),
@@ -324,7 +331,7 @@ var skills = map[int]*Skill{
RecoveryFramesOnHit: int32(30), RecoveryFramesOnHit: int32(30),
ReleaseTriggerType: int32(1), ReleaseTriggerType: int32(1),
BoundChState: ATK_CHARACTER_STATE_ATK1, BoundChState: ATK_CHARACTER_STATE_ATK1,
Hits: []interface{}{ Hits: []AnyBullet{
&MeleeBullet{ &MeleeBullet{
Bullet: &BulletConfig{ Bullet: &BulletConfig{
StartupFrames: int32(7), StartupFrames: int32(7),
@@ -359,7 +366,7 @@ var skills = map[int]*Skill{
RecoveryFramesOnHit: int32(36), RecoveryFramesOnHit: int32(36),
ReleaseTriggerType: int32(1), ReleaseTriggerType: int32(1),
BoundChState: ATK_CHARACTER_STATE_ATK2, BoundChState: ATK_CHARACTER_STATE_ATK2,
Hits: []interface{}{ Hits: []AnyBullet{
&MeleeBullet{ &MeleeBullet{
Bullet: &BulletConfig{ Bullet: &BulletConfig{
StartupFrames: int32(18), StartupFrames: int32(18),
@@ -393,7 +400,7 @@ var skills = map[int]*Skill{
RecoveryFramesOnHit: int32(45), RecoveryFramesOnHit: int32(45),
ReleaseTriggerType: int32(1), ReleaseTriggerType: int32(1),
BoundChState: ATK_CHARACTER_STATE_ATK3, BoundChState: ATK_CHARACTER_STATE_ATK3,
Hits: []interface{}{ Hits: []AnyBullet{
&MeleeBullet{ &MeleeBullet{
Bullet: &BulletConfig{ Bullet: &BulletConfig{
StartupFrames: int32(8), StartupFrames: int32(8),
@@ -422,7 +429,7 @@ var skills = map[int]*Skill{
RecoveryFramesOnHit: int32(30), RecoveryFramesOnHit: int32(30),
ReleaseTriggerType: int32(1), ReleaseTriggerType: int32(1),
BoundChState: ATK_CHARACTER_STATE_ATK1, BoundChState: ATK_CHARACTER_STATE_ATK1,
Hits: []interface{}{ Hits: []AnyBullet{
&MeleeBullet{ &MeleeBullet{
Bullet: &BulletConfig{ Bullet: &BulletConfig{
StartupFrames: int32(7), StartupFrames: int32(7),
@@ -457,7 +464,7 @@ var skills = map[int]*Skill{
RecoveryFramesOnHit: int32(36), RecoveryFramesOnHit: int32(36),
ReleaseTriggerType: int32(1), ReleaseTriggerType: int32(1),
BoundChState: ATK_CHARACTER_STATE_ATK2, BoundChState: ATK_CHARACTER_STATE_ATK2,
Hits: []interface{}{ Hits: []AnyBullet{
&MeleeBullet{ &MeleeBullet{
Bullet: &BulletConfig{ Bullet: &BulletConfig{
StartupFrames: int32(18), StartupFrames: int32(18),
@@ -491,7 +498,7 @@ var skills = map[int]*Skill{
RecoveryFramesOnHit: int32(40), RecoveryFramesOnHit: int32(40),
ReleaseTriggerType: int32(1), ReleaseTriggerType: int32(1),
BoundChState: ATK_CHARACTER_STATE_ATK3, BoundChState: ATK_CHARACTER_STATE_ATK3,
Hits: []interface{}{ Hits: []AnyBullet{
&MeleeBullet{ &MeleeBullet{
Bullet: &BulletConfig{ Bullet: &BulletConfig{
StartupFrames: int32(7), StartupFrames: int32(7),
@@ -520,7 +527,7 @@ var skills = map[int]*Skill{
RecoveryFramesOnHit: int32(38), RecoveryFramesOnHit: int32(38),
ReleaseTriggerType: int32(1), ReleaseTriggerType: int32(1),
BoundChState: ATK_CHARACTER_STATE_ATK4, BoundChState: ATK_CHARACTER_STATE_ATK4,
Hits: []interface{}{ Hits: []AnyBullet{
&FireballBullet{ &FireballBullet{
Speed: int32(float64(6) * WORLD_TO_VIRTUAL_GRID_RATIO), Speed: int32(float64(6) * WORLD_TO_VIRTUAL_GRID_RATIO),
Bullet: &BulletConfig{ Bullet: &BulletConfig{
@@ -550,7 +557,7 @@ var skills = map[int]*Skill{
RecoveryFramesOnHit: int32(60), RecoveryFramesOnHit: int32(60),
ReleaseTriggerType: int32(1), ReleaseTriggerType: int32(1),
BoundChState: ATK_CHARACTER_STATE_ATK5, BoundChState: ATK_CHARACTER_STATE_ATK5,
Hits: []interface{}{ Hits: []AnyBullet{
&MeleeBullet{ &MeleeBullet{
Bullet: &BulletConfig{ Bullet: &BulletConfig{
StartupFrames: int32(3), StartupFrames: int32(3),
@@ -579,7 +586,7 @@ var skills = map[int]*Skill{
RecoveryFramesOnHit: int32(10), RecoveryFramesOnHit: int32(10),
ReleaseTriggerType: int32(1), ReleaseTriggerType: int32(1),
BoundChState: ATK_CHARACTER_STATE_DASHING, BoundChState: ATK_CHARACTER_STATE_DASHING,
Hits: []interface{}{ Hits: []AnyBullet{
&MeleeBullet{ &MeleeBullet{
Bullet: &BulletConfig{ Bullet: &BulletConfig{
StartupFrames: int32(3), StartupFrames: int32(3),
@@ -606,7 +613,7 @@ var skills = map[int]*Skill{
RecoveryFramesOnHit: int32(12), RecoveryFramesOnHit: int32(12),
ReleaseTriggerType: int32(1), ReleaseTriggerType: int32(1),
BoundChState: ATK_CHARACTER_STATE_DASHING, BoundChState: ATK_CHARACTER_STATE_DASHING,
Hits: []interface{}{ Hits: []AnyBullet{
&MeleeBullet{ &MeleeBullet{
Bullet: &BulletConfig{ Bullet: &BulletConfig{
StartupFrames: int32(3), StartupFrames: int32(3),
@@ -633,7 +640,7 @@ var skills = map[int]*Skill{
RecoveryFramesOnHit: int32(8), RecoveryFramesOnHit: int32(8),
ReleaseTriggerType: int32(1), ReleaseTriggerType: int32(1),
BoundChState: ATK_CHARACTER_STATE_DASHING, BoundChState: ATK_CHARACTER_STATE_DASHING,
Hits: []interface{}{ Hits: []AnyBullet{
&MeleeBullet{ &MeleeBullet{
Bullet: &BulletConfig{ Bullet: &BulletConfig{
StartupFrames: int32(4), StartupFrames: int32(4),
@@ -660,7 +667,7 @@ var skills = map[int]*Skill{
RecoveryFramesOnHit: int32(48), RecoveryFramesOnHit: int32(48),
ReleaseTriggerType: int32(1), ReleaseTriggerType: int32(1),
BoundChState: ATK_CHARACTER_STATE_ATK4, BoundChState: ATK_CHARACTER_STATE_ATK4,
Hits: []interface{}{ Hits: []AnyBullet{
&FireballBullet{ &FireballBullet{
Speed: int32(float64(4) * WORLD_TO_VIRTUAL_GRID_RATIO), Speed: int32(float64(4) * WORLD_TO_VIRTUAL_GRID_RATIO),
Bullet: &BulletConfig{ Bullet: &BulletConfig{
@@ -690,7 +697,7 @@ var skills = map[int]*Skill{
RecoveryFramesOnHit: int32(60), RecoveryFramesOnHit: int32(60),
ReleaseTriggerType: int32(1), ReleaseTriggerType: int32(1),
BoundChState: ATK_CHARACTER_STATE_ATK4, BoundChState: ATK_CHARACTER_STATE_ATK4,
Hits: []interface{}{ Hits: []AnyBullet{
&FireballBullet{ &FireballBullet{
Speed: int32(float64(4) * WORLD_TO_VIRTUAL_GRID_RATIO), Speed: int32(float64(4) * WORLD_TO_VIRTUAL_GRID_RATIO),
Bullet: &BulletConfig{ Bullet: &BulletConfig{
@@ -720,7 +727,7 @@ var skills = map[int]*Skill{
RecoveryFramesOnHit: int32(30), RecoveryFramesOnHit: int32(30),
ReleaseTriggerType: int32(1), ReleaseTriggerType: int32(1),
BoundChState: ATK_CHARACTER_STATE_INAIR_ATK1, BoundChState: ATK_CHARACTER_STATE_INAIR_ATK1,
Hits: []interface{}{ Hits: []AnyBullet{
&MeleeBullet{ &MeleeBullet{
Bullet: &BulletConfig{ Bullet: &BulletConfig{
StartupFrames: int32(3), StartupFrames: int32(3),
@@ -749,7 +756,7 @@ var skills = map[int]*Skill{
RecoveryFramesOnHit: int32(20), RecoveryFramesOnHit: int32(20),
ReleaseTriggerType: int32(1), ReleaseTriggerType: int32(1),
BoundChState: ATK_CHARACTER_STATE_INAIR_ATK1, BoundChState: ATK_CHARACTER_STATE_INAIR_ATK1,
Hits: []interface{}{ Hits: []AnyBullet{
&MeleeBullet{ &MeleeBullet{
Bullet: &BulletConfig{ Bullet: &BulletConfig{
StartupFrames: int32(3), StartupFrames: int32(3),
@@ -778,7 +785,7 @@ var skills = map[int]*Skill{
RecoveryFramesOnHit: int32(30), RecoveryFramesOnHit: int32(30),
ReleaseTriggerType: int32(1), ReleaseTriggerType: int32(1),
BoundChState: ATK_CHARACTER_STATE_INAIR_ATK1, BoundChState: ATK_CHARACTER_STATE_INAIR_ATK1,
Hits: []interface{}{ Hits: []AnyBullet{
&MeleeBullet{ &MeleeBullet{
Bullet: &BulletConfig{ Bullet: &BulletConfig{
StartupFrames: int32(4), StartupFrames: int32(4),

View File

@@ -2,6 +2,8 @@ package battle
// TODO: Replace all "int32", "int64", "uint32" and "uint64" with just "int" for better performance in JavaScript! Reference https://github.com/gopherjs/gopherjs#performance-tips // TODO: Replace all "int32", "int64", "uint32" and "uint64" with just "int" for better performance in JavaScript! Reference https://github.com/gopherjs/gopherjs#performance-tips
type AnyBullet interface{}
type Vec2D struct { type Vec2D struct {
X float64 X float64
Y float64 Y float64
@@ -132,7 +134,7 @@ type Skill struct {
RecoveryFramesOnHit int32 RecoveryFramesOnHit int32
ReleaseTriggerType int32 // 1: rising-edge, 2: falling-edge ReleaseTriggerType int32 // 1: rising-edge, 2: falling-edge
BoundChState int32 BoundChState int32
Hits []interface{} // Hits within a "Skill" are automatically triggered Hits []AnyBullet // Hits within a "Skill" are automatically triggered
// [WARN] Multihit of a fireball is more difficult to handle than that of melee, because we have to count from the fireball's first hit; the situation becomes even more complicated when a multihit fireball is in a crowd -- remains to be designed // [WARN] Multihit of a fireball is more difficult to handle than that of melee, because we have to count from the fireball's first hit; the situation becomes even more complicated when a multihit fireball is in a crowd -- remains to be designed
} }

View File

@@ -8,11 +8,11 @@ type Cell struct {
// newCell creates a new cell at the specified X and Y position. Should not be used directly. // newCell creates a new cell at the specified X and Y position. Should not be used directly.
func newCell(x, y int) *Cell { func newCell(x, y int) *Cell {
return &Cell{ c := &Cell{}
X: x, c.X = x
Y: y, c.Y = y
Objects: NewRingBuffer(16), // A single cell is so small thus wouldn't have many touching objects simultaneously c.Objects = NewRingBuffer(16) // A single cell is so small thus wouldn't have many touching objects simultaneously
} return c
} }
// register registers an object with a Cell. Should not be used directly. // register registers an object with a Cell. Should not be used directly.

View File

@@ -10,10 +10,10 @@ type Collision struct {
} }
func NewCollision() *Collision { func NewCollision() *Collision {
return &Collision{ c := &Collision{}
Objects: NewRingBuffer(16), // I don't expect it to exceed 10 actually c.Objects = NewRingBuffer(16) // I don't expect it to exceed 10 actually
Cells: NewRingBuffer(16), c.Cells = NewRingBuffer(16)
} return c
} }
func (cc *Collision) Clear() { func (cc *Collision) Clear() {
@@ -52,7 +52,7 @@ func (cc *Collision) HasTags(tags ...string) bool {
// This slice does not contain the Object that called Check(). // This slice does not contain the Object that called Check().
func (cc *Collision) ObjectsByTags(tags ...string) []*Object { func (cc *Collision) ObjectsByTags(tags ...string) []*Object {
objects := []*Object{} objs := []*Object{}
rb := cc.Objects rb := cc.Objects
for i := rb.StFrameId; i < rb.EdFrameId; i++ { for i := rb.StFrameId; i < rb.EdFrameId; i++ {
@@ -61,30 +61,30 @@ func (cc *Collision) ObjectsByTags(tags ...string) []*Object {
continue continue
} }
if o.HasTags(tags...) { if o.HasTags(tags...) {
objects = append(objects, o) objs = append(objs, o)
} }
} }
return objects return objs
} }
// ContactWithObject returns the delta to move to come into contact with the specified Object. // ContactWithObject returns the delta to move to come into contact with the specified Object.
func (cc *Collision) ContactWithObject(object *Object) Vector { func (cc *Collision) ContactWithObject(obj *Object) Vector {
delta := Vector{0, 0} delta := Vector{0, 0}
if cc.dx < 0 { if cc.dx < 0 {
delta[0] = object.X + object.W - cc.checkingObject.X delta[0] = obj.X + obj.W - cc.checkingObject.X
} else if cc.dx > 0 { } else if cc.dx > 0 {
delta[0] = object.X - cc.checkingObject.W - cc.checkingObject.X delta[0] = obj.X - cc.checkingObject.W - cc.checkingObject.X
} }
if cc.dy < 0 { if cc.dy < 0 {
delta[1] = object.Y + object.H - cc.checkingObject.Y delta[1] = obj.Y + obj.H - cc.checkingObject.Y
} else if cc.dy > 0 { } else if cc.dy > 0 {
delta[1] = object.Y - cc.checkingObject.H - cc.checkingObject.Y delta[1] = obj.Y - cc.checkingObject.H - cc.checkingObject.Y
} }
return delta return delta
@@ -135,10 +135,10 @@ func (cc *Collision) SlideAgainstCell(cell *Cell, avoidTags ...string) Vector {
diffX := oX - ccX diffX := oX - ccX
diffY := oY - ccY diffY := oY - ccY
left := sp.Cell(collidingCell.X-1, collidingCell.Y) left := sp.GetCell(collidingCell.X-1, collidingCell.Y)
right := sp.Cell(collidingCell.X+1, collidingCell.Y) right := sp.GetCell(collidingCell.X+1, collidingCell.Y)
up := sp.Cell(collidingCell.X, collidingCell.Y-1) up := sp.GetCell(collidingCell.X, collidingCell.Y-1)
down := sp.Cell(collidingCell.X, collidingCell.Y+1) down := sp.GetCell(collidingCell.X, collidingCell.Y+1)
slide := Vector{0, 0} slide := Vector{0, 0}

View File

@@ -1,24 +0,0 @@
//go:build !amd64 || noasm
// +build !amd64 noasm
package resolv
// This function is from the gonum repository:
// https://github.com/gonum/gonum/blob/c3867503e73e5c3fee7ab93e3c2c562eb2be8178/internal/asm/f64/axpy.go#L23
func axpyUnitaryTo(dst []float64, alpha float64, x, y []float64) {
dim := len(y)
for i, v := range x {
if i == dim {
return
}
dst[i] = alpha*v + y[i]
}
}
// This function is from the gonum repository:
// https://github.com/gonum/gonum/blob/c3867503e73e5c3fee7ab93e3c2c562eb2be8178/internal/asm/f64/scal.go#L23
func scalUnitaryTo(dst []float64, alpha float64, x []float64) {
for i := range x {
dst[i] *= alpha
}
}

View File

@@ -1,9 +1,5 @@
package resolv package resolv
import (
"math"
)
// Object represents an object that can be spread across one or more Cells in a Space. An Object is essentially an AABB (Axis-Aligned Bounding Box) Rectangle. // Object represents an object that can be spread across one or more Cells in a Space. An Object is essentially an AABB (Axis-Aligned Bounding Box) Rectangle.
type Object struct { type Object struct {
Shape Shape // A shape for more specific collision-checking. Shape Shape // A shape for more specific collision-checking.
@@ -17,29 +13,27 @@ type Object struct {
// NewObject returns a new Object of the specified position and size. // NewObject returns a new Object of the specified position and size.
func NewObjectSingleTag(x, y, w, h float64, tag string) *Object { func NewObjectSingleTag(x, y, w, h float64, tag string) *Object {
o := &Object{ o := &Object{}
X: x, o.X = x
Y: y, o.Y = y
W: w, o.W = w
H: h, o.H = h
TouchingCells: NewRingBuffer(512), // [WARNING] Should make N large enough to cover all "TouchingCells", otherwise some cells would fail to unregister an object, resulting in memory corruption and incorrect detection result! o.TouchingCells = NewRingBuffer(512) // [WARNING] Should make N large enough to cover all "TouchingCells", otherwise some cells would fail to unregister an object, resulting in memory corruption and incorrect detection result!
tags: []string{tag}, o.tags = []string{tag}
ignoreList: map[*Object]bool{}, o.ignoreList = make(map[*Object]bool)
}
return o return o
} }
func NewObject(x, y, w, h float64, tags ...string) *Object { func NewObject(x, y, w, h float64, tags ...string) *Object {
o := &Object{ o := &Object{}
X: x, o.X = x
Y: y, o.Y = y
W: w, o.W = w
H: h, o.H = h
TouchingCells: NewRingBuffer(512), o.TouchingCells = NewRingBuffer(512)
tags: []string{}, o.tags = []string{}
ignoreList: map[*Object]bool{}, o.ignoreList = make(map[*Object]bool)
}
if len(tags) > 0 { if len(tags) > 0 {
o.AddTags(tags...) o.AddTags(tags...)
@@ -67,7 +61,7 @@ func (obj *Object) Clone() *Object {
if obj.Shape != nil { if obj.Shape != nil {
newObj.SetShape(obj.Shape.Clone()) newObj.SetShape(obj.Shape.Clone())
} }
for k := range obj.ignoreList { for k, _ := range obj.ignoreList {
newObj.AddToIgnoreList(k) newObj.AddToIgnoreList(k)
} }
return newObj return newObj
@@ -95,7 +89,7 @@ func (obj *Object) Update() {
for x := cx; x <= ex; x++ { for x := cx; x <= ex; x++ {
c := obj.Space.Cell(x, y) c := obj.Space.GetCell(x, y)
if c != nil { if c != nil {
c.register(obj) c.register(obj)
@@ -258,15 +252,15 @@ func (obj *Object) CheckAllWithHolder(dx, dy float64, cc *Collision) bool {
cc.checkingObject = obj cc.checkingObject = obj
if dx < 0 { if dx < 0 {
dx = math.Min(dx, -1) dx = Min(dx, -1)
} else if dx > 0 { } else if dx > 0 {
dx = math.Max(dx, 1) dx = Max(dx, 1)
} }
if dy < 0 { if dy < 0 {
dy = math.Min(dy, -1) dy = Min(dy, -1)
} else if dy > 0 { } else if dy > 0 {
dy = math.Max(dy, 1) dy = Max(dy, 1)
} }
cc.dx = dx cc.dx = dx
@@ -281,7 +275,7 @@ func (obj *Object) CheckAllWithHolder(dx, dy float64, cc *Collision) bool {
for x := cx; x <= ex; x++ { for x := cx; x <= ex; x++ {
if c := obj.Space.Cell(x, y); c != nil { if c := obj.Space.GetCell(x, y); c != nil {
rb := c.Objects rb := c.Objects
for i := rb.StFrameId; i < rb.EdFrameId; i++ { for i := rb.StFrameId; i < rb.EdFrameId; i++ {

View File

@@ -1,11 +1,14 @@
package resolv package resolv
const ( const (
RING_BUFF_CONSECUTIVE_SET = int32(0) // Declare type "int32" explicitly to prevent go2cs from transpiling them to "var"
RING_BUFF_NON_CONSECUTIVE_SET = int32(1) RING_BUFF_CONSECUTIVE_SET int32 = 0
RING_BUFF_FAILED_TO_SET = int32(2) RING_BUFF_NON_CONSECUTIVE_SET int32 = 1
RING_BUFF_FAILED_TO_SET int32 = 2
) )
type AnyObj interface{}
type RingBuffer struct { type RingBuffer struct {
Ed int32 // write index, open index Ed int32 // write index, open index
St int32 // read index, closed index St int32 // read index, closed index
@@ -13,19 +16,19 @@ type RingBuffer struct {
StFrameId int32 StFrameId int32
N int32 N int32
Cnt int32 // the count of valid elements in the buffer, used mainly to distinguish what "st == ed" means for "Pop" and "Get" methods Cnt int32 // the count of valid elements in the buffer, used mainly to distinguish what "st == ed" means for "Pop" and "Get" methods
Eles []interface{} Eles []AnyObj
} }
func NewRingBuffer(n int32) *RingBuffer { func NewRingBuffer(n int32) *RingBuffer {
return &RingBuffer{ ret := &RingBuffer{}
Ed: 0, ret.Ed = 0
St: 0, ret.St = 0
EdFrameId: 0, ret.EdFrameId = 0
StFrameId: 0, ret.StFrameId = 0
N: n, ret.N = n
Cnt: 0, ret.Cnt = 0
Eles: make([]interface{}, n), ret.Eles = make([]AnyObj, n)
} return ret
} }
func (rb *RingBuffer) DryPut() { func (rb *RingBuffer) DryPut() {
@@ -41,7 +44,7 @@ func (rb *RingBuffer) DryPut() {
} }
} }
func (rb *RingBuffer) Put(pItem interface{}) { func (rb *RingBuffer) Put(pItem AnyObj) {
for 0 < rb.Cnt && rb.Cnt >= rb.N { for 0 < rb.Cnt && rb.Cnt >= rb.N {
// Make room for the new element // Make room for the new element
rb.Pop() rb.Pop()
@@ -55,7 +58,7 @@ func (rb *RingBuffer) Put(pItem interface{}) {
} }
} }
func (rb *RingBuffer) Pop() interface{} { func (rb *RingBuffer) Pop() AnyObj {
if 0 == rb.Cnt { if 0 == rb.Cnt {
return nil return nil
} }
@@ -93,7 +96,7 @@ func (rb *RingBuffer) GetArrIdxByOffset(offsetFromSt int32) int32 {
return -1 return -1
} }
func (rb *RingBuffer) GetByOffset(offsetFromSt int32) interface{} { func (rb *RingBuffer) GetByOffset(offsetFromSt int32) AnyObj {
arrIdx := rb.GetArrIdxByOffset(offsetFromSt) arrIdx := rb.GetArrIdxByOffset(offsetFromSt)
if -1 == arrIdx { if -1 == arrIdx {
return nil return nil
@@ -101,7 +104,7 @@ func (rb *RingBuffer) GetByOffset(offsetFromSt int32) interface{} {
return rb.Eles[arrIdx] return rb.Eles[arrIdx]
} }
func (rb *RingBuffer) GetByFrameId(frameId int32) interface{} { func (rb *RingBuffer) GetByFrameId(frameId int32) AnyObj {
if frameId >= rb.EdFrameId || frameId < rb.StFrameId { if frameId >= rb.EdFrameId || frameId < rb.StFrameId {
return nil return nil
} }
@@ -109,7 +112,7 @@ func (rb *RingBuffer) GetByFrameId(frameId int32) interface{} {
} }
// [WARNING] During a battle, frontend could receive non-consecutive frames (either renderFrame or inputFrame) due to resync, the buffer should handle these frames properly. // [WARNING] During a battle, frontend could receive non-consecutive frames (either renderFrame or inputFrame) due to resync, the buffer should handle these frames properly.
func (rb *RingBuffer) SetByFrameId(pItem interface{}, frameId int32) (int32, int32, int32) { func (rb *RingBuffer) SetByFrameId(pItem AnyObj, frameId int32) (int32, int32, int32) {
oldStFrameId, oldEdFrameId := rb.StFrameId, rb.EdFrameId oldStFrameId, oldEdFrameId := rb.StFrameId, rb.EdFrameId
if frameId < oldStFrameId { if frameId < oldStFrameId {
return RING_BUFF_FAILED_TO_SET, oldStFrameId, oldEdFrameId return RING_BUFF_FAILED_TO_SET, oldStFrameId, oldEdFrameId

View File

@@ -1,9 +1,5 @@
package resolv package resolv
import (
"math"
)
type Shape interface { type Shape interface {
// Intersection tests to see if a Shape intersects with the other given Shape. dx and dy are delta movement variables indicating // Intersection tests to see if a Shape intersects with the other given Shape. dx and dy are delta movement variables indicating
// movement to be applied before the intersection check (thereby allowing you to see if a Shape would collide with another if it // movement to be applied before the intersection check (thereby allowing you to see if a Shape would collide with another if it
@@ -27,10 +23,10 @@ type Line struct {
} }
func NewLine(x, y, x2, y2 float64) *Line { func NewLine(x, y, x2, y2 float64) *Line {
return &Line{ l := &Line{}
Start: Vector{x, y}, l.Start = Vector{x, y}
End: Vector{x2, y2}, l.End = Vector{x2, y2}
} return l
} }
func (line *Line) Normal() Vector { func (line *Line) Normal() Vector {
@@ -82,10 +78,9 @@ type ConvexPolygon struct {
// polygon square, with the vertices at {0,0}, {10,0}, {10, 10}, and {0, 10}. // polygon square, with the vertices at {0,0}, {10,0}, {10, 10}, and {0, 10}.
func NewConvexPolygon(points ...float64) *ConvexPolygon { func NewConvexPolygon(points ...float64) *ConvexPolygon {
cp := &ConvexPolygon{ cp := &ConvexPolygon{}
Points: NewRingBuffer(6), // I don't expected more points to be coped with in this particular game cp.Points = NewRingBuffer(6) // I don't expected more points to be coped with in this particular game
Closed: true, cp.Closed = true
}
cp.AddPoints(points...) cp.AddPoints(points...)
@@ -188,7 +183,7 @@ func (cp *ConvexPolygon) Bounds() (Vector, Vector) {
transformed := cp.Transformed() transformed := cp.Transformed()
topLeft := Vector{transformed[0][0], transformed[0][1]} topLeft := Vector{transformed[0][0], transformed[0][1]}
bottomRight := topLeft.Clone() bottomRight := Vector{transformed[0][0], transformed[0][1]}
for i := 0; i < len(transformed); i++ { for i := 0; i < len(transformed); i++ {
@@ -225,8 +220,8 @@ func (cp *ConvexPolygon) SetPosition(x, y float64) {
// SetPositionVec allows you to set the position of the ConvexPolygon using a Vector. The offset of the vertices compared to the X and Y // SetPositionVec allows you to set the position of the ConvexPolygon using a Vector. The offset of the vertices compared to the X and Y
// position is relative to however you initially defined the polygon and added the vertices. // position is relative to however you initially defined the polygon and added the vertices.
func (cp *ConvexPolygon) SetPositionVec(vec Vector) { func (cp *ConvexPolygon) SetPositionVec(vec Vector) {
cp.X = vec.X() cp.X = vec.GetX()
cp.Y = vec.Y() cp.Y = vec.GetY()
} }
// Move translates the ConvexPolygon by the designated X and Y values. // Move translates the ConvexPolygon by the designated X and Y values.
@@ -237,25 +232,8 @@ func (cp *ConvexPolygon) Move(x, y float64) {
// MoveVec translates the ConvexPolygon by the designated Vector. // MoveVec translates the ConvexPolygon by the designated Vector.
func (cp *ConvexPolygon) MoveVec(vec Vector) { func (cp *ConvexPolygon) MoveVec(vec Vector) {
cp.X += vec.X() cp.X += vec.GetX()
cp.Y += vec.Y() cp.Y += vec.GetY()
}
// Project projects (i.e. flattens) the ConvexPolygon onto the provided axis.
func (cp *ConvexPolygon) Project(axis Vector) Projection {
axis = axis.Unit()
vertices := cp.Transformed()
min := axis.Dot(Vector{vertices[0][0], vertices[0][1]})
max := min
for i := 1; i < len(vertices); i++ {
p := axis.Dot(Vector{vertices[i][0], vertices[i][1]})
if p < min {
min = p
} else if p > max {
max = p
}
}
return Projection{min, max}
} }
// SATAxes returns the axes of the ConvexPolygon for SAT intersection testing. // SATAxes returns the axes of the ConvexPolygon for SAT intersection testing.
@@ -294,11 +272,11 @@ type ContactSet struct {
} }
func NewContactSet() *ContactSet { func NewContactSet() *ContactSet {
return &ContactSet{ cs := &ContactSet{}
Points: []Vector{}, cs.Points = []Vector{}
MTV: Vector{0, 0}, cs.MTV = Vector{0, 0}
Center: Vector{0, 0}, cs.Center = Vector{}
} return cs
} }
// LeftmostPoint returns the left-most point out of the ContactSet's Points slice. If the Points slice is empty somehow, this returns nil. // LeftmostPoint returns the left-most point out of the ContactSet's Points slice. If the Points slice is empty somehow, this returns nil.
@@ -421,22 +399,3 @@ func NewRectangle(x, y, w, h float64) *ConvexPolygon {
x, y+h, x, y+h,
) )
} }
type Projection struct {
Min, Max float64
}
// Overlapping returns whether a Projection is overlapping with the other, provided Projection. Credit to https://www.sevenson.com.au/programming/sat/
func (projection Projection) Overlapping(other Projection) bool {
return projection.Overlap(other) > 0
}
// Overlap returns the amount that a Projection is overlapping with the other, provided Projection. Credit to https://dyn4j.org/2010/01/sat/#sat-nointer
func (projection Projection) Overlap(other Projection) float64 {
return math.Min(projection.Max, other.Max) - math.Max(projection.Min, other.Min)
}
// IsInside returns whether the Projection is wholly inside of the other, provided Projection.
func (projection Projection) IsInside(other Projection) bool {
return projection.Min >= other.Min && projection.Max <= other.Max
}

View File

@@ -0,0 +1,112 @@
package resolv
import "unsafe"
const (
uvnan = 0x7FF8000000000001
uvinf = 0x7FF0000000000000
uvneginf = 0xFFF0000000000000
uvone = 0x3FF0000000000000
mask = 0x7FF
shift = 64 - 11 - 1
bias = 1023
signMask = 1 << 63
fracMask = 1<<shift - 1
MaxFloat64 = 1.79e+308
magic32 = 0x5f3759df
magic64 = 0x5fe6eb50c7b537a9
)
func Max(a, b float64) float64 {
if a > b {
return a
} else {
return b
}
}
func Min(a, b float64) float64 {
if a < b {
return a
} else {
return b
}
}
func Floor(x float64) float64 {
if x == 0 || IsInf(x, 0) || IsNaN(x) {
return x
}
if x < 0 {
d, fract := Modf(-x)
if fract != 0.0 {
d = d + 1
}
return -d
}
d, _ := Modf(x)
return d
}
func Modf(f float64) (outval float64, frac float64) {
if f < 1 {
if f < 0 {
outval1, frac1 := Modf(-f)
return -outval1, -frac1
} else if f == 0 {
return f, f // Return -0, -0 when f == -0
}
return 0, f
}
x := Float64bits(f)
e := ((uint)(x>>shift))&mask - bias
// Keep the top 12+e bits, the integer part; clear the rest.
if e < 64-12 {
x &^= 1<<(64-12-e) - 1
}
outval = Float64frombits(x)
frac = f - outval
return
}
func Float32bits(f float32) uint32 { return *(*uint32)(unsafe.Pointer(&f)) }
func Float32frombits(b uint32) float32 { return *(*float32)(unsafe.Pointer(&b)) }
func Float64bits(f float64) uint64 { return *(*uint64)(unsafe.Pointer(&f)) }
func Float64frombits(b uint64) float64 { return *(*float64)(unsafe.Pointer(&b)) }
func NaN() float64 { return Float64frombits(uvnan) }
func IsNaN(f float64) (is bool) {
return f != f
}
func IsInf(f float64, sign int) bool {
return sign >= 0 && f > MaxFloat64 || sign <= 0 && f < -MaxFloat64
}
// FastInvSqrt reference https://medium.com/@adrien.za/fast-inverse-square-root-in-go-and-javascript-for-fun-6b891e74e5a8
func FastInvSqrt32(n float32) float32 {
if n < 0 {
return float32(NaN())
}
n2, th := n*0.5, float32(1.5)
b := Float32bits(n)
b = magic32 - (b >> 1)
f := Float32frombits(b)
f *= th - (n2 * f * f)
return f
}
func FastInvSqrt64(n float64) float64 {
if n < 0 {
return NaN()
}
n2, th := n*0.5, float64(1.5)
b := Float64bits(n)
b = magic64 - (b >> 1)
f := Float64frombits(b)
f *= th - (n2 * f * f)
return f
}

View File

@@ -1,9 +1,5 @@
package resolv package resolv
import (
"math"
)
// Space represents a collision space. Internally, each Space contains a 2D array of Cells, with each Cell being the same size. Cells contain information on which // Space represents a collision space. Internally, each Space contains a 2D array of Cells, with each Cell being the same size. Cells contain information on which
// Objects occupy those spaces. // Objects occupy those spaces.
type Space struct { type Space struct {
@@ -16,16 +12,12 @@ type Space struct {
// speed of one cell size per collision check to avoid missing any possible collisions. // speed of one cell size per collision check to avoid missing any possible collisions.
func NewSpace(spaceWidth, spaceHeight, cellWidth, cellHeight int) *Space { func NewSpace(spaceWidth, spaceHeight, cellWidth, cellHeight int) *Space {
sp := &Space{ sp := &Space{}
CellWidth: cellWidth, sp.CellWidth = cellWidth
CellHeight: cellHeight, sp.CellHeight = cellHeight
}
sp.Resize(spaceWidth/cellWidth, spaceHeight/cellHeight) sp.Resize(spaceWidth/cellWidth, spaceHeight/cellHeight)
// sp.Resize(int(math.Ceil(float64(spaceWidth)/float64(cellWidth))),
// int(math.Ceil(float64(spaceHeight)/float64(cellHeight))))
return sp return sp
} }
@@ -101,10 +93,10 @@ func (sp *Space) Objects() []*Object {
objectsAdded := map[*Object]bool{} objectsAdded := map[*Object]bool{}
objects := []*Object{} objects := []*Object{}
cyUpper := len(sp.Cells)
for cy := range sp.Cells { for cy := 0; cy < cyUpper; cy++ {
cxUpper := len(sp.Cells[cy])
for cx := range sp.Cells[cy] { for cx := 0; cx < cxUpper; cx++ {
rb := sp.Cells[cy][cx].Objects rb := sp.Cells[cy][cx].Objects
for i := rb.StFrameId; i < rb.EdFrameId; i++ { for i := rb.StFrameId; i < rb.EdFrameId; i++ {
o := rb.GetByFrameId(i).(*Object) o := rb.GetByFrameId(i).(*Object)
@@ -132,9 +124,7 @@ func (sp *Space) Resize(width, height int) {
} }
} }
// Cell returns the Cell at the given cellular / spatial (not world) X and Y position in the Space. If the X and Y position are func (sp *Space) GetCell(x, y int) *Cell {
// out of bounds, Cell() will return nil.
func (sp *Space) Cell(x, y int) *Cell {
if y >= 0 && y < len(sp.Cells) && x >= 0 && x < len(sp.Cells[y]) { if y >= 0 && y < len(sp.Cells) && x >= 0 && x < len(sp.Cells[y]) {
return sp.Cells[y][x] return sp.Cells[y][x]
@@ -151,7 +141,7 @@ func (sp *Space) CheckCells(x, y, w, h int, tags ...string) *Object {
for iy := y; iy < y+h; iy++ { for iy := y; iy < y+h; iy++ {
cell := sp.Cell(ix, iy) cell := sp.GetCell(ix, iy)
if cell != nil { if cell != nil {
rb := cell.Objects rb := cell.Objects
@@ -208,8 +198,9 @@ func (sp *Space) UnregisterAllObjects() {
// WorldToSpace converts from a world position (x, y) to a position in the Space (a grid-based position). // WorldToSpace converts from a world position (x, y) to a position in the Space (a grid-based position).
func (sp *Space) WorldToSpace(x, y float64) (int, int) { func (sp *Space) WorldToSpace(x, y float64) (int, int) {
fx := int(math.Floor(x / float64(sp.CellWidth))) // [WARNING] DON'T use "int(...)" syntax to convert float to int, it's not supported by go2cs!
fy := int(math.Floor(y / float64(sp.CellHeight))) var fx int = (int)(Floor(x / float64(sp.CellWidth)))
var fy int = (int)(Floor(y / float64(sp.CellHeight)))
return fx, fy return fx, fy
} }
@@ -236,8 +227,8 @@ func (sp *Space) Width() int {
func (sp *Space) CellsInLine(startX, startY, endX, endY int) []*Cell { func (sp *Space) CellsInLine(startX, startY, endX, endY int) []*Cell {
cells := []*Cell{} cells := []*Cell{}
cell := sp.Cell(startX, startY) cell := sp.GetCell(startX, startY)
endCell := sp.Cell(endX, endY) endCell := sp.GetCell(endX, endY)
if cell != nil && endCell != nil { if cell != nil && endCell != nil {
@@ -266,7 +257,7 @@ func (sp *Space) CellsInLine(startX, startY, endX, endY int) []*Cell {
} }
cx, cy := sp.WorldToSpace(p[0], p[1]) cx, cy := sp.WorldToSpace(p[0], p[1])
c := sp.Cell(cx, cy) c := sp.GetCell(cx, cy)
if c != cell { if c != cell {
cell = c cell = c
} }

View File

@@ -1,8 +1,6 @@
package resolv package resolv
import ( import "math"
"math"
)
// Vector is the definition of a row vector that contains scalars as // Vector is the definition of a row vector that contains scalars as
// 64 bit floats // 64 bit floats
@@ -14,108 +12,13 @@ type Axis int
const ( const (
// the consts below are used to represent vector axis, they are useful // the consts below are used to represent vector axis, they are useful
// to lookup values within the vector. // to lookup values within the vector.
X Axis = iota X Axis = 0
Y Y Axis = 1
Z Z Axis = 2
) )
// Clone a vector
func Clone(v Vector) Vector {
return v.Clone()
}
// Clone a vector
func (v Vector) Clone() Vector {
clone := make(Vector, len(v))
copy(clone, v)
return clone
}
/*
// Add a vector with a vector or a set of vectors
func Add(v1 Vector, vs ...Vector) Vector {
return v1.Clone().Add(vs...)
}
// Add a vector with a vector or a set of vectors
func (v Vector) Add(vs ...Vector) Vector {
dim := len(v)
for i := range vs {
if len(vs[i]) > dim {
axpyUnitaryTo(v, 1, v, vs[i][:dim])
} else {
axpyUnitaryTo(v, 1, v, vs[i])
}
}
return v
}
// Sub subtracts a vector with another vector or a set of vectors
func Sub(v1 Vector, vs ...Vector) Vector {
return v1.Clone().Sub(vs...)
}
// Sub subtracts a vector with another vector or a set of vectors
func (v Vector) Sub(vs ...Vector) Vector {
dim := len(v)
for i := range vs {
if len(vs[i]) > dim {
axpyUnitaryTo(v, -1, vs[i][:dim], v)
} else {
axpyUnitaryTo(v, -1, vs[i], v)
}
}
return v
}
// Scale vector with a given size
func Scale(v Vector, size float64) Vector {
return v.Clone().Scale(size)
}
// Scale vector with a given size
func (v Vector) Scale(size float64) Vector {
scalUnitaryTo(v, size, v)
return v
}
*/
// Equal compares that two vectors are equal to each other
func Equal(v1, v2 Vector) bool {
return v1.Equal(v2)
}
// Equal compares that two vectors are equal to each other
func (v Vector) Equal(v2 Vector) bool {
if len(v) != len(v2) {
return false
}
for i := range v {
if math.Abs(v[i]-v2[i]) > 1e-8 {
return false
}
}
return true
}
// Magnitude of a vector
func Magnitude(v Vector) float64 {
return v.Magnitude()
}
// Magnitude of a vector
func (v Vector) Magnitude() float64 {
return math.Sqrt(v.Magnitude2())
}
func (v Vector) Magnitude2() float64 { func (v Vector) Magnitude2() float64 {
var result float64 var result float64 = 0.
for _, scalar := range v { for _, scalar := range v {
result += scalar * scalar result += scalar * scalar
@@ -124,129 +27,19 @@ func (v Vector) Magnitude2() float64 {
return result return result
} }
// Unit returns a direction vector with the length of one.
func Unit(v Vector) Vector {
return v.Clone().Unit()
}
// Unit returns a direction vector with the length of one. // Unit returns a direction vector with the length of one.
func (v Vector) Unit() Vector { func (v Vector) Unit() Vector {
l := v.Magnitude() l2 := v.Magnitude2()
if l2 < 1e-16 {
if l < 1e-8 {
return v return v
} }
for i := range v { l := math.Sqrt(l2)
//inv := FastInvSqrt64(l2) // "Fast Inverse Square Root" is arch dependent, it's by far non-trivial to use it in Golang as well as make it feasible in the transpiled JavaScript.
for i := 0; i < len(v); i++ {
v[i] = v[i] / l v[i] = v[i] / l
} //v[i] = v[i] * inv
return v
}
// Dot product of two vectors
func Dot(v1, v2 Vector) float64 {
result, dim1, dim2 := 0., len(v1), len(v2)
if dim1 > dim2 {
v2 = append(v2, make(Vector, dim1-dim2)...)
}
if dim1 < dim2 {
v1 = append(v1, make(Vector, dim2-dim1)...)
}
for i := range v1 {
result += v1[i] * v2[i]
}
return result
}
// Dot product of two vectors
func (v Vector) Dot(v2 Vector) float64 {
return Dot(v, v2)
}
// Cross product of two vectors
func Cross(v1, v2 Vector) Vector {
return v1.Cross(v2)
}
// Cross product of two vectors
func (v Vector) Cross(v2 Vector) Vector {
if len(v) != 3 || len(v2) != 3 {
return nil
}
return Vector{
v[Y]*v2[Z] - v[Z]*v2[Y],
v[Z]*v2[X] - v[X]*v2[Z],
v[X]*v2[Z] - v[Z]*v2[X],
}
}
// Rotate is rotating a vector around a specified axis.
// If no axis are specified, it will default to the Z axis.
//
// If a vector with more than 3-dimensions is rotated, it will cut the extra
// dimensions and return a 3-dimensional vector.
//
// NOTE: the ...Axis is just syntactic sugar that allows the axis to not be
// specified and default to Z, if multiple axis is passed the first will be
// set as the rotation axis
func Rotate(v Vector, angle float64, as ...Axis) Vector {
return v.Clone().Rotate(angle, as...)
}
// Rotate is rotating a vector around a specified axis.
// If no axis are specified, it will default to the Z axis.
//
// If a vector with more than 3-dimensions is rotated, it will cut the extra
// dimensions and return a 3-dimensional vector.
//
// NOTE: the ...Axis is just syntactic sugar that allows the axis to not be
// specified and default to Z, if multiple axis is passed the first will be
// set as the rotation axis
func (v Vector) Rotate(angle float64, as ...Axis) Vector {
axis, dim := Z, len(v)
if dim == 0 {
return v
}
if len(as) > 0 {
axis = as[0]
}
if dim == 1 && axis != Z {
v = append(v, 0, 0)
}
if (dim < 2 && axis == Z) || (dim == 2 && axis != Z) {
v = append(v, 0)
}
x, y := v[X], v[Y]
cos, sin := math.Cos(angle), math.Sin(angle)
switch axis {
case X:
z := v[Z]
v[Y] = y*cos - z*sin
v[Z] = y*sin + z*cos
case Y:
z := v[Z]
v[X] = x*cos + z*sin
v[Z] = -x*sin + z*cos
case Z:
v[X] = x*cos - y*sin
v[Y] = x*sin + y*cos
}
if dim > 3 {
return v[:3]
} }
return v return v
@@ -254,7 +47,7 @@ func (v Vector) Rotate(angle float64, as ...Axis) Vector {
// X is corresponding to doing a v[0] lookup, if index 0 does not exist yet, a // X is corresponding to doing a v[0] lookup, if index 0 does not exist yet, a
// 0 will be returned instead // 0 will be returned instead
func (v Vector) X() float64 { func (v Vector) GetX() float64 {
if len(v) < 1 { if len(v) < 1 {
return 0. return 0.
} }
@@ -264,7 +57,7 @@ func (v Vector) X() float64 {
// Y is corresponding to doing a v[1] lookup, if index 1 does not exist yet, a // Y is corresponding to doing a v[1] lookup, if index 1 does not exist yet, a
// 0 will be returned instead // 0 will be returned instead
func (v Vector) Y() float64 { func (v Vector) GetY() float64 {
if len(v) < 2 { if len(v) < 2 {
return 0. return 0.
} }
@@ -274,7 +67,7 @@ func (v Vector) Y() float64 {
// Z is corresponding to doing a v[2] lookup, if index 2 does not exist yet, a // Z is corresponding to doing a v[2] lookup, if index 2 does not exist yet, a
// 0 will be returned instead // 0 will be returned instead
func (v Vector) Z() float64 { func (v Vector) GetZ() float64 {
if len(v) < 3 { if len(v) < 3 {
return 0. return 0.
} }