Recent Art & Portfolio

Once again I have become neglectful of updating this blog with my artwork, so let’s do a roundup of the last uh… 7 months?!?! and maybe I’ll try to get it back on track from here on. Though I do have a nice portfolio site now that I have been keeping up to date, so if you really like my art you could be following that as well. More on that below.

Socialist Revolutionaries Past & Future

Last October I was trying to get back into the development of my game Just a Robot, and started as I always do by completely re-imagining its entire look. In this case I was inspired by the look of Soviet propaganda and Anarchist woodcuts.

Robot Propaganda

I followed that up in December with this tribute to the luxurious moustache of Irish revolutionary socialist James Connolly.

James Connolly

Spaaaace

For the New Year I committed myself to working on more space and sci-fi themed stuff. I started with this depiction of a space station roughly based on the ISS.

Space station

Also in January, a spaceship approaching Mercury, with some tricky perspective.

Mercury approach

In February I did another piece loosely inspired by Soviet propaganda, and a theme that seemed to be somewhat controversial - the idea of billionaires fleeing into space and leaving the rest of us to our fates. Some people took that as a literal prediction of future events, but I think of it in more allegorical terms - capital is ruining the natural and social environment without any sense of responsibility to the rest of us, while its masters can escape the consequences of their actions without literally leaving the planet - depicting them doing so is just a good way of describing the situation, in my opinion.

Flight of the Billionaires

In March I brought it back to Earth and explored the same theme from another perspective, more or less.

Left Behind

I started a piece in April for Cosmonautics day, but I didn’t get it finished until June - the capsule and final stage of Vostok 1 in orbit. I did a game jam and an oil painting in the meantime though!

Vostok 1

And that pretty much brings us up to date!

Portfolio

My portfolio site, which went live in July 2020, is another statically-generated site based on Pelican, but focused on image galleries instead of blog entries (using the standard gallery plugin). I wanted a central place to put my art that wasn’t a social media platform, and that would display it optimally. I’ve linked to it a few times from here despite never actually mentioning it.

It is inspired by the ideas of Matej Jan on displaying art on the internet, and attempts to display my pieces at the best integer scaling to fit in the browser window, on a background with appropriate contrast, and with no distractions.

Of course, I also wanted to keep it as small and responsive as possible, so it does this with about 3kB of javascript, 11kB of CSS, and a selection of minimal SVG backgrounds. Because all the art is pixel art, transferring it at 1x resolution and resizing it in the browser (as discussed in recent posts) keeps things extremely compact, with the entire portfolio currently only amounting to 390kB. The portfolio does a lot better job of displaying the art than this blog does though, here the images are just resized to max width without attempting an integer scaling.

I think it looks real nice and that my art looks real nice on it, so go check it out!

Image Optimisation

In the last instalment of my epic blogging saga I recounted my discovery that the index page of this site had grown to over 1.7MB of content when loaded fresh, largely due to the images. One of my goals for this site was that it be lean - fast to load and energy efficient - and it was not meeting that goal at all. Clearly avoiding Javascript and CSS frameworks was not enough!

I immediately started thinking about how to improve the situation. Though I had previously ruled out the approach used by Low-tech magazine’s solar-powered website because I didn’t think it would fit my aesthetic, I decided to see if dithering coloured, rather than monochrome, PNGs would work better for me, and improve on the size and quality of an appropriately-sized 75% quality JPEG.

PNG Thunderdome

The one to beat - baseline 75% JPEG, Size: 16.1kb

melanie-pointing_alpha_baseline.jpg

Using Pillow and the same hitherdither library that Low-tech magazine used for their site, I iterated on a script that output hundreds of compressed variations of a given image using different dithering algorithms and parameters.

The best results I found were with the Bayesian algorithm with a 32 colour palette, a 2x2 matrix, and an image size half the expected display size. This produced a result that was relatively readable, and reminiscent of pixel art. The size savings varied by image - sometimes up to 10kB, but often only 2-3kB as for this example. These savings are modest compared to the loss in detail, and I think this approach could only be considered because of the unique aesthetic it produces.

Palette: 32, Dither: bayer, Threshold: 256/8-256-256/8, Order: 2, Size: 13.9kb

melanie-pointing_alpha_halved_pal32_dithbayer_order2_thresh8-1-8.png

