emacs.d/clones/lisp/stevelosh.com/blog/2017/01/chip8-menus/index.html
2022-10-07 15:47:14 +02:00

281 lines
No EOL
24 KiB
HTML

<!DOCTYPE html>
<html lang='en'><head><meta charset='utf-8' /><meta name='pinterest' content='nopin' /><link href='http://stevelosh.com/static/css/style.css' rel='stylesheet' type='text/css' /><link href='http://stevelosh.com/static/css/print.css' rel='stylesheet' type='text/css' media='print' /><title>CHIP-8 in Common Lisp: Menus / Steve Losh</title></head><body><header><a id='logo' href='http://stevelosh.com/'>Steve Losh</a><nav><a href='http://stevelosh.com/blog/'>Blog</a> - <a href='http://stevelosh.com/projects/'>Projects</a> - <a href='http://stevelosh.com/photography/'>Photography</a> - <a href='http://stevelosh.com/links/'>Links</a> - <a href='http://stevelosh.com/rss.xml'>Feed</a></nav></header><hr class='main-separator' /><main id='page-blog-entry'><article><h1><a href='index.html'>CHIP-8 in Common Lisp: Menus</a></h1><p class='date'>Posted on January 10th, 2017.</p><p>Our <a href="https://en.wikipedia.org/wiki/CHIP-8">CHIP-8</a> emulator in Common Lisp is almost complete. It can play
games, and we've got a rudimentary debugging system in place so we can figure
out where things go wrong.</p>
<p>Up to now we've been communicating with the running emulator mostly through
NREPL or SLIME. This is fine for development, but in this post we'll add some
much-needed polish in the form of menus. This is the kind of boring work that
often gets left until the end during game development, so let's just get it out
of the way.</p>
<p>The full series of posts so far:</p>
<ol>
<li><a href="../../../2016/12/chip8-cpu/index.html">CHIP-8 in Common Lisp: The CPU</a></li>
<li><a href="../../../2016/12/chip8-graphics/index.html">CHIP-8 in Common Lisp: Graphics</a></li>
<li><a href="../../../2016/12/chip8-input/index.html">CHIP-8 in Common Lisp: Input</a></li>
<li><a href="../../../2016/12/chip8-sound/index.html">CHIP-8 in Common Lisp: Sound</a></li>
<li><a href="../chip8-disassembly/index.html">CHIP-8 in Common Lisp: Disassembly</a></li>
<li><a href="../chip8-debugging-infrastructure/index.html">CHIP-8 in Common Lisp: Debugging Infrastructure</a></li>
<li><a href="index.html">CHIP-8 in Common Lisp: Menus</a></li>
</ol>
<p>The full emulator source is on <a href="https://bitbucket.org/sjl/cl-chip8">BitBucket</a> and <a href="https://github.com/sjl/cl-chip8">GitHub</a>.</p>
<ol class="table-of-contents"><li><a href="index.html#s1-architecture">Architecture</a></li><li><a href="index.html#s2-adding-a-main-window">Adding a Main Window</a></li><li><a href="index.html#s3-updating-the-screen">Updating the Screen</a></li><li><a href="index.html#s4-adding-menus">Adding Menus</a><ol><li><a href="index.html#s5-the-file-menu">The File Menu</a></li><li><a href="index.html#s6-the-display-menu">The Display Menu</a></li><li><a href="index.html#s7-the-sound-menu">The Sound Menu</a></li></ol></li><li><a href="index.html#s8-results">Results</a></li><li><a href="index.html#s9-future">Future</a></li></ol>
<h2 id="s1-architecture"><a href="index.html#s1-architecture">Architecture</a></h2>
<p>Qtools has some <a href="https://shinmera.github.io/qtools/#QTOOLS:DEFINE-MENU">rudimentary support</a> for menus. Unfortunately it
won't quite work with our emulator as-is, so we'll need to shuffle things around
a bit. When we added the screen to the emulator back in the <a href="../../../2016/12/chip8-graphics/index.html">graphics</a> post
we just created a subclass of <code>QGLWidget</code> and passed it along to
<code>with-main-window</code>. This works for displaying the screen, but if you try to
<code>define-menu</code> on this widget Qtools will signal an error.</p>
<p>What we need to do is create a <code>QMainWindow</code> widget instead, and put our
<code>QGLWidget</code> inside of that. Then we can add some menus to the main window and
everything should work great. It will end up being structured like this:</p>
<pre class="lineart">
QMainWindow
╔══════════════════════════════════╗
║ Menu1 │ Menu2 │ ... ║
║───────┴───────┴──────────────────║
║ ┌──────────────────────────────┐ ║
║ │..............................│ ║
║ │..............................│ ║
║ │..........QGLWidget...........│ ║
║ │.........(the screen).........│ ║
║ │..............................│ ║
║ │..............................│ ║
║ │..............................│ ║
║ └──────────────────────────────┘ ║
╚══════════════════════════════════╝
</pre>
<h2 id="s2-adding-a-main-window"><a href="index.html#s2-adding-a-main-window">Adding a Main Window</a></h2>
<p>We'll start by creating the <code>QMainWindow</code> widget. It will just have a single
slot to keep track of the <code>chip</code> struct it's displaying:</p>
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">define-widget</span></i> main-window <span class="paren2">(<span class="code">QMainWindow</span>)</span>
<span class="paren2">(<span class="code"><span class="paren3">(<span class="code">chip <span class="keyword">:accessor</span> main-chip <span class="keyword">:initarg</span> <span class="keyword">:chip</span></span>)</span></span>)</span></span>)</span></span></code></pre>
<p>Next we'll define our screen widget to be a <em>subwidget</em> of the main window:</p>
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">define-subwidget</span></i> <span class="paren2">(<span class="code">main-window screen</span>)</span> <span class="paren2">(<span class="code">make-instance 'screen</span>)</span>
<span class="paren2">(<span class="code">setf <span class="paren3">(<span class="code">screen-chip screen</span>)</span> chip</span>)</span></span>)</span></span></code></pre>
<p>This ensures the screen widget will get cleaned up properly when the main window
is closed.</p>
<p>Now we need to move some of the initialization code that used to be in the
screen widget up into the main window, and also add a bit more to connect
everything properly:</p>
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">define-initializer</span></i> <span class="paren2">(<span class="code">main-window main-setup</span>)</span>
<span class="paren2">(<span class="code">setf <span class="paren3">(<span class="code">q+:window-title main-window</span>)</span> <span class="string">&quot;cl-chip8&quot;</span>
<span class="paren3">(<span class="code">q+:central-widget main-window</span>)</span> screen
<span class="paren3">(<span class="code">q+:focus-proxy main-window</span>)</span> screen</span>)</span></span>)</span></span></code></pre>
<p>The <code>window-title</code> needs to go on the top-level widget, so we can pull that out
of the screen's initializer. We also set the screen to be the &quot;central widget&quot;
of the main window. You can read the <a href="https://doc.qt.io/qt-4.8/qmainwindow.html">Qt docs</a> for the full
story, but essentially a <code>QMainWindow</code> is just a container for other widgets and
we need to designate one as the primary widget.</p>
<p>We'll also want to set the <a href="https://doc.qt.io/qt-4.8/qwidget.html#setFocusProxy">focus proxy</a> of the main window to be the screen,
because we want the screen to handle keyboard input just like before. Setting
the focus proxy tells Qt that whenever the main window gets focused it should
actually focus the screen instead. If we didn't do this, then if the main
window itself got focused (which can happen when tabbing through applications,
clicking the title bar, etc) it wouldn't propagate the keystrokes down to the
screen.</p>
<p>This is all kind of fiddly stuff, but it's the polish that separates a toy
project from something that actually feels <a href="http://makegames.tumblr.com/post/1136623767/finishing-a-game">finished</a>.</p>
<p>We'll also want to change our <code>die</code> function to close this main window, not just
the screen. I'm going to cheat just a little bit and use a global variable to
hold the currently-running main window for easy access:</p>
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defparameter</span></i> <span class="special">*main-window*</span> nil</span>)</span> <span class="comment">; will get set later
</span>
<span class="paren1">(<span class="code"><i><span class="symbol">defun</span></i> die <span class="paren2">(<span class="code"></span>)</span>
<span class="paren2">(<span class="code">setf <span class="paren3">(<span class="code">chip8::chip-running <span class="paren4">(<span class="code">main-chip <span class="special">*main-window*</span></span>)</span></span>)</span> nil</span>)</span>
<span class="paren2">(<span class="code">q+:close <span class="special">*main-window*</span></span>)</span></span>)</span></span></code></pre>
<p>Finally, <code>run-gui</code> will need to start up a <code>main-window</code> instead of a screen:</p>
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defun</span></i> make-main-window <span class="paren2">(<span class="code">chip</span>)</span>
<span class="paren2">(<span class="code">make-instance 'main-window <span class="keyword">:chip</span> chip</span>)</span></span>)</span>
<span class="paren1">(<span class="code"><i><span class="symbol">defun</span></i> run-gui <span class="paren2">(<span class="code">chip thunk</span>)</span>
<span class="paren2">(<span class="code"><i><span class="symbol">with-main-window</span></i>
<span class="paren3">(<span class="code">window <span class="paren4">(<span class="code">setf <span class="special">*main-window*</span> <span class="paren5">(<span class="code">make-main-window chip</span>)</span></span>)</span></span>)</span> <span class="comment">; NEW
</span> <span class="paren3">(<span class="code">funcall thunk</span>)</span></span>)</span></span>)</span></span></code></pre>
<p>I usually try not to use the return value of a <code>setf</code> form because I think it's
kind of ugly, but it saved an entire <code>let</code> here so decided to I break my own
style rule.</p>
<h2 id="s3-updating-the-screen"><a href="index.html#s3-updating-the-screen">Updating the Screen</a></h2>
<p>The only thing we need to change for the <code>screen</code> is its initializer:</p>
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">define-initializer</span></i> <span class="paren2">(<span class="code">screen screen-setup</span>)</span>
<span class="paren2">(<span class="code">setf <span class="paren3">(<span class="code">q+:focus-policy screen</span>)</span> <span class="paren3">(<span class="code">q+:qt.strong-focus</span>)</span>
<span class="paren3">(<span class="code">q+:fixed-size screen</span>)</span> <span class="paren3">(<span class="code">values <span class="special">*width*</span> <span class="special">*height*</span></span>)</span></span>)</span></span>)</span></span></code></pre>
<p>We've removed the useless <code>window-title</code> that used to be here. Instead we set
the focus policy to the screen to accept all kinds of focus. If you <em>don't</em> set
this the widget will never be able to get focus (and thus receive keyboard
events). We also leave in the size setting. The <code>QMainWindow</code> seems to pick up
this size and scale itself appropriately, which is nice.</p>
<p>That's it for the architectural changes. All the screen's drawing and
input-handling code can remain unchanged.</p>
<h2 id="s4-adding-menus"><a href="index.html#s4-adding-menus">Adding Menus</a></h2>
<p>We'll add a couple of menus to make it a bit easier to use the emulator without
falling back to the Lisp REPL. There's a lot of things we could add, so I'll
just cover a couple basic options.</p>
<h3 id="s5-the-file-menu"><a href="index.html#s5-the-file-menu">The File Menu</a></h3>
<p>Let's start with a simple <code>File</code> menu that will just have two items:</p>
<ul>
<li>Load a ROM</li>
<li>Quit the emulator</li>
</ul>
<p>The menu definition is pretty straightforward:</p>
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">define-menu</span></i> <span class="paren2">(<span class="code">main-window File</span>)</span>
<span class="paren2">(<span class="code"><span class="keyword">:item</span> <span class="paren3">(<span class="code"><span class="string">&quot;Load ROM...&quot;</span> <span class="paren4">(<span class="code">ctrl o</span>)</span></span>)</span> <span class="paren3">(<span class="code">load-rom main-window</span>)</span></span>)</span>
<span class="paren2">(<span class="code"><span class="keyword">:item</span> <span class="paren3">(<span class="code"><span class="string">&quot;Quit&quot;</span> <span class="paren4">(<span class="code">ctrl q</span>)</span></span>)</span> <span class="paren3">(<span class="code">die</span>)</span></span>)</span></span>)</span></span></code></pre>
<p>Note that we use the Windows-centric shortcut key names. Qt will handle
translating those to Mac-friendly versions when running on OS X.</p>
<p>We'll hide the details of loading the ROM in a <code>load-rom</code> helper function:</p>
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defun</span></i> load-rom <span class="paren2">(<span class="code">main-window</span>)</span>
<span class="paren2">(<span class="code"><i><span class="symbol">let</span></i> <span class="paren3">(<span class="code"><span class="paren4">(<span class="code">rom <span class="paren5">(<span class="code">get-rom-path main-window</span>)</span></span>)</span></span>)</span>
<span class="paren3">(<span class="code">when rom
<span class="paren4">(<span class="code">chip8::load-rom <span class="paren5">(<span class="code">main-chip main-window</span>)</span> rom</span>)</span></span>)</span></span>)</span></span>)</span></span></code></pre>
<p><code>load-rom</code> just gets the path to load and loads it into the <code>chip</code> struct,
assuming it's not <code>nil</code>. Once again we use a helper function to hide the
details of getting the path to the ROM, because I'm a firm believer in <a href="https://groups.google.com/forum/message/raw?msg=comp.lang.lisp/9SKZ5YJUmBg/Fj05OZQomzIJ">one
function to a function</a>:</p>
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defparameter</span></i> <span class="special">*default-directory*</span>
<span class="paren2">(<span class="code">uiop:native-namestring
<span class="paren3">(<span class="code">asdf:system-source-directory <span class="keyword">:cl-chip8</span></span>)</span></span>)</span></span>)</span>
<span class="paren1">(<span class="code"><i><span class="symbol">defun</span></i> get-rom-path <span class="paren2">(<span class="code">window</span>)</span>
<span class="paren2">(<span class="code"><i><span class="symbol">let</span></i> <span class="paren3">(<span class="code"><span class="paren4">(<span class="code">path <span class="paren5">(<span class="code">q+:qfiledialog-get-open-file-name
window <span class="comment">; parent widget
</span> <span class="string">&quot;Load ROM&quot;</span> <span class="comment">; dialog title
</span> <span class="special">*default-directory*</span> <span class="comment">; starting directory
</span> <span class="string">&quot;ROM Files (*.rom);;All Files (*)&quot;</span></span>)</span></span>)</span></span>)</span> <span class="comment">; filters
</span> <span class="paren3">(<span class="code"><i><span class="symbol">if</span></i> <span class="paren4">(<span class="code">string= path <span class="string">&quot;&quot;</span></span>)</span>
nil
path</span>)</span></span>)</span></span>)</span></span></code></pre>
<p>Qt has a nice static method <code>QFileDialog.getOpenFileName</code> that we can use to do
the heavy lifting. It takes a parent widget (our main window), a title,
a directory to start in, and a file filter string.</p>
<p>I've used some handy ASDF and UIOP functions to tell the file dialog to start in
the directory where the emulator's code is located, because that's where I store
my own ROMs. Another option would be to have it start in the user's home
directory, or to make the location configurable.</p>
<p>The filter string is actually parsed by Qt, and by default will prevent the user
from selecting any file that doesn't end in <code>.rom</code>. You might want to add a few
more options extensions here if you think people will have named their ROMs
differently. We also add a second filter that will let the user select <em>any</em>
file, in case they have a ROM with a filename we haven't anticipated. The
result looks like this:</p>
<p><a href="http://stevelosh.com/static/images/blog/2017/01/chip8-file-select.png"><img src="http://stevelosh.com/static/images/blog/2017/01/chip8-file-select.png" alt="Screenshot of the file selection dialog"></a></p>
<p>Note that if the user cancels out of the file selection dialog Qt will return an
empty string. We'll check for that and return a more Lispy <code>nil</code> from the
function.</p>
<p>That's it for the <code>File</code> menu. It's basic, but it's a lot nicer to load ROMs
through a normal dialog than to have to poke at the <code>chip</code> in the REPL.</p>
<h3 id="s6-the-display-menu"><a href="index.html#s6-the-display-menu">The Display Menu</a></h3>
<p>Back in the <a href="../../../2016/12/chip8-graphics/index.html">graphics</a> post we saw how some ROMs expect the CHIP-8 to wrap
sprites around the screen when their coordinates get too large, and other ROMs
require that they <em>not</em> wrap. There's no good way to automatically detect this
(aside from hashing particular ROMs and hard-coding the setting for those) so
we'll expose this as an option to the user in a <code>Display</code> menu.</p>
<p>The menu itself is simple — we'll make a submenu that will contain the options
and a helper function to actually do the work:</p>
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">define-menu</span></i> <span class="paren2">(<span class="code">main-window Display</span>)</span>
<span class="paren2">(<span class="code"><span class="keyword">:menu</span> <span class="string">&quot;Screen Wrapping&quot;</span>
<span class="paren3">(<span class="code"><span class="keyword">:item</span> <span class="string">&quot;On&quot;</span> <span class="paren4">(<span class="code">set-screen-wrapping main-window t</span>)</span></span>)</span>
<span class="paren3">(<span class="code"><span class="keyword">:item</span> <span class="string">&quot;Off&quot;</span> <span class="paren4">(<span class="code">set-screen-wrapping main-window nil</span>)</span></span>)</span></span>)</span></span>)</span>
<span class="paren1">(<span class="code"><i><span class="symbol">defun</span></i> set-screen-wrapping <span class="paren2">(<span class="code">main-window enabled</span>)</span>
<span class="paren2">(<span class="code">setf <span class="paren3">(<span class="code">chip8::chip-screen-wrapping-enabled <span class="paren4">(<span class="code">main-chip main-window</span>)</span></span>)</span>
enabled</span>)</span></span>)</span></span></code></pre>
<p>Now we've got a simple little menu for turning screen wrapping off and on:</p>
<p><a href="http://stevelosh.com/static/images/blog/2017/01/chip8-display.png"><img src="http://stevelosh.com/static/images/blog/2017/01/chip8-display.png" alt="Screenshot of the display menu"></a></p>
<p>You might also want to reset the emulator automatically whenever this option is
changed, because toggle screen wrapping as the emulator is running can
produce... <em>interesting</em> results. But I like breaking games in fun ways, so
I left it as-is.</p>
<p>In a perfect world these options wouldn't be vanilla <code>:item</code>s but would instead
be part of a <a href="https://doc.qt.io/qt-4.8/qactiongroup.html"><code>QActionGroup</code></a>. This would tell Qt to treat these
items as a group, put a checkmark next to the currently-selected one, and so on.
Unfortunately Qtools doesn't have a menu content type for action groups and the
thought of implementing <a href="https://github.com/Shinmera/qtools/blob/23e3e44/widget-menu.lisp#L67-L86">something like this</a> makes me nauseas, so
I'm satisfied with the <code>:item</code> kludge.</p>
<h3 id="s7-the-sound-menu"><a href="index.html#s7-the-sound-menu">The Sound Menu</a></h3>
<p>Our final menu will allow the user to select what kind of sound the buzzer
should play, because it would be a shame to let all our work in the <a href="../../../2016/12/chip8-sound/index.html">sound</a>
post go to waste. We'll implement the <code>Sound</code> menu just like the <code>Display</code>
menu:</p>
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">define-menu</span></i> <span class="paren2">(<span class="code">main-window Sound</span>)</span>
<span class="paren2">(<span class="code"><span class="keyword">:menu</span> <span class="string">&quot;Sound Type&quot;</span>
<span class="paren3">(<span class="code"><span class="keyword">:item</span> <span class="string">&quot;Sine&quot;</span> <span class="paren4">(<span class="code">set-sound-type main-window <span class="keyword">:sine</span></span>)</span></span>)</span>
<span class="paren3">(<span class="code"><span class="keyword">:item</span> <span class="string">&quot;Square&quot;</span> <span class="paren4">(<span class="code">set-sound-type main-window <span class="keyword">:square</span></span>)</span></span>)</span>
<span class="paren3">(<span class="code"><span class="keyword">:item</span> <span class="string">&quot;Sawtooth&quot;</span> <span class="paren4">(<span class="code">set-sound-type main-window <span class="keyword">:sawtooth</span></span>)</span></span>)</span>
<span class="paren3">(<span class="code"><span class="keyword">:item</span> <span class="string">&quot;Triangle&quot;</span> <span class="paren4">(<span class="code">set-sound-type main-window <span class="keyword">:triangle</span></span>)</span></span>)</span></span>)</span></span>)</span>
<span class="paren1">(<span class="code"><i><span class="symbol">defun</span></i> set-sound-type <span class="paren2">(<span class="code">main-window type</span>)</span>
<span class="paren2">(<span class="code">setf <span class="paren3">(<span class="code">chip8::chip-sound-type <span class="paren4">(<span class="code">main-chip main-window</span>)</span></span>)</span> type</span>)</span></span>)</span></span></code></pre>
<h2 id="s8-results"><a href="index.html#s8-results">Results</a></h2>
<p>And with that we've got a basic menu system for the emulator:</p>
<p><a href="http://stevelosh.com/static/images/blog/2017/01/chip8-menu.png"><img src="http://stevelosh.com/static/images/blog/2017/01/chip8-menu.png" alt="Screenshot of the full menu"></a></p>
<h2 id="s9-future"><a href="index.html#s9-future">Future</a></h2>
<p>We're nearing the end of the series. The next post will be about adding
a graphical debugging interface, and will probably be the last in the series.</p>
<p>(Unless I get ambitious and try making a curses-based ASCII UI...)</p>
</article></main><hr class='main-separator' /><footer><nav><a href='https://github.com/sjl/'>GitHub</a><a href='https://twitter.com/stevelosh/'>Twitter</a><a href='https://instagram.com/thirtytwobirds/'>Instagram</a><a href='https://hg.stevelosh.com/.plan/'>.plan</a></nav></footer></body></html>