{"id":616,"date":"2017-01-05T11:10:29","date_gmt":"2017-01-05T11:10:29","guid":{"rendered":"http:\/\/smartfoxserver.com\/blog\/?p=616"},"modified":"2017-01-07T08:20:30","modified_gmt":"2017-01-07T08:20:30","slug":"ways-of-working-with-npcs-in-a-multiplayer-game-p-2","status":"publish","type":"post","link":"https:\/\/smartfoxserver.com\/blog\/ways-of-working-with-npcs-in-a-multiplayer-game-p-2\/","title":{"rendered":"Ways of working with NPCs in a multiplayer game (p.2)"},"content":{"rendered":"<p>This is the last\u00a0chapter of our two part series dedicated to managing NPCs. Make sure to review our <a title=\"Part One\" href=\"http:\/\/smartfoxserver.com\/blog\/ways-of-working-with-npcs-in-a-multiplayer-game-p-1\/\">first chapter<\/a> if you haven&#8217;t already done so.<\/p>\n<p><!--more--><\/p>\n<p>In the first article we have outlined different ways of implementing several NPC activities using server side Extensions. Now we&#8217;re going to have\u00a0a look at an alternative take on the same subject: client side NPCs.<\/p>\n<h3>\u00bb Client side NPCs?<\/h3>\n<p>As\u00a0strange as it may initially sound there is a real and sometimes useful option of running NPCs as a\u00a0client application executing\u00a0on a separate machine, typically\u00a0hosted in the same facility of our main server and connected via the local private network.<\/p>\n<p><a href=\"http:\/\/smartfoxserver.com\/blog\/wp-content\/uploads\/2017\/01\/client-npcs.png\"><img loading=\"lazy\" class=\" size-full wp-image-618 aligncenter\" src=\"http:\/\/smartfoxserver.com\/blog\/wp-content\/uploads\/2017\/01\/client-npcs.png\" alt=\"client-npcs\" width=\"420\" height=\"478\" srcset=\"https:\/\/smartfoxserver.com\/blog\/wp-content\/uploads\/2017\/01\/client-npcs.png 420w, https:\/\/smartfoxserver.com\/blog\/wp-content\/uploads\/2017\/01\/client-npcs-264x300.png 264w\" sizes=\"(max-width: 420px) 100vw, 420px\" \/><\/a><\/p>\n<p>The diagram shows how our &#8220;NPC Client&#8221; is connected from\u00a0the\u00a0server side to SmartFoxServer and communicates via the local network, thus incurring in little to no lag or bandwidth limitations.<\/p>\n<p>There can be several reasons for considering this approach:<\/p>\n<ol>\n<li>NPC logic is resource intensive, requiring dedicated hardware to offload SmartFoxServer.<\/li>\n<li>NPC logic requires another language or dependencies not available in Java (e.g. C++ , .Net, Unity etc&#8230;).<\/li>\n<li>Developers are more comfortable with developing NPC AI via the client API instead of the server side.<\/li>\n<\/ol>\n<p>In any of these cases we can\u00a0proceed by writing the non player character&#8217;s logic in any of the\u00a0languages supported by the SmartFox client API and deploy it as a standalone\u00a0application on the server side.<\/p>\n<p>For example let&#8217;s suppose our game is developed in Unity and we need our NPCs to be aware of the game map. The easiest way to proceed would be\u00a0to write\u00a0a Unity client that connects to the server via local network and interacts with it.<br \/>\nThis in turn would allow us to reuse the game level data without resorting to complicated translations of the Unity game map in some other format for the Java server side.<\/p>\n<h3>\u00bb\u00a0Managing multiple NPCs<\/h3>\n<p>Since we&#8217;re likely going to manage multiple NPCs our client should be able to control\u00a0multiple connections, one for each required NPC. This can be done via a &#8220;connection manager&#8221; class that takes care of starting up multiple instances of our basic NPC client. This\u00a0in turn will create its own client API\u00a0instance to connect and communicate with SmartFoxServer 2X.<\/p>\n<p>If you have followed our blog you may remember an article about <a href=\"http:\/\/smartfoxserver.com\/blog\/building-a-simple-stress-test-tool\/\">stress testing<\/a>\u00a0which used exactly the same principle. Here follows a slightly modified version of the connection manager:<\/p>\n<pre class=\"brush: java; title: ; notranslate\" title=\"\">\r\npublic class NPCManager\r\n{\r\n    private final List&lt;NPCClient&gt; clients;\r\n    private final ScheduledThreadPoolExecutor generator;\r\n\r\n    private String clientClassName;     \/\/ name of the client class\r\n    private int generationSpeed = 20;  \/\/ interval between each client is connection\r\n    private int totalNPC = 50;          \/\/ # of NPC\r\n\r\n    private Class&lt;?&gt; clientClass;\r\n    private ScheduledFuture&lt;?&gt; generationTask;\r\n\r\n    public NPCManager(Properties config)\r\n    {\r\n        clients = new LinkedList&lt;&gt;();\r\n        generator = new ScheduledThreadPoolExecutor(1);\r\n\r\n        clientClassName = config.getProperty(&quot;clientClassName&quot;);\r\n\r\n        try { generationSpeed = Integer.parseInt(config.getProperty(&quot;generationSpeed&quot;)); } catch (NumberFormatException e ) {};\r\n        try { totalNPC = Integer.parseInt(config.getProperty(&quot;totalNPC&quot;)); } catch (NumberFormatException e ) {};\r\n\r\n        System.out.printf(&quot;%s, %s, %s\\n&quot;, clientClassName, generationSpeed, totalNPC);\r\n\r\n        try\r\n        {\r\n            \/\/ Load main client class\r\n            clientClass = Class.forName(clientClassName);\r\n\r\n            \/\/ Prepare generation\r\n            generationTask = generator.scheduleAtFixedRate(new GeneratorRunner(), 0, generationSpeed, TimeUnit.MILLISECONDS);\r\n        }\r\n        catch (ClassNotFoundException e)\r\n        {\r\n            System.out.println(&quot;Specified Client class: &quot; + clientClassName + &quot; not found! Quitting.&quot;);\r\n        }\r\n    }\r\n\r\n    void handleClientDisconnect(NPCClient client)\r\n    {\r\n        synchronized (clients)\r\n        {\r\n            clients.remove(client);\r\n        }\r\n\r\n\t\t\/\/ Maybe here we should start a new client\r\n    }\r\n\r\n    public static void main(String[] args) throws Exception\r\n    {\r\n        String defaultCfg = args.length &gt; 0 ? args[0] : &quot;config.properties&quot;;\r\n\r\n        Properties props = new Properties();\r\n        props.load(new FileInputStream(defaultCfg));\r\n\r\n        new NPCManager(props);\r\n    }\r\n\r\n    \/\/=====================================================================\r\n\r\n    private class GeneratorRunner implements Runnable\r\n    {\r\n        @Override\r\n        public void run()\r\n        {\r\n            try\r\n            {\r\n                if (clients.size() &lt; totalNPC)\r\n                    startupNewClient();\r\n                else\r\n                    generationTask.cancel(true);\r\n            }\r\n            catch (Exception e)\r\n            {\r\n                System.out.println(&quot;ERROR Generating client: &quot; + e.getMessage());\r\n            }\r\n        }\r\n\r\n        private void startupNewClient() throws Exception\r\n        {\r\n            NPCClient client = (NPCClient) clientClass.newInstance();\r\n\r\n            synchronized (clients)\r\n            {\r\n                clients.add(client);\r\n            }\r\n\r\n            client.setShell(NPCManager.this);\r\n\r\n            client.startUp();\r\n        }\r\n    }\r\n}\r\n<\/pre>\n<p>The class starts up by loading an external <em>config.properties<\/em> file which looks like this:<span style=\"font-size: 1rem;\">\u00a0<\/span><\/p>\n<pre class=\"brush: java; title: ; notranslate\" title=\"\">\r\nclientClassName=sfs2x.example.npc.SimpleNPCClient\r\ngenerationSpeed=20\r\ntotalCCU=50\r\n<\/pre>\n<p>Here\u00a0we can easily swap different implementations and alter the number of clients without recompiling our code. The <em>generationSpeed\u00a0<\/em>parameter adds a minimum of delay between the spawning of each NPC to avoid overloading the server with simultaneous connections, especially if the number of NPC is very high. (With a 20ms delay between each NPC connection we&#8217;ll have all our bots\u00a0ready in 1 second. Not a long wait)<\/p>\n<h3>\u00bb\u00a0The NPC code<\/h3>\n<p>Finally we can write our client side NPC:<\/p>\n<pre class=\"brush: java; title: ; notranslate\" title=\"\">\r\npublic abstract class NPCClient\r\n{\r\n    private StressTestReplicator shell;\r\n\r\n    public abstract void startUp();\r\n\r\n    public void setShell(StressTestReplicator shell)\r\n    {\r\n        this.shell = shell;\r\n    }\r\n\r\n    protected void onShutDown(NPCClient client)\r\n    {\r\n        shell.handleClientDisconnect(client);\r\n    }\r\n}\r\n<\/pre>\n<p>This is our client base class, as defined in the NPCManager.<\/p>\n<pre class=\"brush: java; title: ; notranslate\" title=\"\">\r\npublic class SimpleNpcClient extends NPCClient\r\n{\r\n    private SmartFox sfs;\r\n    private ConfigData cfg;\r\n    private IEventListener evtListener;\r\n\r\n    @Override\r\n    public void startUp()\r\n    {\r\n        sfs = new SmartFox();\r\n        cfg = new ConfigData();\r\n        evtListener = new SFSEventListener();\r\n\r\n        cfg.setHost(&quot;my.server.address&quot;);\r\n        cfg.setPort(9933);\r\n        cfg.setZone(&quot;BasicExamples&quot;);\r\n\r\n        sfs.addEventListener(SFSEvent.CONNECTION, evtListener);\r\n        sfs.addEventListener(SFSEvent.CONNECTION_LOST, evtListener);\r\n        sfs.addEventListener(SFSEvent.LOGIN, evtListener);\r\n        sfs.addEventListener(SFSEvent.ROOM_JOIN, evtListener);\r\n        sfs.addEventListener(SFSEvent.PUBLIC_MESSAGE, evtListener);\r\n\r\n        sfs.connect(cfg);\r\n    }\r\n\r\n    public class SFSEventListener implements IEventListener\r\n    {\r\n        @Override\r\n        public void dispatch(BaseEvent evt) throws SFSException\r\n        {\r\n            String type = evt.getType();\r\n            Map&lt;String, Object&gt; params = evt.getArguments();\r\n\r\n            if (type.equals(SFSEvent.CONNECTION))\r\n            {\r\n                boolean success = (Boolean) params.get(&quot;success&quot;);\r\n\r\n                if (success)\r\n                    sfs.send(new LoginRequest(&quot;&quot;, &quot;&quot;, cfg.getZone()));\r\n                else\r\n                {\r\n                    System.err.println(&quot;Connection failed&quot;);\r\n                    cleanUp();\r\n                }\r\n            }\r\n\r\n            else if (type.equals(SFSEvent.CONNECTION_LOST))\r\n            {\r\n                System.out.println(&quot;Client disconnected. &quot;);\r\n                cleanUp();\r\n            }\r\n\r\n            else if (type.equals(SFSEvent.LOGIN))\r\n            {\r\n                \/\/ Join room\r\n                sfs.send(new JoinRoomRequest(&quot;The Lobby&quot;));\r\n            }\r\n\r\n            else if (type.equals(SFSEvent.ROOM_JOIN))\r\n            {\r\n                 sfs.send(new PublicMessageRequest(&quot;Hello everyone!&quot;));\r\n            }\r\n\r\n        }\r\n    }\r\n\r\n    private void cleanUp()\r\n    {\r\n        \/\/ Remove listeners\r\n        sfs.removeAllEventListeners();\r\n\r\n        \/\/ Signal end of session to Shell\r\n        onShutDown(this);\r\n    }\r\n}\r\n<\/pre>\n<p>Here we create our SmartFox instance, start a connection, log into our application Zone and finally join a specific Room. From there we can have the NPC listen for events and react based on other\u00a0player&#8217;s settings and the game&#8217;s logic.<\/p>\n<p>For instance:<\/p>\n<ul>\n<li>we could listen for a\u00a0ROOM_JOIN event and welcome the player with a custom message, or even start a more complex interaction, such as inviting the player to join a game etc&#8230;<\/li>\n<li>&#8230;or\u00a0listen for chat messages and reply automatically to specific questions based on keywords etc&#8230;<\/li>\n<li>&#8230;or\u00a0listen for an AOI\/Proximity event in an MMORoom and start an interaction with the player<\/li>\n<\/ul>\n<p>The possibilities are pretty much endless. Also the NPC\u00a0system could also be integrated with the application&#8217;s datastore for accessing extra game state and for the persistence of each non player character&#8217;s state.<\/p>\n<h3>\u00bb What could go wrong:\u00a0exceptions, disconnections &#8230;<\/h3>\n<p>Whether we&#8217;re going to run our NPC application on the same machine with SFS2X or on a dedicated machine we should keep in consideration a few unexpected issues that may occur and how to deal with them.<\/p>\n<ul>\n<li><strong>Connection issues:\u00a0<\/strong>this is the less plausible of all issues. Since we&#8217;re running in a private local network we should need to setup it up once and be good to go. Unless the SmartFoxServer instance becomes unreachable or crashes there should be no other reason for connection issues, unless of course a hardware failure. Usually hosting companies provide email or sms based alerts for such occurrences, so you can be notified in real time.<\/li>\n<li><strong>Disconnections:\u00a0<\/strong>socket connections in a LAN will persist for as long as you wish, provided you keep them alive.\u00a0If the NPCs may remain idle for a long time (e.g. during low traffic hours) it is advisable to send a &#8220;ping&#8221; to the server every 5-10 minutes to avoid socket timeouts and the termination of the connection. A ping can be sent via an empty Extension request to the server. Finally our NPCManager class could react to the sudden disconnection of one or more clients by starting new NPCs\u00a0right away.<\/li>\n<li><strong>Other Exceptions:\u00a0<\/strong>there may be other application errors occurring at runtime. Depending on the NPC logic creating or joining a Room, maybe setting a RoomVariable could cause an Exception. In this case it is recommended to just make sure that all cases are handled, including those erroneous states so that the NPC can recover and not get stuck due to an unhandled\u00a0error.<\/li>\n<li><strong>Application crash:\u00a0<\/strong>what if the whole application managing the NPCs fails? In that case we would see all NPCs disappear from SmartFoxServer as their connections would be shut down. Even though this situation should be a very rare &#8211; if at all &#8211; one could add a process monitor in the system to restart the NPC application immediately.<\/li>\n<\/ul>\n<h3>\u00bb Wrapping up<\/h3>\n<p>With this second part we conclude our overview of the strategies for managing\u00a0NPCs with SmartFoxServer 2X. We hope to have stricken your curiosity and for any comment or question the place for further discussions is, as usual, our <a title=\"Forums\" href=\"http:\/\/www.smartfoxserver.com\/forums\/index.php\">support forum<\/a>.<\/p>\n<h3><\/h3>\n","protected":false},"excerpt":{"rendered":"<p>This is the last\u00a0chapter of our two part series dedicated to managing NPCs. Make sure to review our first chapter if you haven&#8217;t already done so.<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":[],"categories":[23],"tags":[77,10,78,76,7],"_links":{"self":[{"href":"https:\/\/smartfoxserver.com\/blog\/wp-json\/wp\/v2\/posts\/616"}],"collection":[{"href":"https:\/\/smartfoxserver.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/smartfoxserver.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/smartfoxserver.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/smartfoxserver.com\/blog\/wp-json\/wp\/v2\/comments?post=616"}],"version-history":[{"count":12,"href":"https:\/\/smartfoxserver.com\/blog\/wp-json\/wp\/v2\/posts\/616\/revisions"}],"predecessor-version":[{"id":630,"href":"https:\/\/smartfoxserver.com\/blog\/wp-json\/wp\/v2\/posts\/616\/revisions\/630"}],"wp:attachment":[{"href":"https:\/\/smartfoxserver.com\/blog\/wp-json\/wp\/v2\/media?parent=616"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/smartfoxserver.com\/blog\/wp-json\/wp\/v2\/categories?post=616"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/smartfoxserver.com\/blog\/wp-json\/wp\/v2\/tags?post=616"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}