The three “threshold” parameters expected by hitherdither were a bit of a mystery to me. Through trial and error I found that some produced much smaller images, but unfortunately not at a level of quality that I found acceptable. Lower palette sizes also resulted in savings, but below 32 colours they started to look too abstract and unreadable to me.

Palette: 32, Dither: bayer, Threshold: 256/2-256-256/2, Order: 2, Size: 9.6kb

melanie-pointing_alpha_halved_pal32_dithbayer_order2_thresh2-1-2.png

A Challenger Appears

I was about ready to commit to this approach and start converting all the images when my wife reminded me that WEBP exists! After converting a few of my test images to WEBP it was clear that it had my dithered PNGs beat - half the size of the JPEG without any loss of quality.

80% quality WEBP, Size: 9.9kb

melanie-pointing_alpha_baseline.webp

Apparently support for WEBP is pretty good these days, but there are a couple of annoying outliers - Safari only supports it on Big Sur, and IE11 still exists, as I’m sure it always will.

As such, I decided I should probably try and fallback gracefully to a JPEG or PNG where WEBP isn’t supported. This can be achieved using <picture> and <source> elements to allow the browser to choose the format it likes best.

<picture>
    <source type="image/webp" srcset="{optimal image url}"/>
    <source type="image/jpeg" srcset="{compatible image url}"/>
    <img src="{compatible image url}"/>
</picture>

Let’s Automate

The above HTML snippet presents a problem - my posts are not written in HTML but in Markdown, and processed by Pelican into HTML, and that process just results in an <img> tag by default.

I threw together a quick Pelican plugin to post-process the generated HTML and replace any <img> tags with <picture> tags, if the referenced images could be replaced with WEBPs. It also processes the referenced images to create scaled JPEG/PNG versions as well as the WEBP version, so I don’t have to do any of that manually either.

Results

The index of the blog is now 778kB at the time of writing - so reduced by over half! I did also replace a particularly large and troublesome GIF with a static image as well, and converted some PNGs to JPEGs. This results in greater savings because the plugin converts PNGs to lossless WEBPs and JPEGs to lossy ones.

The plugin is actually not even working fully yet - for some reason it is missing some <img> tags on the index pages, leaving them serving their original unprocessed files.

I also haven’t done anything about the pixel art images, which if served at their original resolution could be a significant saving.

So in short, huge progress, but still much scope for improvement!

I am almost sorry I’m not going to end up using these dithered PNGs though…

Tracer and Chun-Li

Update 01/06/2021

I fixed the problem with the plugin and all images are now optimised except the few in this post that are flagged to be skipped. I also swapped out all the pixel art images and gifs for 1x resolution versions. These are just being scaled up by CSS, with reasonable results.

The index page is now 539kB, the whole site is just over 1MB, and it is in the 80th percentile for energy usage according to websitecarbon.com (whatever that’s worth). I think the link above showed 75th or 76th percentile or thereabouts at the time of posting, but it will show 80th now.

Energy Usage Update

When I started this blog, one of my goals was for it to be lean and efficient, both to improve the reader’s experience and to reduce the power usage involved in serving it.

I’ve been planning recently to write a post on a related topic - the energy efficiency of the Python language and how using it as the backend or API of a more dynamic website would fit with those same goals. In preparation for that, I thought to check in on this blog and whether it is still as lean as it was back when I was figuring out how to embed the SVG icons in it.

Unfortunately, it seems that it is not. Unlike one of my inspirations, low-tech magazine’s solar powered website, I never found a solution for reducing the size of image files. For my posts about TV shows and movies I use fairly low quality JPEGs. But for my art and game dev posts I have been unwilling to compromise, and, frankly, a bit lazy, and have just been throwing large PNGs and GIFs into my posts without thinking about their size.

As a result, loading the index of this site now transfers 1.7MB if nothing is already cached. The text content and CSS are in the tens of kilobytes, with the rest being images. This is comparable to the initial load of many corporate news sites and large blogs with all their tracking javascript and ads blocked - an improvement over the norm perhaps, since I’m not doing any of the tracking or advertising that have become such a burden on the web, but not particularly notable. This site is now only in the 57th percentile according to websitecarbon.com.

