mirror of
https://github.com/genxium/DelayNoMore
synced 2025-10-19 21:46:56 +00:00
Compare commits
3 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
365177a3af | ||
|
b79e2dc935 | ||
|
7b0c807496 |
@@ -6,9 +6,13 @@ This project is a demo for a websocket-based rollback netcode inspired by [GGPO]
|
|||||||
|
|
||||||

|

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

|
||||||
|
|
||||||
As lots of feedbacks ask for a discussion on using UDP instead, I tried to summarize my personal opinion about it in [ConcerningEdgeCases](./ConcerningEdgeCases.md) -- **since v0.9.25, the project is actually equipped with UDP capabilities as follows**.
|
As lots of feedbacks ask for a discussion on using UDP instead, I tried to summarize my personal opinion about it in [ConcerningEdgeCases](./ConcerningEdgeCases.md) -- **since v0.9.25, the project is actually equipped with UDP capabilities as follows**.
|
||||||
- When using the so called `native apps` on `Android` and `Windows` (I'm working casually hard to support `iOS` next), the frontends will try to use UDP hole-punching w/ the help of backend as a registry. If UDP hole-punching is working, the rollback is often less than `turn-around frames to recover` and thus not noticeable, being much better than using websocket alone. This video shows how the UDP holepunched p2p performs for [Phone-Wifi v.s. PC-Wifi (viewed by PC side)](https://pan.baidu.com/s/1K6704bJKlrSBTVqGcXhajA?pwd=l7ok).
|
- When using the so called `native apps` on `Android` and `Windows` (I'm working casually hard to support `iOS` next), the frontends will try to use UDP hole-punching w/ the help of backend as a registry. If UDP hole-punching is working, the rollback is often less than `turn-around frames to recover` and thus not noticeable, being much better than using websocket alone. This video shows how the UDP holepunched p2p performs for [Phone-Wifi v.s. PC-Wifi (viewed by PC side)](https://pan.baidu.com/s/1K6704bJKlrSBTVqGcXhajA?pwd=l7ok).
|
||||||
- If UDP hole-punching is not working, e.g. for Symmetric NAT like in 4G/5G cellular network, the frontends will use backend as a UDP tunnel (or relay, whatever you like to call it). This video shows how the UDP tunnel performs for [Phone-4G v.s. PC-Wifi (viewed by PC side)](https://pan.baidu.com/s/1IZVa5wVgAdeH6D-xsZYFUw?pwd=dgkj).
|
- 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).
|
||||||
- Browser vs `native app` is possible but in that case only websocket is used.
|
- Browser vs `native app` is possible but in that case only websocket is used.
|
||||||
|
|
||||||
|
|
||||||
|
BIN
charts/Phone4g_battle_spedup.gif
Normal file
BIN
charts/Phone4g_battle_spedup.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.4 MiB |
@@ -1084,6 +1084,9 @@ fromUDP=${fromUDP}`);
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
let st = performance.now();
|
let st = performance.now();
|
||||||
|
if (cc.sys.isNative) {
|
||||||
|
DelayNoMore.UdpSession.pollUdpRecvRingBuff();
|
||||||
|
}
|
||||||
const noDelayInputFrameId = gopkgs.ConvertToNoDelayInputFrameId(self.renderFrameId);
|
const noDelayInputFrameId = gopkgs.ConvertToNoDelayInputFrameId(self.renderFrameId);
|
||||||
let prevSelfInput = null,
|
let prevSelfInput = null,
|
||||||
currSelfInput = null;
|
currSelfInput = null;
|
||||||
|
File diff suppressed because one or more lines are too long
7
frontend/build-templates/.cocos-project.json
Normal file
7
frontend/build-templates/.cocos-project.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"engine_version": "2.2.1",
|
||||||
|
"has_native": true,
|
||||||
|
"project_type": "js",
|
||||||
|
"projectName": "DelayNoMore",
|
||||||
|
"packageName": "org.genxium.delaynomore"
|
||||||
|
}
|
@@ -18,11 +18,11 @@
|
|||||||
"from": "cocos/scripting/js-bindings/manual/jsb_module_register.cpp",
|
"from": "cocos/scripting/js-bindings/manual/jsb_module_register.cpp",
|
||||||
"to": "frameworks/runtime-src/Classes/jsb_module_register.cpp"
|
"to": "frameworks/runtime-src/Classes/jsb_module_register.cpp"
|
||||||
}, {
|
}, {
|
||||||
"from": "frameworks/runtime-src/Classes/send_ring_buff.hpp",
|
"from": "frameworks/runtime-src/Classes/ring_buff.hpp",
|
||||||
"to": "frameworks/runtime-src/Classes/send_ring_buff.hpp"
|
"to": "frameworks/runtime-src/Classes/ring_buff.hpp"
|
||||||
}, {
|
}, {
|
||||||
"from": "frameworks/runtime-src/Classes/send_ring_buff.cpp",
|
"from": "frameworks/runtime-src/Classes/ring_buff.cpp",
|
||||||
"to": "frameworks/runtime-src/Classes/send_ring_buff.cpp"
|
"to": "frameworks/runtime-src/Classes/ring_buff.cpp"
|
||||||
}, {
|
}, {
|
||||||
"from": "frameworks/runtime-src/Classes/udp_session.hpp",
|
"from": "frameworks/runtime-src/Classes/udp_session.hpp",
|
||||||
"to": "frameworks/runtime-src/Classes/udp_session.hpp"
|
"to": "frameworks/runtime-src/Classes/udp_session.hpp"
|
||||||
|
@@ -0,0 +1,102 @@
|
|||||||
|
#include <string.h>
|
||||||
|
#include "ring_buff.hpp"
|
||||||
|
|
||||||
|
// Sending
|
||||||
|
void SendRingBuff::put(BYTEC* const newBytes, size_t newBytesLen, PeerAddr* pNewPeerAddr) {
|
||||||
|
while (0 < cnt && cnt >= n) {
|
||||||
|
// Make room for the new element
|
||||||
|
this->pop();
|
||||||
|
}
|
||||||
|
eles[ed].bytesLen = newBytesLen;
|
||||||
|
memset(eles[ed].bytes, 0, sizeof eles[ed].bytes);
|
||||||
|
memcpy(eles[ed].bytes, newBytes, newBytesLen);
|
||||||
|
eles[ed].peerAddr = *(pNewPeerAddr);
|
||||||
|
ed++;
|
||||||
|
cnt++;
|
||||||
|
if (ed >= n) {
|
||||||
|
ed -= n; // Deliberately not using "%" operator for performance concern
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SendWork* SendRingBuff::pop() {
|
||||||
|
if (0 == cnt) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
SendWork* ret = &(eles[st]);
|
||||||
|
cnt--;
|
||||||
|
st++;
|
||||||
|
if (st >= n) {
|
||||||
|
st -= n;
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recving
|
||||||
|
void RecvRingBuff::put(char* newBytes, size_t newBytesLen) {
|
||||||
|
RecvWork* slotEle = (&eles[ed.load()]); // Save for later update
|
||||||
|
|
||||||
|
int oldCnt = cnt.load();
|
||||||
|
int oldSt = st.load(); // Used to guard against "cnt decremented in pop(...), but st not yet incremented and thus return value not yet copied to avoid contamination"
|
||||||
|
int tried = 0;
|
||||||
|
while (n <= oldCnt && !ed.compare_exchange_weak(oldSt, oldSt) && 3 > tried) {
|
||||||
|
// Make room for the new element
|
||||||
|
this->pop(NULL);
|
||||||
|
oldCnt = cnt.load(); // If "pop()" above failed, it'd only be due to concurrent calls to "pop()", either way the updated "cnt" should be good to go
|
||||||
|
oldSt = st.load();
|
||||||
|
++tried;
|
||||||
|
}
|
||||||
|
if (n <= oldCnt && !ed.compare_exchange_weak(oldSt, oldSt) && 3 == tried) {
|
||||||
|
// Failed silently, UDP packet can be dropped.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
slotEle->bytesLen = newBytesLen;
|
||||||
|
memset(slotEle->ui8Arr, 0, sizeof slotEle->ui8Arr);
|
||||||
|
for (int i = 0; i < newBytesLen; i++) {
|
||||||
|
*(slotEle->ui8Arr + i) = *(newBytes + i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// No need to compare-and-swap, only "UvRecvThread" will access "RecvRingBuff.ed".
|
||||||
|
ed++;
|
||||||
|
if (ed >= n) {
|
||||||
|
ed -= n; // Deliberately not using "%" operator for performance concern
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only increment cnt when the putting of new element is fully done.
|
||||||
|
cnt++;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RecvRingBuff::pop(RecvWork* out) {
|
||||||
|
int oldCnt = std::atomic_fetch_sub(&cnt, 1);
|
||||||
|
/*
|
||||||
|
[WARNING]
|
||||||
|
|
||||||
|
After here, two cases should be taken care of.
|
||||||
|
1. If "n == oldCnt", we need guard against "put" to avoid contaminating "ret" by the "putting".
|
||||||
|
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"
|
||||||
|
++cnt;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// When concurrent "pop"s reach here, over-popping is definitely avoided.
|
||||||
|
int oldSt = st.load();
|
||||||
|
if (out) {
|
||||||
|
RecvWork* src = (&eles[oldSt]);
|
||||||
|
memset(out->ui8Arr, 0, sizeof out->ui8Arr);
|
||||||
|
memcpy(out->ui8Arr, src, src->bytesLen);
|
||||||
|
out->bytesLen = src->bytesLen;
|
||||||
|
}
|
||||||
|
int newSt = oldSt + 1;
|
||||||
|
if (newSt >= n) {
|
||||||
|
newSt -= n;
|
||||||
|
}
|
||||||
|
if (st.compare_exchange_weak(oldSt, newSt)) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
// Failed concurrent access should recover the "cnt"
|
||||||
|
++cnt;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
@@ -4,6 +4,8 @@
|
|||||||
#include "uv/uv.h"
|
#include "uv/uv.h"
|
||||||
#define __SSIZE_T // Otherwise "ssize_t" would have conflicting macros error that stops compiling
|
#define __SSIZE_T // Otherwise "ssize_t" would have conflicting macros error that stops compiling
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
int const RING_BUFF_CONSECUTIVE_SET = 0;
|
int const RING_BUFF_CONSECUTIVE_SET = 0;
|
||||||
int const RING_BUFF_NON_CONSECUTIVE_SET = 1;
|
int const RING_BUFF_NON_CONSECUTIVE_SET = 1;
|
||||||
int const RING_BUFF_FAILED_TO_SET = 2;
|
int const RING_BUFF_FAILED_TO_SET = 2;
|
||||||
@@ -41,4 +43,32 @@ public:
|
|||||||
SendWork* pop();
|
SendWork* pop();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: Move "RecvXxxx" to a dedicated class.
|
||||||
|
class RecvWork {
|
||||||
|
public:
|
||||||
|
uint8_t ui8Arr[maxUdpPayloadBytes]; // Wasting some RAM here thus no need for explicit recursive destruction
|
||||||
|
size_t bytesLen;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
[WARNING] This class is specific to "RecvWork"; its "put" and "pop" methods are designed to be thread-safe & lock-free for our particular case, i.e. only concurrent access from "UvRecvThread" & "GameThread", in a sense more sophisticated than the Golang or JavaScript versions.
|
||||||
|
|
||||||
|
There's yet no plan to support thread-safe & lock-free "getByFrameId/setByFrameId" -- being thread-safe is easy by use of mutex, which is very SLOWWWWW when used in 60fps race-conditions.
|
||||||
|
|
||||||
|
The generic "thread-safe, lock-free ring buffer or circular buffer" is a big problem, widely discussed over the internet and in literatures, search "lock-free circular buffer" for more information.
|
||||||
|
*/
|
||||||
|
class RecvRingBuff {
|
||||||
|
public:
|
||||||
|
int n;
|
||||||
|
std::atomic_int ed, st, cnt;
|
||||||
|
RecvWork eles[maxBuffedMsgs]; // preallocated on stack to save heap alloc/dealloc time
|
||||||
|
RecvRingBuff(int newN) {
|
||||||
|
this->n = newN;
|
||||||
|
this->st = this->ed = this->cnt = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void put(char* newBytes, size_t newBytesLen);
|
||||||
|
|
||||||
|
bool pop(RecvWork* out);
|
||||||
|
};
|
||||||
#endif
|
#endif
|
@@ -1,31 +0,0 @@
|
|||||||
#include <string.h>
|
|
||||||
#include "send_ring_buff.hpp"
|
|
||||||
|
|
||||||
void SendRingBuff::put(BYTEC* const newBytes, size_t newBytesLen, PeerAddr* pNewPeerAddr) {
|
|
||||||
while (0 < cnt && cnt >= n) {
|
|
||||||
// Make room for the new element
|
|
||||||
this->pop();
|
|
||||||
}
|
|
||||||
eles[ed].bytesLen = newBytesLen;
|
|
||||||
memset(eles[ed].bytes, 0, sizeof eles[ed].bytes);
|
|
||||||
memcpy(eles[ed].bytes, newBytes, newBytesLen);
|
|
||||||
eles[ed].peerAddr = *(pNewPeerAddr);
|
|
||||||
ed++;
|
|
||||||
cnt++;
|
|
||||||
if (ed >= n) {
|
|
||||||
ed -= n; // Deliberately not using "%" operator for performance concern
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SendWork* SendRingBuff::pop() {
|
|
||||||
if (0 == cnt) {
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
SendWork* ret = &(eles[st]);
|
|
||||||
cnt--;
|
|
||||||
st++;
|
|
||||||
if (st >= n) {
|
|
||||||
st -= n;
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
}
|
|
@@ -16,11 +16,15 @@ uv_loop_t *recvLoop = NULL, *sendLoop = NULL;
|
|||||||
uv_mutex_t sendRingBuffLock; // used along with "uvSendLoopTriggerSig" as a "uv_cond_t"
|
uv_mutex_t sendRingBuffLock; // used along with "uvSendLoopTriggerSig" as a "uv_cond_t"
|
||||||
SendRingBuff* sendRingBuff = NULL;
|
SendRingBuff* sendRingBuff = NULL;
|
||||||
|
|
||||||
|
uv_mutex_t recvRingBuffLock;
|
||||||
|
RecvRingBuff* recvRingBuff = NULL;
|
||||||
|
|
||||||
char SRV_IP[256];
|
char SRV_IP[256];
|
||||||
int SRV_PORT = 0;
|
int SRV_PORT = 0;
|
||||||
int UDP_TUNNEL_SRV_PORT = 0;
|
int UDP_TUNNEL_SRV_PORT = 0;
|
||||||
struct PeerAddr udpPunchingServerAddr, udpTunnelAddr;
|
struct PeerAddr udpPunchingServerAddr, udpTunnelAddr;
|
||||||
struct PeerAddr peerAddrList[maxPeerCnt];
|
struct PeerAddr peerAddrList[maxPeerCnt];
|
||||||
|
bool peerPunchedMarks[maxPeerCnt];
|
||||||
|
|
||||||
void _onRead(uv_udp_t* req, ssize_t nread, uv_buf_t const* buf, struct sockaddr const* addr, unsigned flags) {
|
void _onRead(uv_udp_t* req, ssize_t nread, uv_buf_t const* buf, struct sockaddr const* addr, unsigned flags) {
|
||||||
if (nread < 0) {
|
if (nread < 0) {
|
||||||
@@ -29,6 +33,8 @@ void _onRead(uv_udp_t* req, ssize_t nread, uv_buf_t const* buf, struct sockaddr
|
|||||||
free(buf->base);
|
free(buf->base);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
struct sockaddr_in const* sockAddr = (struct sockaddr_in const*)addr;
|
||||||
|
|
||||||
#if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)
|
#if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)
|
||||||
char ip[INET_ADDRSTRLEN];
|
char ip[INET_ADDRSTRLEN];
|
||||||
memset(ip, 0, sizeof ip);
|
memset(ip, 0, sizeof ip);
|
||||||
@@ -38,10 +44,8 @@ void _onRead(uv_udp_t* req, ssize_t nread, uv_buf_t const* buf, struct sockaddr
|
|||||||
// The null check for "addr" is necessary, on Android there'd be such mysterious call to "_onRead"!
|
// The null check for "addr" is necessary, on Android there'd be such mysterious call to "_onRead"!
|
||||||
switch (addr->sa_family) {
|
switch (addr->sa_family) {
|
||||||
case AF_INET: {
|
case AF_INET: {
|
||||||
struct sockaddr_in const* sockAddr = (struct sockaddr_in const*)addr;
|
|
||||||
uv_inet_ntop(sockAddr->sin_family, &(sockAddr->sin_addr), ip, INET_ADDRSTRLEN);
|
uv_inet_ntop(sockAddr->sin_family, &(sockAddr->sin_addr), ip, INET_ADDRSTRLEN);
|
||||||
port = ntohs(sockAddr->sin_port);
|
port = ntohs(sockAddr->sin_port);
|
||||||
CCLOG("UDP received %u bytes from %s:%d", nread, ip, port);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
@@ -53,37 +57,24 @@ void _onRead(uv_udp_t* req, ssize_t nread, uv_buf_t const* buf, struct sockaddr
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
if (6 == nread) {
|
if (6 == nread) {
|
||||||
// holepunching
|
// Peer holepunching
|
||||||
} else if (0 < nread) {
|
for (int i = 0; i < maxPeerCnt; i++) {
|
||||||
// Non-holepunching; it might be more effective in RAM usage to use a threadsafe RingBuff to pass msg to GameThread here, but as long as it's not a performance blocker don't bother optimize here...
|
if (peerAddrList[i].sockAddrIn.sin_addr.s_addr != sockAddr->sin_addr.s_addr) continue;
|
||||||
uint8_t* const ui8Arr = (uint8_t*)malloc(maxUdpPayloadBytes*sizeof(uint8_t));
|
if (peerAddrList[i].sockAddrIn.sin_port != sockAddr->sin_port) continue;
|
||||||
memset(ui8Arr, 0, sizeof(ui8Arr));
|
peerPunchedMarks[i] = true;
|
||||||
for (int i = 0; i < nread; i++) {
|
#if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)
|
||||||
*(ui8Arr+i) = *(buf->base + i);
|
CCLOG("UDP received peer-holepunching from %s:%d", ip, port);
|
||||||
|
#endif
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
cocos2d::Application::getInstance()->getScheduler()->performFunctionInCocosThread([=]() {
|
} else if (0 < nread) {
|
||||||
// [WARNING] Use of the "ScriptEngine" is only allowed in "GameThread a.k.a. CocosThread"!
|
// Non-holepunching; the previously used "cocos2d::Application::getInstance()->getScheduler()->performFunctionInCocosThread(...)" approach was so non-deterministic in terms of the lag till GameThread actually recognizes this latest received packet due to scheduler uncertainty -- and was also heavier in RAM due to lambda usage
|
||||||
se::Value onUdpMessageCb;
|
#if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)
|
||||||
se::ScriptEngine::getInstance()->getGlobalObject()->getProperty("onUdpMessage", &onUdpMessageCb);
|
CCLOG("UDP received %u bytes inputFrameUpsync from %s:%d", nread, ip, port);
|
||||||
// [WARNING] Declaring "AutoHandleScope" is critical here, otherwise "onUdpMessageCb.toObject()" wouldn't be recognized as a function of the ScriptEngine!
|
#endif
|
||||||
se::AutoHandleScope hs;
|
//uv_mutex_lock(&recvRingBuffLock);
|
||||||
//CCLOG("UDP received %d bytes upsync -- 1", nread);
|
recvRingBuff->put(buf->base, nread);
|
||||||
se::Object* const gameThreadMsg = se::Object::createTypedArray(se::Object::TypedArrayType::UINT8, ui8Arr, nread);
|
//uv_mutex_unlock(&recvRingBuffLock);
|
||||||
//CCLOG("UDP received %d bytes upsync -- 2", nread);
|
|
||||||
se::ValueArray args = { se::Value(gameThreadMsg) };
|
|
||||||
if (onUdpMessageCb.isObject() && onUdpMessageCb.toObject()->isFunction()) {
|
|
||||||
// Temporarily assume that the "this" ptr within callback is NULL.
|
|
||||||
bool ok = onUdpMessageCb.toObject()->call(args, NULL);
|
|
||||||
if (!ok) {
|
|
||||||
se::ScriptEngine::getInstance()->clearException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//CCLOG("UDP received %d bytes upsync -- 3", nread);
|
|
||||||
gameThreadMsg->decRef(); // Reference http://docs.cocos.com/creator/2.2/manual/en/advanced-topics/JSB2.0-learning.html#seobject
|
|
||||||
//CCLOG("UDP received %d bytes upsync -- 4", nread);
|
|
||||||
free(ui8Arr);
|
|
||||||
//CCLOG("UDP received %d bytes upsync -- 5", nread);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
free(buf->base);
|
free(buf->base);
|
||||||
|
|
||||||
@@ -169,6 +160,7 @@ void startRecvLoop(void* arg) {
|
|||||||
|
|
||||||
int uvCloseRet = uv_loop_close(l);
|
int uvCloseRet = uv_loop_close(l);
|
||||||
CCLOG("UDP recv loop is closed in UvRecvThread, uvCloseRet=%d", uvCloseRet);
|
CCLOG("UDP recv loop is closed in UvRecvThread, uvCloseRet=%d", uvCloseRet);
|
||||||
|
uv_mutex_destroy(&recvRingBuffLock);
|
||||||
}
|
}
|
||||||
|
|
||||||
void startSendLoop(void* arg) {
|
void startSendLoop(void* arg) {
|
||||||
@@ -196,6 +188,10 @@ int initSendLoop(struct sockaddr const* pUdpAddr) {
|
|||||||
}
|
}
|
||||||
uv_mutex_init(&sendRingBuffLock);
|
uv_mutex_init(&sendRingBuffLock);
|
||||||
sendRingBuff = new SendRingBuff(maxBuffedMsgs);
|
sendRingBuff = new SendRingBuff(maxBuffedMsgs);
|
||||||
|
|
||||||
|
uv_mutex_init(&recvRingBuffLock);
|
||||||
|
recvRingBuff = new RecvRingBuff(maxBuffedMsgs);
|
||||||
|
|
||||||
uv_async_init(sendLoop, &uvSendLoopStopSig, _onUvStopSig);
|
uv_async_init(sendLoop, &uvSendLoopStopSig, _onUvStopSig);
|
||||||
uv_async_init(sendLoop, &uvSendLoopTriggerSig, _onUvSthNewToSend);
|
uv_async_init(sendLoop, &uvSendLoopTriggerSig, _onUvSthNewToSend);
|
||||||
|
|
||||||
@@ -222,6 +218,12 @@ bool DelayNoMore::UdpSession::openUdpSession(int port) {
|
|||||||
struct sockaddr_in udpAddr;
|
struct sockaddr_in udpAddr;
|
||||||
uv_ip4_addr("0.0.0.0", port, &udpAddr);
|
uv_ip4_addr("0.0.0.0", port, &udpAddr);
|
||||||
struct sockaddr const* pUdpAddr = (struct sockaddr const*)&udpAddr;
|
struct sockaddr const* pUdpAddr = (struct sockaddr const*)&udpAddr;
|
||||||
|
|
||||||
|
memset(peerPunchedMarks, false, sizeof(peerPunchedMarks));
|
||||||
|
for (int i = 0; i < maxPeerCnt; i++) {
|
||||||
|
peerAddrList[i].authKey = -1; // hardcoded for now
|
||||||
|
memset((char*)&peerAddrList[i].sockAddrIn, 0, sizeof(peerAddrList[i].sockAddrIn));
|
||||||
|
}
|
||||||
/*
|
/*
|
||||||
[WARNING] On Android, the libuv documentation of "UV_UDP_REUSEADDR" is true, i.e. only the socket that binds later on the same port will be triggered the recv callback; however on Windows, experiment shows that the exact reverse is true instead.
|
[WARNING] On Android, the libuv documentation of "UV_UDP_REUSEADDR" is true, i.e. only the socket that binds later on the same port will be triggered the recv callback; however on Windows, experiment shows that the exact reverse is true instead.
|
||||||
|
|
||||||
@@ -259,11 +261,8 @@ bool DelayNoMore::UdpSession::closeUdpSession() {
|
|||||||
uv_thread_join(&recvTid);
|
uv_thread_join(&recvTid);
|
||||||
free(udpRecvSocket);
|
free(udpRecvSocket);
|
||||||
free(recvLoop);
|
free(recvLoop);
|
||||||
|
delete recvRingBuff;
|
||||||
|
|
||||||
for (int i = 0; i < maxPeerCnt; i++) {
|
|
||||||
peerAddrList[i].authKey = -1; // hardcoded for now
|
|
||||||
memset((char*)&peerAddrList[i].sockAddrIn, 0, sizeof(peerAddrList[i].sockAddrIn));
|
|
||||||
}
|
|
||||||
CCLOG("Closed udp session and dealloc all resources in GameThread...");
|
CCLOG("Closed udp session and dealloc all resources in GameThread...");
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -321,8 +320,7 @@ bool DelayNoMore::UdpSession::broadcastInputFrameUpsync(BYTEC* const bytes, size
|
|||||||
uv_mutex_lock(&sendRingBuffLock);
|
uv_mutex_lock(&sendRingBuffLock);
|
||||||
// Might want to send several times for better arrival rate
|
// Might want to send several times for better arrival rate
|
||||||
for (int j = 0; j < broadcastUpsyncCnt; j++) {
|
for (int j = 0; j < broadcastUpsyncCnt; j++) {
|
||||||
// Send to room udp tunnel in case of hole punching failure
|
int peerPunchedCnt = 0;
|
||||||
sendRingBuff->put(bytes, bytesLen, &udpTunnelAddr);
|
|
||||||
for (int i = 0; i < roomCapacity; i++) {
|
for (int i = 0; i < roomCapacity; i++) {
|
||||||
if (i + 1 == selfJoinIndex) {
|
if (i + 1 == selfJoinIndex) {
|
||||||
continue;
|
continue;
|
||||||
@@ -331,8 +329,17 @@ bool DelayNoMore::UdpSession::broadcastInputFrameUpsync(BYTEC* const bytes, size
|
|||||||
// Peer addr not initialized
|
// Peer addr not initialized
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (false == peerPunchedMarks[i]) {
|
||||||
|
// Not punched yet, save some bandwidth
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
sendRingBuff->put(bytes, bytesLen, &(peerAddrList[i]));
|
||||||
|
++peerPunchedCnt;
|
||||||
|
}
|
||||||
|
|
||||||
sendRingBuff->put(bytes, bytesLen, &(peerAddrList[i])); // Content hardcoded for now
|
if (peerPunchedCnt + 1 < roomCapacity) {
|
||||||
|
// Send to room udp tunnel in case of ANY hole punching failure
|
||||||
|
sendRingBuff->put(bytes, bytesLen, &udpTunnelAddr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,3 +348,37 @@ bool DelayNoMore::UdpSession::broadcastInputFrameUpsync(BYTEC* const bytes, size
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool DelayNoMore::UdpSession::pollUdpRecvRingBuff() {
|
||||||
|
// This function is called by GameThread 60 fps.
|
||||||
|
|
||||||
|
//uv_mutex_lock(&recvRingBuffLock);
|
||||||
|
while (true) {
|
||||||
|
RecvWork f;
|
||||||
|
bool res = recvRingBuff->pop(&f);
|
||||||
|
if (!res) return false;
|
||||||
|
|
||||||
|
// [WARNING] Declaring "AutoHandleScope" is critical here, otherwise "onUdpMessageCb.toObject()" wouldn't be recognized as a function of the ScriptEngine!
|
||||||
|
se::AutoHandleScope hs;
|
||||||
|
// [WARNING] Use of the "ScriptEngine" is only allowed in "GameThread a.k.a. CocosThread"!
|
||||||
|
se::Value onUdpMessageCb;
|
||||||
|
se::ScriptEngine::getInstance()->getGlobalObject()->getProperty("onUdpMessage", &onUdpMessageCb);
|
||||||
|
if (onUdpMessageCb.isObject() && onUdpMessageCb.toObject()->isFunction()) {
|
||||||
|
//CCLOG("UDP received %d bytes upsync -- 1", nread);
|
||||||
|
se::Object* const gameThreadMsg = se::Object::createTypedArray(se::Object::TypedArrayType::UINT8, f.ui8Arr, f.bytesLen);
|
||||||
|
//CCLOG("UDP received %d bytes upsync -- 2", nread);
|
||||||
|
se::ValueArray args = { se::Value(gameThreadMsg) };
|
||||||
|
|
||||||
|
// Temporarily assume that the "this" ptr within callback is NULL.
|
||||||
|
bool ok = onUdpMessageCb.toObject()->call(args, NULL);
|
||||||
|
if (!ok) {
|
||||||
|
se::ScriptEngine::getInstance()->clearException();
|
||||||
|
}
|
||||||
|
//CCLOG("UDP received %d bytes upsync -- 3", nread);
|
||||||
|
gameThreadMsg->decRef(); // Reference http://docs.cocos.com/creator/2.2/manual/en/advanced-topics/JSB2.0-learning.html#seobject
|
||||||
|
//CCLOG("UDP received %d bytes upsync -- 4", nread);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//uv_mutex_unlock(&recvRingBuffLock);
|
||||||
|
return true;
|
||||||
|
}
|
@@ -1,7 +1,7 @@
|
|||||||
#ifndef udp_session_hpp
|
#ifndef udp_session_hpp
|
||||||
#define udp_session_hpp
|
#define udp_session_hpp
|
||||||
|
|
||||||
#include "send_ring_buff.hpp"
|
#include "ring_buff.hpp"
|
||||||
|
|
||||||
int const maxPeerCnt = 10;
|
int const maxPeerCnt = 10;
|
||||||
|
|
||||||
@@ -14,6 +14,7 @@ namespace DelayNoMore {
|
|||||||
//static bool clearPeerUDPAddrList();
|
//static bool clearPeerUDPAddrList();
|
||||||
static bool punchToServer(CHARC* const srvIp, int const srvPort, BYTEC* const bytes, size_t bytesLen, int const udpTunnelSrvPort, BYTEC* const udpTunnelBytes, size_t udpTunnelBytesBytesLen);
|
static bool punchToServer(CHARC* const srvIp, int const srvPort, BYTEC* const bytes, size_t bytesLen, int const udpTunnelSrvPort, BYTEC* const udpTunnelBytes, size_t udpTunnelBytesBytesLen);
|
||||||
static bool broadcastInputFrameUpsync(BYTEC* const bytes, size_t bytesLen, int roomCapacity, int selfJoinIndex);
|
static bool broadcastInputFrameUpsync(BYTEC* const bytes, size_t bytesLen, int roomCapacity, int selfJoinIndex);
|
||||||
|
static bool pollUdpRecvRingBuff();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
@@ -126,6 +126,20 @@ bool upsertPeerUdpAddr(se::State& s) {
|
|||||||
}
|
}
|
||||||
SE_BIND_FUNC(upsertPeerUdpAddr)
|
SE_BIND_FUNC(upsertPeerUdpAddr)
|
||||||
|
|
||||||
|
bool pollUdpRecvRingBuff(se::State& s) {
|
||||||
|
const auto& args = s.args();
|
||||||
|
size_t argc = args.size();
|
||||||
|
CC_UNUSED bool ok = true;
|
||||||
|
if (0 == argc) {
|
||||||
|
SE_PRECONDITION2(ok, false, "upsertPeerUdpAddr: Error processing arguments");
|
||||||
|
return DelayNoMore::UdpSession::pollUdpRecvRingBuff();
|
||||||
|
}
|
||||||
|
SE_REPORT_ERROR("wrong number of arguments: %d, was expecting %d; or wrong arg type!", (int)argc, 0);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
SE_BIND_FUNC(pollUdpRecvRingBuff)
|
||||||
|
|
||||||
static bool udpSessionFinalize(se::State& s) {
|
static bool udpSessionFinalize(se::State& s) {
|
||||||
CCLOGINFO("jsbindings: finalizing JS object %p (DelayNoMore::UdpSession)", s.nativeThisObject());
|
CCLOGINFO("jsbindings: finalizing JS object %p (DelayNoMore::UdpSession)", s.nativeThisObject());
|
||||||
auto iter = se::NonRefNativePtrCreatedByCtorMap::find(s.nativeThisObject());
|
auto iter = se::NonRefNativePtrCreatedByCtorMap::find(s.nativeThisObject());
|
||||||
@@ -158,6 +172,7 @@ bool registerUdpSession(se::Object* obj) {
|
|||||||
cls->defineStaticFunction("broadcastInputFrameUpsync", _SE(broadcastInputFrameUpsync));
|
cls->defineStaticFunction("broadcastInputFrameUpsync", _SE(broadcastInputFrameUpsync));
|
||||||
cls->defineStaticFunction("closeUdpSession", _SE(closeUdpSession));
|
cls->defineStaticFunction("closeUdpSession", _SE(closeUdpSession));
|
||||||
cls->defineStaticFunction("upsertPeerUdpAddr", _SE(upsertPeerUdpAddr));
|
cls->defineStaticFunction("upsertPeerUdpAddr", _SE(upsertPeerUdpAddr));
|
||||||
|
cls->defineStaticFunction("pollUdpRecvRingBuff", _SE(pollUdpRecvRingBuff));
|
||||||
cls->defineFinalizeFunction(_SE(udpSessionFinalize));
|
cls->defineFinalizeFunction(_SE(udpSessionFinalize));
|
||||||
cls->install();
|
cls->install();
|
||||||
|
|
||||||
|
@@ -15,5 +15,5 @@ SE_DECLARE_FUNC(punchToServer);
|
|||||||
SE_DECLARE_FUNC(broadcastInputFrameUpsync);
|
SE_DECLARE_FUNC(broadcastInputFrameUpsync);
|
||||||
SE_DECLARE_FUNC(closeUdpSession);
|
SE_DECLARE_FUNC(closeUdpSession);
|
||||||
SE_DECLARE_FUNC(upsertPeerUdpAddr);
|
SE_DECLARE_FUNC(upsertPeerUdpAddr);
|
||||||
|
SE_DECLARE_FUNC(pollUdpRecvRingBuff);
|
||||||
#endif
|
#endif
|
||||||
|
@@ -15,7 +15,7 @@ LOCAL_SRC_FILES := hellojavascript/main.cpp \
|
|||||||
../../../Classes/jsb_module_register.cpp \
|
../../../Classes/jsb_module_register.cpp \
|
||||||
../../../Classes/udp_session.cpp \
|
../../../Classes/udp_session.cpp \
|
||||||
../../../Classes/udp_session_bridge.cpp \
|
../../../Classes/udp_session_bridge.cpp \
|
||||||
../../../Classes/send_ring_buff.cpp
|
../../../Classes/ring_buff.cpp
|
||||||
|
|
||||||
LOCAL_C_INCLUDES := $(LOCAL_PATH)/../../../Classes
|
LOCAL_C_INCLUDES := $(LOCAL_PATH)/../../../Classes
|
||||||
|
|
||||||
|
@@ -190,11 +190,11 @@ copy "$(ProjectDir)..\..\..\project.json" "$(OutDir)\" /Y</Command>
|
|||||||
<ClCompile Include="..\Classes\AppDelegate.cpp" />
|
<ClCompile Include="..\Classes\AppDelegate.cpp" />
|
||||||
<ClCompile Include="..\Classes\udp_session.cpp" />
|
<ClCompile Include="..\Classes\udp_session.cpp" />
|
||||||
<ClCompile Include="..\Classes\udp_session_bridge.cpp" />
|
<ClCompile Include="..\Classes\udp_session_bridge.cpp" />
|
||||||
<ClCompile Include="..\Classes\send_ring_buff.cpp" />
|
<ClCompile Include="..\Classes\ring_buff.cpp" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ClInclude Include="main.h" />
|
<ClInclude Include="main.h" />
|
||||||
<ClInclude Include="..\Classes\send_ring_buff.hpp" />
|
<ClInclude Include="..\Classes\ring_buff.hpp" />
|
||||||
<ClInclude Include="..\Classes\udp_session.hpp" />
|
<ClInclude Include="..\Classes\udp_session.hpp" />
|
||||||
<ClInclude Include="..\Classes\udp_session_bridge.hpp" />
|
<ClInclude Include="..\Classes\udp_session_bridge.hpp" />
|
||||||
<ClInclude Include="..\Classes\AppDelegate.h" />
|
<ClInclude Include="..\Classes\AppDelegate.h" />
|
||||||
|
@@ -22,7 +22,7 @@
|
|||||||
<ClCompile Include="..\Classes\jsb_module_register.cpp">
|
<ClCompile Include="..\Classes\jsb_module_register.cpp">
|
||||||
<Filter>Classes</Filter>
|
<Filter>Classes</Filter>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
<ClCompile Include="..\Classes\send_ring_buff.cpp">
|
<ClCompile Include="..\Classes\ring_buff.cpp">
|
||||||
<Filter>Classes</Filter>
|
<Filter>Classes</Filter>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
<ClCompile Include="..\Classes\udp_session.cpp">
|
<ClCompile Include="..\Classes\udp_session.cpp">
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
<Filter>win32</Filter>
|
<Filter>win32</Filter>
|
||||||
</ClInclude>
|
</ClInclude>
|
||||||
<ClInclude Include="resource.h" />
|
<ClInclude Include="resource.h" />
|
||||||
<ClInclude Include="..\Classes\send_ring_buff.hpp">
|
<ClInclude Include="..\Classes\ring_buff.hpp">
|
||||||
<Filter>Classes</Filter>
|
<Filter>Classes</Filter>
|
||||||
</ClInclude>
|
</ClInclude>
|
||||||
<ClInclude Include="..\Classes\udp_session.hpp">
|
<ClInclude Include="..\Classes\udp_session.hpp">
|
||||||
|
@@ -755,7 +755,7 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *RingBuffer
|
|||||||
// Reset playerCollider position from the "virtual grid position"
|
// Reset playerCollider position from the "virtual grid position"
|
||||||
newVx, newVy := currPlayerDownsync.VirtualGridX+currPlayerDownsync.VelX, currPlayerDownsync.VirtualGridY+currPlayerDownsync.VelY
|
newVx, newVy := currPlayerDownsync.VirtualGridX+currPlayerDownsync.VelX, currPlayerDownsync.VirtualGridY+currPlayerDownsync.VelY
|
||||||
if 0 >= thatPlayerInNextFrame.Hp && 0 == thatPlayerInNextFrame.FramesToRecover {
|
if 0 >= thatPlayerInNextFrame.Hp && 0 == thatPlayerInNextFrame.FramesToRecover {
|
||||||
// Revive
|
// Revive from Dying
|
||||||
newVx, newVy = currPlayerDownsync.RevivalVirtualGridX, currPlayerDownsync.RevivalVirtualGridY
|
newVx, newVy = currPlayerDownsync.RevivalVirtualGridX, currPlayerDownsync.RevivalVirtualGridY
|
||||||
thatPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_IDLE1
|
thatPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_IDLE1
|
||||||
thatPlayerInNextFrame.Hp = currPlayerDownsync.MaxHp
|
thatPlayerInNextFrame.Hp = currPlayerDownsync.MaxHp
|
||||||
@@ -945,7 +945,8 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *RingBuffer
|
|||||||
if fallStopping {
|
if fallStopping {
|
||||||
thatPlayerInNextFrame.VelY = 0
|
thatPlayerInNextFrame.VelY = 0
|
||||||
thatPlayerInNextFrame.VelX = 0
|
thatPlayerInNextFrame.VelX = 0
|
||||||
if ATK_CHARACTER_STATE_BLOWN_UP1 == thatPlayerInNextFrame.CharacterState {
|
if ATK_CHARACTER_STATE_DYING == thatPlayerInNextFrame.CharacterState {
|
||||||
|
} else if ATK_CHARACTER_STATE_BLOWN_UP1 == thatPlayerInNextFrame.CharacterState {
|
||||||
thatPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_LAY_DOWN1
|
thatPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_LAY_DOWN1
|
||||||
thatPlayerInNextFrame.FramesToRecover = chConfig.LayDownFramesToRecover
|
thatPlayerInNextFrame.FramesToRecover = chConfig.LayDownFramesToRecover
|
||||||
} else {
|
} else {
|
||||||
@@ -960,9 +961,11 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *RingBuffer
|
|||||||
thatPlayerInNextFrame.FramesToRecover = 0
|
thatPlayerInNextFrame.FramesToRecover = 0
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// landedOnGravityPushback not fallStopping, could be in LayDown or GetUp
|
// landedOnGravityPushback not fallStopping, could be in LayDown or GetUp or Dying
|
||||||
if _, existent := nonAttackingSet[thatPlayerInNextFrame.CharacterState]; existent {
|
if _, existent := nonAttackingSet[thatPlayerInNextFrame.CharacterState]; existent {
|
||||||
if ATK_CHARACTER_STATE_LAY_DOWN1 == thatPlayerInNextFrame.CharacterState {
|
if ATK_CHARACTER_STATE_DYING == thatPlayerInNextFrame.CharacterState {
|
||||||
|
// No update needed for Dying
|
||||||
|
} else if ATK_CHARACTER_STATE_LAY_DOWN1 == thatPlayerInNextFrame.CharacterState {
|
||||||
if 0 == thatPlayerInNextFrame.FramesToRecover {
|
if 0 == thatPlayerInNextFrame.FramesToRecover {
|
||||||
thatPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_GET_UP1
|
thatPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_GET_UP1
|
||||||
thatPlayerInNextFrame.FramesToRecover = chConfig.GetUpFramesToRecover
|
thatPlayerInNextFrame.FramesToRecover = chConfig.GetUpFramesToRecover
|
||||||
|
Reference in New Issue
Block a user