Skip to content

C++控制台的实现

本文将会使用C++控制台程序来完成上一节中的客户端实现。

开发环境

本教程中使用C++14标准,编译工具支持CMake和Visual Studio,无第三方引擎依赖。

先附上客户端C++控制台的源代码:点我下载(里面包含了对应的服务端源代码)。

C++控制台客户端是最轻量级的实现方式,非常适合SDK功能验证、自动化测试、压力测试等场景。它不依赖任何图形引擎,直接在命令行中运行。

第一步:生成客户端SDK

KBEngine提供了专属的SDK生成器,它是为了方便开发者而专门制作的工具,使开发者面对不同的客户端引擎时都可以游刃有余。

引擎提供的SDK生成器会自动根据引擎开发过程中涉及的通讯协议、数据结构(包括自定义的数据结构)、Entity实体定义等方面与客户端SDK进行一一对应,保证高度一致性。

介绍完SDK生成器,我们来看看如何修改配置,使其对应C++控制台。

1. 修改SDK生成路径:

在本项目的资产库"getstarted_assets"下,找到gensdk.bat,使用编辑工具或记事本打开,找到最后一行:

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/.cppSDK入口封装类,提供init()login()便捷方法
KBEngineArgs.h/.cpp初始化参数(IP、端口、客户端类型等)
NetworkInterfaceTCP.h/.cpp / NetworkInterfaceKCP.h/.cppTCP和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/.cppFirstEntity实体的抽象基类(自动生成)
EntityCallFirstEntityBase.h/.cppFirstEntity的远程调用代理(自动生成)
ikcp.c/.hKCP可靠UDP协议实现
blowfish/Blowfish加密算法实现

生成规则:

1、服务端定义了有客户端部分的实体(声明了hasClient="true"的),则会生成类似实体名+Base.h/.cpp的文件,它是一个抽象类,我们只需继承它、实现它,并使用实体名为类名即可,如这里的FirstEntityBase,该类中会包含def中声明的客户端方法,如本教程中的onEnteronSay

2、被生成的实体,会对应包含一个类似EntityCall+实体名+Base.h/.cpp的文件,该文件是对应实体的EntityCall的实现。该类中会包含该实体的被暴露给客户端的通讯方法(被设置了Exposed标签的),如FirstEntity在def中声明的say方法。

4. 配置CMake编译

使用CMakeLists.txt来组织编译:

cmake
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方法告知所有在同一空间的客户端。

客户端设计:

控制台程序没有图形界面,我们设计流程如下:

  1. main.cpp入口——创建KBEApp实例,设置IP和端口,调用init()login()
  2. 登录成功后,SDK内部会自动创建FirstEntity客户端实例。
  3. 进入空间后onEnter被触发,打印提示信息。
  4. 用户通过命令行输入say hello world来触发say请求。
  5. 收到onSay广播后,将内容打印到控制台。

好了,让我们开始动手吧!

2. 实现程序入口(main.cpp)

控制台客户端从main函数开始,直接创建SDK入口并登录:

cpp
#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:

cpp
#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:

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:

cpp
#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:

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编译:

bash
cd cmake
mkdir build && cd build
cmake ..
cmake --build . --config Release

2.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 world

4、看到以上输出,说明客户端和服务端通讯成功!

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总结》中进行回顾、梳理和总结吧。

点我进入《GetStarted总结》