UE和Spring Boot的通信方式常见的有三种:REST API 、WebSocket和 gRPC。

核心特性对比

维度 REST API WebSocket gRPC
通信模式 请求-响应(单向) 全双工双向(长连接) 支持单向/双向流(基于 HTTP/2)
连接类型 短连接(每次请求新建) 长连接(一次握手,持久通信) 长连接(复用 HTTP/2 多路复用)
数据格式 JSON / XML(文本) 任意(通常 JSON 文本或二进制) Protocol Buffers(二进制,高效)
服务端主动推送 ❌ 不支持 ✅ 原生支持 ✅ 支持(通过 Server Streaming)
延迟 高(需完整 HTTP 请求) 极低(毫秒级) 极低(二进制 + HTTP/2)
带宽效率 低(冗余 HTTP 头) 高(仅 payload) 极高(紧凑二进制编码)
UE 支持 ✅ 内置 HttpModule ✅ 内置 WebSocketsModule ❌ 需集成第三方库(如 gRPC C++)
Spring Boot 支持 ✅ 原生(@RestController ✅ 原生(WebSocketHandler ✅ 需额外依赖(grpc-spring-boot-starter
跨平台兼容性 ✅ 极佳(所有设备支持 HTTP) ✅ 良好(现代浏览器/引擎支持) ⚠️ 较差(需编译 native 库)
调试难度 ✅ 简单(Postman、curl) ⚠️ 中等(需专用工具) ❌ 困难(二进制协议,需 proto 定义)

根据上述分析我们不难发现:

  • REST API适用的场景是客户端主动发起操作:登录,购买,提交表单等。不适用于高频通信和服务端主动推送等操作。
  • WebSocket适合服务端主动推送系统通知,任务更新;低频到中频的状态同步等。
  • gRP用于通讯有点大材小用,这里不再详细介绍

因此根据需求我们可以选择不同的通信方式来实现UE和Spring Boot的通信,这里我们只介绍WebSocket的通信方式。

使用WebSocket通信

UE端

1.启用模块(xx.Build.cs

PublicDependencyModuleNames.AddRange(new string[] {
    "Core", "CoreUObject", "Engine", "WebSockets"
});

2.创建WebSocket管理器

// WebSocketManager.h
#pragma once
#include "CoreMinimal.h"
#include "WebSocketsModule.h"
#include "IWebSocket.h"

// 声明委托类型:接收原始消息字符串
DECLARE_MULTICAST_DELEGATE_OneParam(FOnWebSocketMessageReceived, const FString&);

class FWebSocketManager
{
public:
    // 公共委托,供其他系统绑定
    static FOnWebSocketMessageReceived OnMessageReceived;
    
    static void Connect(const FString& Url);
    static void Disconnect();
    static void SendMessage(const FString& Message);

private:
    static TSharedPtr<IWebSocket> WebSocket;
    static void OnConnected();
    static void OnMessage(const FString& Message);
    static void OnClosed(int32 StatusCode, const FString& Reason, bool bWasClean);
};
// WebSocketManager.cpp
#include "WebSocketManager.h"
#include "Misc/Paths.h"
#include "Misc/CommandLine.h"
#include "Engine/Engine.h"

// 定义静态委托实例
FOnWebSocketMessageReceived FWebSocketManager::OnMessageReceived;
TSharedPtr<IWebSocket> FWebSocketManager::WebSocket = nullptr;

void FWebSocketManager::Connect(const FString& Url)
{
    if (WebSocket.IsValid())
    {
        UE_LOG(LogTemp, Warning, TEXT("Already connected"));
        return;
    }

    WebSocket = FWebSocketsModule::Get().CreateWebSocket(Url, TEXT(""));

    WebSocket->OnConnected().AddStatic(&FWebSocketManager::OnConnected);
    WebSocket->OnMessage().AddStatic(&FWebSocketManager::OnMessage);
    WebSocket->OnClosed().AddStatic(&FWebSocketManager::OnClosed);

    WebSocket->Connect();
}

void FWebSocketManager::Disconnect()
{
    if (WebSocket.IsValid())
    {
        WebSocket->Close();
        WebSocket.Reset();
    }
}

void FWebSocketManager::SendMessage(const FString& Message)
{
    if (WebSocket.IsValid() && WebSocket->IsConnected())
    {
        WebSocket->Send(Message);
    }
}

void FWebSocketManager::OnConnected()
{
    UE_LOG(LogTemp, Log, TEXT("WebSocket connected to server"));
    // 可发送认证信息
    // SendMessage(TEXT("{\"cmd\":\"auth\",\"token\":\"abc123\"}"));
}

void FWebSocketManager::OnMessage(const FString& Message)
{
    UE_LOG(LogTemp, Log, TEXT("Received from server: %s"), *Message);
    
    // 🔔 广播给所有监听者
    OnMessageReceived.Broadcast(Message);

    // 示例:解析 JSON
    TSharedPtr<FJsonObject> JsonObject;
    TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Message);
    if (FJsonSerializer::Deserialize(Reader, JsonObject) && JsonObject.IsValid())
    {
        FString Type = JsonObject->GetStringField("type");
        if (Type == TEXT("system"))
        {
            // 处理系统推送
            FString Content = JsonObject->GetStringField("content");
            GEngine->AddOnScreenDebugMessage(-1, 10.f, FColor::Yellow, Content);
        }
    }
}

void FWebSocketManager::OnClosed(int32 StatusCode, const FString& Reason, bool bWasClean)
{
    UE_LOG(LogTemp, Warning, TEXT("WebSocket closed: %s (Code: %d)"), *Reason, StatusCode);
    WebSocket.Reset();

    // 可选:自动重连(避免频繁重试)
    // FTimerHandle Handle;
    // GWorld->GetTimerManager().SetTimer(Handle, []{ FWebSocketManager::Connect(TEXT("wss://...")); }, 5.0f, false);
}

3.在游戏逻辑中调用

例如在BeginPlay()中连接:

// ChatSystem.cpp
void UChatSystem::BeginPlay()
{
    Super::BeginPlay();
	
    // 连接socket
    FWebSocketManager::Connect(TEXT("ws://127.0.0.1:8080/ws"));
    // 绑定到 WebSocket 接收消息的函数:HandleWebSocketMessage
    FWebSocketManager::OnMessageReceived.AddUObject(this, &UChatSystem::HandleWebSocketMessage);
}

void UChatSystem::HandleWebSocketMessage(const FString& Message)
{
    TSharedPtr<FJsonObject> Json;
    TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Message);
    if (FJsonSerializer::Deserialize(Reader, Json) && Json.IsValid())
    {
        if (Json->GetStringField("type") == TEXT("chat"))
        {
            FString Content = Json->GetStringField("content");
            AddChatMessage(Content);
        }
    }
}

// 别忘了在销毁时解绑!
void UChatSystem::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
    FWebSocketManager::OnMessageReceived.RemoveAll(this);
    Super::EndPlay(EndPlayReason);
}

Spring Boot端

1.添加依赖(pom.xml

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

2.配置WebSocket(启用+路由)

// WebSocketConfig.java
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    /**
     * 注册WebSocket处理器
     * 创建了一个GameWebSocketHandler对象,用它来处理WebSocket连接和消息
     * 将处理器映射到 /ws 路径,客户端可以通过这个端口建立websocket连接,并允许跨域访问 (开发阶段)
     */

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        // 允许跨域(开发阶段),生产环境应限制 origin
        registry.addHandler(new GameWebSocketHandler(), "/ws")
                .setAllowedOrigins("*");
    }
}

