Inter-Process Communication in UE4
I often link applications developed with Unreal Engine to apps created with C# etc., and the most common method I use for this is socket communication like TCP/IP or UDP.
Until now, whenever needed, I created features tailored to the specifications at that time.
However, implementing this each time is tedious, and the communication part is delicate, making it prone to bugs if not built properly.
Therefore, I aimed for reusability by turning it into a plugin.
Although there are still planned items yet to be implemented, the current implementation can be checked from the following repository.
Basic usage should be understandable from the README, but since it’s written in my weak English, it might be hard to read…
So, I’ll write it again in this article.
Concept
The plugin I created this time (ObjectDeliverer) is based on the following three concepts.
Compactness
UE4’s socket communication, especially TCP/IP implementation, is quite low-level, requiring more implementation effort compared to C# etc.
While this introduces some constraints, the design concept was to make the implementation amount as compact as possible when using it.
Usability in Blueprint
Another concept was to make it usable in Blueprint, which is a strength of UE4. Although more could be done if it were a C++ only plugin abandoning Blueprint support, I insisted on this.
Preventing Blueprint Disconnection
ObjectDeliverer supports multiple communication protocols (with plans to add more), but if they were implemented as separate types, switching the protocol type in an already implemented Blueprint would cause disconnection.
Therefore, I aimed for a design that minimizes disconnection as much as possible during specification changes after implementation.
Feature Explanation
Here’s a brief explanation of ObjectDeliverer’s features.
Communication Protocols
Currently, the following four protocols are supported.
- TCP/IP Server (supports multiple client connections)
- TCP/IP Client
- UDP (Send only)
- UDP (Receive only)
TCP/IP supports both sending and receiving for both server and client.
In a situation where multiple clients are connected to the server, the default setting is to broadcast transmissions from the server.
For UDP, I hesitated, but considering that use cases for UDP rarely involve simultaneous sending and receiving, I separated them. Of course, using both simultaneously enables sending and receiving.
Data Splitting Rules
In UDP, received data is often treated as a single data unit per reception. However, in TCP/IP, received data might arrive partially, or multiple data units might arrive combined, necessitating splitting rules.
Therefore, I have currently implemented three splitting rules that I often use.
- Fixed Size
- Header + Body
- Terminator Symbol
The fixed-size method divides data into chunks of a predetermined size every time. Even if the sent/received data size is less than the fixed size, the fixed size amount must be sent.
Conversely, it does not support cases where the data size to be sent in one go exceeds the fixed size.
In the Header + Body format, the body size is stored at the beginning of the data.
Therefore, the receiving side first reads the size field to recognize the size of the following body part, and then reads the body.
The byte count and endianness of the size part can be changed externally.
Personally, I find this the most convenient.
The final terminator symbol method was created to support common patterns like “delimited by newline characters”.
Send/Receive Data Format
By default, ObjectDeliverer sends and receives data in byte array format (TArray
However, by using an option called DeliveryBox, it’s possible to send and receive strings or objects directly.
Currently, the following two patterns are supported.
- UTF-8 String
- UObject via JSON string format
How to Use
Clone the GitHub repository and copy the Plugins folder into your project folder to use it. Please enable the ObjectDeliverer plugin in the Editor.
Usage in Blueprint
- Create an instance of ObjectDelivererManager.
- If necessary, create an instance of DeliveryBox.
- Set the Protocol and PacketRule for the ObjectDelivererManager and call the Start method.
Usage in C++
The usage procedure is the same as in Blueprint.
// Create an instance of ObjectDelivererManager
auto deliverer = NewObject<UObjectDelivererManager>();
// Monitor the receive event
deliverer->ReceiveData.AddDynamic(this, &UMyClass::OnReceive);
// Set Protocol and PacketRule and Start
deliverer->Start(UProtocolFactory::CreateProtocolTcpIpServer(9099),
UPacketRuleFactory::CreatePacketRuleSizeBody());
Sending and Receiving Data
The implementation method differs depending on whether the data to be sent is a byte array or something else.
For Byte Arrays
Sending and receiving are done via the ObjectDelivererManager instance.
// Create an instance of ObjectDelivererManager
auto deliverer = NewObject<UObjectDelivererManager>();
// Monitor the receive event
deliverer->ReceiveData.AddDynamic(this, &UMyClass::OnReceive);
// Set Protocol and PacketRule and Start
deliverer->Start(UProtocolFactory::CreateProtocolTcpIpServer(9099),
UPacketRuleFactory::CreatePacketRuleSizeBody());
TArray<uint8> buffer;
// Fill buffer with data...
// Send byte array
deliverer->Send(buffer);
void UMyClass::OnReceive(UObjectDelivererProtocol* ClientSocket, const TArray<uint8>& Buffer)
{
// Use the received byte array
}
For Non-Byte Arrays
This is done via the DeliveryBox set in ObjectDelivererManager.
// Create a DeliveryBox with Object send/receive functionality via JSON serialization
auto deliverybox = NewObject<UObjectDeliveryBoxUsingJson>();
// Monitor the object receive event for the DeliveryBox
deliverybox->Received.AddDynamic(this, &UMyClass::OnReceiveObject);
deliverer->Start(UProtocolFactory::CreateProtocolTcpIpServer(9099),
UPacketRuleFactory::CreatePacketRuleSizeBody(),
deliverybox);
auto obj = NewObject<USampleObject>();
// Set properties of obj...
// Send the object using DeliveryBox
deliverybox->Send(obj);
void UMyClass::OnReceiveObject(UObject* ReceivedObject)
{
// Extract the received object
USampleObject* obj = Cast<USampleObject>(ReceivedObject);
if (obj)
{
// Use the received object
}
}
Future Plans
ObjectDeliverer is currently under development, so changes will likely continue for some time.
Therefore, although I will try to avoid it as much as possible, there might be changes that break backward compatibility. Please be aware of this if you decide to try it.
Planned Additions
The following features are planned for implementation.
- Create shared memory as a communication protocol
- Create a DeliveryBox that uses MessagePack for serialization
- Save communication content to log files and a function to replay saved log files