Compare commits
13 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
7e89d703ba | ||
|
dc1e6d3e09 | ||
|
28e5c18f00 | ||
|
a241912e7a | ||
|
c582071f4f | ||
|
59d6300880 | ||
|
6713feded1 | ||
|
da1204dc63 | ||
|
ea14ced958 | ||
|
b9beee549f | ||
|
04d8013cbb | ||
|
6b503ec95d | ||
|
71f2a1ecdf |
29
README.md
@ -1,15 +1,21 @@
|
||||
Please refer to [DelayNoMoreUnity](https://github.com/genxium/DelayNoMoreUnity) for a Unity rebuild with .net backend.
|
||||
|
||||
# Preface
|
||||
|
||||
This project is a demo for a websocket-based rollback netcode inspired by [GGPO](https://github.com/pond3r/ggpo/blob/master/doc/README.md).
|
||||
This project is a demo for a websocket-based rollback netcode inspired by [GGPO](https://github.com/pond3r/ggpo/blob/master/doc/README.md).
|
||||
|
||||
[Demo recorded over INTERNET (Phone-Wifi v.s. PC-Wifi UDP holepunched) using an input delay of 6 frames](https://pan.baidu.com/s/1UArwqDShLoPjYppjjqsTqQ?pwd=10wc), and it feels SMOOTH when playing!
|
||||
|
||||

|
||||
|
||||
(battle between 2 celluar 4G users using Android phones, [original video here](https://pan.baidu.com/s/1RL-9M-cK8cFS_Q8afMTrJA?pwd=ryzv))
|
||||
(battle between 2 celluar 4G users using Android phones, [original video here](https://pan.baidu.com/s/1m50d-VZxEGT3IgeZtww49g?pwd=eqx1))
|
||||
|
||||

|
||||
|
||||
**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/c582071f4f2e3dd7e83d65562c7c99981252c358/jsexport/battle/battle.go#L647)
|
||||
- [change#2](https://github.com/genxium/DelayNoMore/blob/c582071f4f2e3dd7e83d65562c7c99981252c358/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**.
|
||||
- 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).
|
||||
@ -17,9 +23,9 @@ As lots of feedbacks ask for a discussion on using UDP instead, I tried to summa
|
||||
|
||||
|
||||
# Notable Features
|
||||
- Backend dynamics toggle via [Room.BackendDynamicsEnabled](https://github.com/genxium/DelayNoMore/blob/v0.9.14/battle_srv/models/room.go#L786)
|
||||
- Backend dynamics toggle via [Room.BackendDynamicsEnabled](https://github.com/genxium/DelayNoMore/blob/c582071f4f2e3dd7e83d65562c7c99981252c358/battle_srv/models/room.go#L147)
|
||||
- Recovery upon reconnection (only if backend dynamics is ON)
|
||||
- Automatically correction for "slow ticker", especially "active slow ticker" which is well-known to be a headache for input synchronization
|
||||
- Automatic correction for "slow ticker", especially "active slow ticker" which is well-known to be a headache for input synchronization
|
||||
- Frame data logging toggle for both frontend & backend, useful for debugging out of sync entities when developing new features
|
||||
|
||||
_(how input delay roughly works)_
|
||||
@ -92,7 +98,14 @@ The easy way is to try out 2 players with test accounts on a same machine.
|
||||
- Open one browser instance, visit _http://localhost:7456?expectedRoomId=1_, input `add`on the username box and click to request a captcha, this is a test account so a captcha would be returned by the backend and filled automatically (as shown in the figure below), then click and click to proceed to a matching scene.
|
||||
- Open another browser instance, visit _http://localhost:7456?expectedRoomId=1_, input `bdd`on the username box and click to request a captcha, this is another test account so a captcha would be returned by the backend and filled automatically, then click and click to proceed, when matched a `battle`(but no competition rule yet) would start.
|
||||
- Try out the onscreen virtual joysticks to move the cars and see if their movements are in-sync.
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## 2 Troubleshooting
|
||||
|
||||
@ -107,9 +120,9 @@ Just restart your `redis-server` process.
|
||||
The most important reason for not showing "PING value" is simple: in most games the "PING value" is collected by a dedicated kernel thread which doesn't interfere the UI thread or the primary networking thread. As this demo primarily runs on browser by far, I don't have this capability easily.
|
||||
|
||||
Moreover, in practice I found that to spot sync anomalies, the following tools are much more useful than the "PING VALUE".
|
||||
- Detection of [prediction mismatch on the frontend](https://github.com/genxium/DelayNoMore/blob/v0.9.19/frontend/assets/scripts/Map.js#L842).
|
||||
- Detection of [type#1 forceConfirmation on the backend](https://github.com/genxium/DelayNoMore/blob/v0.9.19/battle_srv/models/room.go#L1246).
|
||||
- Detection of [type#2 forceConfirmation on the backend](https://github.com/genxium/DelayNoMore/blob/v0.9.19/battle_srv/models/room.go#L1259).
|
||||
- Detection of [prediction mismatch on the frontend](https://github.com/genxium/DelayNoMore/blob/c582071f4f2e3dd7e83d65562c7c99981252c358/frontend/assets/scripts/Map.js#L968).
|
||||
- Detection of [type#1 forceConfirmation on the backend](https://github.com/genxium/DelayNoMore/blob/c582071f4f2e3dd7e83d65562c7c99981252c358/battle_srv/models/room.go#L1315).
|
||||
- Detection of [type#2 forceConfirmation on the backend](https://github.com/genxium/DelayNoMore/blob/c582071f4f2e3dd7e83d65562c7c99981252c358/battle_srv/models/room.go#L1328).
|
||||
|
||||
There's also some useful information displayed on the frontend when `true == Map.showNetworkDoctorInfo`.
|
||||

|
||||
|
@ -156,10 +156,9 @@ type Room struct {
|
||||
TmxPointsMap StrToVec2DListMap
|
||||
TmxPolygonsMap StrToPolygon2DListMap
|
||||
|
||||
rdfIdToActuallyUsedInput map[int32]*pb.InputFrameDownsync
|
||||
allowUpdateInputFrameInPlaceUponDynamics bool
|
||||
LastIndividuallyConfirmedInputFrameId []int32
|
||||
LastIndividuallyConfirmedInputList []uint64
|
||||
rdfIdToActuallyUsedInput map[int32]*pb.InputFrameDownsync
|
||||
LastIndividuallyConfirmedInputFrameId []int32
|
||||
LastIndividuallyConfirmedInputList []uint64
|
||||
|
||||
BattleUdpTunnelLock sync.Mutex
|
||||
BattleUdpTunnelAddr *pb.PeerUdpAddr
|
||||
@ -808,7 +807,6 @@ func (pR *Room) OnDismissed() {
|
||||
pR.RenderFrameBuffer = resolv.NewRingBuffer(pR.RenderCacheSize)
|
||||
pR.InputsBuffer = resolv.NewRingBuffer((pR.RenderCacheSize >> 1) + 1)
|
||||
pR.rdfIdToActuallyUsedInput = make(map[int32]*pb.InputFrameDownsync)
|
||||
pR.allowUpdateInputFrameInPlaceUponDynamics = true
|
||||
pR.LastIndividuallyConfirmedInputFrameId = make([]int32, pR.Capacity)
|
||||
for i := 0; i < pR.Capacity; i++ {
|
||||
pR.LastIndividuallyConfirmedInputFrameId[i] = MAGIC_LAST_SENT_INPUT_FRAME_ID_NORMAL_ADDED
|
||||
@ -1201,7 +1199,8 @@ func (pR *Room) markConfirmationIfApplicable(inputFrameUpsyncBatch []*pb.InputFr
|
||||
}
|
||||
if clientInputFrameId < player.LastConsecutiveRecvInputFrameId {
|
||||
// [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
|
||||
}
|
||||
if clientInputFrameId > pR.InputsBuffer.EdFrameId {
|
||||
@ -1214,13 +1213,13 @@ func (pR *Room) markConfirmationIfApplicable(inputFrameUpsyncBatch []*pb.InputFr
|
||||
targetInputFrameDownsync.ConfirmedList |= uint64(1 << uint32(player.JoinIndex-1))
|
||||
|
||||
if false == fromUDP {
|
||||
/*
|
||||
[WARNING] We have to distinguish whether or not the incoming batch is from UDP here, otherwise "pR.LatestPlayerUpsyncedInputFrameId - pR.LastAllConfirmedInputFrameId" might become unexpectedly large in case of "UDP packet loss + slow ws session"!
|
||||
/**
|
||||
[WARNING] We have to distinguish whether or not the incoming batch is from UDP here, otherwise "pR.LatestPlayerUpsyncedInputFrameId - pR.LastAllConfirmedInputFrameId" might become unexpectedly large in case of "UDP packet loss + slow ws session"!
|
||||
|
||||
Moreover, only ws session upsyncs should advance "player.LastConsecutiveRecvInputFrameId" & "pR.LatestPlayerUpsyncedInputFrameId".
|
||||
Moreover, only ws session upsyncs should advance "player.LastConsecutiveRecvInputFrameId" & "pR.LatestPlayerUpsyncedInputFrameId".
|
||||
|
||||
Kindly note that the updates of "player.LastConsecutiveRecvInputFrameId" could be discrete before and after reconnection.
|
||||
*/
|
||||
Kindly note that the updates of "player.LastConsecutiveRecvInputFrameId" could be discrete before and after reconnection.
|
||||
*/
|
||||
player.LastConsecutiveRecvInputFrameId = clientInputFrameId
|
||||
if clientInputFrameId > pR.LatestPlayerUpsyncedInputFrameId {
|
||||
pR.LatestPlayerUpsyncedInputFrameId = clientInputFrameId
|
||||
@ -1259,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
|
||||
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)))
|
||||
}
|
||||
}
|
||||
|
||||
@ -1283,7 +1282,7 @@ func (pR *Room) markConfirmationIfApplicable(inputFrameUpsyncBatch []*pb.InputFr
|
||||
snapshotStFrameId := (pR.LastAllConfirmedInputFrameId - newAllConfirmedCount)
|
||||
refRenderFrameIdIfNeeded := pR.CurDynamicsRenderFrameId - 1
|
||||
refSnapshotStFrameId := battle.ConvertToDelayedInputFrameId(refRenderFrameIdIfNeeded)
|
||||
if refSnapshotStFrameId < snapshotStFrameId {
|
||||
if pR.BackendDynamicsEnabled && refSnapshotStFrameId < snapshotStFrameId {
|
||||
snapshotStFrameId = refSnapshotStFrameId
|
||||
}
|
||||
Logger.Debug(fmt.Sprintf("markConfirmationIfApplicable for roomId=%v returning newAllConfirmedCount=%d: InputsBuffer=%v", pR.Id, newAllConfirmedCount, pR.InputsBufferString(false)))
|
||||
@ -1308,9 +1307,6 @@ 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)))
|
||||
}
|
||||
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)
|
||||
inputFrameDownsync.ConfirmedList = allConfirmedMask
|
||||
pR.onInputFrameDownsyncAllConfirmed(inputFrameDownsync, -1)
|
||||
@ -1340,7 +1336,7 @@ func (pR *Room) forceConfirmationIfApplicable(prevRenderFrameId int32) uint64 {
|
||||
func (pR *Room) produceInputsBufferSnapshotWithCurDynamicsRenderFrameAsRef(unconfirmedMask uint64, snapshotStFrameId, snapshotEdFrameId int32) *pb.InputsBufferSnapshot {
|
||||
// [WARNING] This function MUST BE called while "pR.InputsBufferLock" is locked!
|
||||
refRenderFrameIdIfNeeded := pR.CurDynamicsRenderFrameId - 1
|
||||
if 0 > refRenderFrameIdIfNeeded {
|
||||
if pR.BackendDynamicsEnabled && 0 > refRenderFrameIdIfNeeded {
|
||||
return nil
|
||||
}
|
||||
// Duplicate downsynced inputFrameIds will be filtered out by frontend.
|
||||
@ -1391,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++
|
||||
}
|
||||
}
|
||||
|
BIN
charts/How-to-play-1.png
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
charts/How-to-play-2.png
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
charts/How-to-play-3.png
Normal file
After Width: | Height: | Size: 92 KiB |
BIN
charts/How-to-play-4.png
Normal file
After Width: | Height: | Size: 64 KiB |
Before Width: | Height: | Size: 4.4 MiB After Width: | Height: | Size: 22 MiB |
Before Width: | Height: | Size: 684 KiB |
@ -518,7 +518,7 @@
|
||||
"array": [
|
||||
0,
|
||||
0,
|
||||
217.5376534000385,
|
||||
210.4441731196186,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
|
@ -512,7 +512,8 @@ cc.Class({
|
||||
window.clearBoundRoomIdInBothVolatileAndPersistentStorage();
|
||||
window.initPersistentSessionClient(self.initAfterWSConnected, null /* Deliberately NOT passing in any `expectedRoomId`. -- YFLu */ );
|
||||
};
|
||||
resultPanelScriptIns.onCloseDelegate = () => {};
|
||||
resultPanelScriptIns.onCloseDelegate = () => {
|
||||
};
|
||||
|
||||
self.gameRuleNode = cc.instantiate(self.gameRulePrefab);
|
||||
self.gameRuleNode.width = self.canvasNode.width;
|
||||
@ -713,9 +714,9 @@ cc.Class({
|
||||
if (notSelfUnconfirmed) {
|
||||
shouldForceDumping2 = 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=${rdf.GetId()}
|
||||
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}]`);
|
||||
}
|
||||
@ -947,7 +948,7 @@ accompaniedInputFrameDownsyncBatchRange=[${accompaniedInputFrameDownsyncBatch[0]
|
||||
_handleIncorrectlyRenderedPrediction(firstPredictedYetIncorrectInputFrameId, batch, fromUDP) {
|
||||
if (null == firstPredictedYetIncorrectInputFrameId) return;
|
||||
const self = this;
|
||||
const renderFrameId1 = gopkgs.ConvertToFirstUsedRenderFrameId(firstPredictedYetIncorrectInputFrameId) - 1;
|
||||
const renderFrameId1 = gopkgs.ConvertToFirstUsedRenderFrameId(firstPredictedYetIncorrectInputFrameId);
|
||||
if (renderFrameId1 >= self.chaserRenderFrameId) return;
|
||||
|
||||
/*
|
||||
@ -1002,7 +1003,7 @@ fromUDP=${fromUDP}`);
|
||||
const inputFrameId = inputFrame.inputFrameId;
|
||||
const peerEncodedInput = (true == fromUDP ? inputFrame.encoded : inputFrame.inputList[peerJoinIndex - 1]);
|
||||
if (false == self.allowRollbackOnPeerUpsync && inputFrameId <= renderedInputFrameIdUpper) {
|
||||
// [WARNING] Avoid obfuscating already rendered history, even at "inputFrameId == renderedInputFrameIdUpper", due to the use of "INPUT_SCALE_FRAMES" some previous render frames might already be rendered with "inputFrameId"!
|
||||
// [WARNING] Avoid obfuscating already rendered history, even at "inputFrameId == renderedInputFrameIdUpper", due to the use of "INPUT_SCALE_FRAMES" some previous render frames might've already been rendered with "inputFrameId"!
|
||||
continue;
|
||||
}
|
||||
if (inputFrameId <= self.lastAllConfirmedInputFrameId) {
|
||||
@ -1021,12 +1022,11 @@ fromUDP=${fromUDP}`);
|
||||
self.lastIndividuallyConfirmedInputList[peerJoinIndex - 1] = peerEncodedInput;
|
||||
}
|
||||
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 value in "newInputList"!
|
||||
const existingInputList = existingInputFrame.GetInputList();
|
||||
let newInputList = existingInputFrame.GetInputList().slice();
|
||||
newInputList[peerJoinIndex - 1] = peerEncodedInput;
|
||||
let newConfirmedList = (existingConfirmedList | peerJoinIndexMask);
|
||||
const newInputFrameDownsyncLocal = gopkgs.NewInputFrameDownsync(inputFrameId, newInputList, newConfirmedList);
|
||||
const newInputFrameDownsyncLocal = gopkgs.NewInputFrameDownsync(inputFrameId, newInputList, existingConfirmedList);
|
||||
//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);
|
||||
|
||||
@ -1047,6 +1047,15 @@ fromUDP=${fromUDP}`);
|
||||
self.networkDoctor.logPeerInputFrameUpsync(batch[0].inputFrameId, batch[batch.length - 1].inputFrameId);
|
||||
}
|
||||
if (true == self.allowRollbackOnPeerUpsync) {
|
||||
/*
|
||||
[WARNING]
|
||||
|
||||
Deliberately NOT setting "existingInputFrame.ConfirmedList = (existingConfirmedList | peerJoinIndexMask)", thus NOT helping the move of "lastAllConfirmedInputFrameId" in "_markConfirmationIfApplicable()".
|
||||
|
||||
The edge case of concern here is "type#1 forceConfirmation". Assume that there is a battle among [P_u, P_v, P_x, P_y] where [P_x] is being an "ActiveSlowerTicker", then for [P_u, P_v, P_y] there might've been some "inputFrameUpsync"s received from [P_x] by UDP peer-to-peer transmission EARLIER THAN BUT CONFLICTING WITH the "accompaniedInputFrameDownsyncBatch of type#1 forceConfirmation" -- in such case the latter should be respected -- by "conflicting", the backend actually ignores those "inputFrameUpsync"s from [P_x] by "forceConfirmation".
|
||||
|
||||
However, we should still call "_handleIncorrectlyRenderedPrediction(...)" here to break rollbacks into smaller chunks, because even if not used for "inputFrameDownsync.ConfirmedList", a "UDP inputFrameUpsync" is still more accurate than the locally predicted inputs.
|
||||
*/
|
||||
self._handleIncorrectlyRenderedPrediction(firstPredictedYetIncorrectInputFrameId, batch, fromUDP);
|
||||
}
|
||||
},
|
||||
@ -1164,25 +1173,25 @@ fromUDP=${fromUDP}`);
|
||||
rollbackFrames = 0;
|
||||
}
|
||||
self.networkDoctor.logRollbackFrames(rollbackFrames);
|
||||
let prevRdf = latestRdfResults[0],
|
||||
rdf = latestRdfResults[1];
|
||||
let prevRdf = latestRdfResults[0], // Having "prevRdf.Id == self.renderFrameId"
|
||||
rdf = latestRdfResults[1]; // Having "rdf.Id == self.renderFrameId+1"
|
||||
/*
|
||||
const nonTrivialChaseEnded = (prevChaserRenderFrameId < nextChaserRenderFrameId && nextChaserRenderFrameId == self.renderFrameId);
|
||||
if (nonTrivialChaseEnded) {
|
||||
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())) {
|
||||
const delayedInputFrameId = gopkgs.ConvertToDelayedInputFrameId(rdf.GetId());
|
||||
const othersForcedDownsyncRenderFrame = self.othersForcedDownsyncRenderFrameDict.get(rdf.GetId());
|
||||
const [pbOthersForcedDownsyncRenderFrame, othersForcedDownsyncRenderFrame] = self.othersForcedDownsyncRenderFrameDict.get(rdf.GetId());
|
||||
if (self.lastAllConfirmedInputFrameId >= delayedInputFrameId && !self.equalRoomDownsyncFrames(othersForcedDownsyncRenderFrame, rdf)) {
|
||||
if (CC_DEBUG) {
|
||||
console.warn(`Mismatched render frame@rdf.id=${rdf.GetId()} w/ inputFrameId=${delayedInputFrameId}:
|
||||
rdf=${self._stringifyGopkgRdfForFrameDataLogging(rdf)}
|
||||
othersForcedDownsyncRenderFrame=${self._stringifyGopkgRdfForFrameDataLogging(othersForcedDownsyncRenderFrame)}`);
|
||||
}
|
||||
rdf = 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());
|
||||
}
|
||||
}
|
||||
@ -1438,16 +1447,15 @@ othersForcedDownsyncRenderFrame=${self._stringifyGopkgRdfForFrameDataLogging(oth
|
||||
const j = gopkgs.ConvertToDelayedInputFrameId(i);
|
||||
const delayedInputFrame = gopkgs.GetInputFrameDownsync(self.recentInputCache, j);
|
||||
|
||||
const allowUpdateInputFrameInPlaceUponDynamics = (!isChasing);
|
||||
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) {
|
||||
/*
|
||||
The backend counterpart doesn't need this rollback because
|
||||
1. Backend only applies all-confirmed inputFrames to calc dynamics.
|
||||
2. Backend applies an all-confirmed inputFrame to all applicable render frames at once.
|
||||
*/
|
||||
[WARNING]
|
||||
If we don't rollback at this spot, when the mutated "delayedInputFrame.inputList" a.k.a. "inputFrame#j" matches the later downsynced version, rollback WOULDN'T be triggered for the incorrectly rendered "renderFrame#(ii+1)", and it would STAY IN HISTORY FOREVER -- as the history becomes incorrect, EVERY LATEST renderFrame since "inputFrame#j" was mutated would be ALWAYS incorrectly rendering too!
|
||||
*/
|
||||
self._handleIncorrectlyRenderedPrediction(j, null, false);
|
||||
}
|
||||
}
|
||||
|
@ -100,7 +100,7 @@ bool RecvRingBuff::pop(RecvWork* out) {
|
||||
2. If "0 >= oldCnt", we need guard against another "pop" to avoid over-popping.
|
||||
*/
|
||||
if (0 >= oldCnt) {
|
||||
// "pop" could be accessed by either "GameThread/pollUdpRecvRingBuff" or "UvRecvThread/put", thus we should be proactively guard against concurrent popping while "1 == cnt"
|
||||
// "pop" could be accessed by either "GameThread/pollUdpRecvRingBuff" or "UvRecvThread/put", thus we should be proactively guarding against concurrent popping while "1 == cnt"
|
||||
++cnt;
|
||||
return false;
|
||||
}
|
||||
|
@ -372,7 +372,7 @@ bool DelayNoMore::UdpSession::pollUdpRecvRingBuff() {
|
||||
// This function is called by GameThread 60 fps.
|
||||
|
||||
//uv_mutex_lock(&recvRingBuffLock);
|
||||
while (true) {
|
||||
while (true && NULL != recvLoop) {
|
||||
RecvWork f;
|
||||
bool res = recvRingBuff->pop(&f);
|
||||
if (!res) {
|
||||
|
@ -76,7 +76,7 @@
|
||||
"shelter_z_reducer",
|
||||
"shelter"
|
||||
],
|
||||
"last-module-event-record-time": 1677337364473,
|
||||
"last-module-event-record-time": 1680159688511,
|
||||
"simulator-orientation": false,
|
||||
"simulator-resolution": {
|
||||
"height": 640,
|
||||
|
@ -7,7 +7,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
MAX_FLOAT64 = 1.7e+308
|
||||
MAX_FLOAT64 = resolv.MaxFloat64
|
||||
MAX_INT32 = int32(999999999)
|
||||
COLLISION_PLAYER_INDEX_PREFIX = (1 << 17)
|
||||
COLLISION_BARRIER_INDEX_PREFIX = (1 << 16)
|
||||
@ -655,7 +655,7 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *resolv.Rin
|
||||
|
||||
jumpedOrNotList[i] = jumpedOrNot
|
||||
joinIndex := currPlayerDownsync.JoinIndex
|
||||
skillId := chConfig.SkillMapper(patternId, currPlayerDownsync)
|
||||
skillId := chConfig.SkillMapper(patternId, currPlayerDownsync, chConfig.SpeciesId)
|
||||
if skillConfig, existent := skills[skillId]; existent {
|
||||
thatPlayerInNextFrame.ActiveSkillId = int32(skillId)
|
||||
thatPlayerInNextFrame.ActiveSkillHit = 0
|
||||
@ -786,7 +786,7 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *resolv.Rin
|
||||
// Revive from Dying
|
||||
newVx, newVy = currPlayerDownsync.RevivalVirtualGridX, currPlayerDownsync.RevivalVirtualGridY
|
||||
thatPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_GET_UP1
|
||||
thatPlayerInNextFrame.FramesInChState = ATK_CHARACTER_STATE_GET_UP1
|
||||
thatPlayerInNextFrame.FramesInChState = 0
|
||||
thatPlayerInNextFrame.FramesToRecover = chConfig.GetUpFramesToRecover
|
||||
thatPlayerInNextFrame.FramesInvinsible = chConfig.GetUpInvinsibleFrames
|
||||
thatPlayerInNextFrame.Hp = currPlayerDownsync.MaxHp
|
||||
|
@ -1,6 +1,6 @@
|
||||
package battle
|
||||
|
||||
type SkillMapperType func(patternId int, currPlayerDownsync *PlayerDownsync) int
|
||||
type SkillMapperType = func(patternId int, currPlayerDownsync *PlayerDownsync, speciesId int) int
|
||||
|
||||
type CharacterConfig struct {
|
||||
SpeciesId int
|
||||
@ -31,6 +31,118 @@ type CharacterConfig struct {
|
||||
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{
|
||||
0: &CharacterConfig{
|
||||
SpeciesId: 0,
|
||||
@ -58,41 +170,7 @@ var Characters = map[int]*CharacterConfig{
|
||||
WallJumpingInitVelY: int32(float64(7) * WORLD_TO_VIRTUAL_GRID_RATIO),
|
||||
WallSlidingVelY: int32(float64(-1) * WORLD_TO_VIRTUAL_GRID_RATIO),
|
||||
|
||||
SkillMapper: func(patternId int, currPlayerDownsync *PlayerDownsync) int {
|
||||
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
|
||||
},
|
||||
SkillMapper: defaultSkillMapper,
|
||||
},
|
||||
1: &CharacterConfig{
|
||||
SpeciesId: 1,
|
||||
@ -120,40 +198,7 @@ var Characters = map[int]*CharacterConfig{
|
||||
WallJumpingInitVelY: int32(float64(7) * WORLD_TO_VIRTUAL_GRID_RATIO),
|
||||
WallSlidingVelY: int32(float64(-1) * WORLD_TO_VIRTUAL_GRID_RATIO),
|
||||
|
||||
SkillMapper: func(patternId int, currPlayerDownsync *PlayerDownsync) int {
|
||||
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
|
||||
},
|
||||
SkillMapper: defaultSkillMapper,
|
||||
},
|
||||
4096: &CharacterConfig{
|
||||
SpeciesId: 4096,
|
||||
@ -177,45 +222,7 @@ var Characters = map[int]*CharacterConfig{
|
||||
DashingEnabled: true,
|
||||
OnWallEnabled: false,
|
||||
|
||||
SkillMapper: func(patternId int, currPlayerDownsync *PlayerDownsync) int {
|
||||
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
|
||||
},
|
||||
SkillMapper: defaultSkillMapper,
|
||||
},
|
||||
}
|
||||
|
||||
@ -226,7 +233,7 @@ var skills = map[int]*Skill{
|
||||
RecoveryFramesOnHit: int32(30),
|
||||
ReleaseTriggerType: int32(1),
|
||||
BoundChState: ATK_CHARACTER_STATE_ATK1,
|
||||
Hits: []interface{}{
|
||||
Hits: []AnyBullet{
|
||||
&MeleeBullet{
|
||||
Bullet: &BulletConfig{
|
||||
StartupFrames: int32(7),
|
||||
@ -261,7 +268,7 @@ var skills = map[int]*Skill{
|
||||
RecoveryFramesOnHit: int32(36),
|
||||
ReleaseTriggerType: int32(1),
|
||||
BoundChState: ATK_CHARACTER_STATE_ATK2,
|
||||
Hits: []interface{}{
|
||||
Hits: []AnyBullet{
|
||||
&MeleeBullet{
|
||||
Bullet: &BulletConfig{
|
||||
StartupFrames: int32(18),
|
||||
@ -295,7 +302,7 @@ var skills = map[int]*Skill{
|
||||
RecoveryFramesOnHit: int32(50),
|
||||
ReleaseTriggerType: int32(1),
|
||||
BoundChState: ATK_CHARACTER_STATE_ATK3,
|
||||
Hits: []interface{}{
|
||||
Hits: []AnyBullet{
|
||||
&MeleeBullet{
|
||||
Bullet: &BulletConfig{
|
||||
StartupFrames: int32(8),
|
||||
@ -324,7 +331,7 @@ var skills = map[int]*Skill{
|
||||
RecoveryFramesOnHit: int32(30),
|
||||
ReleaseTriggerType: int32(1),
|
||||
BoundChState: ATK_CHARACTER_STATE_ATK1,
|
||||
Hits: []interface{}{
|
||||
Hits: []AnyBullet{
|
||||
&MeleeBullet{
|
||||
Bullet: &BulletConfig{
|
||||
StartupFrames: int32(7),
|
||||
@ -359,7 +366,7 @@ var skills = map[int]*Skill{
|
||||
RecoveryFramesOnHit: int32(36),
|
||||
ReleaseTriggerType: int32(1),
|
||||
BoundChState: ATK_CHARACTER_STATE_ATK2,
|
||||
Hits: []interface{}{
|
||||
Hits: []AnyBullet{
|
||||
&MeleeBullet{
|
||||
Bullet: &BulletConfig{
|
||||
StartupFrames: int32(18),
|
||||
@ -393,7 +400,7 @@ var skills = map[int]*Skill{
|
||||
RecoveryFramesOnHit: int32(45),
|
||||
ReleaseTriggerType: int32(1),
|
||||
BoundChState: ATK_CHARACTER_STATE_ATK3,
|
||||
Hits: []interface{}{
|
||||
Hits: []AnyBullet{
|
||||
&MeleeBullet{
|
||||
Bullet: &BulletConfig{
|
||||
StartupFrames: int32(8),
|
||||
@ -422,7 +429,7 @@ var skills = map[int]*Skill{
|
||||
RecoveryFramesOnHit: int32(30),
|
||||
ReleaseTriggerType: int32(1),
|
||||
BoundChState: ATK_CHARACTER_STATE_ATK1,
|
||||
Hits: []interface{}{
|
||||
Hits: []AnyBullet{
|
||||
&MeleeBullet{
|
||||
Bullet: &BulletConfig{
|
||||
StartupFrames: int32(7),
|
||||
@ -457,7 +464,7 @@ var skills = map[int]*Skill{
|
||||
RecoveryFramesOnHit: int32(36),
|
||||
ReleaseTriggerType: int32(1),
|
||||
BoundChState: ATK_CHARACTER_STATE_ATK2,
|
||||
Hits: []interface{}{
|
||||
Hits: []AnyBullet{
|
||||
&MeleeBullet{
|
||||
Bullet: &BulletConfig{
|
||||
StartupFrames: int32(18),
|
||||
@ -491,7 +498,7 @@ var skills = map[int]*Skill{
|
||||
RecoveryFramesOnHit: int32(40),
|
||||
ReleaseTriggerType: int32(1),
|
||||
BoundChState: ATK_CHARACTER_STATE_ATK3,
|
||||
Hits: []interface{}{
|
||||
Hits: []AnyBullet{
|
||||
&MeleeBullet{
|
||||
Bullet: &BulletConfig{
|
||||
StartupFrames: int32(7),
|
||||
@ -520,7 +527,7 @@ var skills = map[int]*Skill{
|
||||
RecoveryFramesOnHit: int32(38),
|
||||
ReleaseTriggerType: int32(1),
|
||||
BoundChState: ATK_CHARACTER_STATE_ATK4,
|
||||
Hits: []interface{}{
|
||||
Hits: []AnyBullet{
|
||||
&FireballBullet{
|
||||
Speed: int32(float64(6) * WORLD_TO_VIRTUAL_GRID_RATIO),
|
||||
Bullet: &BulletConfig{
|
||||
@ -550,7 +557,7 @@ var skills = map[int]*Skill{
|
||||
RecoveryFramesOnHit: int32(60),
|
||||
ReleaseTriggerType: int32(1),
|
||||
BoundChState: ATK_CHARACTER_STATE_ATK5,
|
||||
Hits: []interface{}{
|
||||
Hits: []AnyBullet{
|
||||
&MeleeBullet{
|
||||
Bullet: &BulletConfig{
|
||||
StartupFrames: int32(3),
|
||||
@ -579,7 +586,7 @@ var skills = map[int]*Skill{
|
||||
RecoveryFramesOnHit: int32(10),
|
||||
ReleaseTriggerType: int32(1),
|
||||
BoundChState: ATK_CHARACTER_STATE_DASHING,
|
||||
Hits: []interface{}{
|
||||
Hits: []AnyBullet{
|
||||
&MeleeBullet{
|
||||
Bullet: &BulletConfig{
|
||||
StartupFrames: int32(3),
|
||||
@ -606,7 +613,7 @@ var skills = map[int]*Skill{
|
||||
RecoveryFramesOnHit: int32(12),
|
||||
ReleaseTriggerType: int32(1),
|
||||
BoundChState: ATK_CHARACTER_STATE_DASHING,
|
||||
Hits: []interface{}{
|
||||
Hits: []AnyBullet{
|
||||
&MeleeBullet{
|
||||
Bullet: &BulletConfig{
|
||||
StartupFrames: int32(3),
|
||||
@ -633,7 +640,7 @@ var skills = map[int]*Skill{
|
||||
RecoveryFramesOnHit: int32(8),
|
||||
ReleaseTriggerType: int32(1),
|
||||
BoundChState: ATK_CHARACTER_STATE_DASHING,
|
||||
Hits: []interface{}{
|
||||
Hits: []AnyBullet{
|
||||
&MeleeBullet{
|
||||
Bullet: &BulletConfig{
|
||||
StartupFrames: int32(4),
|
||||
@ -660,7 +667,7 @@ var skills = map[int]*Skill{
|
||||
RecoveryFramesOnHit: int32(48),
|
||||
ReleaseTriggerType: int32(1),
|
||||
BoundChState: ATK_CHARACTER_STATE_ATK4,
|
||||
Hits: []interface{}{
|
||||
Hits: []AnyBullet{
|
||||
&FireballBullet{
|
||||
Speed: int32(float64(4) * WORLD_TO_VIRTUAL_GRID_RATIO),
|
||||
Bullet: &BulletConfig{
|
||||
@ -690,7 +697,7 @@ var skills = map[int]*Skill{
|
||||
RecoveryFramesOnHit: int32(60),
|
||||
ReleaseTriggerType: int32(1),
|
||||
BoundChState: ATK_CHARACTER_STATE_ATK4,
|
||||
Hits: []interface{}{
|
||||
Hits: []AnyBullet{
|
||||
&FireballBullet{
|
||||
Speed: int32(float64(4) * WORLD_TO_VIRTUAL_GRID_RATIO),
|
||||
Bullet: &BulletConfig{
|
||||
@ -720,7 +727,7 @@ var skills = map[int]*Skill{
|
||||
RecoveryFramesOnHit: int32(30),
|
||||
ReleaseTriggerType: int32(1),
|
||||
BoundChState: ATK_CHARACTER_STATE_INAIR_ATK1,
|
||||
Hits: []interface{}{
|
||||
Hits: []AnyBullet{
|
||||
&MeleeBullet{
|
||||
Bullet: &BulletConfig{
|
||||
StartupFrames: int32(3),
|
||||
@ -749,7 +756,7 @@ var skills = map[int]*Skill{
|
||||
RecoveryFramesOnHit: int32(20),
|
||||
ReleaseTriggerType: int32(1),
|
||||
BoundChState: ATK_CHARACTER_STATE_INAIR_ATK1,
|
||||
Hits: []interface{}{
|
||||
Hits: []AnyBullet{
|
||||
&MeleeBullet{
|
||||
Bullet: &BulletConfig{
|
||||
StartupFrames: int32(3),
|
||||
@ -778,7 +785,7 @@ var skills = map[int]*Skill{
|
||||
RecoveryFramesOnHit: int32(30),
|
||||
ReleaseTriggerType: int32(1),
|
||||
BoundChState: ATK_CHARACTER_STATE_INAIR_ATK1,
|
||||
Hits: []interface{}{
|
||||
Hits: []AnyBullet{
|
||||
&MeleeBullet{
|
||||
Bullet: &BulletConfig{
|
||||
StartupFrames: int32(4),
|
||||
|
@ -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
|
||||
|
||||
type AnyBullet interface{}
|
||||
|
||||
type Vec2D struct {
|
||||
X float64
|
||||
Y float64
|
||||
@ -132,7 +134,7 @@ type Skill struct {
|
||||
RecoveryFramesOnHit int32
|
||||
ReleaseTriggerType int32 // 1: rising-edge, 2: falling-edge
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -8,11 +8,11 @@ type Cell struct {
|
||||
|
||||
// newCell creates a new cell at the specified X and Y position. Should not be used directly.
|
||||
func newCell(x, y int) *Cell {
|
||||
return &Cell{
|
||||
X: x,
|
||||
Y: y,
|
||||
Objects: NewRingBuffer(16), // A single cell is so small thus wouldn't have many touching objects simultaneously
|
||||
}
|
||||
c := &Cell{}
|
||||
c.X = x
|
||||
c.Y = y
|
||||
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.
|
||||
|
@ -10,10 +10,10 @@ type Collision struct {
|
||||
}
|
||||
|
||||
func NewCollision() *Collision {
|
||||
return &Collision{
|
||||
Objects: NewRingBuffer(16), // I don't expect it to exceed 10 actually
|
||||
Cells: NewRingBuffer(16),
|
||||
}
|
||||
c := &Collision{}
|
||||
c.Objects = NewRingBuffer(16) // I don't expect it to exceed 10 actually
|
||||
c.Cells = NewRingBuffer(16)
|
||||
return c
|
||||
}
|
||||
|
||||
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().
|
||||
func (cc *Collision) ObjectsByTags(tags ...string) []*Object {
|
||||
|
||||
objects := []*Object{}
|
||||
objs := []*Object{}
|
||||
|
||||
rb := cc.Objects
|
||||
for i := rb.StFrameId; i < rb.EdFrameId; i++ {
|
||||
@ -61,30 +61,30 @@ func (cc *Collision) ObjectsByTags(tags ...string) []*Object {
|
||||
continue
|
||||
}
|
||||
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.
|
||||
func (cc *Collision) ContactWithObject(object *Object) Vector {
|
||||
func (cc *Collision) ContactWithObject(obj *Object) Vector {
|
||||
|
||||
delta := Vector{0, 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 {
|
||||
delta[0] = object.X - cc.checkingObject.W - cc.checkingObject.X
|
||||
delta[0] = obj.X - cc.checkingObject.W - cc.checkingObject.X
|
||||
}
|
||||
|
||||
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 {
|
||||
delta[1] = object.Y - cc.checkingObject.H - cc.checkingObject.Y
|
||||
delta[1] = obj.Y - cc.checkingObject.H - cc.checkingObject.Y
|
||||
}
|
||||
|
||||
return delta
|
||||
@ -135,10 +135,10 @@ func (cc *Collision) SlideAgainstCell(cell *Cell, avoidTags ...string) Vector {
|
||||
diffX := oX - ccX
|
||||
diffY := oY - ccY
|
||||
|
||||
left := sp.Cell(collidingCell.X-1, collidingCell.Y)
|
||||
right := sp.Cell(collidingCell.X+1, collidingCell.Y)
|
||||
up := sp.Cell(collidingCell.X, collidingCell.Y-1)
|
||||
down := sp.Cell(collidingCell.X, collidingCell.Y+1)
|
||||
left := sp.GetCell(collidingCell.X-1, collidingCell.Y)
|
||||
right := sp.GetCell(collidingCell.X+1, collidingCell.Y)
|
||||
up := sp.GetCell(collidingCell.X, collidingCell.Y-1)
|
||||
down := sp.GetCell(collidingCell.X, collidingCell.Y+1)
|
||||
|
||||
slide := Vector{0, 0}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -1,9 +1,5 @@
|
||||
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.
|
||||
type Object struct {
|
||||
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.
|
||||
func NewObjectSingleTag(x, y, w, h float64, tag string) *Object {
|
||||
o := &Object{
|
||||
X: x,
|
||||
Y: y,
|
||||
W: w,
|
||||
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!
|
||||
tags: []string{tag},
|
||||
ignoreList: map[*Object]bool{},
|
||||
}
|
||||
o := &Object{}
|
||||
o.X = x
|
||||
o.Y = y
|
||||
o.W = w
|
||||
o.H = h
|
||||
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!
|
||||
o.tags = []string{tag}
|
||||
o.ignoreList = make(map[*Object]bool)
|
||||
|
||||
return o
|
||||
}
|
||||
|
||||
func NewObject(x, y, w, h float64, tags ...string) *Object {
|
||||
o := &Object{
|
||||
X: x,
|
||||
Y: y,
|
||||
W: w,
|
||||
H: h,
|
||||
TouchingCells: NewRingBuffer(512),
|
||||
tags: []string{},
|
||||
ignoreList: map[*Object]bool{},
|
||||
}
|
||||
o := &Object{}
|
||||
o.X = x
|
||||
o.Y = y
|
||||
o.W = w
|
||||
o.H = h
|
||||
o.TouchingCells = NewRingBuffer(512)
|
||||
o.tags = []string{}
|
||||
o.ignoreList = make(map[*Object]bool)
|
||||
|
||||
if len(tags) > 0 {
|
||||
o.AddTags(tags...)
|
||||
@ -67,7 +61,7 @@ func (obj *Object) Clone() *Object {
|
||||
if obj.Shape != nil {
|
||||
newObj.SetShape(obj.Shape.Clone())
|
||||
}
|
||||
for k := range obj.ignoreList {
|
||||
for k, _ := range obj.ignoreList {
|
||||
newObj.AddToIgnoreList(k)
|
||||
}
|
||||
return newObj
|
||||
@ -95,7 +89,7 @@ func (obj *Object) Update() {
|
||||
|
||||
for x := cx; x <= ex; x++ {
|
||||
|
||||
c := obj.Space.Cell(x, y)
|
||||
c := obj.Space.GetCell(x, y)
|
||||
|
||||
if c != nil {
|
||||
c.register(obj)
|
||||
@ -258,15 +252,15 @@ func (obj *Object) CheckAllWithHolder(dx, dy float64, cc *Collision) bool {
|
||||
cc.checkingObject = obj
|
||||
|
||||
if dx < 0 {
|
||||
dx = math.Min(dx, -1)
|
||||
dx = Min(dx, -1)
|
||||
} else if dx > 0 {
|
||||
dx = math.Max(dx, 1)
|
||||
dx = Max(dx, 1)
|
||||
}
|
||||
|
||||
if dy < 0 {
|
||||
dy = math.Min(dy, -1)
|
||||
dy = Min(dy, -1)
|
||||
} else if dy > 0 {
|
||||
dy = math.Max(dy, 1)
|
||||
dy = Max(dy, 1)
|
||||
}
|
||||
|
||||
cc.dx = dx
|
||||
@ -281,7 +275,7 @@ func (obj *Object) CheckAllWithHolder(dx, dy float64, cc *Collision) bool {
|
||||
|
||||
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
|
||||
for i := rb.StFrameId; i < rb.EdFrameId; i++ {
|
||||
|
@ -1,11 +1,14 @@
|
||||
package resolv
|
||||
|
||||
const (
|
||||
RING_BUFF_CONSECUTIVE_SET = int32(0)
|
||||
RING_BUFF_NON_CONSECUTIVE_SET = int32(1)
|
||||
RING_BUFF_FAILED_TO_SET = int32(2)
|
||||
// Declare type "int32" explicitly to prevent go2cs from transpiling them to "var"
|
||||
RING_BUFF_CONSECUTIVE_SET int32 = 0
|
||||
RING_BUFF_NON_CONSECUTIVE_SET int32 = 1
|
||||
RING_BUFF_FAILED_TO_SET int32 = 2
|
||||
)
|
||||
|
||||
type AnyObj interface{}
|
||||
|
||||
type RingBuffer struct {
|
||||
Ed int32 // write index, open index
|
||||
St int32 // read index, closed index
|
||||
@ -13,19 +16,19 @@ type RingBuffer struct {
|
||||
StFrameId 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
|
||||
Eles []interface{}
|
||||
Eles []AnyObj
|
||||
}
|
||||
|
||||
func NewRingBuffer(n int32) *RingBuffer {
|
||||
return &RingBuffer{
|
||||
Ed: 0,
|
||||
St: 0,
|
||||
EdFrameId: 0,
|
||||
StFrameId: 0,
|
||||
N: n,
|
||||
Cnt: 0,
|
||||
Eles: make([]interface{}, n),
|
||||
}
|
||||
ret := &RingBuffer{}
|
||||
ret.Ed = 0
|
||||
ret.St = 0
|
||||
ret.EdFrameId = 0
|
||||
ret.StFrameId = 0
|
||||
ret.N = n
|
||||
ret.Cnt = 0
|
||||
ret.Eles = make([]AnyObj, n)
|
||||
return ret
|
||||
}
|
||||
|
||||
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 {
|
||||
// Make room for the new element
|
||||
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 {
|
||||
return nil
|
||||
}
|
||||
@ -93,7 +96,7 @@ func (rb *RingBuffer) GetArrIdxByOffset(offsetFromSt int32) int32 {
|
||||
return -1
|
||||
}
|
||||
|
||||
func (rb *RingBuffer) GetByOffset(offsetFromSt int32) interface{} {
|
||||
func (rb *RingBuffer) GetByOffset(offsetFromSt int32) AnyObj {
|
||||
arrIdx := rb.GetArrIdxByOffset(offsetFromSt)
|
||||
if -1 == arrIdx {
|
||||
return nil
|
||||
@ -101,7 +104,7 @@ func (rb *RingBuffer) GetByOffset(offsetFromSt int32) interface{} {
|
||||
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 {
|
||||
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.
|
||||
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
|
||||
if frameId < oldStFrameId {
|
||||
return RING_BUFF_FAILED_TO_SET, oldStFrameId, oldEdFrameId
|
||||
|
@ -1,9 +1,5 @@
|
||||
package resolv
|
||||
|
||||
import (
|
||||
"math"
|
||||
)
|
||||
|
||||
type Shape interface {
|
||||
// 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
|
||||
@ -27,10 +23,10 @@ type Line struct {
|
||||
}
|
||||
|
||||
func NewLine(x, y, x2, y2 float64) *Line {
|
||||
return &Line{
|
||||
Start: Vector{x, y},
|
||||
End: Vector{x2, y2},
|
||||
}
|
||||
l := &Line{}
|
||||
l.Start = Vector{x, y}
|
||||
l.End = Vector{x2, y2}
|
||||
return l
|
||||
}
|
||||
|
||||
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}.
|
||||
func NewConvexPolygon(points ...float64) *ConvexPolygon {
|
||||
|
||||
cp := &ConvexPolygon{
|
||||
Points: NewRingBuffer(6), // I don't expected more points to be coped with in this particular game
|
||||
Closed: true,
|
||||
}
|
||||
cp := &ConvexPolygon{}
|
||||
cp.Points = NewRingBuffer(6) // I don't expected more points to be coped with in this particular game
|
||||
cp.Closed = true
|
||||
|
||||
cp.AddPoints(points...)
|
||||
|
||||
@ -188,7 +183,7 @@ func (cp *ConvexPolygon) Bounds() (Vector, Vector) {
|
||||
transformed := cp.Transformed()
|
||||
|
||||
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++ {
|
||||
|
||||
@ -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
|
||||
// position is relative to however you initially defined the polygon and added the vertices.
|
||||
func (cp *ConvexPolygon) SetPositionVec(vec Vector) {
|
||||
cp.X = vec.X()
|
||||
cp.Y = vec.Y()
|
||||
cp.X = vec.GetX()
|
||||
cp.Y = vec.GetY()
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (cp *ConvexPolygon) MoveVec(vec Vector) {
|
||||
cp.X += vec.X()
|
||||
cp.Y += vec.Y()
|
||||
}
|
||||
|
||||
// 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}
|
||||
cp.X += vec.GetX()
|
||||
cp.Y += vec.GetY()
|
||||
}
|
||||
|
||||
// SATAxes returns the axes of the ConvexPolygon for SAT intersection testing.
|
||||
@ -294,11 +272,11 @@ type ContactSet struct {
|
||||
}
|
||||
|
||||
func NewContactSet() *ContactSet {
|
||||
return &ContactSet{
|
||||
Points: []Vector{},
|
||||
MTV: Vector{0, 0},
|
||||
Center: Vector{0, 0},
|
||||
}
|
||||
cs := &ContactSet{}
|
||||
cs.Points = []Vector{}
|
||||
cs.MTV = 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.
|
||||
@ -421,22 +399,3 @@ func NewRectangle(x, y, w, h float64) *ConvexPolygon {
|
||||
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
|
||||
}
|
||||
|
112
resolv_tailored/simple_math.go
Normal 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
|
||||
}
|
@ -1,9 +1,5 @@
|
||||
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
|
||||
// Objects occupy those spaces.
|
||||
type Space struct {
|
||||
@ -16,16 +12,12 @@ type Space struct {
|
||||
// speed of one cell size per collision check to avoid missing any possible collisions.
|
||||
func NewSpace(spaceWidth, spaceHeight, cellWidth, cellHeight int) *Space {
|
||||
|
||||
sp := &Space{
|
||||
CellWidth: cellWidth,
|
||||
CellHeight: cellHeight,
|
||||
}
|
||||
sp := &Space{}
|
||||
sp.CellWidth = cellWidth
|
||||
sp.CellHeight = 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
|
||||
|
||||
}
|
||||
@ -101,10 +93,10 @@ func (sp *Space) Objects() []*Object {
|
||||
|
||||
objectsAdded := map[*Object]bool{}
|
||||
objects := []*Object{}
|
||||
|
||||
for cy := range sp.Cells {
|
||||
|
||||
for cx := range sp.Cells[cy] {
|
||||
cyUpper := len(sp.Cells)
|
||||
for cy := 0; cy < cyUpper; cy++ {
|
||||
cxUpper := len(sp.Cells[cy])
|
||||
for cx := 0; cx < cxUpper; cx++ {
|
||||
rb := sp.Cells[cy][cx].Objects
|
||||
for i := rb.StFrameId; i < rb.EdFrameId; i++ {
|
||||
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
|
||||
// out of bounds, Cell() will return nil.
|
||||
func (sp *Space) Cell(x, y int) *Cell {
|
||||
func (sp *Space) GetCell(x, y int) *Cell {
|
||||
|
||||
if y >= 0 && y < len(sp.Cells) && x >= 0 && x < len(sp.Cells[y]) {
|
||||
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++ {
|
||||
|
||||
cell := sp.Cell(ix, iy)
|
||||
cell := sp.GetCell(ix, iy)
|
||||
|
||||
if cell != nil {
|
||||
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).
|
||||
func (sp *Space) WorldToSpace(x, y float64) (int, int) {
|
||||
fx := int(math.Floor(x / float64(sp.CellWidth)))
|
||||
fy := int(math.Floor(y / float64(sp.CellHeight)))
|
||||
// [WARNING] DON'T use "int(...)" syntax to convert float to int, it's not supported by go2cs!
|
||||
var fx int = (int)(Floor(x / float64(sp.CellWidth)))
|
||||
var fy int = (int)(Floor(y / float64(sp.CellHeight)))
|
||||
return fx, fy
|
||||
}
|
||||
|
||||
@ -236,8 +227,8 @@ func (sp *Space) Width() int {
|
||||
func (sp *Space) CellsInLine(startX, startY, endX, endY int) []*Cell {
|
||||
|
||||
cells := []*Cell{}
|
||||
cell := sp.Cell(startX, startY)
|
||||
endCell := sp.Cell(endX, endY)
|
||||
cell := sp.GetCell(startX, startY)
|
||||
endCell := sp.GetCell(endX, endY)
|
||||
|
||||
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])
|
||||
c := sp.Cell(cx, cy)
|
||||
c := sp.GetCell(cx, cy)
|
||||
if c != cell {
|
||||
cell = c
|
||||
}
|
||||
|
@ -1,8 +1,6 @@
|
||||
package resolv
|
||||
|
||||
import (
|
||||
"math"
|
||||
)
|
||||
import "math"
|
||||
|
||||
// Vector is the definition of a row vector that contains scalars as
|
||||
// 64 bit floats
|
||||
@ -14,108 +12,13 @@ type Axis int
|
||||
const (
|
||||
// the consts below are used to represent vector axis, they are useful
|
||||
// to lookup values within the vector.
|
||||
X Axis = iota
|
||||
Y
|
||||
Z
|
||||
X Axis = 0
|
||||
Y Axis = 1
|
||||
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 {
|
||||
var result float64
|
||||
var result float64 = 0.
|
||||
|
||||
for _, scalar := range v {
|
||||
result += scalar * scalar
|
||||
@ -124,129 +27,19 @@ func (v Vector) Magnitude2() float64 {
|
||||
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.
|
||||
func (v Vector) Unit() Vector {
|
||||
l := v.Magnitude()
|
||||
|
||||
if l < 1e-8 {
|
||||
l2 := v.Magnitude2()
|
||||
if l2 < 1e-16 {
|
||||
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
|
||||
}
|
||||
|
||||
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]
|
||||
//v[i] = v[i] * inv
|
||||
}
|
||||
|
||||
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
|
||||
// 0 will be returned instead
|
||||
func (v Vector) X() float64 {
|
||||
func (v Vector) GetX() float64 {
|
||||
if len(v) < 1 {
|
||||
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
|
||||
// 0 will be returned instead
|
||||
func (v Vector) Y() float64 {
|
||||
func (v Vector) GetY() float64 {
|
||||
if len(v) < 2 {
|
||||
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
|
||||
// 0 will be returned instead
|
||||
func (v Vector) Z() float64 {
|
||||
func (v Vector) GetZ() float64 {
|
||||
if len(v) < 3 {
|
||||
return 0.
|
||||
}
|
||||
|