4 | ;;;;
5 | ;;;; Permission is hereby granted, free of charge, to any person
6 | ;;;; obtaining a copy of this software and associated documentation
7 | ;;;; files (the "Software"), to deal in the Software without
8 | ;;;; restriction, including without limitation the rights to use, copy,
9 | ;;;; modify, merge, publish, distribute, sublicense, and/or sell copies
10 | ;;;; of the Software, and to permit persons to whom the Software is
11 | ;;;; furnished to do so, subject to the following conditions:
12 | ;;;;
13 | ;;;; The above copyright notice and this permission notice shall be
14 | ;;;; included in all copies or substantial portions of the Software.
15 | ;;;;
16 | ;;;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | ;;;; EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | ;;;; MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 | ;;;; NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
20 | ;;;; HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
21 | ;;;; WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | ;;;; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
23 | ;;;; DEALINGS IN THE SOFTWARE.
24 | ;;;;
25 |
26 | (in-package :blackthorn-net)
27 |
28 | ;;;
29 | ;;; Network - Sockets and Serialization
30 | ;;;
31 |
32 | (defconstant +max-buffer-size+ 65536)
33 |
34 | (defvar *mode*)
35 | (defvar *host*)
36 | (defvar *hosts*)
37 | (defvar *local-port*)
38 | (defvar *remote-host*)
39 | (defvar *remote-port*)
40 | (defvar *server-socket*)
41 | (defvar *socket*)
42 | (defvar *sockets*)
43 | (defvar *players*)
44 |
45 | (defun hostname ()
46 | *host*)
47 |
48 | (defun hostnames ()
49 | *hosts*)
50 |
51 | (defun default-abort-handler ()
52 | (error "Failed to connect.~%"))
53 |
54 | (defun net-init (mode host port players)
55 | (assert (not (boundp '*mode*)))
56 | (ecase mode
57 | ((:server)
58 | (let ((socket (usocket:socket-listen usocket:*wildcard-host* port
59 | :element-type '(unsigned-byte 8))))
60 | (setf *server-socket* socket
61 | *local-port*
62 | ;; FIXME: This seems to blow up in CLISP.
63 | #-clisp (usocket:get-local-port socket)
64 | #+clisp port)))
65 | ((:client)
66 | (when (eql mode :client)
67 | (setf *remote-host* host *remote-port* port)))
68 | ((:normal)))
69 | (setf *mode* mode *players* (if players (max players 2) 2)))
70 |
71 | (defun net-connect (&optional (abort-handler #'default-abort-handler))
72 | (ecase *mode*
73 | ((:server)
74 | (setf *sockets* (iter (repeat (1- *players*))
75 | (collect (usocket:socket-accept *server-socket*))))
76 | (iter (for socket in *sockets*)
77 | (let ((request (net-receive socket)))
78 | (unless (equal (assoc :request request) '(:request :init))
79 | (funcall abort-handler))
80 | (net-send '((:response :init)) socket))))
81 | ((:client)
82 | (setf *socket* (usocket:socket-connect *remote-host* *remote-port*
83 | :element-type '(unsigned-byte 8)))
84 | (net-send `((:request :init)) *socket*)
85 | (unless (equal (net-receive *socket* :timeout 10) '((:response :init)))
86 | (funcall abort-handler)))
87 | ((:normal))))
88 |
89 | (defun net-exit ()
90 | (assert (boundp '*mode*))
91 | (ecase *mode*
92 | ((:client)
93 | (usocket:socket-close *socket*))
94 | ((:server)
95 | (iter (for socket in *sockets*) (usocket:socket-close socket))
96 | (usocket:socket-close *server-socket*))
97 | ((:normal)))
98 | (makunbound '*socket*)
99 | (makunbound '*mode*))
100 |
101 | (defun net-receive (socket &key timeout)
102 | (assert (or (eql *mode* :server) (eql *mode* :client)))
103 | (cl-store:restore (usocket:socket-stream socket)))
104 |
105 | (defun net-send (message socket)
106 | (assert (or (eql *mode* :server) (eql *mode* :client)))
107 | (cl-store:store message (usocket:socket-stream socket))
108 | (force-output (usocket:socket-stream socket)))
109 |
110 | (defmacro with-serve-request ((request socket &key timeout) &body body)
111 | (let ((s (gensym)))
112 | `(let* ((,s ,socket)
113 | (,request (net-receive ,s :timeout ,timeout)))
114 | (net-send (progn ,@body) ,s))))
115 |
116 | (defmacro with-serve-requests ((requests sockets &key timeout) &body body)
117 | (let ((ss (gensym)) (s (gensym)) (reply (gensym)))
118 | `(let* ((,ss ,sockets)
119 | (,requests (iter (for ,s in ,ss)
120 | (collect (net-receive ,s :timeout ,timeout))))
121 | (,reply (progn ,@body)))
122 | (iter (for ,s in ,ss)
123 | (net-send ,reply ,s)))))
124 |
125 | (defun net-send-request (request socket &key timeout)
126 | (net-send request socket)
127 | (net-receive socket :timeout timeout))
128 |
129 | ;;;
130 | ;;; Network - Game Protocol
131 | ;;;
132 |
133 | (defun net-game-connect (&optional (abort-handler #'default-abort-handler))
134 | (when (eql *mode* :server)
135 | (format t "Waiting for a connection on port ~a. Please start client.~%"
136 | *local-port*))
137 | (net-connect abort-handler)
138 | (ecase *mode*
139 | ((:server)
140 | (let ((hosts (iter (for i from 0 below *players*) (collect i))))
141 | (setf *host* 0 *hosts* hosts)
142 | (iter (for socket in *sockets*) (for i from 1)
143 | (with-serve-request (request socket :timeout nil)
144 | (if (equal (assoc :request request) '(:request :connect))
145 | `((:response :connect)
146 | (:host ,i) (:hosts ,hosts)
147 | (:random-state
148 | ,(mt19937::random-state-state mt19937:*random-state*)))
149 | (funcall abort-handler)))))
150 | (format t "Connected.~%"))
151 | ((:client)
152 | (handler-case
153 | (let ((response (net-send-request '((:request :connect)) *socket*)))
154 | (unless (equal (assoc :response response) '(:response :connect))
155 | (error "Client didn't understand response.~%"))
156 | (setf *host* (cadr (assoc :host response))
157 | *hosts* (cadr (assoc :hosts response))
158 | mt19937:*random-state*
159 | (mt19937::make-random-object
160 | :state (cadr (assoc :random-state response)))))
161 | (usocket:connection-refused-error ()
162 | (funcall abort-handler)))
163 | (format t "Connected.~%"))
164 | ((:normal) (setf *host* 0 *hosts* '(0)))))
165 |
166 | (defun net-game-start (&optional (abort-handler #'default-abort-handler))
167 | (ecase *mode*
168 | ((:server)
169 | (format t "Server waiting for client to start.~%")
170 | (iter (for socket in *sockets*)
171 | (with-serve-request (request socket :timeout nil)
172 | (if (equal (assoc :request request) '(:request :start))
173 | `((:response :start))
174 | (error "Server didn't understand request.~%"))))
175 | (format t "Starting.~%"))
176 | ((:client)
177 | (format t "Attempting to start...~%")
178 | (handler-case
179 | (let ((response (net-send-request '((:request :start)) *socket*)))
180 | (unless (equal (assoc :response response) '(:response :start))
181 | (error "Client didn't understand response.~%")))
182 | (usocket:connection-refused-error ()
183 | (funcall abort-handler)))
184 | (format t "Starting.~%"))
185 | ((:normal))))
186 |
187 | (defun net-game-update (input-queue process-event
188 | &optional (abort-handler #'default-abort-handler))
189 | (ecase *mode*
190 | ((:server)
191 | (with-serve-requests (requests *sockets* :timeout nil)
192 | (let ((events
193 | (nconc
194 | (containers:collect-elements input-queue)
195 | (iter (for request in requests)
196 | (cond ((equal (assoc :request request) '(:request :update))
197 | (nconcing (cadr (assoc :events request))))
198 | ((equal (assoc :request request) '(:request :quit))
199 | (funcall abort-handler))
200 | (t
201 | (error "Server didn't understand request.~%")))))))
202 | (iter (for e in events) (funcall process-event e))
203 | (containers:empty! input-queue)
204 | `((:response :update) (:events ,events)))))
205 | ((:client)
206 | (let ((response
207 | (net-send-request
208 | `((:request :update)
209 | (:events ,(containers:collect-elements input-queue)))
210 | *socket*)))
211 | (cond ((equal (assoc :response response) '(:response :update))
212 | (let ((events (cadr (assoc :events response))))
213 | (iter (for e in events) (funcall process-event e))
214 | (containers:empty! input-queue)))
215 | ((equal (assoc :response response) '(:response :quit))
216 | (funcall abort-handler))
217 | (t
218 | (error "Client didn't understand response.~%")))))
219 | ((:normal)
220 | (let ((events (containers:collect-elements input-queue)))
221 | (iter (for e in events) (funcall process-event e))
222 | (containers:empty! input-queue)))))
223 |
224 | (defun net-game-quit (&optional (abort-handler #'default-abort-handler))
225 | (ecase *mode*
226 | ((:server)
227 | (format t "Server disconnecting from client.~%")
228 | (iter (for socket in *sockets*)
229 | (net-send `((:response :quit)) socket))
230 | (format t "Disconnected.~%"))
231 | ((:client)
232 | (format t "Attempting to disconnect...~%")
233 | (net-send `((:request :quit)) *socket*)
234 | (format t "Disconnected.~%"))
235 | ((:normal))))
236 |
--------------------------------------------------------------------------------
/README.html:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 | Blackthorn: Lisp Game Engine
6 |
7 |
8 |
9 |
10 |
47 |
48 |
49 |
50 | Blackthorn: Lisp Game EngineElliott Slaughter |
51 |
52 | Contents
53 | 1 What is Blackthorn?
Blackthorn is a framework for writing 2D games in Common Lisp. Blackthorn is attempt to write an efficient, dynamic, persistent 2D game engine in an expressive language which makes it easy to write games.
54 | 2 Why another game engine?
Games are hard to write. The effort needed to write a usable game engine from scratch, especially when dealing with the low-level details of languages like C, make the cost of writing games prohibitive. Libraries like SDL get many of the driver-level graphics details out of the way, but still leave the user writing in C. Libraries like PyGame and LISPBUILDER-SDL wrap more of these low-level details, but still don’t provide a full game engine needed for writing substantial games.
There are, of course, game engines which provide this functionality to the user. Game Maker, for example, is an engine which provides everything needed to make a basic game, and an extention language for writing more complex behavior. Using Game Maker, an experienced user can write a basic game in five minutes. However, Game Maker (and similar programs the authors have tried) have some substantial flaws. Problems with Game Maker, specifically, include:
-
55 | Game Maker only runs on Windows. A Linux port is still a dream, and porting to any sort of mobile device is completely unimaginable.
56 |
- Game Maker’s extension language, GML, is a kludge, and inefficient. (The lack of a rich set of built-in datastructures is something I hear GML users complaining about frequently.)
57 |
- Game Maker is closed source, so it would be impossible for anyone other than the authors to fix any of the above problems with Game Maker.
58 |
59 | 3 What does Blackthorn provide?
Blackthorn attempts to fix many of the problems above. Blackthorn provides:
-
60 | A not-yet-complete subset of the functionality provided by Game Maker. Despite being incomplete, Blackthorn is already capable of supporting simple games.
61 |
- Blackthorn is written in Common Lisp, providing:
62 |
-
63 | Efficiency which is (depending on the implementation, and the benchmark) capable of competing with C.
64 |
- Portable to any platform supported by a compliant ANSI Common Lisp compiler. Blackthorn currently runs on Windows, Linux, and Mac OSX. Porting Blackthorn to a new compiler takes a couple of hours.
65 |
- Dynamic behavior, because the entire compiler is available at runtime. An on-screen development REPL (read-eval-print loop, i.e. a development console) with an on-screen debugger is provided, giving the user the ability to rewrite arbitrary pieces of code on the fly.
66 |
- Extensibility, because the game engine itself is an open platform, and because user code operates at the same level as the game engine.
67 |
- And finally, because Blackthorn is open source, it is open to improvements from the community.
68 |
69 |
70 | 4 Technical details
Blackthorn uses LISPBUILDER-SDL for graphics support (which internally uses SDL and SDL_image), and CL-STORE as an internal database for object persistence.
Blackthorn currently runs on Windows, Linux, and Mac OS X, under Allegro CL, CLISP, Clozure CL, and SBCL. Blackthorn has been tested successfully on the following OS/Lisp combinations:
| | Windows | Linux | Mac OS X |
71 | | Allegro CL | Yes | ?? | ?? |
72 | | CLISP | Yes | Yes | Yes |
73 | | Clozure CL | Yes | Yes | No |
74 | | SBCL | Yes | Yes | Yes |
75 |
Among the compatible compilers, SBCL is suggested because it is (a) free and open source, (b) compatible with Windows, Linux and Mac, and (c) has the best performance of the compilers listed. Allegro CL is also a good choice, but is commercial software (although a free version is available).
76 | 4.1 Direct dependencies
83 | 4.2 Windows only (optional)
87 | 5 Installation
Download the source using darcs
darcs get http://common-lisp.net/~eslaughter/darcs/blackthorn
88 |
To start Blackthorn from the shell, merely call make
make
89 |
Optionally, use parameters to specify the build environment, e.g.
make cl=sbcl db=nodb driver=load.lisp system=bunnyslayer
90 |
If instead you prefer to start Blackthorn interactively, start your Lisp and
(load "load")
91 |
92 | 6 Download
Binary distributions are made semi-frequently and are available for download at http://elliottslaughter.net/bunnyslayer/download.
93 | License
Blackthorn is free and open source software, see the COPYRIGHT file for details.
94 |
95 |
96 |
97 |
This document was translated from LATEX by
98 | HEVEA.
99 |
100 |
--------------------------------------------------------------------------------