Managing a client & server during development with Emacs

Shout-out to NoBugs for an amazing book trilogy on writing online multiplayer games

After backing and studying an excellent trilogy of books on developing online multiplayer games, I have begun to develop my own online 2D title using Python and Pygame within Emacs. Python’s simplicity lets me express a game engine in practically psuedocode, and Emacs with exwm allows any codebase to control its own environment, workflow, and window management with Lisp.

During development, I am often editing server and client files at the same time. After a set of changes, I usually desire to restart both the client and server at the same time. Ideally (but not always), the server suspends the state of the game world to something like memcache, which it then reloads upon restart.

In my .emacs.d/ I reserve the keystroke C-x c for running whatever code or project is present in the current buffer. This saves me some mental overhead by unifying classic IDE “Run” behavior across all languages and projects. The default behavior is set to compile :

(global-set-key (kbd "C-x c") 'compile)

However, I have some modes and projects override the compile command or the keystroke itself. For example, an elisp hook for this keystroke sets it as eval-buffer :

(add-hook 'emacs-lisp-mode-hook

(lambda ()

(local-set-key (kbd "C-x c") 'eval-buffer)))

For Python scripts, I like it usually to run the current buffer in python3 :

(add-hook 'python-mode-hook

(lambda ()

(set (make-local-variable 'compile-command)

(concat "python3 " (buffer-name)))))

However, simply running the current Python file would not work for my game. Therefore, I decided to use a .dir-locals.el file which lets me create local buffer settings for all files within a folder. That file sits at the root of my game’s project directory. It looks like:

((nil . ((eval .

(progn (defun game-run ()

"Compile and run game server and client"

(interactive) ;; Move to reserved workspace

(exwm-workspace-switch-create 9) ;; Kill game if it is already running

(delete-other-windows)

(if (get-buffer "Game")

(kill-buffer "Game")) (defun start-server ()

"Start game server"

(let ((default-directory "~/src/Game/server/"))

(start-process "my-process" "Game" "~/src/Game/server/bin/python3.6" "server.py"))) (defun start-client ()

"Start game client"

(let ((default-directory "~/src/Game/client/"))

(start-process "my-process" "Game" "~/src/Game/client/bin/python3.6" "client.py" "enzuru" "0" "0.0.0.0"))) (defun process-launched (process status)

"Print process status"

(message (concat process " status " status))) (defun setup-workspace ()

"Setup tiling on my workspace using exwm"

(switch-to-buffer "Game")

(split-window-vertically)

(other-window 1)

(split-window-horizontally)

(switch-to-buffer-other-frame (get-buffer "server.py"))

(exwm-floating-toggle-floating)

(other-window 1)

(switch-to-buffer-other-frame (get-buffer "client.py"))

(exwm-floating-toggle-floating)

(switch-to-buffer-other-window "Game")) ;; Start processes monitored by sentinels, and after a 5 second delay setup the workspace

(let ((process (start-server)))

(when server-process

(progn

(set-process-sentinel server-process 'process-launched)

(let ((client-process (start-client)))

(when client-process

(progn

(set-process-sentinel client-process 'process-launched)

(run-at-time "5 sec" nil 'setup-workspace)))))))) (local-set-key (kbd "C-x c") 'game-run))))))

Now when I run C-x c when editing any file in my game’s folder (and only that folder), it will:

Switch or create a reserved workspace via exwm: (exwm-workspace-switch-create 9) Delete all windows except one: (delete-other-windows) Delete the process if it already exists: (if (get-buffer "Game") (kill-buffer "Game")) Start the server and monitor it with a sentinel: (set-process-sentinel server-process 'process-launched) Start the client and monitor it with a sentinel: (set-process-sentinel client-process 'process-launched) Wait 5 seconds for those floating windows to appear and then do some busy work to nicely tile the client, server, and log windows next to each other: (run-at-time "5 sec" nil 'setup-workspace)

When I finally kill this Game buffer, both client and server are killed as well. If I continue to iterate on the game code, a simple C-x c will kill any running server and client and start new ones.