For pixel art, one solution does present itself immediately. For my portfolio site, all of the artwork is sent to the browser at 1x resolution and resized there by an integer factor by a small amount of javascript, and perhaps the same thing could be achieved with css. The index of that site is under 400kB despite including all of my artwork, and does not require specially resized image thumbnails or anything like that. I could do the same thing here instead of throwing manually resized art into posts.

I’m less sure what to do about screenshots and screencaps. The solution used by low-tech magazine doesn’t really work for me, I think it compromises the readability of the images too much and doesn’t fit this site’s aesthetic. I tried a number of different techniques for vectorising, posterising and dithering screencaps from TV shows and movies to reduce the number of colours, but they didn’t result in significant savings. On the other hand, they did actually look kind of cool and stylised!

Vectorised Beth Harmon

Something I should probably try is scaling such images down significantly and then back up to the required display size in CSS. Obviously this will degrade the quality, but it might be an acceptable trade-off.

Screenshots of things like a text-editor or IDE are even more problematic - if the quality is degraded there they are probably not useful. I will probably just have to try to keep that kind of thing to a minimum, and use tighter crops where necessary.

It’s easy to become complacent about efficiency when computers are so powerful that a few tens or hundreds of kilobytes don’t seem to cause them any strain, but when you have that mindset they can quickly add up to quite a significant waste of time and power. I think this is well demonstrated by USA Today’s pissy response to the GDPR, where instead of the usual 5.2MB bloated garbage site, readers from the EU were served a lean, content focused site that loaded almost instantly and didn’t track them.

Nothing I do is going to have the kind of impact that a site like USA Today improving their efficiency would have, purely because of the scale of the traffic involved, but I still want to try to do better.

Ludum Dare 48 Results

Ludum Dare 48 results are out a while now, but I forgot to post about them. I did not do as well as last time, but since my results for Gophers were exceptional for me, that was probably to be expected. This time I placed 433rd overall, with the stand-out category being graphics, where I just about broke the top 100, at 99th place.

Category Rating Placing Percentile
Overall 3.81 433 84th
Fun 3.259 1044 61st
Theme 3.672 873 67th
Innovation 3.345 754 72nd
Humor 3.596 277 89th
Graphics 4.466 99 96th
Audio 3.621 526 80th
Mood 3.846 460 83rd

Graphs

A couple of graphs demonstrating my trends.

Ratings Graph Placings Graph

Still haven’t surpassed that Rattendorf peak, sigh

More Out of Gas

I am committed to working on a post-jam release of this game, as I think with a few tweaks and a bit more content it could be something quite special (more special than the ratings above suggest anyway!)

I will be posting here with development updates, and also over on the itch.io page, where you can also still play the jam version for now.

Out of Gas

Out of Gas

Out of Gas is my entry for the recent Ludum Dare 48 game jam. It was intended to be a blatant FTL rip-off, but in having much simplified clicker-like combat mechanics and an unusual cars-in-space aesthetic, I think it is sufficiently its own thing (I hope anyway!)

Concept

My initial thought was more of a straight sci-fi concept of travelling into deep space, facing combat and resource management challenges similar to those which ended up in the game. The first thing that came to mind as a title for this was “Out of Gas”, after the Modest Mouse song of the same name.

Thinking about that song got me thinking in another direction - essentially the same mechanics, but Earth-bound, set in a Modest Mouse world of highways and drifters, fleeing problems and starting over. The problem to flee in this case would obviously be debt - something that you can get deeper and deeper into.

As you can probably tell by now I ended up combining these two ideas! I didn’t want to let either of them go, and I quickly fell in love with the idea of a universe where space combat meant winding down your window and firing a handgun at your enemy.

The combined concept suggested that a new, sci-fi twist on the debt problem was required, and that’s where the idea of a constantly growing cryptocurrency debt came in. I’ve been thinking about cryptocurrencies a lot lately since they’re going through another hype cycle. One commonly touted feature of them (or some, like Bitcoin, at least), is that they’re deflationary, so any debt denominated in them would constantly grow in value instead of decreasing in value like debts denominated in inflationary fiat currencies. This seemed like the perfect notion for the bizarre world of my game. It doesn’t really have any impact on the gameplay, but I think it’s a nice bit of narrative flavour, and it fits the theme perfectly.

Late payment

