1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
|
#+TITLE: Wheelwork
#+OPTIONS: html-style:nil
: Wheelwork \Wheel"work`\, n. (Mach.)
: A combination of wheels, and their connection, in a machine
: or mechanism.
: [1913 Webster]
/A Sprite System in Common Lisp for Games and GUIs/
** Installation
Ensure that sdl2 is installed on your system.
*** Fetch Systems That Are Not In Quicklisp
#+begin_src shell
mkdir ~/wheelwork-playground
cd ~/wheelwork-playground
git clone https://cicadas.surf/cgit/colin/wheelwork.git wheelwork
#+end_src
*** Fire up Slime
You'll want to let quickisp know about the =wheelwork-playground= directory
#+begin_src lisp
(pushnew #P"~/wheelwork-playground/" ql:*local-project-directories*)
(ql:register-local-projects )
(ql:quickload :wheelwork)
#+end_src
*** Try the examples
Then load one of the example files and call its "start" function:
#+begin_src lisp
(ql:quickload :wheelwork-examples)
(ww.examples/6:start)
#+end_src
** Basic Use
The best introduction to wheelwork comes through looking at and playing with the examples. IN addition to playing with examples, consider what follows as a supplement that helps explain how some of the pieces fit together.
*** The Application
When you want to use Wheelwork, you must make a sublcass of ~APPLICATION~. The ~APPLICATION~ is the main container supplied by wheelwork: it is the root of the [[*The Display Tree Protocol: Units and Containers][display tree]], holds references to [[*The Asset Protocol][loaded assets]], and handles [[*Events & Event Handling][events]] from the user.
For example, from the pong example in =examples/08-pong.lisp=, the application class looks like
#+begin_src lisp
(defclass/std solo-pong (ww:application)
((paddle ball game-over intro-text)))
#+end_src
/note: I'm using defclass-std:defclass/std to define the above, see [[https://quickdocs.org/defclass-std][here]] for more./
This defines a subclass of application along with some state needed for the pong game.
**** The Window & Scale & Coordinates
Wheelwork uses SDL2 to create windows and generate events. The application includes a global scale factor that affects how the game interprets coordinates inside the window. A window, for example, can be 800x600 pixels on your computer monitor, but if the application's scale factor is 2.0, then it will only have a 400x300 logical space of coordinates. If you add a sprite that is 30x30 pixels big, it will appear twice as large, but it will occupy 30x30 "logical pixels".
The =0,0= coordinate is the bottom left corner of the game window, and the top right corner is =w,h=, the width and height of the scaled screen, respectively.
**** The Boot Method
It isn't enough to define a subclass, you must also implement a ~ww:boot~ method for your application class. The boot method is called right after the OpenGL context becomes available. Inside boot, you are expected to do everything necessary to start your game: load assets, create some display units, add them to the scene, and add event handlers. Here is what the boot method looks like for the pong game.
#+begin_src lisp
(defmethod ww::boot ((app solo-pong))
"Adds the intro text and sets up the start button handler."
(sdl2:hide-cursor)
(let ((intro-text
(make-instance
'ww:text
:content "Press any key to start"
:font (ww::get-asset "Ticketing.ttf")
:x 160
:y 300
:scale-x 3.0
:scale-y 3.0)))
(setf (intro-text app) intro-text)
(ww:add-unit app intro-text)) ; add to scene
(ww:add-handler app #'press-to-start))
#+end_src
You can see that it does very little. It first creates an instance of ~text~ with the appropriate scale and starting position. Then it just sets the app's ~intro-text~ slot to the newly created text before adding to the scene.
It ends by adding a handler to the app called ~press-to-start~, where presumably, the rest of the game is set up.
**** The Shutdown Method
The shutdown method is optional and is called right before the application exits. If supplied, it is a good place to do things like: make save files, close network connections, clean up foreign memory resources that are not already managed by the [[*The Asset Protocol][asset protocol]].
*** The Display Tree Protocol: Units and Containers
Objects that render to the screen are organized into a display tree. There are two basic kinds of objects: **units** and **containers**. Roughly, units are the things you want to display and containers help to control when and where you to display them.
Units are added to and removed from containers, and a unit will belong to at most one container at a time. In general, the last thing added to a container will be the last thing rendered - i.e. it will appear to be "on top". Containers are themselves units, so they too can be added to other containers. Nesting of containers allows you to render a one set of units before or after some other set of units.
The application is itself a container, and is the only unit that does not need to be added to a scene. No unit will be displayed until it becomes part of the display tree rooted at the application.
Containers have "bounds", which are screen coordinates for the left, right, top, and bottom of the region inside of which units will be displayed. If a unit moves out of bounds, it will not show up on the screen, and will not receive mouse events. (It may, however, still be focused - and hence receive key events.) The bounds of the application are the visible window itself. Other containers may have custom bounds.
See =examples/07-scrollarea.lisp= for an example that uses a container.
**** Containers
The main thing you can do with containers is add and remove units:
: (add-unit app my-unit)
: (drop-unit my-unit) ;; it knows its container
You can also adjust their bounds, for example:
: (setf (container-top my-container) 100)
**** The Affine Units
Most units of any interest implement the "affine protocol". I.e they have orientation and scale in the 2d plane of the game window.
More specifically, you can use the following accessor functions on them:
: x
: y
: width
: height
: scale-x
: scale-y
: rotation
There are a few convenience functions also defined that use the above functions under-the-hood. I'm not including them here because the API is still stabilizing.
The affine units are things like:
+ ~bitmap~: display an image that has been loaded from a file asset (currently only png is supported)
+ ~text~: display text
+ ~frameset~: display an animated sequence of images
+ ~sprite~: display a "bundle" of framesets
+ ~canvas~: display a region of mutable pixels
*** Events & Event Handling
Anything that "happens" in a wheelwork application happens through the course of handling some event.
Wheelwork defines an ~event-handler~ class that is used to to create functions for handling events. Instances of ~event-handler~ are funcallable objects with a slot that specifies the kind of event being handled.
Event handlers are added to instances of ~interactive~, which includes most kinds of units and the application itself. Notably, the base ~container~ class is not a subclass of ~interactive~ and so cannot handle events.
Wheelwork provides a macro called ~defhandler~ that can be used easily create instances of ~event-handler~ and bind them to a name. Most event handlers are created using a macro that looks like ~on-EVENTNAME~, which are discussed below.
Here is a simple example where a handler is defined, from the pong game:
#+begin_src lisp
(ww:defhandler pong-mousemove
(ww:on-mousemotion (app)
(with-slots (paddle) app
(setf (ww:x paddle) (- x (* 0.5 (ww:width paddle)))
(dx paddle) xrel))))
#+end_src
See the example for details.
There are two kinds of events: User Interaction Events and Psuedoevents.
**** User Interaction Events
User Interaction events are, curiously enough, generated by user interaction. These include
: keydown
: keyup
: mousewheel
: mousedown
: mouseup
: mousemotion
The first three will fire on whichever object has focus. The last three will fire on the first visible object that intersects with the cursor.
**** Psuedoevents
Psuedoevents are generated by the wheelwork itself, and include the following:
Display tree events:
: after-added
: before-added
: before-dropped
Focus events
: focus
: blur
Frame events
: perframe
See the documentation for the ~on-*~ forms for these events to get a sense of how to handle them.
**** Defining event handlers
Event handlers can be defined with ~ww:defhandler~ and ~ww:on-EVENTTYPE~ macros. For example in,
#+begin_src lisp
(ww::defhandler thing-clicked
(ww::on-mousedown (target x y)
(format t "~a was clicked at ~a,~a!~%" target x y)))
#+end_src
The ~on-mousedown~ form creates the event handler, and the ~defhandler~ form assigns it to a name in the function namespace. It has been written this way to allow you to redefine ~thing-clicked~ while your application is running in order to experiment with handlers - i.e. in order to support interactive development.
Another reason that the ~defhandler~ form is separate from the ~on-EVENTNAME~ forms is that each of the ~on-*~ forms accept different arguments, all optional, depending on the event they are handling.
For example, ~on-mousedown~ is a macro. The above could have been written
#+begin_src lisp
(ww::defhandler thing-clicked
;; lambda list variable names are all optional.
(ww::on-mousedown ()
(format t "~a was clicked at ~a,~a!~%" target x y)))
#+end_src
Or it could have been written
#+begin_src lisp
(ww::defhandler thing-clicked
;; lambda list variable names are all optional.
(ww::on-mousedown (my-unit my-x my-y)
(format t "~a was clicked at ~a,~a!~%" my-unit my-x my-y)))
#+end_src
See the docstrings for each ~on-*~ form for specifics.
The upshot is, if you are using SLIME, you will get hints about what arguments your handler code expects.
**** Handling Events
To get an object (either a unit or the application itself) to handle an event, you add a handler to that object. Handlers know what event they are meant to handle, so you just need to call:
: (ww:add-handler my-unit my-handler)
Where ~my-handler~ is either an ~event-handler~ instance.
Likewise you can drop an event handler
: (ww:remove-handler my-unit handler-or-event-type)
If ~handler-or-event-type~ is an event handler instance, it is removed if present. If ~handler-or-event-type~ is symbol whose ~symbol-name~ is the name of an event (e.g. ~:mousedown~ or ~'perframe~), then all handlers of that type are removed from the unit.
*** The Asset Protocol
Assets are resources loaded from disk. The application's ~asset-classifiers~ list associates file extensions (like "ttf", and "png") with classes (like ~font~ and ~png~) that load and prepare assets for use in an application.
Every asset has a "key", which is just a string path name that is relative to the application's ~asset-root~. These keys are used by ~get-asset~ to fetch assets, possibly loading them for the first time if they have not been previously fetched.
Some classes (like ~text~ or ~bitmap~) require an instance of an asset class to fill one of their instance slots (like ~font~ or ~texture~) in order to work properly.
E.g. In ~examples/03-font-render.lisp~ you see
#+begin_src lisp
(make-instance
'ww::text
:content (format nil "Hell!~%Oh World...")
:font (ww::get-asset "Ticketing.ttf"
:asset-args '(:oversample 2)))
#+end_src
which fetches the ~"Ticketing.ttf"~ font asset for use in rendering the text content.
The asset root relative to which ~"Ticketing.ttf"~ is resolved can be set during instantiation of the application.
e.g., for the same example:
#+begin_src lisp
(make-instance
'font-display
:fps 60
:refocus-on-mousedown-p nil
:width 800
:height 600
:title "Wheelwork Example: Font display"
:asset-root (merge-pathnames
"examples/"
(asdf:system-source-directory :wheelwork)))
#+end_src
|