First client/server snapshot implementation

This commit is contained in:
Anuken 2018-06-08 12:22:07 -04:00
parent 0bd0602655
commit 8e00bdcf30
7 changed files with 132 additions and 25 deletions

View file

@ -1,8 +1,12 @@
package io.anuke.mindustry.core;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.utils.reflect.ClassReflection;
import com.badlogic.gdx.utils.reflect.ReflectionException;
import io.anuke.annotations.Annotations.Remote;
import io.anuke.mindustry.core.GameState.State;
import io.anuke.mindustry.entities.Player;
import io.anuke.mindustry.entities.traits.SyncTrait;
import io.anuke.mindustry.gen.Call;
import io.anuke.mindustry.net.Net;
import io.anuke.mindustry.net.Net.SendMode;
@ -10,10 +14,17 @@ import io.anuke.mindustry.net.NetworkIO;
import io.anuke.mindustry.net.Packets.*;
import io.anuke.ucore.core.Timers;
import io.anuke.ucore.entities.Entities;
import io.anuke.ucore.entities.EntityGroup;
import io.anuke.ucore.io.ReusableByteArrayInputStream;
import io.anuke.ucore.io.delta.DEZDecoder;
import io.anuke.ucore.modules.Module;
import io.anuke.ucore.util.Log;
import io.anuke.ucore.util.Timer;
import java.io.DataInputStream;
import java.io.IOException;
import java.util.Arrays;
import static io.anuke.mindustry.Vars.*;
public class NetClient extends Module {
@ -27,6 +38,15 @@ public class NetClient extends Module {
private boolean quiet = false;
/**Counter for data timeout.*/
private float timeoutTime = 0f;
/**Last snapshot recieved.*/
private byte[] lastSnapshot;
/**Last snapshot ID recieved.*/
private int lastSnapshotID = -1;
/**Decoder for uncompressing snapshots.*/
private DEZDecoder decoder = new DEZDecoder();
/**Byte stream for reading in snapshots.*/
private ReusableByteArrayInputStream byteStream = new ReusableByteArrayInputStream();
private DataInputStream dataStream = new DataInputStream(byteStream);
public NetClient(){
@ -135,6 +155,7 @@ public class NetClient extends Module {
Player player = players[0];
ClientSnapshotPacket packet = new ClientSnapshotPacket();
packet.lastSnapshot = lastSnapshotID;
packet.player = player;
Net.send(packet, SendMode.udp);
}
@ -143,4 +164,73 @@ public class NetClient extends Module {
Net.updatePing();
}
}
@Remote(one = true, all = false, unreliable = true)
public static void onSnapshot(byte[] snapshot, int snapshotID){
//skip snapshot IDs that have already been recieved
if(snapshotID == netClient.lastSnapshotID){
return;
}
try {
byte[] result;
int length;
if (snapshotID == -1) { //-1 = fresh snapshot
result = snapshot;
length = snapshot.length;
netClient.lastSnapshot = snapshot;
} else { //otherwise, last snapshot must not be null, decode it
netClient.decoder.init(netClient.lastSnapshot, snapshot);
result = netClient.decoder.decode();
length = netClient.decoder.getDecodedLength();
//set last snapshot to a copy to prevent issues
netClient.lastSnapshot = Arrays.copyOf(result, length);
}
//set stream bytes to begin write
netClient.byteStream.setBytes(result, 0, length);
//get data input for reading from the stream
DataInputStream input = netClient.dataStream;
byte totalGroups = input.readByte();
//for each group...
for (int i = 0; i < totalGroups; i++) {
//read group info
byte groupID = input.readByte();
short amount = input.readShort();
long timestamp = input.readLong();
EntityGroup<?> group = Entities.getGroup(groupID);
//go through each entity
for (int j = 0; j < amount; j++) {
int id = input.readInt();
SyncTrait entity = (SyncTrait) group.getByID(id);
//entity must not be added yet, so create it
if(entity == null){
entity = (SyncTrait) ClassReflection.newInstance(group.getType()); //TODO solution without reflection?
entity.add();
}
//read the entity
entity.read(input, timestamp);
}
}
//confirm that snapshot 0 has been recieved if this is the initial snapshot
if(snapshotID == -1){
netClient.lastSnapshotID = 0;
}else{ //confirm that the snapshot has been recieved
netClient.lastSnapshotID = snapshotID;
}
}catch (IOException | ReflectionException e){
throw new RuntimeException(e);
}
}
}

View file

@ -23,7 +23,6 @@ import io.anuke.ucore.io.delta.ByteMatcherHash;
import io.anuke.ucore.io.delta.DEZEncoder;
import io.anuke.ucore.modules.Module;
import io.anuke.ucore.util.Log;
import io.anuke.ucore.util.Timer;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
@ -43,7 +42,6 @@ public class NetServer extends Module{
/**Maps connection IDs to players.*/
private IntMap<Player> connections = new IntMap<>();
private boolean closing = false;
private Timer timer = new Timer(5);
/**Stream for writing player sync data to.*/
private ByteArrayOutputStream syncStream = new ByteArrayOutputStream();
@ -131,8 +129,8 @@ public class NetServer extends Module{
Platform.instance.updateRPC();
});
Net.handleServer(ClientSnapshotPacket.class, (id, packet) -> {});
//update last recieved snapshot based on client snapshot
Net.handleServer(ClientSnapshotPacket.class, (id, packet) -> Net.getConnection(id).lastSnapshotID = packet.lastSnapshot);
Net.handleServer(InvokePacket.class, (id, packet) -> RemoteReadServer.readPacket(packet.writeBuffer, packet.type, connections.get(id)));
}
@ -190,6 +188,19 @@ public class NetServer extends Module{
//iterate through each player
for (Player player : connections.values()) {
NetConnection connection = Net.getConnection(player.clientid);
if(!player.timer.get(Player.timeSync, serverSyncTime)) continue;
//if the player hasn't acknolwedged that it has recieved the packet, send the same thing again
if(connection.lastSentSnapshotID > connection.lastSnapshotID){
Call.onSnapshot(connection.id, connection.lastSentSnapshot, connection.lastSentSnapshotID);
return;
}else{
//set up last confirmed snapshot to the last one that was sent, otherwise
connection.lastSnapshot = connection.lastSentSnapshot;
}
//reset stream to begin writing
syncStream.reset();
@ -207,24 +218,32 @@ public class NetServer extends Module{
//TODO range-check sync positions?
if (group.isEmpty() || !(group.all().get(0) instanceof SyncTrait)) continue;
//make sure mapping is enabled for this group
if(!group.mappingEnabled()){
throw new RuntimeException("Entity group '" + group.getType() + "' contains SyncTrait entities, yet mapping is not enabled. In order for syncing to work, you must enable mapping for this group.");
}
//write group ID + group size
dataStream.writeByte(group.getID());
dataStream.writeShort(group.size());
//write timestamp
dataStream.writeLong(TimeUtils.millis());
for(Entity entity : group.all()){
//write all entities now
dataStream.writeInt(entity.getID());
((SyncTrait)entity).write(dataStream);
}
}
NetConnection connection = Net.getConnection(player.clientid);
byte[] bytes = syncStream.toByteArray();
if(connection.lastSnapshot == null){
//no snapshot to diff, send it all
Call.onSnapshot(connection.id, bytes, -1);
}else{
//send diff otherwise
//increment snapshot ID
connection.lastSnapshotID ++;
//send diff, otherwise
byte[] diff = ByteDeltaEncoder.toDiff(new ByteMatcherHash(connection.lastSnapshot, bytes), encoder);
Call.onSnapshot(connection.id, diff, connection.lastSnapshotID);
@ -240,7 +259,7 @@ public class NetServer extends Module{
@Remote(server = false)
public static void connectConfirm(Player player){
player.add();
Log.info("&y{0} has connected.", player.name);
netCommon.sendMessage("[accent]" + player.name + " has connected.");
Log.info("&y{0} has connected.", player.name);
}
}

View file

@ -41,6 +41,10 @@ public class Player extends Unit implements BuilderTrait, CarryTrait {
private static final float dashSpeed = 1.8f;
private static final Vector2 movement = new Vector2();
public static final int timerShootLeft = 0;
public static final int timerShootRight = 1;
public static final int timeSync = 2;
//region instance variables, constructor
public float baseRotation;

View file

@ -7,9 +7,12 @@ public abstract class NetConnection {
public final String address;
/**ID of last snapshot this connection is guaranteed to have recieved.*/
public int lastSnapshotID;
/**Byte array of last sent snapshot data that is confirmed to be recieved..*/
public int lastSnapshotID = -1;
/**Byte array of last sent snapshot data that is confirmed to be recieved.*/
public byte[] lastSnapshot;
/**ID of last sent snapshot.*/
public int lastSentSnapshotID = -1;
/**Byte array of last sent snapshot.*/
public byte[] lastSentSnapshot;

View file

@ -1,17 +1,5 @@
package io.anuke.mindustry.net;
import io.anuke.annotations.Annotations.Remote;
import io.anuke.mindustry.entities.Player;
public class NetEvents {
@Remote(one = true, all = false, unreliable = true)
public static void onSnapshot(byte[] snapshot, int snapshotID){
}
@Remote(unreliable = true, server = false)
public static void onRecievedSnapshot(Player player, int snapshotID){
Net.getConnection(player.clientid).lastSnapshotID = snapshotID;
}
}

View file

@ -100,11 +100,13 @@ public class Packets {
public static class ClientSnapshotPacket implements Packet{
public Player player;
public int lastSnapshot;
@Override
public void write(ByteBuffer buffer) {
ByteBufferOutput out = new ByteBufferOutput(buffer);
buffer.putInt(lastSnapshot);
buffer.putInt(player.id);
buffer.putLong(TimeUtils.millis());
player.write(out);
@ -114,6 +116,7 @@ public class Packets {
public void read(ByteBuffer buffer) {
ByteBufferInput in = new ByteBufferInput(buffer);
lastSnapshot = buffer.getInt();
int id = buffer.getInt();
long time = buffer.getLong();
player = Vars.playerGroup.getByID(id);

View file

@ -50,8 +50,8 @@ public class Weapon extends Upgrade {
}
public void update(Player p, boolean left, float pointerX, float pointerY){
int t = left ? 1 : 2;
int t2 = !left ? 1 : 2;
int t = left ? Player.timerShootLeft : Player.timerShootRight;
int t2 = !left ? Player.timerShootRight : Player.timerShootLeft;
if(p.inventory.hasAmmo() && p.timer.get(t, reload)){
if(roundrobin){
p.timer.reset(t2, reload/2f);
@ -70,7 +70,7 @@ public class Weapon extends Upgrade {
}
public float getRecoil(Player player, boolean left){
return 1f-Mathf.clamp(player.timer.getTime(left ? 1 : 2)/reload);
return 1f-Mathf.clamp(player.timer.getTime(left ? Player.timerShootLeft : Player.timerShootRight)/reload);
}
public float getReload(){