Compare commits

..

29 Commits

Author SHA1 Message Date
APEX FIGHT
cf607fa060 Merge branch 'main' of https://gitea.apexfight.net/apex/CSNC 2025-02-24 21:52:36 -05:00
APEX FIGHT
ef87716a8c added timeout to CSNC server 2025-02-24 21:52:30 -05:00
8459e7df21 Update README.md 2025-01-03 05:46:18 +00:00
9785da2e0d Update Client/Unity/README.md 2025-01-03 05:42:03 +00:00
e67060aaf7 Update README.md 2025-01-03 05:41:41 +00:00
a84cb21d8b Update README.md 2025-01-03 05:40:32 +00:00
aa1be3c082 Update README.md 2025-01-03 05:34:50 +00:00
APEX FIGHT
62a1d22528 added tick interval options (to reduce network load from 80tps lololol) 2025-01-03 00:33:48 -05:00
APEX FIGHT
ef941cd344 added uuid support for synchronization 2025-01-02 18:22:56 -05:00
APEX FIGHT
0810be6809 clearer documentation 2025-01-02 18:22:33 -05:00
APEX FIGHT
7d799457f6 clearer naming 2025-01-02 18:22:20 -05:00
APEX FIGHT
2a445343a2 meta 2025-01-02 18:22:03 -05:00
APEX FIGHT
af9e800716 gameobjectmanager 2025-01-02 18:21:58 -05:00
APEX FIGHT
72113dc2f2 prefab update 2025-01-02 18:21:48 -05:00
APEX FIGHT
a83220ea7c method call to sync objects 2025-01-02 18:21:37 -05:00
APEX FIGHT
3faaba9d0c added object uuid for synchronization 2025-01-02 18:20:54 -05:00
APEX FIGHT
3a30091559 made private public 2025-01-02 18:20:15 -05:00
APEX FIGHT
51132ceb5c updated csncobject class 2025-01-02 18:20:02 -05:00
APEX FIGHT
258f299fa0 default eztransform constructor 2025-01-02 18:19:24 -05:00
APEX FIGHT
a06db78d42 gameobjectmanager 2025-01-02 18:19:09 -05:00
APEX FIGHT
08db445a53 TODO 2025-01-02 16:23:23 -05:00
APEX FIGHT
39a9200d4a CSNC implementation on NODEjs 2025-01-02 16:23:18 -05:00
APEX FIGHT
f7926482d3 Unity Instructions 2025-01-02 16:23:05 -05:00
APEX FIGHT
351348a9d2 TODO 2025-01-02 16:22:46 -05:00
APEX FIGHT
9389114175 syncdata script 2025-01-02 16:22:35 -05:00
APEX FIGHT
2b2de1b52b GameObject registry script 2025-01-02 16:22:22 -05:00
APEX FIGHT
ed2e2c63ef CSNC prefab 2025-01-02 16:22:08 -05:00
APEX FIGHT
988f39144d CSNC script 2025-01-02 16:21:58 -05:00
APEX FIGHT
ec734b6f1e client sync behavior script 2025-01-02 16:21:47 -05:00
21 changed files with 577 additions and 5 deletions

0
Client/Godot/empty Normal file
View File

117
Client/Unity/CSNC.cs Normal file
View File

