Compare commits

...

25 Commits

Author SHA1 Message Date
Wing
7e89d703ba Fixed typo. 2023-11-28 08:37:11 +08:00
yflu
dc1e6d3e09 Updated frontend "onPeerInputFrameUpsync" handling for force confirmation. 2023-04-18 07:03:22 +08:00
genxium
28e5c18f00 Fixed typo. 2023-04-14 07:28:01 +08:00
genxium
a241912e7a Updated README. 2023-03-30 15:47:02 +08:00
genxium
c582071f4f Fixes for BackendDynamics disabled. 2023-03-30 15:05:34 +08:00
genxium
59d6300880 Updated README. 2023-03-18 13:22:58 +08:00
yflu
6713feded1 Added back important comment. 2023-03-16 17:35:47 +08:00
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
genxium
7fd96b335a Minor fix. 2023-02-26 23:33:47 +08:00
genxium
8cd5f1d475 Fixed resync. 2023-02-26 23:25:47 +08:00
genxium
21806a3754 Minor updates. 2023-02-26 20:57:40 +08:00
genxium
e213fdfb04 Removed redundant collision related codes. 2023-02-26 20:39:30 +08:00
genxium
b9827f8430 Refined experimental toggle for in-place inputsBuffer update upon dynamics. 2023-02-26 14:59:27 +08:00
genxium
91d16b1cc4 Added an experimental toggle for in-place inputsBuffer update upon dynamics. 2023-02-25 23:40:57 +08:00
genxium
b19868920a Minor update. 2023-02-25 23:27:01 +08:00
genxium
be5200663c Updated input prediction approach upon dynamics. 2023-02-25 23:05:25 +08:00
36 changed files with 617 additions and 1287 deletions

View File

