Some time ago I made a Behavior tree implementation for this game. While the implementation seems to work, the NPCs sometimes behave very silly, so I thought it would clarify their behavior when I could look into their “head”. Classical debugging a game or game server is always a pain. When everything is executed 60 or 20 times per second it's hard to step through and inspect variables. This is even more difficult with a game server which may disconnect the client when you don't inspect the variables fast enough, because of some timeout.
IPC
I solved this problem with writing an IPC server that sends the status of the behavior tree to a connected IPC client (debug client), which displays the status. That's not real debugging, because I can only inspect the status and not pause or step through it. It communicates over TCP sockets using asio.
The IPC library is really simple and can send anything (i.e. struct or class) from server to client and vice versa. Server and client communicate with so called “Messages” which are simple structs (or even classes) which just need a public Serialize() function:
struct MyMessage
{
int intValue;
std::string strValue;
template<typename _Ar>
void Serialize(_Ar& ar)
{
ar.value(intValue);
ar.value(strValue);
}
};
It can even have variable sized containers:
struct MyMessage3
{
struct Item
{
uint16_t type;
uint32_t index;
uint8_t place;
std::string name;
};
uint16_t count;
std::vector<Item> items;
template<typename _Ar>
void Serialize(_Ar& ar)
{
ar.value(count);
items.resize(count);
for (uint16_t i = 0; i < count; ++i)
{
auto& item = items[i];
ar.value(item.type);
ar.value(item.index);
ar.value(item.place);
ar.value(item.name);
}
}
};
Starting the Server
Starting a server is just a few lines of code and it can reuse an existing asio::io_service:
#include "IpcServer.h"
asio::io_service io;
asio::ip::tcp::endpoint endpoint(asio::ip::address(asio::ip::address_v4(INADDR_ANY)), 1234);
IPC::Server server(io, endpoint);
io.run();
Client connecting to server
#include "IpcClient.h"
asio::io_service io;
IPC::Client client(io);
client.Connect("localhost", 1234);
io.run();
// Or io.poll() or whatever fits the application.
Sending messages
The client and server can send a message with a proper Serialize() to each other with the Send() or SendTo() function:
// Client to server
MyMessage msg { 42, "Hello friends" };
client_.Send(msg);
MyMessage msg { 42, "Hello friends" };
// Server sends the message to all connected clients
server.Send(msg);
// Server sends the message to a specific client
server.SendTo(client, msg);
Handling messages
When the client or server get some message they can set an message handler:
// Client
handlers_.Add<MyMessage>([](const MyMessage& msg)
{
std::cout << "intValue = " << msg.intValue << std::endl;
std::cout << "strValue = " << msg.strValue << std::endl;
});
// Server
handlers_.Add<MyMessage>([](IPC::ServerConnection& client, const MyMessage& msg)
{
std::cout << "intValue = " << msg.intValue << std::endl;
std::cout << "strValue = " << msg.strValue << std::endl;
});
Debug client
The client is a terminal application using ncurses on Linux and PDCurses on Windows, but it doesn't work well on Windows, because of some limitations of the Windows' terminal. Anyway, since the IPC works over TCP sockets, I can just connect from a Linux running the Debug client to a Windows running the game server.
The client has three windows:
- Game selector. Because one game server can run many games, this is for selecting the game to inspect.
- NPC selector to select the NPC to inspect
- NPC details, which shows some basic stats and the status of the behavior tree in real time.
Conclusion
At the moment I didn't find any obvious reasons why the NPCs act silly, but this gives me the tools to investigate it further.
Clever approach, hopefully you can pinpoint your AI problems! ?