The final piece of the conceptual puzzle was the characters. I couldn’t very well have two anonymous nobodies riding along with the player on a trip like this! I turned to two characters I had vague notions of making a game about years ago, Haze & Lee, a gun-toting, post-apocalyptic outlaw couple. Unfortunately I don’t think their personalities really come across in the game due to time constraints, but at least they have names!

Art

As usual, I did all the art in Pyxel Edit. I started off very rough with just pink boxes for the ships and a partial circle for the background, and moved immediately onto the gameplay with those placeholders. This is a somewhat unusual approach for me as I usually try to tie down the look of a game early on.

First mockup

The thing about rough mockups is that by the time I came back to the art a bunch of stuff in the game was already tied to the resolution and shape of the outlines. I wasn’t even sure I had done the perspective correctly, but I was stuck with it anyway, and I guess it was close enough because nobody has complained!

First ships

I had planned to do some character portraits as well for the narrative side of the game, and express encounters as dialogues between characters, but unfortunately there wasn’t time for that.

The map screen also didn’t get much love, with the initial placeholder art surviving into the final game.

Map screen

Code

As for my last Ludum Dare entry, Gophers, a cutscene graph editor plugin for Godot that I had previously developed was essential to getting this game done within the time constraints of the jam.

Those darn teenagers!

I had done a lot of work on this plugin in recent months, applying lessons learned from using it for Gophers to improve the workflow, functionality, and stability. This paid huge dividends, and I found almost no issues while churning out encounter cutscenes. The cutscene graphs created by this plugin power every encounter in the game up to and after combat.

My main task on the first day was to get the map screen working - it wouldn’t be much of a game if you couldn’t travel between systems. I implemented it as a graph defined by custom MapSystem and MapConnection control nodes.

Map graph

The new fun thing about this for me was declaring the MapConnection class to be a “tool” script, allowing the connections between systems to be drawn in the editor. This kind of custom tooling is important for the efficient design of systems in larger games, so it was fun to give it a try. Here, it mostly served to prevent me from getting confused about which MapSystem nodes I had already connected to each other and which I hadn’t.

func _draw():
    if usable or used or _can_draw_in_editor():
        _load_nodes_for_editor()
        # TODO: Decide on proper colours for this
        var color = Color.gray
        if usable:
            color = Color.hotpink
        # No idea why the offset is required
        var source_center = _source_node.rect_global_position + CENTER_OFFSET
        var target_center = _target_node.rect_global_position + CENTER_OFFSET
        var start = source_center + ((target_center - source_center).normalized() * SPACE)
        var finish = target_center + ((source_center - target_center).normalized() * SPACE)
        draw_line(
            start,
            finish,
            color,
            LINE_THICKNESS,
            false
        )

Implementing the combat was a lot tougher. The battle scene is almost entirely UI nodes, which was perhaps a mistake, and for some reason I struggled most of day two to even get player input to register properly. I don’t really remember now what the obstacle was to this - maybe I was just tired.

Battle UI

Sound and Music

There’s nothing special about the sound this time around. Unlike for Gophers I didn’t have time to do any foley work, and I figured I would probably have a hard time creating gunshot and explosion sounds anyway. Instead I fell back to SFXR (specifically, jsfxr).

The music I put together in BeepBox, my favourite game jam tool for chiptune music these days due to its simplicity. I tend not to get as bogged down with it as I do with LMMS. I initially wanted to try to create something in the style of Modest Mouse, given the game’s inspiration, but it was hard to recreate a guitar sound. Instead I ended up with a simple arpeggiated chord progression over a bass drone that I hope feels kind of epic, kind of longing and lonesome, I dunno.

The battle music is a variation of the same tune but just swaps out one of the instruments, doubles up the drone, and compresses and spreads the arpeggios across more octaves for a more urgent and chaotic feeling. The two tracks play in sync throughout the game and just cut between each other as necessary.

Abandoned Ideas

As usual there were many ideas that I had to put aside due to time constraints. I had planned to include a small number of missiles which could be fired during combat with devastating effect, to allow the player to pull themselves back from the brink of defeat. I had also intended to have targetable/damageable systems on the ships, including injury to the characters, so that you could take out the enemy’s weapon to gain a reprieve from being fired upon, or their engines and reduce their chance to dodge. You can see the proposed UI of these mechanics in the initial mockup above. Finally, I had planned a system of weapon upgrades which almost made it in, but not quite.