@@ -0,0 +1,117 @@
using Newtonsoft.Json;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;
public class CSNC : MonoBehaviour
{
public float tickInterval = 5f; // x times per sec
public string ip;
public GameObjectManager manager;
private float lastPing = 0; //last time connected to server
public class EZtransform //ez consistent serialization of transforms
{
public List<float> position;
public List<float> rotation;
public EZtransform(Transform transform)
{
position = new List<float>(3);
rotation = new List<float>(4);
position.Add(transform.position.x);
position.Add(transform.position.y);
position.Add(transform.position.z);
rotation.Add(transform.rotation.x);
rotation.Add(transform.rotation.y);
rotation.Add(transform.rotation.z);
rotation.Add(transform.rotation.w);
}
public EZtransform()
{
position = new List<float>();
rotation = new List<float>();
}
}
public class CSNCObject
{
public string id;
public string type;
public EZtransform transform;
public CSNCObject()
{
id = "";
type = "";
transform = new EZtransform();
}
public CSNCObject(string type, Transform trans, string id)
{
this.id = id;
this.type = type;
this.transform = new EZtransform(trans);
}
public CSNCObject(string type, EZtransform trans, string id)
{
this.id = id;
this.type = type;
this.transform = trans;
}
}
public class SendData
{
public string uuid;
public List<CSNCObject> gameObjects;
public SendData(List<CSNCObject> gameobjects)
{
uuid = GameObjectRegistry.instance.uuid;
gameObjects = gameobjects;
}
}
void Start()
{
ip = CHANGEME; //CHANGE THIS VALUE to the ip address
}
void FixedUpdate()
{
if (Time.timeSinceLevelLoad - (1000f / tickInterval) > lastPing)
{
lastPing = Time.timeSinceLevelLoad;
IEnumerator req = request();
StartCoroutine(req);
}
}
IEnumerator request()
{
//Debug.Log(GameObjectRegistry.instance.registeredObjects);
List<CSNCObject> gameObjects = new List<CSNCObject>();
foreach (SyncData obj in GameObjectRegistry.instance.registeredObjects)
{
gameObjects.Add(new CSNCObject(obj.type, obj.transform, obj.id));
}
SendData sendData = new SendData(gameObjects);
//Debug.Log(JsonConvert.SerializeObject(sendData));
using (UnityWebRequest req = UnityWebRequest.Post(ip, JsonConvert.SerializeObject(sendData), "application/json"))
{
req.SetRequestHeader("request-type", "csnc");
yield return req.SendWebRequest();
if (req.result != UnityWebRequest.Result.Success)
{
Debug.LogError(req.error);
}
else
{
manager.SyncObjects(JsonConvert.DeserializeObject<GameObjectManager.RecievedData>(req.downloadHandler.text));
}
}
}
}

11
Client/Unity/CSNC.cs.meta Normal file
View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6772f36d5b93fd5498e8f3f1d8155ff2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

77
Client/Unity/CSNC.prefab Normal file
View File