3.实现消息处理器

创建对象+JSON序列化库,来传输JSON数据

(1)定义消息DTO类

@Data
@AllArgsConstructor
@NoArgsConstructor
public class SendSocketMessage {

    private String type;
    private String content;

}

(2)在消息处理器中使用定义的DTO类发送JSON数据

// GameWebSocketHandler.java
@Component
public class GameWebSocketHandler extends TextWebSocketHandler {

    @Autowired
    private ObjectMapper objectMapper;

    // 存储所有连接(实际项目建议按用户ID分组)
    private static final Set<WebSocketSession> sessions = ConcurrentHashMap.newKeySet();

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        sessions.add(session);
        System.out.println("Client connected: " + session.getId());
        session.sendMessage(new TextMessage("{\"type\":\"welcome\",\"msg\":\"Connected to server\"}"));
    }

    // 处理接收到的消息
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        String payload = message.getPayload();
        System.out.println("Received from client: " + payload);

        // 回显消息(或处理业务逻辑)
        session.sendMessage(new TextMessage("{\"echo\":" + payload + "}"));
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        sessions.remove(session);
        System.out.println("Client disconnected: " + session.getId());
    }
    
    // 提供全局广播方法(用于系统推送)
    public void broadcast(Object message) {
        try {
            String json = objectMapper.writeValueAsString(message);
            broadcastJSON(json);
        } catch (JsonProcessingException e) {
            System.out.println("Broadcast failed:"+e);
        }
    }

    private static void broadcastJSON(String json) {
        sessions.forEach(session -> {
            try {
                if (session.isOpen()) {
                    session.sendMessage(new TextMessage(json));
                }
            } catch (IOException e) {
                // handle
            }
        });
    }
}

4.消息推送示例

@PostMapping("/start")
public ResponseEntity<String> start() {
    // 使用websocket 向UE端发送消息
    gameWebSocketHandler.broadcast(new SendSocketMessage("moveMessage","开始移动"));
    return ResponseEntity.ok("BoxMoveStart");
}