On the art side, I had wanted to include a number of different backgrounds to improve the sense of travelling between systems.

A big regret that I have is that I wasn’t able to include the character portraits, and express the encounters primarily as dialogue between characters. I think this would have introduced a lot more personality to the narrative side of the game, as opposed to the third-person narration that I had to go with.

Post-Jam

I like this game a lot, and I feel it is one that could be polished up and rounded out into a complete experience with relative ease, so I am determined to do a post-jam release with some of the missing features described above, and some of the problems with the jam release resolved (such as the RNG sometimes producing the same encounter multiple times in a row). I have already begun work on this, and I hope to have it completed in the next few months. I’ll do another post about my progress on it so far, but for now here’s a little sneak peek:

Haze speaks!

Coroutine Callbacks

I’m working on a cutscene/dialogue graph editor plugin for the Godot game engine (I mentioned using it for the Ludum Dare in a previous post). When I came to deciding how to actually process the graphs it creates in order to display the dialogue and perform actions in the game, I discovered an interesting use for coroutines that I haven’t seen described elsewhere. I’m not all that used to working with coroutines so maybe what I’ve done is completely normal and everybody does it all the time, or indeed it might be ill-advised for some reason. Either way, I will describe it here and you can ignore me if I’m being an idiot!

Graph editor

Processing a graph that results in activity in-game is a task which frequently needs to be put on hold - waiting for text to be displayed in the UI in a juicy manner, moving characters around, waiting for input etc. This could be achieved in a Node by implementing _process() and maintaining state about whether you’re currently walking a graph or waiting on something, and having methods that other nodes can use to indicate when processing can continue. I have found such methods to be quite messy in the past.

I decided instead to process the graph in a while loop and yield as necessary while other nodes perform tasks initiated by a number of signals. Where tasks are expected to take some time to complete, a coroutine state object is passed to the signal which the listener can use to indicate that processing should continue, or in some cases return values.

Let’s See Some Code

Below is an abridged version of the main method that processes the graphs. Processing a dialogue node yields so that the UI that displays the text can wait for player input to continue. Processing a branch node and others like it just proceed directly to the next iteration of the loop.

func process_cutscene(cutscene):
    _local_store = {}
    _current_graph = cutscene
    _current_node = _current_graph.root_node
    while _current_node != null:
        if _current_node is DialogueTextNode:
            yield(_process_dialogue_node(), "completed")
        elif _current_node is BranchNode:
            _process_branch_node()
    ...

Here is the code that processes dialogue nodes and raises the signal for their text to be displayed in-game. A coroutine is put in a frozen state to be passed with the signal, and the call to emit the signal is deferred so we can make sure we are waiting for the coroutine to complete before any signal listeners have a chance to resume it.

func _await_response():
    return yield()


func _emit_dialogue_signal(
    text,
    character_name,
    character_variant,
    process
):
    emit_signal(
        "dialogue_display_requested",
        text,
        character_name,
        character_variant,
        process
    )


func _process_dialogue_node():
    var text = _current_node.text

    var process = _await_response()
    call_deferred(
        "_emit_dialogue_signal",
        text,
        _current_node.character.character_name,
        _current_node.character_variant.variant_name,
        process
    )
    yield(process, "completed")

    _current_node = _current_node.next

The code that consumes the signal can take however long it likes to do its thing, and when it’s ready it can just call resume() on the coroutine state and the graph processing will proceed to the next node.

func _on_CutsceneController_dialogue_display_requested(
    text,
    character_name,
    character_variant,
    process
):
    _process = process
    ...


func _on_ContinueButton_pressed():
    if _process != null:
        _process.resume()

If a node in the graph needs a response in order to know what to do next (e.g. a choice between multiple dialogue options), we can just accept the return value of the coroutine passed in the signal. The consumer can just pass the value to the resume() call.

func _process_choice_node():
  ...
    var process = _await_response()
    call_deferred(
        "_emit_choices_signal",
        choices,
        process
    )
    var choice = yield(process, "completed")
    _current_node = _current_node.branches[choice]

Potential Pitfalls

