I did some research on how Unity handles commands and remote procedure calls (documentation for those here:
https://docs.unity3d.com/Manual/UNetActions.html)Unity rewrites the code to call a generated method that sends a message to clients (or servers, for Commands) containing the arguments. This makes it incredibly easy to add and use network messages.
Let me give an example by comparing it to GoldSource's approach and the approach i was planning on using with Protobuf.
Here's how you
declare a network message and implement it:
//In player.cpp
int gmsgMyMsg = 0;
//In LinkUserMessages
//Maximum name length is 12 bytes
//Sending a string, so variable length message (-1)
//Maximum message size is 192 bytes
gmsgMyMsg = REG_USER_MSG( "MyMsg", -1 );
//In hud.h
int _cdecl MsgFunc_MyMsg(const char *pszName, int iSize, void *pbuf);
//In hud.cpp
int __MsgFunc_MyMsg(const char *pszName, int iSize, void *pbuf)
{
return gHUD.MsgFunc_MyMsg(pszName, iSize, pbuf );
}
//In CHud::Init
HOOK_MESSAGE( MyMsg );
//In hud_msg.cpp
int CHud:: MsgFunc_MyMsg( const char *pszName, int iSize, void *pbuf )
{
BEGIN_READ( pbuf, iSize );
CenterPrint( READ_STRING() );
return 1;
}
This is just for a message in CHud, it's a bit different for a message in another Hud element.
And here's how you use it:
extern int gmsgMyMsg;
MESSAGE_BEGIN( MSG_ALL, gmsgTextMsg );
WRITE_STRING( "my message" );
MESSAGE_END();
Assuming you're sending it to all clients reliably.
The Protobuf approach would be like this:
Declaring the message:
package SharpLife.Game.Networking.Messages;
message MyMsg
{
string text = 1;
}
Receiving it in code:
public class SomeClass : IMessageReceiveHandler<MyMsg>
{
public SomeClass(MessagesReceiveHandler handler)
{
handler.RegisterHandler(this);
}
public void ReceiveMessage(NetConnection connection, MyMsg message)
{
//-1 means centered for an axis
_graphics.Text.DrawText(message.Text, -1, -1);
}
}
And using it:
Context.Messages.SendMessage(new MyMsg{ Text = "my message" });
And now the Unity approach:
//Singleton component on some entity used to communicate from server to client for the hud
public class HudText : Component
{
[ClientRpc]
public void CenterPrint(string message)
{
_graphics.Text.DrawText(message, -1, -1);
}
}
//Some other code in some other component
World.Instance.Hud.CenterPrint("my message");
Where
World
is a component used to identify the game world, placed on the entity that represents the world, that has a property
public HudText Hud { get; }
.
No registration is needed since that's all automatically generated behind the scenes, there's no need to define separate message types, no manual serialization or sending of messages, and the destination does not need to explicitly register itself since it is identified through its network identity.
This approach is way better and allows for optimizations that are not possible when user code has to manually register things. Since the game codebase is the same assembly for client and server there is no need to send the metadata to validate it either, as long as you've verified that the client is running the same assembly as the server, which is easy to do by comparing the
AssemblyIdentity (strong name signing required to prevent spoofing).
It's also more efficient than Protobuf because it doesn't require the creation of objects to serialize.
Also unlike GoldSource and i think Source as well, Unity allows these sort of calls from client to server. In GoldSource the only way to communicate with the server is to send string commands, which is very restrictive. Being able to directly call the server with a specific command on a specific object can make things much easier.
As far as the technical side of things goes, i've found two libraries that could be used to do this: Reflection.Emit (part of .NET) and Mono.Cecil (also works on Core).
Since i've never worked with either i've asked for more information here:
https://old.reddit.com/r/dotnet/comments/apg4qb/reflectionemit_vs_monocecil_which_is_best_suited/?st=js0fdqxt&sh=8db88032I know that Cecil can do this, but if Reflection.Emit can do the same there is no reason to use a third party library.
As for how this works, it would be a post build step that modifies the assembly. The engine will then load the assembly and use the generated metadata to handle the remote calls.
This article indicates that it should be possible to debug assemblies that have been modified:
https://johnhmarks.wordpress.com/2011/01/19/getting-mono-cecil-to-rewrite-pdb-files-to-enable-debugging/So it shouldn't affect that.
This process of modifying assemblies is called weaving, and i've found a
framework that does it. However, it's expected that you become a patron of the project if you use it, and that would mean everyone who uses SharpLife would also have to be a patron. Since this is supposed to be completely free i can't use that.
I don't expect that it will be too hard to implement a basic weaving tool by myself since it's largely the same as using Reflection, the only major difference being that i'll have to write IL instructions, which i'd probably have to learn anyway when using Fody:
https://github.com/Fody/Home/blob/master/pages/addin-development.mdI think it may also be possible to use the same framework to optimize parts of the component system's internals, like the calls to non-virtual API methods (Update, Activate, etc), so that could prove useful as well.