在主类添加@EnableScheduling

@SpringBootApplication
@MapperScan("org.wms.pre.mapper")
@EnableScheduling
public class BackendApplication {
    public static void main(String[] args) {
        SpringApplication.run(BackendApplication.class, args);
    }
}

测试是否连接成功

1.UE端查看监听的OnConnected()回调函数

void FWebSocketManager::OnConnected()
{
    UE_LOG(LogTemp, Log, TEXT("WebSocket connected to server"));
    // 可发送认证信息
    // SendMessage(TEXT("{\"cmd\":\"auth\",\"token\":\"abc123\"}"));
}

查看是否在控制台打印输出该字符串,只要 OnConnected() 被触发,就说明 TCP 握手 + WebSocket 协议升级成功,通信通道已建立。

2.Spring Boot服务端查看afterConnectionEstablished是否被触发

@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
    sessions.add(session);
    System.out.println("Client connected: " + session.getId());
    session.sendMessage(new TextMessage("{\"type\":\"welcome\",\"msg\":\"Connected to server\"}"));
}

只要这个方法执行,说明WebSocket握手完成,连接已经建立成功

同时在UE端查看是否收到了传来的确认消息,UE 客户端收到该消息后,可视为 双向通信验证成功

使用REST API通信

UE端

由于这个方法比较简单,且用的不多,介绍的并不详细。

简单来说就是UE端发送请求,Spring Boot接收请求并返回相应的数据。

原理与 Vue和Spring Boot通信机制相同

1.启动模块xx.Build.cs

PublicDependencyModuleNames.AddRange(new string[] {
    "Http","Json"
});

2.创建Http请求函数和回调函数

SendGetHelloRequest为请求函数,OnGetHelloResponse为回调函数。

请求时只需调用SendGetHelloRequest即可,可以用tick调用或游戏刚开始运行时候启动均可以。

void AHttpCommunicationActor::SendGetHelloRequest()
{
	if(!bisStartMove)
	{
		TSharedRef<IHttpRequest> Request = FHttpModule::Get().CreateRequest();
		Request->SetURL(TEXT("http://localhost:8080/api/setBoxMove"));
		Request->SetVerb(TEXT("POST"));
		Request->SetHeader(TEXT("User-Agent"), TEXT("UE5-Client"));
		Request->OnProcessRequestComplete().BindUObject(this, &AHttpCommunicationActor::OnGetHelloResponse);
		Request->ProcessRequest();
	}
	
}

void AHttpCommunicationActor::OnGetHelloResponse(FHttpRequestPtr Request, FHttpResponsePtr Response,
	bool bWasSuccessful)
{
	if (bWasSuccessful && Response.IsValid() && EHttpResponseCodes::IsOk(Response->GetResponseCode()))
	{
		// 停止每帧对后端接口数据的访问
		bisStartMove = true;
		UE_LOG(LogTemp, Log, TEXT("[GET] Request successful! Response code: %d"), Response->GetResponseCode());
		// 查找场景中的ABoxMove Actor并调用MoveBySpline,实现人物移动
		for (TActorIterator<ABoxMove> It(GetWorld()); It; ++It)
		{
			ABoxMove* BoxMoveActor = *It;
			if (BoxMoveActor)
			{
				BoxMoveActor->MoveBySpline();
				break; // 找到第一个就停止
			}
		}
	}
	else
	{
		// UE_LOG(LogTemp, Error, TEXT("[GET] Request failed! Response code: %d"),
		// 	   Response.IsValid() ? Response->GetResponseCode() : -1);
	}
}

Spring Boot端

创建Controller请求处理即可

@RestController
@RequestMapping("/api")
public class ueController {

    @Autowired
    private GameWebSocketHandler gameWebSocketHandler;

    private  boolean isStart = false;
    private  boolean isStop = false;

    @CrossOrigin
    @PostMapping("/setBoxMove")
    public ResponseEntity<String> setBoxMove() {

        if( isStart && !isStop){
            // 5s后恢复isStart的值
            // 创建一个定时任务,在5秒后将isStart设为false,用来重复测试移动效果
            new Thread(() -> {
                try {
                    Thread.sleep(5000);
                    isStart = false;
                    isStop = false; // 同时重置isStop状态
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }).start();
            isStop = true;
            return ResponseEntity.ok("BoxMoveStart");
        }

        // 否则返回错误信息
        return ResponseEntity.badRequest().body("BoxMoveStart need isStart true");
    }

    @PostMapping("/start")
    public ResponseEntity<String> start() {
//        isStart = true;
        // 使用websocket 向UE端发送消息
        gameWebSocketHandler.broadcast(new SendSocketMessage("moveMessage","开始移动"));
        return ResponseEntity.ok("BoxMoveStart");
    }

}
Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