C++控制台的实现
本文将会使用C++控制台程序来完成上一节中的客户端实现。
开发环境
本教程中使用C++14标准,编译工具支持CMake和Visual Studio,无第三方引擎依赖。
先附上客户端C++控制台的源代码:点我下载(里面包含了对应的服务端源代码)。
C++控制台客户端是最轻量级的实现方式,非常适合SDK功能验证、自动化测试、压力测试等场景。它不依赖任何图形引擎,直接在命令行中运行。
第一步:生成客户端SDK
KBEngine提供了专属的SDK生成器,它是为了方便开发者而专门制作的工具,使开发者面对不同的客户端引擎时都可以游刃有余。
引擎提供的SDK生成器会自动根据引擎开发过程中涉及的通讯协议、数据结构(包括自定义的数据结构)、Entity实体定义等方面与客户端SDK进行一一对应,保证高度一致性。
介绍完SDK生成器,我们来看看如何修改配置,使其对应C++控制台。
1. 修改SDK生成路径:
在本项目的资产库"getstarted_assets"下,找到gensdk.bat,使用编辑工具或记事本打开,找到最后一行:
start %KBE_BIN_PATH%/kbcmd.exe --clientsdk=cxx --outpath=%curpath%/kbengine_cxx_plugins其中: clientsdk:指定输出SDK的客户端类型,这里填写cxx。生成器会自动生成纯C++的SDK,不依赖任何特定引擎。
outpath:指定SDK的输出路径。请确保路径是在你控制台工程的对应目录下。
TIP
C++控制台工程的SDK通常直接放在工程目录下,如:your_project/kbe_cxx_plugins。由于没有引擎框架,SDK的所有源文件会直接参与编译。
2. 执行工具,生成SDK代码:
编辑完成后保存退出,双击执行gensdk.bat。
等待生成完毕,进入客户端对应的文件夹查看。
3. 文件夹结构介绍:
生成完成后,kbe_cxx_plugins文件夹下包含以下核心文件:
| 文件/模块 | 说明 |
|---|---|
KBEngine.h/.cpp | 核心引擎入口,KBEngineApp单例,管理连接、实体生命周期、消息分发 |
KBEMain.h/.cpp | SDK入口封装类,提供init()和login()便捷方法 |
KBEngineArgs.h/.cpp | 初始化参数(IP、端口、客户端类型等) |
NetworkInterfaceTCP.h/.cpp / NetworkInterfaceKCP.h/.cpp | TCP和KCP双网络后端 |
Entity.h/.cpp / EntityCall.h/.cpp | 实体基类和远程调用机制 |
EntityDef.h/.cpp | 数据类型、实体方法、属性的自动生成定义 |
Messages.h/.cpp | 协议消息ID和处理器映射 |
KBEvent.h/.cpp / KBEventTypes.h/.cpp | 事件系统 |
MemoryStream.h/.cpp / Bundle.h/.cpp | 二进制序列化和消息打包 |
FirstEntityBase.h/.cpp | FirstEntity实体的抽象基类(自动生成) |
EntityCallFirstEntityBase.h/.cpp | FirstEntity的远程调用代理(自动生成) |
ikcp.c/.h | KCP可靠UDP协议实现 |
blowfish/ | Blowfish加密算法实现 |
生成规则:
1、服务端定义了有客户端部分的实体(声明了hasClient="true"的),则会生成类似实体名+Base.h/.cpp的文件,它是一个抽象类,我们只需继承它、实现它,并使用实体名为类名即可,如这里的FirstEntityBase,该类中会包含def中声明的客户端方法,如本教程中的onEnter、onSay;
2、被生成的实体,会对应包含一个类似EntityCall+实体名+Base.h/.cpp的文件,该文件是对应实体的EntityCall的实现。该类中会包含该实体的被暴露给客户端的通讯方法(被设置了Exposed标签的),如FirstEntity在def中声明的say方法。
4. 配置CMake编译
使用CMakeLists.txt来组织编译:
cmake_minimum_required(VERSION 3.10)
project(cxx_sdk_demo)
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
if(MSVC)
add_compile_options("/utf-8")
endif()
add_definitions(-D_WIN32 -DWITH_KCP=1)
# SDK头文件路径
include_directories(${CMAKE_SOURCE_DIR}/kbe_cxx_plugins)
# 自动收集SDK和脚本的所有源文件
file(GLOB_RECURSE KBE_SDK_SOURCES
${CMAKE_SOURCE_DIR}/kbe_cxx_plugins/*.cpp
${CMAKE_SOURCE_DIR}/kbe_cxx_plugins/*.c
)
file(GLOB_RECURSE KBE_SCRIPT_SOURCES
${CMAKE_SOURCE_DIR}/kbe_scripts/*.cpp
)
add_executable(cxx_sdk_demo
main.cpp KBEApp.cpp
${KBE_SDK_SOURCES}
${KBE_SCRIPT_SOURCES}
)
if(WIN32)
target_link_libraries(cxx_sdk_demo PRIVATE Ws2_32)
endif()TIP
C++控制台客户端将SDK的所有源文件直接参与编译,这是最灵活的方式——你可以自由修改SDK源码进行调试和定制。
第二步:实现Client部分
1. 客户端设计概述
服务器回顾:
先来回顾下本教程的服务器设计,我们把FirstEntity与账户入口关联,使得客户端一旦连接服务器并通过登录认证后就会创建出FirstEntity实体,此时该实体的客户端部分也会被创建。一旦创建完毕后会被立即传送到FirstSpace所在空间中去,完成后会通过onEnter的远程方法通知客户端。接着,客户端向服务器发出say请求后,服务器会进行广播,并通过客户端的onSay方法告知所有在同一空间的客户端。
客户端设计:
控制台程序没有图形界面,我们设计流程如下:
main.cpp入口——创建KBEApp实例,设置IP和端口,调用init()和login()。- 登录成功后,SDK内部会自动创建FirstEntity客户端实例。
- 进入空间后
onEnter被触发,打印提示信息。 - 用户通过命令行输入
say hello world来触发say请求。 - 收到
onSay广播后,将内容打印到控制台。
好了,让我们开始动手吧!
2. 实现程序入口(main.cpp)
控制台客户端从main函数开始,直接创建SDK入口并登录:
#include <iostream>
#include "kbe_cxx_plugins/KBEngine.h"
#include "KBEApp.h"
int main(int argc, char* argv[])
{
// 创建应用实例
auto* app = new KBEApp();
app->ip = "127.0.0.1";
app->port = 20013;
app->disableMainLoop = false; // SDK内部线程驱动process
// 初始化SDK
app->init();
// 登录服务器
app->login("myaccount", "mypassword", KBArray<uint8>());
// 命令行交互循环
std::string str;
while (std::getline(std::cin, str))
{
if (str == "close")
{
KBEngine::KBEngineApp::getSingleton().destroy();
break;
}
else if (str.find("say ") == 0)
{
// 提取say后面的内容
std::string content = str.substr(4);
// 获取自己的实体并发送say
KBEngine::FirstEntity* entity =
dynamic_cast<KBEngine::FirstEntity*>(
KBEngine::KBEngineApp::getSingleton().player());
if (entity)
{
entity->pCellEntityCall->say(content.c_str());
}
}
}
return 0;
}要点说明:
KBEApp继承自KBEMain,是SDK的上层封装。disableMainLoop = false表示SDK内部会启动一个线程来驱动process(),这样主线程可以阻塞在std::getline等待用户输入。- 命令行输入
say xxx会调用pCellEntityCall->say()向服务器发送请求。 - 输入
close则销毁SDK并退出。
3. 实现KBEApp(应用入口封装)
KBEApp继承自KBEMain,负责注册事件回调:
KBEApp.h:
#pragma once
#include "kbe_cxx_plugins/KBEMain.h"
class KBEApp : public KBEMain
{
public:
KBEApp();
~KBEApp();
protected:
virtual void installEvents() override;
virtual void deregisterEvents() override;
private:
void onVersionNotMatch(std::shared_ptr<UKBEventData> pEventData);
void onScriptVersionNotMatch(std::shared_ptr<UKBEventData> pEventData);
};KBEApp.cpp:
#include "KBEApp.h"
#include "kbe_cxx_plugins/KBEventTypes.h"
KBEApp::KBEApp()
{
installEvents();
}
KBEApp::~KBEApp()
{
deregisterEvents();
}
void KBEApp::installEvents()
{
KBENGINE_REGISTER_EVENT_IN(
KBEngine::KBEventTypes::onScriptVersionNotMatch,
onScriptVersionNotMatch);
KBENGINE_REGISTER_EVENT_IN(
KBEngine::KBEventTypes::onVersionNotMatch,
onVersionNotMatch);
}
void KBEApp::deregisterEvents()
{
KBENGINE_DEREGISTER_ALL_EVENT();
}
void KBEApp::onVersionNotMatch(std::shared_ptr<UKBEventData> pEventData)
{
printf("KBEApp::onVersionNotMatch: SDK版本不匹配\n");
}
void KBEApp::onScriptVersionNotMatch(std::shared_ptr<UKBEventData> pEventData)
{
printf("KBEApp::onScriptVersionNotMatch: 脚本版本不匹配\n");
}4. 实现FirstEntity
按照刚才的设计,登录成功后,FirstEntity的客户端部分会被创建,我们来实现FirstEntity的客户端部分:
FirstEntity.h:
#pragma once
#include "kbe_cxx_plugins/FirstEntityBase.h"
namespace KBEngine
{
class FirstEntity : public FirstEntityBase
{
public:
FirstEntity();
virtual ~FirstEntity();
virtual void __init__() override;
virtual void onEnter() override;
virtual void onSay(const KBString& content) override;
};
}FirstEntity.cpp:
#include "FirstEntity.h"
#include "kbe_cxx_plugins/KBDebug.h"
#include "kbe_cxx_plugins/EntityFactory.h"
namespace KBEngine
{
FirstEntity::FirstEntity() :
FirstEntityBase()
{
}
FirstEntity::~FirstEntity()
{
}
void FirstEntity::__init__()
{
FirstEntityBase::__init__();
printf("[FirstEntity] __init__: 实体 %d 已创建\n", id());
}
void FirstEntity::onEnter()
{
DEBUG_MSG("FirstEntity::onEnter");
printf("========================================\n");
printf("[FirstEntity] 已进入空间!\n");
printf(" 输入 'say hello world' 来向服务器打招呼\n");
printf(" 输入 'close' 退出程序\n");
printf("========================================\n");
}
void FirstEntity::onSay(const KBString& content)
{
printf("[FirstEntity::onSay] 收到广播: %s\n", content.c_str());
}
}
// 静态注册实体类型 —— 必须!
namespace {
const bool registered = []() {
EntityFactory::instance().registerType("FirstEntity", []() {
return new KBEngine::FirstEntity();
});
return true;
}();
}onEnter:当实体被传送到空间后,SDK会调用此方法。我们在控制台打印提示信息,告诉用户可以进行操作了。
onSay:当收到服务器的广播消息时,SDK会调用此方法。我们直接printf将收到的内容打印到控制台。
注意
每个被指定有Client部分的实体,必须通过EntityFactory::instance().registerType进行静态注册。这是C++版本SDK的特殊要求——因为C++没有动态反射机制,必须手动注册实体类的工厂函数,使SDK能通过实体名字符串找到对应的实现类。类名必须和实体名字一致!
5. 运行流程
整个程序的运行流程如下:
启动程序
↓
KBEApp::init() → 初始化SDK、连接服务器
↓
KBEApp::login("myaccount", "mypassword") → 登录
↓
FirstEntity::__init__() → 实体被创建
↓
FirstEntity::onEnter() → 进入空间,打印提示
↓
用户输入 "say hello world"
↓
pCellEntityCall->say("hello world") → 发送请求到服务器
↓
服务器广播 → FirstEntity::onSay("Entity: 1 hello world")
↓
控制台打印收到的消息接下来,让我们迎来激动人心的时刻!服务器和客户端的联通验证!
第三步:验证
1. 启动引擎
在本项目的资产库"getstarted_assets"下,找到start_server.bat,并双击运行。
等待所有服务器组件的窗口都出现"Found all the components!"字样,就说明成功启动了。
2. 编译并运行客户端
2.1 使用CMake编译:
cd cmake
mkdir build && cd build
cmake ..
cmake --build . --config Release2.2 或者直接使用Visual Studio打开vs/ConsoleApplication1.sln编译。
2.3 运行生成的cxx_sdk_demo.exe。
3. Hello world
1、启动后,程序会自动连接服务器并登录。
2、登录成功后,控制台打印进入空间的提示信息。
========================================
[FirstEntity] 已进入空间!
输入 'say hello world' 来向服务器打招呼
输入 'close' 退出程序
========================================3、输入say hello world,会向服务端发起say的远程调用。
say hello world
[FirstEntity::onSay] 收到广播: Entity: 1 hello world4、看到以上输出,说明客户端和服务端通讯成功!
TIP
显示内容格式为"Entity: " + self.id + content,这是服务端FirstEntity中定义好的格式。其中数字"1"是实体在服务器上的ID。
恭喜你,C++控制台的客户端实现已完成!
通过C++控制台客户端的实现,我们利用FirstEntity实体的Client部分与服务器建立了连接,并立即进入了FirstSpace所在的空间中,接着,在空间内我们向第一个实体FirstEntity进行了say的操作,并收到了onSay的广播。
C++控制台客户端是最简洁的实现,它展示了KBEngine SDK的核心通讯流程——没有任何引擎环节的包装,所有API调用都直接可见。理解了控制台版本,再去理解Unity/UE/Godot/Cocos等引擎版本就会更加清晰。
这是GetStarted章节中客户端C++控制台的源代码:点我下载(里面包含了对应的服务端源代码)
开发者肯定对整个的通讯过程、业务流程还存在一些疑问,让我们进入下一节《GetStarted总结》中进行回顾、梳理和总结吧。