@@ -0,0 +1,77 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &9009233966360587498
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 7121321073155658785}
- component: {fileID: 7629741995735410931}
- component: {fileID: 5596437124550187285}
- component: {fileID: 1768507704600374837}
m_Layer: 0
m_Name: CSNC
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &7121321073155658785
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 9009233966360587498}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &7629741995735410931
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 9009233966360587498}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 1c20e30fdef1b0841aa0fdb48748a6a6, type: 3}
m_Name:
m_EditorClassIdentifier:
uuid:
gameObjects: []
ids: []
--- !u!114 &5596437124550187285
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 9009233966360587498}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 6772f36d5b93fd5498e8f3f1d8155ff2, type: 3}
m_Name:
m_EditorClassIdentifier:
ip:
manager: {fileID: 1768507704600374837}
--- !u!114 &1768507704600374837
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 9009233966360587498}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 7f3dbff6bcfe57f4fb9ba717c6fecc0e, type: 3}
m_Name:
m_EditorClassIdentifier:

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: bd3a56f07635e3c46b71afc5c7cf0e2f
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,15 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ClientSyncBehavior : MonoBehaviour
{
public string id; //the id of the gameobject (as of gameobjectregistry) to synchronize across clients
void Start()
{
GameObjectRegistry.instance.registeredObjects.Add(new SyncData(transform, id));
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3d577c3c9b709324fbfb594192714936
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,99 @@
using Newtonsoft.Json;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GameObjectManager : MonoBehaviour
{
/*
This MonoBehaviour Synchronizes the position of All the game objects recieved from other clients
*/
// ID GAMEOBJECT+TYPE
Dictionary<string, GameObjectDataWrapper> inSceneGameObjects = new Dictionary<string, GameObjectDataWrapper>();
void Start()
{
}
public class RecievedData
{
/*
{
"gameObjects": //list of CSNCObjects
[
{
"id": "uuid here",
"type": "razorblade",
"transform": {
"position": [
0.0,
0.53,
0.0
],
"rotation": [
0.0,
0.0,
0.0,
1.0
]
}
},
]
}
*/
public RecievedData(List<CSNC.CSNCObject> gameObjects)
{
this.gameObjects = gameObjects;
}
public List<CSNC.CSNCObject> gameObjects;
}
public class GameObjectDataWrapper
{
public GameObject gameObject;
public string type;
public GameObjectDataWrapper(string type, GameObject gameObject)
{
this.type = type;
this.gameObject = gameObject;
}
}
public void SyncObjects(RecievedData data)
{
//Debug.Log(JsonConvert.SerializeObject(data));
foreach (CSNC.CSNCObject obj in data.gameObjects)
{
if (inSceneGameObjects.ContainsKey(obj.id)) //if in registry update
{
Quaternion rot = new Quaternion(obj.transform.rotation[0], obj.transform.rotation[1], obj.transform.rotation[2], obj.transform.rotation[3]);
Debug.Log(JsonConvert.SerializeObject(obj.transform.position));
Vector3 pos = new Vector3(obj.transform.position[0], obj.transform.position[1], obj.transform.position[2]);
IEnumerator cor = LerpGameObjectFromFixedUpdate(inSceneGameObjects[obj.id].gameObject, rot, pos);
StartCoroutine(cor);
} else //otherwise add to registry
{
inSceneGameObjects.Add(obj.id, new GameObjectDataWrapper(obj.type,Instantiate(GameObjectRegistry.instance.registry[obj.type]))); //Dubious ahh line of code
GameObject g = inSceneGameObjects[obj.id].gameObject;
g.SetActive(true);
g.transform.position = new Vector3(obj.transform.position[0], obj.transform.position[1], obj.transform.position[2]); //these 2 might be even more dubious than the last
g.transform.rotation = new Quaternion(obj.transform.rotation[0], obj.transform.rotation[1], obj.transform.rotation[2], obj.transform.rotation[3]);
}
}
}
IEnumerator LerpGameObjectFromFixedUpdate(GameObject obj, Quaternion rot, Vector3 pos)
{
obj.transform.position = pos;
obj.transform.rotation = rot;
yield return null;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7f3dbff6bcfe57f4fb9ba717c6fecc0e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,35 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GameObjectRegistry : MonoBehaviour
{
public static GameObjectRegistry instance; //instance of gameobject registry in scene
public string uuid; //unique identifier for this client
public List<GameObject> gameObjects; //list to make it easy to register new gameobjects
public List<string> types;
public Dictionary<string, GameObject> registry = new Dictionary<string, GameObject>(); //registry of game objects that can be synced (needs to be identical on both clients
public List<SyncData> registeredObjects = new List<SyncData>(); //game objcets that are currently being synced
void Start()
{
GameObjectRegistry.instance = this;
uuid = System.Guid.NewGuid().ToString(); //create a uuid for this player
initializeRegistry();
}
void initializeRegistry()
{
int i = 0;
foreach (string id in types)
{
registry.Add(id, gameObjects[i]);
i++;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1c20e30fdef1b0841aa0fdb48748a6a6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: -20
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

37
Client/Unity/README.md Normal file
View File

@@ -0,0 +1,37 @@
# Unity Setup
First, set the ip of the csnc compatible server. \
(it needs to be formatted as such http://192.51.100.123:1234/ or http://mydomain.tld:1234/)
![image](./InstructionalImages/ipchange.png)
Then schedule the GameObjectRegistry script to be\
(Go To: Edit / Project Settings / Script Execution Order)
![image](./InstructionalImages/scheduling.png)
then add the CSNC prefab to your scene
from here you can add references to the models that you want to synchronize across projects as per the image
(these are references to models that will be shown on the client, but controlled by another players client)
![image](./InstructionalImages/ineditor.png)
take note that the objects are children of the CSNC prefab and they are not enabled
in addition to this, the objects under CSNC have NO behaviour scripts attached to them, this is important because it may cause desynchronization if you dont know what you're doing
## Client Controlled GameObjects
Now that you have set up CSNC you can start adding client controlled GameObjects
it is quite trivial to add new Client Synchronized GameObjects, simply attach the "Client Sync Behavior" component to the gameobject you want to sync
![image](./InstructionalImages/clientobjectsetup.png)
As you can see, the client sync behavior is attached to the gameobject and given the id "razorblade" \
(remember what you entered earlier from when we set up the game object registry)
### Congratulations!!
And thats it!! once you have set this up the two gameobjects will be synchronized across clients (assuming you have set up the server - side)

17
Client/Unity/SyncData.cs Normal file
View File

@@ -0,0 +1,17 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SyncData //structure for gameobjects and data sent over to the server
{
public SyncData(Transform pos, string type)
{
this.id = System.Guid.NewGuid().ToString();
this.transform = pos;
this.type = type;
}
public Transform transform;
public string id;
public string type; //key in registry data that defines which gameobject to instantiate with transform: position
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: dbe7422037114f74c80f641e2b629993
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,7 +1,16 @@
# CSNC
Client-Sync-Net-Code for unity game objects
A set of very basic drop-in scripts for client-server games that allows 'p2p' style synchronization
great for synchronizing simple visual things that are mostly controlled by clients such as particles or thrown objects
Client-Sync-Net-Code for unity (but potentially not just unity) game objects
A set of very basic drop-in scripts for client-server games that allows 'p2p' style synchronization
great for synchronizing simple visual things that are mostly controlled by clients such as particles or thrown objects
## Unity Setup
Please refer to : \
https://gitea.apexfight.net/apex/CSNC/src/branch/main/Client/Unity#unity-setup
## NodeJS Setup
Email me at vlrtch3571@gmail.com if you want me to make a tutorial or need help
this project is about 90% complete other than the godot and go implementations which will be done if i ever decide to make anything with either one

0
Server/Go/empty Normal file
View File

104
Server/Node/CSNC.js Normal file
View File

@@ -0,0 +1,104 @@
const timeoutTickRate = 1; //timeout check every x seconds
const timeoutLength = 5000; //timeout length in ms
var gameObjectStore = {}; //object which stores CSNCgameobjects for each client
//below is an example of the structure of gameobjectstore
/**
* {
* "2c0a48e9-40c6-4139-9697-992397773b12":
* time: Date.now(),
* gameObjects: [
* {
"type": "razorblade",
"transform": {
"position": [
0.0,
0.53,
0.0
],
"rotation": [
0.0,
0.0,
0.0,
1.0
]
}
},
{
"type": "boomerang",
"transform": {
"position": [
0.0,
0.0,
0.0
],
"rotation": [
-0.7071068,
0.0,
0.0,
0.7071067
]
}
}
* ]
* }
*
*
*/
/**
* @param {string} uuid
*/
function removePlayer(uuid) {
delete gameObjectStore[uuid];
}
function checkTimeout() {
for (let key in gameObjectStore) {
if (Date.now() - gameObjectStore[key].time > timeoutLength) {
removePlayer(key);
}
}
}
/**
* @param {http.ServerResponse<http.IncomingMessage>} res
* @param {string} req
*/
//reads data from client
async function handleResponse(res, req) {
try {
var request = JSON.parse(req);
let gameObjects = request.gameObjects;
gameObjectStore[request.uuid] = {gameObjects: gameObjects, time: Date.now()}; //store gameobjects to send to other players
res.writeHead(200);
res.write(JSON.stringify(responseObjectHelper(request.uuid)));
res.end();
} catch (err) {
console.log("Invalid Request");
console.log(err);
res.end();
}
}
//preps and returns data to send back to client
function responseObjectHelper(uuid) {
var responseObject = {
gameObjects: []
};
for (let key in gameObjectStore) {
if (key == uuid) continue; //dont send back players own data
responseObject.gameObjects = responseObject.gameObjects.concat(gameObjectStore[key].gameObjects);
}
return responseObject;
}
setInterval(checkTimeout, timeoutTickRate * 1000);
exports.handleResponse = handleResponse;