@@ -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!
![Merged_cut_annotated_spedup](./charts/Merged_cut_annotated_spedup.gif)
(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))
![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/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.
![screenshot-2](./charts/screenshot-2.png)
![How-to-play-1](./charts/How-to-play-1.png)
![How-to-play-2](./charts/How-to-play-2.png)
![How-to-play-3](./charts/How-to-play-3.png)
![How-to-play-4](./charts/How-to-play-4.png)
## 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`.
![networkstats](./charts/networkstats.png)

View File

@@ -85,7 +85,6 @@ func (p *playerController) SMSCaptchaGet(c *gin.Context) {
c.Set(api.RET, Constants.RetCode.SmsCaptchaRequestedTooFrequently)
return
}
Logger.Info("A new SmsCaptcha record is needed for: ", zap.String("key", redisKey))
pass := false
var succRet int
if Conf.General.ServerEnv == SERVER_ENV_TEST {
@@ -93,32 +92,28 @@ func (p *playerController) SMSCaptchaGet(c *gin.Context) {
if nil == err && nil != player {
pass = true
succRet = Constants.RetCode.IsTestAcc
Logger.Info("A new SmsCaptcha record is needed for: ", zap.String("key", redisKey), zap.Any("player", player))
}
}
if !pass {
player, err := models.GetPlayerByName(req.Num)
if nil == err && nil != player {
pass = true
succRet = Constants.RetCode.IsBotAcc
}
}
if !pass {
if RE_PHONE_NUM.MatchString(req.Num) {
succRet = Constants.RetCode.Ok
pass = true
}
if req.CountryCode == "86" {
if RE_CHINA_PHONE_NUM.MatchString(req.Num) {
succRet = Constants.RetCode.Ok
pass = true
} else {
succRet = Constants.RetCode.InvalidRequestParam
pass = false
/*
// Real phonenum is not supported yet!
if !pass {
if RE_PHONE_NUM.MatchString(req.Num) {
succRet = Constants.RetCode.Ok
pass = true
}
if req.CountryCode == "86" {
if RE_CHINA_PHONE_NUM.MatchString(req.Num) {
succRet = Constants.RetCode.Ok
pass = true
} else {
succRet = Constants.RetCode.InvalidRequestParam
pass = false
}
}
}
}
}
*/
if !pass {
c.Set(api.RET, Constants.RetCode.InvalidRequestParam)
return
@@ -481,16 +476,6 @@ func (p *playerController) maybeCreateNewPlayer(req smsCaptchaReq) (*models.Play
Logger.Info("Got a test env player:", zap.Any("phonenum", req.Num), zap.Any("playerId", player.Id))
return player, nil
}
} else {
botPlayer, err := models.GetPlayerByName(req.Num)
if err != nil {
Logger.Error("Seeking bot player error:", zap.Error(err))
return nil, err
}
if botPlayer != nil {
Logger.Info("Got a bot player:", zap.Any("phonenum", req.Num), zap.Any("playerId", botPlayer.Id))
return botPlayer, nil
}
}
bind, err := models.GetPlayerAuthBinding(Constants.AuthChannel.Sms, extAuthID)

View File

@@ -74,7 +74,7 @@ func Test_SMSCaptchaGet_illegalPhone(t *testing.T) {
func Test_SMSCaptchaGet_testAcc(t *testing.T) {
player, err := getTestPlayer()
if err == nil && player != nil {
if nil == err && nil != player {
resp := mustDoSmsCaptchaGetReq(fakeSMSCaptchReq(player.Name), t)
if resp.Ret != Constants.RetCode.IsTestAcc {
t.Fail()

View File

@@ -47,11 +47,10 @@ type Player struct {
TutorialStage int `db:"tutorial_stage"`
// other in-battle info fields
LastReceivedInputFrameId int32
LastUdpReceivedInputFrameId int32
LastSentInputFrameId int32
AckingFrameId int32
AckingInputFrameId int32
LastConsecutiveRecvInputFrameId int32
LastSentInputFrameId int32
AckingFrameId int32
AckingInputFrameId int32
UdpAddr *PeerUdpAddr
BattleUdpTunnelAddr *net.UDPAddr // This addr is used by backend only, not visible to frontend
@@ -78,13 +77,14 @@ func getPlayer(cond sq.Eq) (*Player, error) {
return nil, err
}
rows, err := storage.MySQLManagerIns.Queryx(query, args...)
if err != nil {
if nil != err {
return nil, err
}
cols, err := rows.Columns()
if nil != err {
panic(err)
}
cnt := 0
for rows.Next() {
// TODO: Do it more elegantly, but by now I don't have time to learn reflection of Golang
vals := rowValues(rows, cols)
@@ -106,9 +106,14 @@ func getPlayer(cond sq.Eq) (*Player, error) {
}
}
Logger.Debug("Queried player from db", zap.Any("cond", cond), zap.Any("p", p), zap.Any("pd", pd), zap.Any("cols", cols), zap.Any("rowValues", vals))
cnt++
}
if 0 < cnt {
p.PlayerDownsync = pd
return &p, nil
} else {
return nil, nil
}
p.PlayerDownsync = pd
return &p, nil
}
func (p *Player) Insert(tx *sqlx.Tx) error {

View File

@@ -136,7 +136,7 @@ type Room struct {
EffectivePlayerCount int32
DismissalWaitGroup sync.WaitGroup
InputsBuffer *resolv.RingBuffer // Indices are STRICTLY consecutive
InputsBufferLock sync.Mutex // Guards [InputsBuffer, LatestPlayerUpsyncedInputFrameId, LastAllConfirmedInputFrameId, LastAllConfirmedInputList, LastAllConfirmedInputFrameIdWithChange, LastIndividuallyConfirmedInputList, player.LastReceivedInputFrameId, player.LastUdpReceivedInputFrameId]
InputsBufferLock sync.Mutex // Guards [InputsBuffer, LatestPlayerUpsyncedInputFrameId, LastAllConfirmedInputFrameId, LastAllConfirmedInputList, LastAllConfirmedInputFrameIdWithChange, LastIndividuallyConfirmedInputFrameId, LastIndividuallyConfirmedInputList, player.LastConsecutiveRecvInputFrameId]
RenderFrameBuffer *resolv.RingBuffer // Indices are STRICTLY consecutive
LatestPlayerUpsyncedInputFrameId int32
LastAllConfirmedInputFrameId int32
@@ -156,8 +156,9 @@ type Room struct {
TmxPointsMap StrToVec2DListMap
TmxPolygonsMap StrToPolygon2DListMap
rdfIdToActuallyUsedInput map[int32]*pb.InputFrameDownsync
LastIndividuallyConfirmedInputList []uint64
rdfIdToActuallyUsedInput map[int32]*pb.InputFrameDownsync
LastIndividuallyConfirmedInputFrameId []int32
LastIndividuallyConfirmedInputList []uint64
BattleUdpTunnelLock sync.Mutex
BattleUdpTunnelAddr *pb.PeerUdpAddr
@@ -194,8 +195,7 @@ func (pR *Room) AddPlayerIfPossible(pPlayerFromDbInit *Player, speciesId int, se
pPlayerFromDbInit.AckingFrameId = -1
pPlayerFromDbInit.AckingInputFrameId = -1
pPlayerFromDbInit.LastSentInputFrameId = MAGIC_LAST_SENT_INPUT_FRAME_ID_NORMAL_ADDED
pPlayerFromDbInit.LastReceivedInputFrameId = MAGIC_LAST_SENT_INPUT_FRAME_ID_NORMAL_ADDED
pPlayerFromDbInit.LastUdpReceivedInputFrameId = MAGIC_LAST_SENT_INPUT_FRAME_ID_NORMAL_ADDED
pPlayerFromDbInit.LastConsecutiveRecvInputFrameId = MAGIC_LAST_SENT_INPUT_FRAME_ID_NORMAL_ADDED
pPlayerFromDbInit.BattleState = PlayerBattleStateIns.ADDED_PENDING_BATTLE_COLLIDER_ACK
pPlayerFromDbInit.ColliderRadius = DEFAULT_PLAYER_RADIUS // Hardcoded
@@ -237,7 +237,7 @@ func (pR *Room) ReAddPlayerIfPossible(pTmpPlayerInstance *Player, session *webso
pEffectiveInRoomPlayerInstance.AckingFrameId = -1
pEffectiveInRoomPlayerInstance.AckingInputFrameId = -1
pEffectiveInRoomPlayerInstance.LastSentInputFrameId = MAGIC_LAST_SENT_INPUT_FRAME_ID_READDED
// [WARNING] DON'T reset "player.LastReceivedInputFrameId" & "player.LastUdpReceivedInputFrameId" upon reconnection!
// [WARNING] DON'T reset "player.LastConsecutiveRecvInputFrameId" & "pR.LastIndividuallyConfirmedInputFrameId[...]" upon reconnection!
pEffectiveInRoomPlayerInstance.BattleState = PlayerBattleStateIns.READDED_PENDING_BATTLE_COLLIDER_ACK
pEffectiveInRoomPlayerInstance.ColliderRadius = DEFAULT_PLAYER_RADIUS // Hardcoded
@@ -406,6 +406,9 @@ func (pR *Room) rdfIdToActuallyUsedInputString() string {
}
fireballsStrBldr := make([]string, 0, len(rdf.FireballBullets))
for _, fireball := range rdf.FireballBullets {
if 0 > fireball.BattleAttr.BulletLocalId {
break
}
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])))
@@ -800,10 +803,14 @@ func (pR *Room) OnDismissed() {
pR.PlayerSignalToCloseDict = make(map[int32]SignalToCloseConnCbType)
pR.PlayerSecondarySignalToCloseDict = make(map[int32]SignalToCloseConnCbType)
pR.JoinIndexBooleanArr = make([]bool, pR.Capacity)
pR.RenderCacheSize = 256
pR.RenderCacheSize = 1024
pR.RenderFrameBuffer = resolv.NewRingBuffer(pR.RenderCacheSize)
pR.InputsBuffer = resolv.NewRingBuffer((pR.RenderCacheSize >> 1) + 1)
pR.rdfIdToActuallyUsedInput = make(map[int32]*pb.InputFrameDownsync)
pR.LastIndividuallyConfirmedInputFrameId = make([]int32, pR.Capacity)
for i := 0; i < pR.Capacity; i++ {
pR.LastIndividuallyConfirmedInputFrameId[i] = MAGIC_LAST_SENT_INPUT_FRAME_ID_NORMAL_ADDED
}
pR.LastIndividuallyConfirmedInputList = make([]uint64, pR.Capacity)
pR.LatestPlayerUpsyncedInputFrameId = -1
@@ -817,7 +824,7 @@ func (pR *Room) OnDismissed() {
pR.collisionHolder = resolv.NewCollision()
pR.effPushbacks = make([]*battle.Vec2D, pR.Capacity)
for i := 0; i < len(pR.effPushbacks); i++ {
for i := 0; i < pR.Capacity; i++ {
pR.effPushbacks[i] = &battle.Vec2D{X: 0, Y: 0}
}
pR.hardPushbackNormsArr = make([][]*battle.Vec2D, pR.Capacity)
@@ -1190,9 +1197,10 @@ func (pR *Room) markConfirmationIfApplicable(inputFrameUpsyncBatch []*pb.InputFr
Logger.Debug(fmt.Sprintf("Omitting obsolete inputFrameUpsync#1: roomId=%v, playerId=%v, clientInputFrameId=%v, InputsBuffer=%v", pR.Id, playerId, clientInputFrameId, pR.InputsBufferString(false)))
continue
}
if clientInputFrameId < player.LastReceivedInputFrameId {
// [WARNING] It's important for correctness that we use "player.LastReceivedInputFrameId" instead of "player.LastUdpReceivedInputFrameId" here!
Logger.Debug(fmt.Sprintf("Omitting obsolete inputFrameUpsync#2: roomId=%v, playerId=%v, clientInputFrameId=%v, playerLastReceivedInputFrameId=%v, InputsBuffer=%v", pR.Id, playerId, clientInputFrameId, player.LastReceivedInputFrameId, pR.InputsBufferString(false)))
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", pR.Id, playerId, clientInputFrameId, player.LastConsecutiveRecvInputFrameId))
continue
}
if clientInputFrameId > pR.InputsBuffer.EdFrameId {
@@ -1205,22 +1213,22 @@ 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.LastReceivedInputFrameId" & "pR.LatestPlayerUpsyncedInputFrameId".
Moreover, only ws session upsyncs should advance "player.LastConsecutiveRecvInputFrameId" & "pR.LatestPlayerUpsyncedInputFrameId".
Kindly note that the updates of "player.LastReceivedInputFrameId" could be discrete before and after reconnection.
*/
player.LastReceivedInputFrameId = clientInputFrameId
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
}
}
if clientInputFrameId > player.LastUdpReceivedInputFrameId {
// No need to update "player.LastUdpReceivedInputFrameId" only when "true == fromUDP", we should keep "player.LastUdpReceivedInputFrameId >= player.LastReceivedInputFrameId" at any moment.
player.LastUdpReceivedInputFrameId = clientInputFrameId
if clientInputFrameId > pR.LastIndividuallyConfirmedInputFrameId[player.JoinIndex-1] {
// No need to update "pR.LastIndividuallyConfirmedInputFrameId[player.JoinIndex-1]" only when "true == fromUDP", we should keep "pR.LastIndividuallyConfirmedInputFrameId[player.JoinIndex-1] >= player.LastConsecutiveRecvInputFrameId" at any moment.
pR.LastIndividuallyConfirmedInputFrameId[player.JoinIndex-1] = clientInputFrameId
// It's safe (in terms of getting an eventually correct "RenderFrameBuffer") to put the following update of "pR.LastIndividuallyConfirmedInputList" which is ONLY used for prediction in "InputsBuffer" out of "false == fromUDP" block.
pR.LastIndividuallyConfirmedInputList[player.JoinIndex-1] = inputFrameUpsync.Encoded
}
@@ -1250,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)))
}
}
@@ -1274,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)))
@@ -1304,7 +1312,7 @@ func (pR *Room) forceConfirmationIfApplicable(prevRenderFrameId int32) uint64 {
pR.onInputFrameDownsyncAllConfirmed(inputFrameDownsync, -1)
}
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 {
// Type#2 helps resolve the edge case when all players are disconnected temporarily
@@ -1328,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.
@@ -1379,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)
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++
}
}
@@ -1530,6 +1538,7 @@ func (pR *Room) downsyncToAllPlayers(inputsBufferSnapshot *pb.InputsBufferSnapsh
break
}
}
}
for _, player := range pR.PlayersArr {
@@ -1546,11 +1555,16 @@ func (pR *Room) downsyncToAllPlayers(inputsBufferSnapshot *pb.InputsBufferSnapsh
if playerDownsyncChan, existent := pR.PlayerDownsyncChanDict[player.Id]; existent {
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 {
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) {
@@ -1593,9 +1607,10 @@ func (pR *Room) downsyncToSinglePlayer(playerId int32, player *Player, refRender
pbRefRenderFrame := toPbRoomDownsyncFrame(refRenderFrame)
pbRefRenderFrame.SpeciesIdList = pR.SpeciesIdList
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 {
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 {
pR.sendSafely(nil, toSendInputFrameDownsyncsSnapshot, DOWNSYNC_MSG_ACT_INPUT_BATCH, playerId, false, MAGIC_JOIN_INDEX_DEFAULT)

BIN
charts/How-to-play-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
charts/How-to-play-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
charts/How-to-play-3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

BIN
charts/How-to-play-4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 MiB

After

Width:  |  Height:  |  Size: 22 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 684 KiB

View File

@@ -241,7 +241,7 @@
"ctor": "Float64Array",
"array": [
-379.577,
90.576,
140,
0,
0,
0,
@@ -468,7 +468,7 @@
"ctor": "Float64Array",
"array": [
0,
-68.939,
-13.057,
0,
0,
0,

View File

@@ -400,7 +400,7 @@
"ctor": "Float64Array",
"array": [
0,
0,
32,
0,
0,
0,
@@ -536,7 +536,7 @@
"array": [
0,
0,
216.05530045313827,
210.43877906529718,
0,
0,
0,
@@ -2129,7 +2129,7 @@
"_left": 0,
"_right": 0,
"_top": 0,
"_bottom": 640,
"_bottom": 672,
"_verticalCenter": 0,
"_horizontalCenter": 0,
"_isAbsLeft": true,

View File

@@ -461,7 +461,7 @@
"array": [
0,
0,
210.43877906529718,
209.57814771583418,
0,
0,
0,

View File

@@ -382,7 +382,7 @@
"ctor": "Float64Array",
"array": [
0,
0,
32,
0,
0,
0,
@@ -518,7 +518,7 @@
"array": [
0,
0,
216.50635094610968,
210.4441731196186,
0,
0,
0,

View File

@@ -714,7 +714,12 @@ 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=${rdfId}
backendUnconfirmedMask=${pbRdf.backendUnconfirmedMask}
accompaniedInputFrameDownsyncBatchRange=[${null == accompaniedInputFrameDownsyncBatch ? null : accompaniedInputFrameDownsyncBatch[0].inputFrameId}, ${null == accompaniedInputFrameDownsyncBatch ? null : accompaniedInputFrameDownsyncBatch[accompaniedInputFrameDownsyncBatch.length - 1].inputFrameId}]`);
}
}
/*
TODO
@@ -745,10 +750,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
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 {
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;
@@ -866,13 +876,20 @@ cc.Class({
_markConfirmationIfApplicable() {
const self = this;
let newAllConfirmedCnt = 0;
while (self.recentInputCache.GetStFrameId() <= self.lastAllConfirmedInputFrameId && self.lastAllConfirmedInputFrameId < self.recentInputCache.GetEdFrameId()) {
const inputFrameDownsync = self.recentInputCache.GetByFrameId(self.lastAllConfirmedInputFrameId);
let candidateInputFrameId = (self.lastAllConfirmedInputFrameId + 1);
if (candidateInputFrameId < self.recentInputCache.GetStFrameId()) {
candidateInputFrameId = self.recentInputCache.GetStFrameId();
}
while (self.recentInputCache.GetStFrameId() <= candidateInputFrameId && candidateInputFrameId < self.recentInputCache.GetEdFrameId()) {
const inputFrameDownsync = gopkgs.GetInputFrameDownsync(self.recentInputCache, candidateInputFrameId);
if (null == inputFrameDownsync) break;
if (self._allConfirmed(inputFrameDownsync.ConfirmedList)) break;
++self.lastAllConfirmedInputFrameId;
if (false == self._allConfirmed(inputFrameDownsync.GetConfirmedList())) break;
++candidateInputFrameId;
++newAllConfirmedCnt;
}
if (0 < newAllConfirmedCnt) {
self.lastAllConfirmedInputFrameId = candidateInputFrameId - 1;
}
return newAllConfirmedCnt;
},
@@ -899,18 +916,17 @@ cc.Class({
}
// [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 = self.recentInputCache.GetByFrameId(inputFrameDownsyncId);
const localInputFrame = gopkgs.GetInputFrameDownsync(self.recentInputCache, inputFrameDownsyncId);
if (null != localInputFrame
&&
null == firstPredictedYetIncorrectInputFrameId
&&
!self.equalInputLists(localInputFrame.InputList, inputFrameDownsync.inputList)
!self.equalInputLists(localInputFrame.GetInputList(), inputFrameDownsync.inputList)
) {
firstPredictedYetIncorrectInputFrameId = inputFrameDownsyncId;
}
// [WARNING] Take all "inputFrameDownsync" from backend as all-confirmed, it'll be later checked by "rollbackAndChase".
inputFrameDownsync.confirmedList = (1 << self.playerRichInfoDict.size) - 1;
inputFrameDownsync.confirmedList = (1 << window.boundRoomCapacity) - 1;
const inputFrameDownsyncLocal = gopkgs.NewInputFrameDownsync(inputFrameDownsync.inputFrameId, inputFrameDownsync.inputList, inputFrameDownsync.confirmedList); // "battle.InputFrameDownsync" in "jsexport"
for (let j in self.playerRichInfoArr) {
const jj = parseInt(j);
@@ -932,7 +948,7 @@ cc.Class({
_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;
/*
@@ -953,7 +969,7 @@ cc.Class({
firstPredictedYetIncorrectInputFrameId: ${firstPredictedYetIncorrectInputFrameId}
lastAllConfirmedInputFrameId=${self.lastAllConfirmedInputFrameId}
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}`);
}
self.chaserRenderFrameId = renderFrameId1;
@@ -987,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) {
@@ -996,8 +1012,9 @@ fromUDP=${fromUDP}`);
}
const peerJoinIndexMask = (1 << (peerJoinIndex - 1));
self.getOrPrefabInputFrameUpsync(inputFrameId, false); // Make sure that inputFrame exists locally
const existingInputFrame = self.recentInputCache.GetByFrameId(inputFrameId);
if (0 < (existingInputFrame.ConfirmedList & peerJoinIndexMask)) {
const existingInputFrame = gopkgs.GetInputFrameDownsync(self.recentInputCache, inputFrameId);
const existingConfirmedList = existingInputFrame.GetConfirmedList();
if (0 < (existingConfirmedList & peerJoinIndexMask)) {
continue;
}
if (inputFrameId > self.lastIndividuallyConfirmedInputFrameId[peerJoinIndex - 1]) {
@@ -1005,11 +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"!
let newInputList = existingInputFrame.InputList.slice();
// 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 = (existingInputFrame.ConfirmedList | peerJoinIndex);
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);
@@ -1019,7 +1036,7 @@ fromUDP=${fromUDP}`);
if (
null == firstPredictedYetIncorrectInputFrameId
&&
existingInputFrame.InputList[peerJoinIndex - 1] != peerEncodedInput
existingInputList[peerJoinIndex - 1] != peerEncodedInput
) {
firstPredictedYetIncorrectInputFrameId = inputFrameId;
}
@@ -1030,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);
}
},
@@ -1147,23 +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)) {
console.warn(`Mismatched render frame@rdf.id=${rdf.GetId()} w/ inputFrameId=${delayedInputFrameId}:
rdf=${JSON.stringify(rdf)}
othersForcedDownsyncRenderFrame=${JSON.stringify(othersForcedDownsyncRenderFrame)}`);
rdf = othersForcedDownsyncRenderFrame;
if (CC_DEBUG) {
console.warn(`Mismatched render frame@rdf.id=${rdf.GetId()} w/ inputFrameId=${delayedInputFrameId}:
rdf=${self._stringifyGopkgRdfForFrameDataLogging(rdf)}
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());
}
}
@@ -1419,8 +1447,21 @@ othersForcedDownsyncRenderFrame=${JSON.stringify(othersForcedDownsyncRenderFrame
const j = gopkgs.ConvertToDelayedInputFrameId(i);
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) {
/*
[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);
}
}
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 = {
inputFrameId: j,
inputList: actuallyUsedInputClone,
@@ -1428,7 +1469,6 @@ othersForcedDownsyncRenderFrame=${JSON.stringify(othersForcedDownsyncRenderFrame
};
self.rdfIdToActuallyUsedInput.set(i, inputFrameDownsyncClone);
}
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);
const nextRdf = gopkgs.GetRoomDownsyncFrame(self.recentRenderCache, i + 1);
if (true == isChasing) {
@@ -1544,34 +1584,41 @@ othersForcedDownsyncRenderFrame=${JSON.stringify(othersForcedDownsyncRenderFrame
if (null == inputFrameDownsync) return "{}";
const self = this;
let s = [];
s.push(`InputFrameId:${inputFrameDownsync.InputFrameId}`);
s.push(`InputFrameId:${inputFrameDownsync.GetInputFrameId()}`);
let ss = [];
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(`ConfirmedList:${inputFrameDownsync.ConfirmedList}`);
s.push(`ConfirmedList:${inputFrameDownsync.GetConfirmedList()}`);
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() {
const self = this;
let s = [];
for (let i = self.recentRenderCache.GetStFrameId(); i < self.recentRenderCache.GetEdFrameId(); i++) {
const actuallyUsedInputClone = self.rdfIdToActuallyUsedInput.get(i);
const rdf = self.recentRenderCache.GetByFrameId(i);
const playersStrBldr = [];
for (let k in rdf.GetPlayersArr()) {
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(',')}]
const rdf = gopkgs.GetRoomDownsyncFrame(self.recentRenderCache, i);
const rdfStr = self._stringifyGopkgRdfForFrameDataLogging(rdf);
s.push(`${rdfStr}
actuallyUsedinputList:{${self.inputFrameDownsyncStr(actuallyUsedInputClone)}}`);
}

File diff suppressed because one or more lines are too long

View File

@@ -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;
}

View File

@@ -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) {

View File

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

View File

@@ -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)
@@ -22,7 +22,7 @@ const (
GRAVITY_X = int32(0)
GRAVITY_Y = -int32(float64(0.5) * WORLD_TO_VIRTUAL_GRID_RATIO) // makes all "playerCollider.Y" a multiple of 0.5 in all cases
INPUT_DELAY_FRAMES = int32(6) // in the count of render frames
INPUT_DELAY_FRAMES = int32(4) // in the count of render frames
/*
[WARNING]
@@ -490,6 +490,29 @@ func calcHardPushbacksNorms(joinIndex int32, currPlayerDownsync, thatPlayerInNex
return retCnt
}
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++ {
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
continue
}
if 0 < (confirmedList & (1 << uint32(i))) {
// This in-place update on the "inputsBuffer" is only correct when "delayed input for this player is not yet confirmed"
continue
}
if lastIndividuallyConfirmedInputFrameId[i] >= inputFrameId {
continue
}
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) {
// returns (patternId, jumpedOrNot, effectiveDx, effectiveDy)
delayedInputFrameId := ConvertToDelayedInputFrameId(currRenderFrame.Id)
@@ -503,10 +526,13 @@ func deriveOpPattern(currPlayerDownsync, thatPlayerInNextFrame *PlayerDownsync,
return PATTERN_ID_UNABLE_TO_OP, false, 0, 0
}
delayedInputList := inputsBuffer.GetByFrameId(delayedInputFrameId).(*InputFrameDownsync).InputList
delayedInputFrameDownsync := inputsBuffer.GetByFrameId(delayedInputFrameId).(*InputFrameDownsync)
delayedInputList := delayedInputFrameDownsync.InputList
var delayedInputListForPrevRdf []uint64 = nil
if 0 < delayedInputFrameIdForPrevRdf {
delayedInputListForPrevRdf = inputsBuffer.GetByFrameId(delayedInputFrameIdForPrevRdf).(*InputFrameDownsync).InputList
delayedInputFrameDownsyncForPrevRdf := inputsBuffer.GetByFrameId(delayedInputFrameIdForPrevRdf).(*InputFrameDownsync)
delayedInputListForPrevRdf = delayedInputFrameDownsyncForPrevRdf.InputList
}
jumpedOrNot := false
@@ -564,7 +590,8 @@ 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.
*/
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) 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)
nextRenderFrameId := currRenderFrameId + 1
roomCapacity := len(currRenderFrame.PlayersArr)
@@ -610,6 +637,17 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *resolv.Rin
bulletLocalId := currRenderFrame.BulletLocalIdCounter
// 1. Process player inputs
delayedInputFrameId := ConvertToDelayedInputFrameId(currRenderFrame.Id)
if 0 < delayedInputFrameId {
delayedInputFrameDownsync := inputsBuffer.GetByFrameId(delayedInputFrameId).(*InputFrameDownsync)
delayedInputList := delayedInputFrameDownsync.InputList
roomCapacity := len(delayedInputList)
if allowUpdateInputFrameInPlaceUponDynamics {
hasInputFrameUpdatedOnDynamics = UpdateInputFrameInPlaceUponDynamics(delayedInputFrameId, roomCapacity, delayedInputFrameDownsync.ConfirmedList, delayedInputList, lastIndividuallyConfirmedInputFrameId, lastIndividuallyConfirmedInputList, toExcludeJoinIndexUpdateInputFrameInPlaceUponDynamics)
}
}
for i, currPlayerDownsync := range currRenderFrame.PlayersArr {
chConfig := chConfigsOrderedByJoinIndex[i]
thatPlayerInNextFrame := nextRenderFramePlayers[i]
@@ -617,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
@@ -700,7 +738,7 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *resolv.Rin
} else {
// Updates CharacterState and thus the animation to make user see graphical feedback asap.
thatPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_WALKING
thatPlayerInNextFrame.FramesToRecover = ((chConfig.InertiaFramesToRecover >> 1) + (chConfig.InertiaFramesToRecover >> 2))
thatPlayerInNextFrame.FramesToRecover = (chConfig.InertiaFramesToRecover >> 1)
}
} else {
thatPlayerInNextFrame.CapturedByInertia = false
@@ -748,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
@@ -1179,7 +1217,7 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *resolv.Rin
ret.Id = nextRenderFrameId
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 {

View File

@@ -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),

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
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
}

View File

@@ -106,9 +106,9 @@ func GetCharacterConfigsOrderedByJoinIndex(speciesIdList []int) []*js.Object {
return ret
}
func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrameJs(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) bool {
func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrameJs(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 {
// We need access to all fields of RoomDownsyncFrame for displaying in frontend
return ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer, currRenderFrameId, collisionSys, collisionSysMap, collisionSpaceOffsetX, collisionSpaceOffsetY, chConfigsOrderedByJoinIndex, renderFrameBuffer, collision, effPushbacks, hardPushbackNormsArr, jumpedOrNotList, dynamicRectangleColliders)
return ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer, currRenderFrameId, collisionSys, collisionSysMap, collisionSpaceOffsetX, collisionSpaceOffsetY, chConfigsOrderedByJoinIndex, renderFrameBuffer, collision, effPushbacks, hardPushbackNormsArr, jumpedOrNotList, dynamicRectangleColliders, lastIndividuallyConfirmedInputFrameId, lastIndividuallyConfirmedInputList, allowUpdateInputFrameInPlaceUponDynamics, toExcludeJoinIndexUpdateInputFrameInPlaceUponDynamics)
}
func GetRoomDownsyncFrame(renderFrameBuffer *resolv.RingBuffer, frameId int32) *js.Object {

View File

@@ -1,164 +0,0 @@
// This file contains code from the gonum repository:
// https://github.com/gonum/gonum/blob/master/internal/asm/f64/scalunitaryto_amd64.s
// it is distributed under the 3-Clause BSD license:
//
// Copyright ©2013 The Gonum Authors. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
// * Neither the name of the Gonum project nor the names of its authors and
// contributors may be used to endorse or promote products derived from this
// software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
// Some of the loop unrolling code is copied from:
// http://golang.org/src/math/big/arith_amd64.s
// which is distributed under these terms:
//
// Copyright (c) 2012 The Go Authors. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
// +build !noasm
#include "textflag.h"
#define X_PTR SI
#define Y_PTR DX
#define DST_PTR DI
#define IDX AX
#define LEN CX
#define TAIL BX
#define ALPHA X0
#define ALPHA_2 X1
// func axpyUnitaryTo(dst []float64, alpha float64, x, y []float64)
TEXT ·axpyUnitaryTo(SB), NOSPLIT, $0
MOVQ dst_base+0(FP), DST_PTR // DST_PTR := &dst
MOVQ x_base+32(FP), X_PTR // X_PTR := &x
MOVQ y_base+56(FP), Y_PTR // Y_PTR := &y
MOVQ x_len+40(FP), LEN // LEN = min( len(x), len(y), len(dst) )
CMPQ y_len+64(FP), LEN
CMOVQLE y_len+64(FP), LEN
CMPQ dst_len+8(FP), LEN
CMOVQLE dst_len+8(FP), LEN
CMPQ LEN, $0
JE end // if LEN == 0 { return }
XORQ IDX, IDX // IDX = 0
MOVSD alpha+24(FP), ALPHA
SHUFPD $0, ALPHA, ALPHA // ALPHA := { alpha, alpha }
MOVQ Y_PTR, TAIL // Check memory alignment
ANDQ $15, TAIL // TAIL = &y % 16
JZ no_trim // if TAIL == 0 { goto no_trim }
// Align on 16-byte boundary
MOVSD (X_PTR), X2 // X2 := x[0]
MULSD ALPHA, X2 // X2 *= a
ADDSD (Y_PTR), X2 // X2 += y[0]
MOVSD X2, (DST_PTR) // y[0] = X2
INCQ IDX // i++
DECQ LEN // LEN--
JZ end // if LEN == 0 { return }
no_trim:
MOVQ LEN, TAIL
ANDQ $7, TAIL // TAIL := n % 8
SHRQ $3, LEN // LEN = floor( n / 8 )
JZ tail_start // if LEN == 0 { goto tail_start }
MOVUPS ALPHA, ALPHA_2 // ALPHA_2 := ALPHA for pipelining
loop: // do {
// y[i] += alpha * x[i] unrolled 8x.
MOVUPS (X_PTR)(IDX*8), X2 // X_i = x[i]
MOVUPS 16(X_PTR)(IDX*8), X3
MOVUPS 32(X_PTR)(IDX*8), X4
MOVUPS 48(X_PTR)(IDX*8), X5
MULPD ALPHA, X2 // X_i *= alpha
MULPD ALPHA_2, X3
MULPD ALPHA, X4
MULPD ALPHA_2, X5
ADDPD (Y_PTR)(IDX*8), X2 // X_i += y[i]
ADDPD 16(Y_PTR)(IDX*8), X3
ADDPD 32(Y_PTR)(IDX*8), X4
ADDPD 48(Y_PTR)(IDX*8), X5
MOVUPS X2, (DST_PTR)(IDX*8) // y[i] = X_i
MOVUPS X3, 16(DST_PTR)(IDX*8)
MOVUPS X4, 32(DST_PTR)(IDX*8)
MOVUPS X5, 48(DST_PTR)(IDX*8)
ADDQ $8, IDX // i += 8
DECQ LEN
JNZ loop // } while --LEN > 0
CMPQ TAIL, $0 // if TAIL == 0 { return }
JE end
tail_start: // Reset loop registers
MOVQ TAIL, LEN // Loop counter: LEN = TAIL
SHRQ $1, LEN // LEN = floor( TAIL / 2 )
JZ tail_one // if LEN == 0 { goto tail }
tail_two: // do {
MOVUPS (X_PTR)(IDX*8), X2 // X2 = x[i]
MULPD ALPHA, X2 // X2 *= alpha
ADDPD (Y_PTR)(IDX*8), X2 // X2 += y[i]
MOVUPS X2, (DST_PTR)(IDX*8) // y[i] = X2
ADDQ $2, IDX // i += 2
DECQ LEN
JNZ tail_two // } while --LEN > 0
ANDQ $1, TAIL
JZ end // if TAIL == 0 { goto end }
tail_one:
MOVSD (X_PTR)(IDX*8), X2 // X2 = x[i]
MULSD ALPHA, X2 // X2 *= a
ADDSD (Y_PTR)(IDX*8), X2 // X2 += y[i]
MOVSD X2, (DST_PTR)(IDX*8) // y[i] = X2
end:
RET

View File

@@ -1,137 +0,0 @@
// This file contains code from the gonum repository:
// https://github.com/gonum/gonum/blob/master/internal/asm/f64/axpyunitaryto_amd64.s
// it is distributed under the 3-Clause BSD license:
//
// Copyright ©2013 The Gonum Authors. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
// * Neither the name of the Gonum project nor the names of its authors and
// contributors may be used to endorse or promote products derived from this
// software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
// Some of the loop unrolling code is copied from:
// http://golang.org/src/math/big/arith_amd64.s
// which is distributed under these terms:
//
// Copyright (c) 2012 The Go Authors. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
// +build !noasm
#include "textflag.h"
#define MOVDDUP_ALPHA LONG $0x44120FF2; WORD $0x2024 // @ MOVDDUP 32(SP), X0 /*XMM0, 32[RSP]*/
#define X_PTR SI
#define DST_PTR DI
#define IDX AX
#define LEN CX
#define TAIL BX
#define ALPHA X0
#define ALPHA_2 X1
// func scalUnitaryTo(dst []float64, alpha float64, x []float64)
// This function assumes len(dst) >= len(x).
TEXT ·scalUnitaryTo(SB), NOSPLIT, $0
MOVQ x_base+32(FP), X_PTR // X_PTR = &x
MOVQ dst_base+0(FP), DST_PTR // DST_PTR = &dst
MOVDDUP_ALPHA // ALPHA = { alpha, alpha }
MOVQ x_len+40(FP), LEN // LEN = len(x)
CMPQ LEN, $0
JE end // if LEN == 0 { return }
XORQ IDX, IDX // IDX = 0
MOVQ LEN, TAIL
ANDQ $7, TAIL // TAIL = LEN % 8
SHRQ $3, LEN // LEN = floor( LEN / 8 )
JZ tail_start // if LEN == 0 { goto tail_start }
MOVUPS ALPHA, ALPHA_2 // ALPHA_2 = ALPHA for pipelining
loop: // do { // dst[i] = alpha * x[i] unrolled 8x.
MOVUPS (X_PTR)(IDX*8), X2 // X_i = x[i]
MOVUPS 16(X_PTR)(IDX*8), X3
MOVUPS 32(X_PTR)(IDX*8), X4
MOVUPS 48(X_PTR)(IDX*8), X5
MULPD ALPHA, X2 // X_i *= ALPHA
MULPD ALPHA_2, X3
MULPD ALPHA, X4
MULPD ALPHA_2, X5
MOVUPS X2, (DST_PTR)(IDX*8) // dst[i] = X_i
MOVUPS X3, 16(DST_PTR)(IDX*8)
MOVUPS X4, 32(DST_PTR)(IDX*8)
MOVUPS X5, 48(DST_PTR)(IDX*8)
ADDQ $8, IDX // i += 8
DECQ LEN
JNZ loop // while --LEN > 0
CMPQ TAIL, $0
JE end // if TAIL == 0 { return }
tail_start: // Reset loop counters
MOVQ TAIL, LEN // Loop counter: LEN = TAIL
SHRQ $1, LEN // LEN = floor( TAIL / 2 )
JZ tail_one // if LEN == 0 { goto tail_one }
tail_two: // do {
MOVUPS (X_PTR)(IDX*8), X2 // X_i = x[i]
MULPD ALPHA, X2 // X_i *= ALPHA
MOVUPS X2, (DST_PTR)(IDX*8) // dst[i] = X_i
ADDQ $2, IDX // i += 2
DECQ LEN
JNZ tail_two // while --LEN > 0
ANDQ $1, TAIL
JZ end // if TAIL == 0 { return }
tail_one:
MOVSD (X_PTR)(IDX*8), X2 // X_i = x[i]
MULSD ALPHA, X2 // X_i *= ALPHA
MOVSD X2, (DST_PTR)(IDX*8) // dst[i] = X_i
end:
RET

View File

@@ -1,9 +0,0 @@
//go:build !noasm
// +build !noasm
package resolv
// functions from the gonum package that optimizes arithmetic
// operations on lists of float64 values
func axpyUnitaryTo(dst []float64, alpha float64, x, y []float64)
func scalUnitaryTo(dst []float64, alpha float64, x []float64)

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.
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.

View File

@@ -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}

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
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++ {

View File

@@ -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

View File

@@ -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,14 +23,10 @@ type Line struct {
}
func NewLine(x, y, x2, y2 float64) *Line {
return &Line{
Start: Vector{x, y},
End: Vector{x2, y2},
}
}
func (line *Line) Project(axis Vector) Vector {
return line.Vector().Scale(axis.Dot(line.Start.Sub(line.End)))
l := &Line{}
l.Start = Vector{x, y}
l.End = Vector{x2, y2}
return l
}
func (line *Line) Normal() Vector {
@@ -43,10 +35,6 @@ func (line *Line) Normal() Vector {
return Vector{dy, -dx}.Unit()
}
func (line *Line) Vector() Vector {
return line.End.Clone().Sub(line.Start).Unit()
}
// IntersectionPointsLine returns the intersection point of a Line with another Line as a Vector. If no intersection is found, it will return nil.
func (line *Line) IntersectionPointsLine(other *Line) Vector {
@@ -79,51 +67,6 @@ func (line *Line) IntersectionPointsLine(other *Line) Vector {
}
// IntersectionPointsCircle returns a slice of Vectors, each indicating the intersection point. If no intersection is found, it will return an empty slice.
func (line *Line) IntersectionPointsCircle(circle *Circle) []Vector {
points := []Vector{}
cp := Vector{circle.X, circle.Y}
lStart := line.Start.Sub(cp)
lEnd := line.End.Sub(cp)
diff := lEnd.Sub(lStart)
a := diff[0]*diff[0] + diff[1]*diff[1]
b := 2 * ((diff[0] * lStart[0]) + (diff[1] * lStart[1]))
c := (lStart[0] * lStart[0]) + (lStart[1] * lStart[1]) - (circle.Radius * circle.Radius)
det := b*b - (4 * a * c)
if det < 0 {
// Do nothing, no intersections
} else if det == 0 {
t := -b / (2 * a)
if t >= 0 && t <= 1 {
points = append(points, Vector{line.Start[0] + t*diff[0], line.Start[1] + t*diff[1]})
}
} else {
t := (-b + math.Sqrt(det)) / (2 * a)
// We have to ensure t is between 0 and 1; otherwise, the collision points are on the circle as though the lines were infinite in length.
if t >= 0 && t <= 1 {
points = append(points, Vector{line.Start[0] + t*diff[0], line.Start[1] + t*diff[1]})
}
t = (-b - math.Sqrt(det)) / (2 * a)
if t >= 0 && t <= 1 {
points = append(points, Vector{line.Start[0] + t*diff[0], line.Start[1] + t*diff[1]})
}
}
return points
}
type ConvexPolygon struct {
Points *RingBuffer
X, Y float64
@@ -135,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...)
@@ -241,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++ {
@@ -278,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.
@@ -290,43 +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()
}
// Center returns the transformed Center of the ConvexPolygon.
func (cp *ConvexPolygon) Center() Vector {
pos := Vector{0, 0}
vertices := cp.Transformed()
for _, v := range vertices {
pos.Add(v)
}
denom := float64(len(vertices))
pos[0] /= denom
pos[1] /= denom
return pos
}
// 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.
@@ -365,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.
@@ -453,13 +360,7 @@ func (cp *ConvexPolygon) Intersection(dx, dy float64, other Shape) *ContactSet {
cp.X += dx
cp.Y += dy
if circle, isCircle := other.(*Circle); isCircle {
for _, line := range cp.Lines() {
contactSet.Points = append(contactSet.Points, line.IntersectionPointsCircle(circle)...)
}
} else if poly, isPoly := other.(*ConvexPolygon); isPoly {
if poly, isPoly := other.(*ConvexPolygon); isPoly {
for _, line := range cp.Lines() {
@@ -476,29 +377,11 @@ func (cp *ConvexPolygon) Intersection(dx, dy float64, other Shape) *ContactSet {
}
if len(contactSet.Points) > 0 {
for _, point := range contactSet.Points {
contactSet.Center = contactSet.Center.Add(point)
}
contactSet.Center[0] /= float64(len(contactSet.Points))
contactSet.Center[1] /= float64(len(contactSet.Points))
if mtv := cp.calculateMTV(contactSet, other); mtv != nil {
contactSet.MTV = mtv
}
// Do nothing
} else {
contactSet = nil
}
// If dx or dy aren't 0, then the MTV will be greater to compensate; this adjusts the vector back.
if contactSet != nil && (dx != 0 || dy != 0) {
deltaMagnitude := Vector{dx, dy}.Magnitude()
ogMagnitude := contactSet.MTV.Magnitude()
contactSet.MTV = contactSet.MTV.Unit().Scale(ogMagnitude - deltaMagnitude)
}
cp.X = ogX
cp.Y = ogY
@@ -506,76 +389,6 @@ func (cp *ConvexPolygon) Intersection(dx, dy float64, other Shape) *ContactSet {
}
// calculateMTV returns the MTV, if possible, and a bool indicating whether it was possible or not.
func (cp *ConvexPolygon) calculateMTV(contactSet *ContactSet, otherShape Shape) Vector {
delta := Vector{0, 0}
smallest := Vector{math.MaxFloat64, 0}
switch other := otherShape.(type) {
case *ConvexPolygon:
for _, axis := range cp.SATAxes() {
if !cp.Project(axis).Overlapping(other.Project(axis)) {
return nil
}
overlap := cp.Project(axis).Overlap(other.Project(axis))
if smallest.Magnitude() > overlap {
smallest = axis.Scale(overlap)
}
}
for _, axis := range other.SATAxes() {
if !cp.Project(axis).Overlapping(other.Project(axis)) {
return nil
}
overlap := cp.Project(axis).Overlap(other.Project(axis))
if smallest.Magnitude() > overlap {
smallest = axis.Scale(overlap)
}
}
// Removed support of "Circle" to remove dependency of "sort" module
}
delta[0] = smallest[0]
delta[1] = smallest[1]
return delta
}
// ContainedBy returns if the ConvexPolygon is wholly contained by the other shape provided.
func (cp *ConvexPolygon) ContainedBy(otherShape Shape) bool {
switch other := otherShape.(type) {
case *ConvexPolygon:
for _, axis := range cp.SATAxes() {
if !cp.Project(axis).IsInside(other.Project(axis)) {
return false
}
}
for _, axis := range other.SATAxes() {
if !cp.Project(axis).IsInside(other.Project(axis)) {
return false
}
}
}
return true
}
// NewRectangle returns a rectangular ConvexPolygon with the vertices in clockwise order. In actuality, an AABBRectangle should be its own
// "thing" with its own optimized Intersection code check.
func NewRectangle(x, y, w, h float64) *ConvexPolygon {
@@ -586,159 +399,3 @@ func NewRectangle(x, y, w, h float64) *ConvexPolygon {
x, y+h,
)
}
type Circle struct {
X, Y, Radius float64
}
// NewCircle returns a new Circle, with its center at the X and Y position given, and with the defined radius.
func NewCircle(x, y, radius float64) *Circle {
circle := &Circle{
X: x,
Y: y,
Radius: radius,
}
return circle
}
func (circle *Circle) Clone() Shape {
return NewCircle(circle.X, circle.Y, circle.Radius)
}
// Bounds returns the top-left and bottom-right corners of the Circle.
func (circle *Circle) Bounds() (Vector, Vector) {
return Vector{circle.X - circle.Radius, circle.Y - circle.Radius}, Vector{circle.X + circle.Radius, circle.Y + circle.Radius}
}
// Intersection tests to see if a Circle 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
// were in a different relative location). If an Intersection is found, a ContactSet will be returned, giving information regarding
// the intersection.
func (circle *Circle) Intersection(dx, dy float64, other Shape) *ContactSet {
var contactSet *ContactSet
ox := circle.X
oy := circle.Y
circle.X += dx
circle.Y += dy
// here
switch shape := other.(type) {
case *ConvexPolygon:
// Maybe this would work?
contactSet = shape.Intersection(-dx, -dy, circle)
if contactSet != nil {
contactSet.MTV = contactSet.MTV.Scale(-1)
}
case *Circle:
contactSet = NewContactSet()
contactSet.Points = circle.IntersectionPointsCircle(shape)
if len(contactSet.Points) == 0 {
return nil
}
contactSet.MTV = Vector{circle.X - shape.X, circle.Y - shape.Y}
dist := contactSet.MTV.Magnitude()
contactSet.MTV = contactSet.MTV.Unit().Scale(circle.Radius + shape.Radius - dist)
for _, point := range contactSet.Points {
contactSet.Center = contactSet.Center.Add(point)
}
contactSet.Center[0] /= float64(len(contactSet.Points))
contactSet.Center[1] /= float64(len(contactSet.Points))
// if contactSet != nil {
// contactSet.MTV[0] -= dx
// contactSet.MTV[1] -= dy
// }
// contactSet.MTV = Vector{circle.X - shape.X, circle.Y - shape.Y}
}
circle.X = ox
circle.Y = oy
return contactSet
}
// Move translates the Circle by the designated X and Y values.
func (circle *Circle) Move(x, y float64) {
circle.X += x
circle.Y += y
}
// MoveVec translates the Circle by the designated Vector.
func (circle *Circle) MoveVec(vec Vector) {
circle.X += vec.X()
circle.Y += vec.Y()
}
// SetPosition sets the center position of the Circle using the X and Y values given.
func (circle *Circle) SetPosition(x, y float64) {
circle.X = x
circle.Y = y
}
// SetPosition sets the center position of the Circle using the Vector given.
func (circle *Circle) SetPositionVec(vec Vector) {
circle.X = vec.X()
circle.Y = vec.Y()
}
// Position() returns the X and Y position of the Circle.
func (circle *Circle) Position() (float64, float64) {
return circle.X, circle.Y
}
// PointInside returns if the given Vector is inside of the circle.
func (circle *Circle) PointInside(point Vector) bool {
return point.Sub(Vector{circle.X, circle.Y}).Magnitude() <= circle.Radius
}
// IntersectionPointsCircle returns the intersection points of the two circles provided.
func (circle *Circle) IntersectionPointsCircle(other *Circle) []Vector {
d := math.Sqrt(math.Pow(other.X-circle.X, 2) + math.Pow(other.Y-circle.Y, 2))
if d > circle.Radius+other.Radius || d < math.Abs(circle.Radius-other.Radius) || d == 0 && circle.Radius == other.Radius {
return nil
}
a := (math.Pow(circle.Radius, 2) - math.Pow(other.Radius, 2) + math.Pow(d, 2)) / (2 * d)
h := math.Sqrt(math.Pow(circle.Radius, 2) - math.Pow(a, 2))
x2 := circle.X + a*(other.X-circle.X)/d
y2 := circle.Y + a*(other.Y-circle.Y)/d
return []Vector{
{x2 + h*(other.Y-circle.Y)/d, y2 - h*(other.X-circle.X)/d},
{x2 - h*(other.Y-circle.Y)/d, y2 + h*(other.X-circle.X)/d},
}
}
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
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
}

View File

@@ -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,106 +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
@@ -122,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
@@ -252,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.
}
@@ -262,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.
}
@@ -272,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.
}