There are two pitfalls that come to mind with this approach, but they’re not really any different than what could occur with a _process() based approach like the one I described above.

  1. Processing of another graph could be triggered while one is still in progress. If the previous one is yielding it would end up processing the new graph as well when it resumes. This is quite easy to avoid by just noping out of the method if there is already work in progress.
  2. If there are multiple listeners connected to the signals there could be ambiguity about which one is responsible for resuming. If there are multiple resume calls, all but the first will result in a runtime exception.

In the event that you actually want multiple listeners to report in before resuming, it might be possible to craft a different coroutine that takes the number of listeners and yields until they have all resumed. I think it would look something like this:

func _await_many(count):
  while count > 0:
    yield()
    count -= 1


func _await_many_responses(count):
  var responses = []
  while count > 0:
    responses.append(yield())
    count -= 1
  return responses

I haven’t tested that though.

Conclusion

I don’t really have one, I just thought it was a neat pattern that doesn’t require the listeners to keep a reference to the node doing the processing or have any knowledge of it aside from the signals it emits. You can check out the full code on GitHub as it stands currently. I haven’t tested it in a game yet, but I will probably enter the Ludum Dare later this month and I might get a chance to use it then. I will update afterwards whether it is a disaster or a triumph!

America Wins Again

Chess lives here

This post contains spoilers for the TV show “The Queen’s Gambit”

I loved pretty much everything about The Queen’s Gambit, and as you’ve probably gathered if you’ve read any of my other posts, I don’t have much to say about things if I’m not complaining about them!

What I do feel obligated to talk about, however, are the differences between this show and “For All Mankind” in terms of how they deal with the Soviet Union. I’ve written previously about how “For All Mankind” re-imagines the space race such that the USA remains the underdog after several successful Soviet moon landings - erasing real Soviet accomplishments in favour of fictional ones, and providing an impetus for the US to include women in its space program, something that it didn’t do in reality until the 1980’s.

The Queen’s Gambit does not rewrite history to the same extent. The chess world was dominated by Soviet players in the 50s, 60s and beyond, and the show acknowledges that readily, with “The Russian”, Vasily Borgov, being Beth Harmon’s ultimate opponent. Her breakthrough somewhat reflects that of American prodigy Bobby Fischer, so it’s not unprecedented. However, where For All Mankind featured a number of real-life historical figures, all the competitors in The Queen’s Gambit are fictional.

Team USA

In a way, however, Beth’s real opponent is not Borgov or any of the other men she defeats on the road to face him, but herself, her emotional problems, and her addictions. The games she plays are almost entirely without malice, with just a touch of smug arrogance on occasion, and everybody she faces ends up with enormous admiration for her. This is especially true of the Soviet players she faces, who almost seem happier to have been beaten by her than they would have been to have won. What cold war political animosity is present comes mostly from her CIA handler, and is treated as a ridiculous, petty distraction by Beth. Doesn’t he know that there’s chess to be played??

For All Mankind, in contrast, has that animosity at its core. Its the motivation behind all of the American government’s actions in the show, though not necessarily of everybody at NASA, while the one Soviet character seems to validate their suspicions.

The Queen’s Gambit’s portrayal of the Soviet Union is actually extraordinarily sympathetic as a result of being viewed through the lens of chess enthusiasm. Whereas at home chess is a niche interest, when Beth arrives in the USSR she discovers that it is a national obsession. At home her prospects are probably akin to those of Benny Watts, the US chess champion before her - obscurity, and a dingy basement apartment at best. In the USSR she is mobbed by an adoring crowd after every match, which she plays in a dedicated chess hall instead of whatever spaces are available. She even has to adopt their strategy of cooperating during adjournments before she is able to achieve victory, with a team of all the chess friends she’s acquired on her journey advising her on how to approach the rest of the game - an apparent admission of the superiority of collective cooperation over competition and individualism.

Welcome home

The final scene sees Beth slipping her handler’s grasp on the way to the airport. She wanders the streets of Moscow unmolested to find the old guys playing chess in the park, and they greet her warmly before inviting her to play. There’s a distinct sense that she has found a home here, where the game she loves is played openly in the park instead of hidden away in the basement, as in the orphanage where she grew up.

Although this series is still about an American character and mostly set in America, it is much closer to being the view from “the other side” that I hoped for in my previous post.

