Bonjour
Je fais un jeu en 3D isométrique. Pour économiser la mémoire du browser (client), le jeu est décomposé en chuncks de 40x40 tiles. Les chuncks chargés par le client sont mis à jour à chaque fin de scroll : quand l’utilisateur a fini de scroller, le client fait deus choses : 1) il envoie sa position au serveur, qui lui retourne ensuite les chuncks encore non chargés, l’idée étant que le client a toujours en mémoire 25 chuncks (5x5), le centre de ces 5x5 chuncks étant le chunck qu’il affiche à l’écran. 2) le client supprime de sa mémoire les chuncks déjà chargés, mais qui sont hors de son scope, c’est à dire qui sont au delà d’un radius de 2 par rapport à là où il regarde.
Dans le dessin A, les carrés à bords noirs sont des canvas, celui à bords rouge étant celui que le client ‘regarde’ (affiche à l’écran). Les chuncks sont de la data pure (herbe à tel endroit, eau à tel autre) et sont dessinés sur les canvas. Dans ma typolgie, un chunck peut être de type ‘full’ ou ‘cross’ : un full occupera un canvas entièrement, tandis qu’un cross sera dessiné et divisé sur les 4 canvas adjacents. exemple : le chunck 0,0, en rouge sur le dessin A, est un chunck full. le chunck 0,1 est un cross (divisé en 4 : vert, gris, cyan, jaune), chaque quart étant dessiné sur un canvas différent.
Dans le dessin B, le client a scrollé vers le bas, et est donc passé du chunck 0,0 au chunck -2,2 en visu. J’ai laissé, remplis de blanc, les canvas que le client a supprimé mais en vrai ils sont totalement effacés de la mémoire du client. En foncé, sont les chuncks (ou les quarts de chunck) que le client a déjà dessiné. En clair, sont les chuncks (le serveur ne renvoie que des chuncks complets, pas de quarts) que le client va devoir dessiner, considérant le changement de scroll.
Jusque là j’espère être clairParce que je n’avais pas du tout anticipé mon problème, que je n’arrive en fait pas du tout à expliquer : au démarrage du jeu, node lance le serveur, le client se connecte, envoie au serveur qu’il regarde en 0,0, le serveur lui renvoie tous les chuncks de x=-2 à x=2 et de y=-2 à y=2, le client les reçoit, dessine d’abord tous les chuncks full en ajoutant un nouveau canvas au préalable, puis dessine tous les cross. Jusque là tout va bien. En revanche, quand je scroll vers le bas, et regarde le chunck -2,2, si je prends l’exemple de ce chunck -2,2 en particulier (mais c’est valable pour les autres aussi), le client ne devrait avoir à dessiner que les quarts de cross cyan et gris, puisque, quand il regardait en 0,0, le reste de ce canvas était déjà dessiné. Au lieu de ça, une fois que le client a reçu ses chuncks du serveur et les a dessinés, il s’avère que le canvas qui contient le chunck -2,2 est complètement effacé (donc transparent) pour une raison que j’ignore, et ensuite (comme prévu), les deux nouveaux quarts (cyan et gris donc), sont dessinés. Formulé autrement : après un scroll, si le client doit compléter un canvas qui n’était pas entièrement dessiné, le canvas est complètement effacé avant (et c’est bien là mon problème).
Je précise que le client tient un répertoire des chuncks, et que dans le cas des chuncks cross, il note dans un bitset quels quarts sont dessinés et lesquels ne le sont pas, de sorte à s’économiser à la réception des chuncks.
Je vous mets quelques fonctions pour que cela soit un peu plus clair :
// côté serveur : en fin de scroll, le client renvoie un message 'iLookhere' avec sa position et le serveur lui renvoie les chuncks qui vont bien :
// côté client : quand je reçois un message chunck, je crée les canvas si ce sont des full, et dessine sur les canvas :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47 socket.on('iLookHere', function(msg) // fol { // position actuelle du client : servira à calculer quels chncks le client a déjà, par rapport à la nouvelle position et ce qu'on doit envoyer let prevPos = world._socketManager._sockets[socket.id].pos; world._socketManager._sockets[socket.id].pos = msg; let i, j; [i, j] = msg; let packet = []; // pr chaque chunck autour de cx, cy, à 2 de radius for (let cx=i-2; cx<i+1+2; cx++) { for (let cy=j-2; cy<j+1+2; cy++) { // si ce chcunk n'était pas dans le scope de previousPos // (et donc n'a pas été envoyé dans la précédene udpte de chuncks envoyée) if (prevPos == undefined || cx < prevPos[0]-2 || cx > prevPos[0]+2 || cy < prevPos[1]-2 || cy > prevPos[1]+2) { let obj = { pos : [cx, cy], data : { _ground : [], _entities : [], } }; let ccx = cx * 40; let ccy = cy * 40; for (let x=0; x<40; x++) for (let y=0; y<40; y++) obj.data._ground.push(world._map[[ccx+x,ccy+y]].ground); packet.push(obj); } } } world._socketManager.send('chunck', packet, [socket.id]); });
// côté client toujours : cette fonction est appelée dès que le client se connecte et après chaque fin de scroll.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242 world._socket.on('chunck', function(packet) // fol { let cx, cy; [cx, cy] = world.chunckAt(-world._isometric._leftOffset, -world._isometric._topOffset); console.log('looking at', cx, cy); let cnt = 0; // on store les offsets de _isometric parce qu'on va les manipuler dans les boucles let oldLeftOffset = world._isometric._leftOffset; let oldTopOffset = world._isometric._topOffset; // 1 : on crée les canvas en damier et on dessine les fulls for (let n=0; n<packet.length; n++) { let msg = packet[n]; // ici, on ne rentre qu'une fois (on ne dessine qu'une fois un full sur un cavnas) if (world._chuncks[[...msg.pos]]._type === 'full' && world._chuncks[[...msg.pos]]._loaded == false) { world._chuncks[[...msg.pos]]['_canvas'] = document.createElement('canvas'); world._chuncks[[...msg.pos]]['_entities'] = new Set(); world._chuncks[[...msg.pos]]['_reliefs'] = []; world._chuncks[[...msg.pos]]._canvas.width = 2400; world._chuncks[[...msg.pos]]._canvas.height = 1200; world._chuncks[[...msg.pos]]._canvas.style.zIndex = 0; world._chuncks[[...msg.pos]]._canvas.style.left = 8 + (40 * (msg.pos[0]+msg.pos[1]) + world._isometric._leftOffset + world._isometric._topOffset) * 30 + "px"; world._chuncks[[...msg.pos]]._canvas.style.top = 8 - 585 + (40 * (msg.pos[1]-msg.pos[0]) + world._isometric._topOffset - world._isometric._leftOffset) * 15 + "px"; document.body.appendChild(world._chuncks[[...msg.pos]]._canvas); // les fulls for (let i=0; i<msg.data._ground.length; i++) // fol { let top = i % 40; let left = (i - top) / 40; let groundValue = msg.data._ground[i]; // on store les offsets de _isometric world._isometric._leftOffset = 0; world._isometric._topOffset = 0; let pos = world._isometric.absoluteToScreen(left, top); pos.y += 585; world._isometric._leftOffset = oldLeftOffset; world._isometric._topOffset = oldTopOffset; if (groundValue.includes('grass')) { let regExp = /grass\/([0-9]*)/; let matches = regExp.exec(groundValue); world._chuncks[[...msg.pos]]._canvas.getContext('2d').drawImage(world._preloadedImages['grass/'+matches[1]], pos.x, pos.y); cnt++; } else { world._chuncks[[...msg.pos]]._canvas.getContext('2d').drawImage(world._preloadedImages[groundValue], pos.x, pos.y); cnt++; } } world._chuncks[[...msg.pos]]._loaded = true; } } // les cross. ici, on peut rentrer pluseurs fois : un cx cy demandera qu'on dessine certaines area, un autre cx cy en demandera d'autres // 2 : on dessine les cross (mais au moins les canvas existent déjà grace à 1) for (let n=0; n<packet.length; n++) { let msg = packet[n]; // 40x40 // 15 = 4 bits chargés (1 bit par area : bit0 = 1, bit2. =2, bit3 = 4..) 1+2+4+8 = 15 // si 15, les 4 areas sont dessinées if (world._chuncks[[...msg.pos]]._type === 'cross' && world._chuncks[[...msg.pos]]._areaBool != 0b1111) { if (!world._chuncks[[...msg.pos]].hasOwnProperty('_entities')) { world._chuncks[[...msg.pos]]['_entities'] = new Set(); world._chuncks[[...msg.pos]]['_reliefs'] = []; } let areaBool = world._chuncks[[...msg.pos]]._areaBool; for (let i=0; i<msg.data._ground.length; i++) // fol { let top = i % 40; let left = (i - top) / 40; // Where to draw ?? sur un aure chunck, un qui a un cavnas, ça depend de top et left, ils déterminerot sur quel chcnk on dessinera // Imaginon un tab 4x4 et xy : // 00 10 20 30 // 01 11 21 31 // 02 12 22 32 // 03 13 23 33 // condition a : (x + y < 3) -> donne la moitité nord ouest, sinon sud est (b) -> on prndra 39 au lieu de 3 // conditin c : (x < y) -> donne la moitié sud ouest, sinon nord est (d) // attention au valeurs intermédiares, les <= // les quarts sont déterminés par cmbinaison des tests a et c // * * // ** | ** // x ** | ** // ** axd | bxd ** // ** --------------|------------- ** // ** axc | bxc ** // y ** | ** // ** | ** // *|* // si on affiche que les a = 2, on dessine que sur le top left de chaue sprite. Normal ? // js pas bloquant et la diff entre full et cros c'est que cross dessine suvvessivement // très vite sur diffrents chuncks à chaque icnrementation // peut etre que ça fait bugger le système // refaire la loop pour dessiner en 4 gros coups : // zone 0, 1, 2, 3 // au lieu de fare top left et de déterminer // non pq mm en dessinan que area 0, 2,2 n'affiche que l'angle 0 et pas le full /* pb : pr les packets cross recus apre!s le gros paquet initial, si l'area x d'un cross doit être dessinée dans l'angle d'un full qui est déjà dessiné, le full est commlètement erase avant */ let area = [0,0,0,0]; // un pr chaque quart : N E S W area[0] = (left+top<=39) && (top<=left); // a x d area[1] = (left+top>=39) && (top<=left); // b x d area[2] = (left+top>=39) && (left<=top); // b x c area[3] = (left+top<=39) && (left<=top); // a x c // draw : faut il la dessine sur cette area ? // delta : chucnk où je dois draw, en delta, par rapport à pos // offset : l'area Est du chunck -1,0 sera dessinée en Ouest sur le chuck 0,0, donc en relatif sur un chunk, on dessine pas au mm endroit area[0] = { draw:area[0], delta:{'x':0,'y':-1} }; area[1] = { draw:area[1], delta:{'x':1,'y':0} }; area[2] = { draw:area[2], delta:{'x':0,'y':1} }; area[3] = { draw:area[3], delta:{'x':-1,'y':0} }; for (let a=0; a<4; a++) { // c'est pas sur pos (cross) qu'il faut controler si dessiné, si ??? le _areaBool c'est celi de quel chunk ?? // si dans notre config, si ce tile doit être dessiné dans la zone a et qu'il n'y est pas déjà dessiné if (area[a].draw == true && (world._chuncks[[...msg.pos]]._areaBool & (1 << a)) == 0) { // chcnk sur lequel on va dessiner let drawOn = [msg.pos[0]+area[a].delta.x, msg.pos[1]+area[a].delta.y]; if (world._chuncks[drawOn] != undefined) { // 0 -> 1 // 1 -> 2 // 2 -> 4 // 3 -> 8 // on réécrit un 1 autant de fois qu'il y a de tile par area areaBool |= (1 << a); let groundValue = msg.data._ground[i]; world._isometric._leftOffset = -area[a].delta.x * 40; world._isometric._topOffset = -area[a].delta.y * 40; let pos = world._isometric.absoluteToScreen(left, top); pos.y += 585; world._isometric._leftOffset = oldLeftOffset; world._isometric._topOffset = oldTopOffset; if (groundValue.includes('grass')) { let regExp = /grass\/([0-9]*)/; let matches = regExp.exec(groundValue); world._chuncks[drawOn]._canvas.getContext('2d').drawImage(world._preloadedImages['grass/'+matches[1]], pos.x, pos.y); } else { world._chuncks[drawOn]._canvas.getContext('2d').drawImage(world._preloadedImages[groundValue], pos.x, pos.y); } } } } } world._chuncks[[...msg.pos]]._areaBool = areaBool; } } });
// les chuncks non nécessaires sont supprimés et une fonction détermine à l'avance si le chunck sera de type full ou cross
Voilà c'est peut être pas très bien codé et je m'en excuse, normalement la logique est bonne mais j'ai dû passer à coté de quelque chose
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94 updateArea:function() { let cx, cy; [cx, cy] = world.chunckAt(-world._isometric._leftOffset, -world._isometric._topOffset); // si je me déplace de 2 cases (et pas de 2 chunks), peu de chances de changer de chunk en fait if (world._cx == cx && world._cy == cy) return; world._cx = cx; world._cy = cy; // server comprend tout seul qu'il doit envoyer radius 2 autour world._socket.emit('iLookHere', world.chunckAt(-world._isometric._leftOffset, -world._isometric._topOffset)); for (let i=cx-world._radius; i<cx+1+world._radius; i++) { for (let j=cy-world._radius; j<cy+1+world._radius; j++) { if (world._chuncks[[i,j]] == undefined) { world._chuncks[[i,j]] = {}; if ((i+j) % 2 == 0) { world._chuncks[[i,j]]['_type'] = 'full'; world._chuncks[[i,j]]['_loaded'] = false; } else { world._chuncks[[i,j]]['_type'] = 'cross'; world._chuncks[[i,j]]['_areaBool'] = 0b0000; } } } } for (let c in world._chuncks) { let posX = parseInt(c.split(',')[0]); let posY = parseInt(c.split(',')[1]); // s'il n'ets plus nécessaire if (posX < cx-world._radius || posX > cx+world._radius || posY < cy-world._radius || posY > cy+world._radius) { // si chunk pas encore chargé (rien de dessiné dessus) -> entites ajouté dans index.js if (!world._chuncks[c].hasOwnProperty('_entities')) { // on le release de la mémoire world._chuncks[c] = null; delete world._chuncks[c]; } else { // s'il est chargé et dessiné world._chuncks[c]._entities.forEach((id) => { world._entities[id].destroy(); delete world._entities[id]; }); delete world._chuncks[c]._entities; // si le chuncks que je supprime est un full // il affectera areaBool des chuncks voisins qui eux sont en cross if (world._chuncks[c].hasOwnProperty('_canvas')) { //console.log('from', c); let c0 = parseInt(c.split(',')[0]); let c1 = parseInt(c.split(',')[1]); // chuncks des cross arrounds, dans l'ordre des areas à supprimer let arrs = [ [c0,c1+1], // area 0 à supprimer [c0-1,c1], [c0,c1-1], [c0+1,c1], ]; for (let a=0; a<4; a++) { if (world._chuncks.hasOwnProperty(arrs[a])) world._chuncks[arrs[a]]._areaBool &= ~(1 << a); } console.log('canvas', c, 'removed'); document.body.removeChild(world._chuncks[c]._canvas); world._chuncks[c]._canvas = null; } world._chuncks[c] = null; delete world._chuncks[c]; } } } },
C'est peut être plus un problème de javascript que de node, mais en même temps je ne suis pas sûr que j'aurais eu ce problèmeen full JS.
Dernière précision mais pas des moindres : dans la fonction client world._socket.on('chunck'), supprimer les deux lignes "world._chuncks[drawOn]._canvas.getContext('2d').drawImage....." résout plus ou moins le bug : les canvas ne sont plus effacés, en revanche bien sûr les quarts de sont pas complétés.
Partager