Cybercentrism

You're a wizard Wade!

This post contains spoilers for the movie “Ready Player One”

I finally gave in and watched Ready Player One, prompted by seeing the Internet dump on the sequel to the novel. I avoided it before now because the commentary on the novel made it sound like pandering nonsense, and I assumed the movie would be much the same. But, I wanted to see for myself.

Perhaps as a result of those lowered expectations, I actually found it quite entertaining on a surface level. The pop culture references were certainly omnipresent, but maybe not as tiresome as having them explained in writing would be. In movie form, a lot of them that are not plot-relevant just form a visual backdrop that actually help define a plausible metaverse, albeit one with a culture that is inexplicable stuck 30 years in the past.

But on the other hand, the centrality of pop culture to the plot, and why it is central, is also what leaves a bad taste in my mouth. In the universe of Ready Player One, the online world is dominated by a metaverse-like virtual reality game called the OASIS, a game owned and controlled by a single corporation, and ultimately a single man, its creator, James Halliday. When he dies, he decrees that his successor will be decided by a tripartite fetch quest within the virtual world. In a monumental act of narcissism, the quest’s challenges are not based on the skills required to run a massive company or a piece of essential infrastructure, or tests of morality or wisdom, but on intimate knowledge of his own life and his pop cultural obsessions. This might be an interesting setup if Halliday were the antagonist, but he’s not, he’s practically worshipped by the users of the OASIS.

Wow!! Cool Future!!

This movie has the trappings of cyberpunk, but little of the critique. Victory in this story is not the smashing of a corporate monopoly or frustrating of its goals, but the passing of the reins of power from a virtuous benevolent founder to a handful of virtuous disciples, through an absurd faux-meritocratic process where merit means liking the right books and games and movies.

The antagonist, Sorrento, is the CEO of a company that runs debtor’s prisons, yet somehow the movie gives the impression that his greater sin is being a poseur who’s only pretending to “get” the stuff that Halliday liked. The denouement even reveals that Halliday could have used his monopoly power to end the practice of indentured servitude at any time, but apparently chose not to, or just didn’t think to, and this is not framed as a critique of monopoly capitalism, but is just a throwaway line to demonstrate how benevolent the protagonist is in his new role.

Remember Tracer? Remember Chun-Li? Remember various skeletons?

The third act involves an epic battle in the OASIS, with the user-base coming together to fight Sorrento’s forces and prevent them from completing the quest. It’s presented as an empowering grass-roots uprising to save the OASIS from an evil corporation, and fine, they are pretty evil… But the status quo is also intolerable. They’re not fighting for their own empowerment, but merely to keep the unaccountable corporate power that dominates their lives out of even more abusive hands. It’s a pretty uninspiring cause.

The movie’s true feelings on the nature of power, and the relationship of ordinary people to it, is revealed most starkly at its climax. The conflict in the OASIS has spilled over into the real world, with our intrepid heroes being chased through the streets in a van, exposed at every turn by CCTV tracking technology and drones. While Sorrento sets out to confront them personally, they put out a call for aid, for their supporters to gather in the slums and defend them. When Sorrento reaches them, a huge crowd of people stream out of the stacks to oppose him.

It's dangerous to go alone! Take this!

For a moment, it seems as though the plot really is going to be resolved by collective direct action. People power, woo! Instead, Sorrento pulls out a gun, and the crowd parts. They could easily disarm him, there are hundreds of them all around him. Instead they gawp uselessly at him, right up to the point where he is threatening to shoot some children in the face. It is only the police arriving that prevents him from committing murder right in front of them.

With Sorrento out of the way thanks to the police, a bunch of lawyers arrive to certify the protagonist’s completion of the quest and acquisition of complete control over the OASIS. The crowd are reduced to placid spectators in a boardroom drama that doesn’t involve them or empower them. This is, I feel, the perfect encapsulation of the movie’s worldview. The crowd are the audience - expected to worship and trust benevolent CEOs, to react strongly in defence of a commodified culture where they are otherwise passive consumers, but to look on silently and not interfere as control over that culture changes hands, maintaining the status quo.

Avocados

Avocados

I’m not very good at doing these art posts in a timely manner; I did this still-life of a couple of avocados for pixel dailies back in September!

Timelapse

Avocados