Games & VR
Making games, from Pico-8 to vanilla web tech, and VR, including Cardboctober.
2026
- Cardboctober, ten years on
It has been 10 years since I ran Cardboctober, a month-long hackathon in which I created something new for Google Cardboard each day and wrote about it. I considered doing it again this year, but my time is spread thinner these days than it was in 2016, so I thought I’d do a bit of a retrospective on the WebVR and Google Cardboard scene instead.
- Building a Balatro-lite game with vanilla web tech
I’ve spent countless hours playing Balatro, and I’ve always wanted to create my own game with similar mechanics. When TrinketOS announced a game jam, it was the perfect excuse to make it happen.
2022
- The Inevitable Oculus Quest Post
I sold my HTC Vive back in 2020 and bought an Oculus Quest. Was it an impulsive lockdown treat? Perhaps; but it was time for an upgrade. I went for the 128GB model, and had to order from the German Amazon store as nowhere in the UK had stock at the time. I was impressed with Amazon Prime in this case, and then in awe of their logistics network. I ordered at around midday, and then received the Oculus Quest 24 hours later from Germany at no extra cost.
2019
- Approaches for a simple state system
There are lots of ways to achieve even the most simple goal in programming, and Pico-8 – even with it’s limited nature – is no different to any other system. Today I’m looking at state systems. There are lots of approaches to handling state out there, and each has their own pros and cons. In Pico-8 the main concern is keeping the token count low so that you can add more features to your game. Beginner state system This first state system uses a state variable and an enum table of available states to decide which state we’re currently in. The state variable is updated by user input (button press, or death) and it’s contents controls which functions are executed in the _update() and _draw() functions. If you’ve got a huge game the conditions in the _update() and _draw() functions can quickly become unweildly. The main benefit here is that you can control what to execute in the _update() and _draw() functions from one place. This approach is quite token hungry. function _init() t = 0 -- create timer state = 1 states = { menu = 1, game = 2, gameover = 3 } hp = 100 end function _update() t += 1 -- increment timer if state == states.menu then update_menu() elseif state == states.game then update_game() else update_gameover() end end function _draw() if state == states.menu then draw_menu() elseif state == states.game then draw_game() else draw_gameover() end end function update_menu() if btn(4) and btn(5) then state = states.game end end function draw_menu() print("Press z and x to start") end function update_game() if hp < 1 then state = states.gameover end end function draw_game() print("HP: "..hp) end function update_gameover() if btn(4) and btn(5) then state = states.game end end function draw_gameover() print("Press z and x to start over") end Improved state system This is the approach I prefer, the _update() and _draw() functions call a variable that points to another function. To change state you simply update this variable. There is no variable or enum needed to keep track of state, but you do need to explicitly redeclare the _upd and _drw variables each time you change state. This is approach is quite light on the token count. -- core init/update/draw functions function _init() t = 0 -- create timer hp = 100 _upd = upd_menu _drw = drw_menu end function _update() t += 1 -- increment timer _upd() end function _draw() cls() _drw() end -- menu update/draw functions function upd_menu() if btn(4) and btn(5) then _upd = upd_game _drw = drw_game end end function drw_menu() print("Press z and x to start") end -- game update/draw functions function upd_game() if hp < 1 then _upd = upd_gameover _drw = drw_gameover end end function drw_game() print("HP: "..hp) end -- gameover update/draw functions function upd_gameover() if btn(4) and btn(5) then _upd = upd_game _drw = drw_game end end function drw_gameover() print("Press z and x to start over") end
- Outlining text and sprites
It’s pretty simple to create an outlined text effect with a simple function, such as: function printo(text,x,y,text_color,border_color) local text_color = text_color or 7 local border_color = border_color or 0 -- render the "outline" in each direction for _x=-1,1 do for _y=-1,1 do print(text,x+_x,y+_y,border_color) end end -- print the foreground text print(text,x,y,text_color) end -- usage: printo("Hello world!",64,64,12,8) The process for outlining a sprite is very similar, but it uses the pal() function to change colors and works best of all if your sprite is a single color… -- where spr_table is like: -- {spr_index, base_color} function spro(spr_table,x,y,spr_color,border_color) local spr_color = spr_color or 7 local border_color = border_color or 0 -- render the "outline" in each direction for _x=-1,1 do for _y=-1,1 do -- replace the base_color with new border_color pal(spr_table[2],border_color) -- draw the sprite with the new color spr(spr_table[1],x+_x,y+_y) end end -- replace the base_color with the new spr_color pal(spr_table[2],spr_color) -- render the recolored sprite spr(spr_table[1],x,y) -- reset the rendering palette pal() end -- usage: spro({1,7},64,64,12,8) And obviously you can set this spro function up to handle more of the properties available to spr, such as number of tiles in x/y, and whether to flip or not. As a final solution if you’re going to be doing this a lot you could combine the previous two functions to one mega outline function: function outline(what,x,y,fc,bc) if typeof(what) == "string" then printo(what,x,y,fc,bc) else spro(what,x,y,fc,bc) end end And here’s how the final effect looks when applied to a 1-color sprite and some text, with random colors:
- A menu and state system
I’ve been playing with state systems lately, and decided to build a menu system to play with going between each state. First of all, my helper functions… I’m using this to print outlined text: -- print text with an outline function printo(str,x,y,c1,c2) for xoff=-1,1 do for yoff=-1,1 do print(str,x+xoff,y+yoff,c1) end end print(str,x,y,c2) end The menu function has two modes, one for running in _update() and one for running in _draw(), you control this by passing "u" or "d" as the second argument. The first agument takes a table containing a menu definition. I’ll talk more about this with an example after the wall of code: function menu(m, mode) local ic=#m.items if mode == "u" then if btnp(2) then -- up m.selection -= 1 if m.selection < 1 then m.selection = 1 end end if btnp(3) then -- down m.selection += 1 if m.selection > ic then m.selection = ic end end if btnp(5) then -- x if m.items[m.selection][2] then m.items[m.selection][2]() end end end if mode == "d" then local padding={4,4,0,6}-- t/r/b/l local lineheight=8 -- find longest string local widest_str=0 for i,item in pairs(m.items) do if #item[1] > widest_str then widest_str = #item[1] end end local width = (widest_str*4)+padding[2]+padding[4] local height = (ic*lineheight)+padding[1]+padding[3] local xoff = 64 - (width/2) local yoff = 64 - (height/2) local x0 = xoff local y0 = yoff local x1 = xoff + width local y1 = yoff + height -- box rect(x0-2,y0+1,x1+2,y1+2, 5) rect(x0-1,y0-1,x1+1,y1+1,0) rectfill(x0,y0,x1,y1,1) rect(x0,y0,x1,y1,7) -- aditional text if m.guide then local _w = #m.guide*4 local _x = 64 - (_w/2) local _y = y1+padding[1]+2 printo(m.guide,_x,_y, 0, 7) end -- items for i,item in pairs(m.items) do if i == m.selection then -- selection highlight printo( item[1], xoff+padding[4], yoff+padding[1]+((i-1)*lineheight), 0,7 ) if m.items[m.selection][2] then palt(0,false) palt(1,true) local xadj=(5 - time()%3) spr(1,x0-xadj,y0+padding[1]+((i-1)*lineheight) - 1) pal() end else -- normal item printo( item[1], xoff+padding[4], yoff+padding[1]+((i-1)*lineheight), 0,6 ) end end end end There’s a lot going on there, but it’s basically maintain a counter for the user’s current selection, and then if there is a function associated with a menu item call it when the x button is pressed. Here’s how the menu definitions look _menu_start = { selection = 1, items = { {"new game", game}, {"new game", game}, {"new game", game}, {"new game", game}, {"credits", credits} }, guide="❎ to select" } _menu_game = { selection = 1, items = { {"game over", die}, {"congratulations", win}, {"---"}, {"back", start} } } _menu_credits = { selection = 1, items = { {"this sub menu contains lots of"}, {"strings without actions bound "}, {"to them, so you can just show "}, {"multiple lines of text."}, {"30 characters is a rough limit"}, {"---"}, {"back", start} }, guide="enjoy ♥" } You can go between menus using a state system. Here’s the barebones of a state system. We assign a new function to _upd() and _drw() each time the state changes. function _init() start() -- defined later end --update function _update() _upd() end function update_start() menu(_menu_start, "u") end function update_game() menu(_menu_game, "u") end function update_credits() menu(_menu_credits, "u") end --draw function _draw() cls(13) _drw() end function draw_start() menu(_menu_start, "d") end function draw_game() menu(_menu_game, "d") end function draw_credits() menu(_menu_credits, "d") end -- functions that are called directly -- through menu selections function start() _upd=update_start _drw=draw_start end function game() _upd=update_game _drw=draw_game end function credits() _upd=update_credits _drw=draw_credits end And here’s how it looks in action. I’ve got a custom sprite for the selection hand drawn in sprite slot 1:
2018
- A custom font
Out of the box Pico-8 has one font. It’s the utilitarian answer to most problems. 3px by 5px and fairly legible. This font isn’t always good enough. How about making your own font? I’ve seen lots of approaches for creating fonts in Pico-8 – mostly using sprites for each glyph of the font, but then you’re limited to 8x8px and a have to mess with pal modes if you want to adjust colors on-the-fly. My approach uses the raw pixel data encoded in strings that are rendered using calls to pset, falling back to the built-in font for any missing glyphs. First, here are my helper functions… -- merge tables function mt(t1,t2) local new = {} for k,v in pairs(t1) do new[k] = v end for k,v in pairs(t2) do new[k] = v end return new end -- less verbose way to do ternary operations function ternary(cond ,t ,f ) if cond then return t else return f end end I had to come up with a compact way to represent the glyphs. Initially I used something like: { glyph = "o", pixels = { 0,1,1,1,0, 1,0,0,0,1, 1,0,0,0,1, 1,0,0,0,1, 1,0,0,0,1, 1,0,0,0,1, 0,1,1,1,0, }} But that contains a lot of tokens and lots of repeated/dead values to represent the empty pixels in a glyph. Eventually I moved to something like: {"o", "0.1.3.1.1.3.2.3.2.3.2.3.2.3.1.1.3"} That’s the same pixel data, but encoded in to a much shorter string. The first number indicates our starting parity – this can be 0 or 1 and tells us if the top/left pixel is empty or filled. Each time we have a . the parity flips, so with the example of our “o” glyph it starts with 0 for 1 count, then 1 for 3 counts, 0 for 1 count, 1 for 1 count, etc. When the remaining pixels in a glyph are 0 we can just omit them from the glyph data as they’re not drawn. The real wins are in the more simple/empty glyphs, such as “.” for example. Before: { glyph = ".", pixels = { 0,0,0,0,0, 0,0,0,0,0, 0,0,0,0,0, 0,0,0,0,0, 0,0,0,0,0, 0,0,0,0,0, 0,0,1,0,0, }} After: {".", "0.32.1"} Making this change took me from an initial token count of roughly 2800 to a much more manageable count of 560-ish. So with that technical talk out of the way, here’s the complete glyph set in code, and the function to render text. I’ll give some usage examples after. global_y = 0 function write(str,opts) opts = opts or {} _opts = { x=0, y=global_y, c=7 } opts = mt(_opts,opts) font = { w=5, h=7, -- first num indicates starting value, 0 or 1 -- each number indicates number of 0 or 1 in a row -- flips at each . glyphs = { {"0", "0.1.3.1.1.3.2.3.2.3.2.3.2.3.1.1.3"}, {"1", "0.2.1.3.2.2.1.1.1.4.1.4.1.4.1.2.5"}, {"2", "0.1.3.1.1.3.1.4.1.3.1.3.1.3.1.3.5"}, {"3", "0.1.3.1.1.3.1.4.1.2.2.5.2.3.1.1.3"}, {"4", "0.3.2.2.1.1.1.1.1.2.2.3.6.4.1.4.1"}, {"5", "1.6.4.1.5.3.5.2.3.1.1.3"}, {"6", "0.1.3.1.1.3.2.4.4.1.1.3.2.3.1.1.3"}, {"7", "1.5.4.1.4.1.3.1.3.1.3.1.3.1"}, {"8", "0.1.3.1.1.3.2.3.1.1.3.1.1.3.2.3.1.1.3"}, {"9", "0.1.3.1.1.3.2.3.1.1.4.4.2.3.1.1.3"}, {"a", "0.1.3.1.1.3.2.3.7.3.2.3.2.3.1"}, {"b", "1.4.1.1.3.2.3.5.1.1.3.2.3.5"}, {"c", "0.1.3.1.1.3.2.4.1.4.1.4.1.3.1.1.3.1"}, {"d", "1.4.1.1.3.2.3.2.3.2.3.2.3.5"}, {"e", "1.6.4.1.4.4.1.1.4.1.4.5"}, {"f", "1.6.4.1.4.4.1.1.4.1.4.1"}, {"g", "0.1.3.1.1.3.2.3.2.4.1.1.4.3.1.1.3"}, {"h", "1.1.3.2.3.2.3.7.3.2.3.2.3.1"}, {"i", "1.5.2.1.4.1.4.1.4.1.4.1.2.5"}, {"j", "1.5.2.1.4.1.4.1.2.1.1.1.2.1.1.1.3.2"}, {"k", "1.1.3.2.2.1.1.1.1.1.2.2.3.1.1.1.2.1.2.1.1.1.3.1"}, {"l", "1.1.4.1.4.1.4.1.4.1.4.1.4.5"}, {"m", "1.1.3.3.1.3.1.1.1.2.3.2.3.2.3.2.3.1"}, {"n", "1.1.3.3.2.2.1.1.1.2.2.3.3.2.3.2.3.1"}, {"o", "0.1.3.1.1.3.2.3.2.3.2.3.2.3.1.1.3"}, {"p", "1.4.1.1.3.2.3.5.1.1.4.1.4.1"}, {"q", "0.1.3.1.1.3.2.3.2.3.2.3.1.1.3.5.1"}, {"r", "1.4.1.1.3.2.3.5.1.1.1.1.2.1.2.1.1.1.3.1"}, {"s", "0.1.3.1.1.3.2.5.3.5.2.3.1.1.3"}, {"t", "1.5.2.1.4.1.4.1.4.1.4.1.4.1"}, {"u", "1.1.3.2.3.2.3.2.3.2.3.2.3.1.1.3"}, {"v", "1.1.3.2.3.1.1.1.1.1.2.1.1.1.2.1.1.1.3.1.4.1"}, {"w", "1.1.3.2.3.2.3.2.1.1.1.2.1.1.1.2.1.1.1.1.1.1.1.1"}, {"x", "1.1.3.2.3.1.1.1.1.1.3.1.3.1.1.1.1.1.3.2.3.1"}, {"y", "1.1.3.1.1.1.1.1.3.1.4.1.4.1.4.1.4.1"}, {"z", "1.5.4.1.3.1.3.1.3.1.3.1.4.5"}, {" ", "0.35"}, {".", "0.32.1"}, {",", "0.27.1.3.1"}, {"!", "0.2.1.4.1.4.1.4.1.4.1.9.1"}, {"?", "0.1.3.1.1.3.1.4.1.3.1.3.1.9.1"}, {"'", "0.3.1.3.1"}, {"\"", "0.1.1.1.1.1.1.1.1"}, {":", "0.12.1.14.1"}, {";", "0.12.1.14.1.3.1"}, {"(", "0.3.1.3.1.3.1.4.1.4.1.5.1.5.1"}, {")", "0.1.1.5.1.5.1.4.1.4.1.3.1.3.1"}, {"[", "0.1.3.2.1.4.1.4.1.4.1.4.1.4.3"}, {"]", "0.1.3.4.1.4.1.4.1.4.1.4.1.2.3"}, {"{", "0.2.2.3.1.4.1.3.2.4.1.4.1.4.2"}, {"}", "0.1.2.4.1.4.1.4.2.3.1.4.1.3.2"}, {"<", "0.3.1.3.1.3.1.3.1.5.1.5.1.5.1"}, {">", "0.1.1.5.1.5.1.5.1.3.1.3.1.3.1"}, {"-", "0.16.3"}, {"+", "0.12.1.3.3.3.1"}, {"/", "0.4.1.3.1.4.1.3.1.3.1.4.1.3.1"}, {"\\", "1.1.5.1.4.1.5.1.5.1.4.1.5.1"}, } } _x = opts.x _y = opts.y letters = {} for i=1,#str do letter = sub(str,i,i) add(letters,letter) end for k,v in pairs(letters) do letter = nil for _glyph in all(font.glyphs) do if _glyph[1] == v then letter = _glyph[2] end end local _sx = _x local _sy = _y if letter != nil then local _px = _sx local _py = _sy pixels = {} local _p = sub(letter,1,1) local prev = nil for i=2,#letter do cur = sub(letter,i,i) if type(prev) == "number" and cur != "." then cur = sub(letter, i-1, i) end if cur == "." then _p = ternary(_p == "0", "1", "0") else for j=1,tonum(cur) do add(pixels, _p) end end prev = tonum(cur) end for i,p in pairs(pixels) do i = i - 1 if i % font.w == 0 then _py += 1 _sx = _x - (font.w * (i / font.w)) end _px = _sx + i if p == "0" then pset(_px, _py, opts.c) end end else font.w = 8 print(v, _sx, _y + ((font.h - 2)/2), opts.c) end _x += font.w + 1 end global_y += font.h + 2 end If you’re still with me, congratulations! Here is how you use the function: function _draw() cls() global_y = 0 -- reset on each draw call write("hello world") -- passing a color is possible! write("this is max 🐱", {c=12}) write("my font brings all") write("the devs to the yard") write("0123456789") write("abcdefghijklm") write("nopqrstuvwxyz") write(".,?!:;()[]{}-+/\\<>") write("yes, this is a lot") write("nicer to read! ♥") write("") write("█▒🐱⬇️░✽●♥☉웃⌂⬅️😐") write("♪🅾️◆…➡️★⧗⬆️ˇ∧❎▤▥") write("@#$%^&*") end And this is how it looks:
- A Dialog System
This is a flexible dialog system I’ve come up with. It has the following features: automatically wrap long lines of text automatically page super long text to additional dialogs delay control per-letter display First, here are some of the helper functions that I’m using… -- draw a box with a frame -- arguments: -- x,y position -- w,h dimensions -- p padding -- fg,bg foreground and background colors function box(x,y,w,h,p,fg,bg) rectfill(x,y,x+w,y+h,bg) rect(x+p,y+p,x+w-p-1,y+h-p-1,fg) end -- joins two tables function jt(table1,table2) local new = {} for k,v in pairs(table1) do new[k] = v end for k,v in pairs(table2) do new[k] = v end return new end -- sleep for a set number of seconds (using 30fps mode) function sleep(s) for i=1,s*30 do flip() end end And lastly, the meat of the dialog system. I’ve scattered some comments through-out the code to try to explain what I’m doing. I’ll provide some initialisation examples after… function dialog(d) d = d or {} -- defaults _d = { a = true, bg = 5, c = false, dl = 5, fg = 7, p = 8, t = true, ts = 0.05, txt = "", w = 128, y = 96 } -- slightly dynamic defaults _d.x = (128 / 2) - (_d.w / 2) _d.h = 128 - _d.y -- merge passed options with default _d = jt(_d, d) -- text _ch = 6 -- char height _wi = _d.w - (_d.p * 2) _wrap = (_wi / 4) - 1 -- 27 local lines = {} local limit = ceil(#_d.txt / _wrap) local _tb = _d.txt while limit > 0 do local substr = sub(_tb, 1, _wrap) -- grab first bit -- ensure line doesn't have a space at the start if sub(substr,1,1) == " " then substr = sub(substr, 2) end add(lines, substr) -- stick it in our lines table _tb = sub(_tb, _wrap + 1, -1) -- get rest of text limit -= 1 end -- lines limit _ll = flr((_d.h - _d.p - _ch) / _ch) if _d.t == true then -- type it out box(_d.x, _d.y, _d.w, _d.h, _d.p / 2, _d.fg, _d.bg) for l=1,_ll do local _l = lines[l] or "" -- vertical alignment local va = _d.y + _d.p + ((l - 1) * 6) local _lld = _ll while _lld > 1 do if #lines < _lld then va += (_ch / 2) end _lld -= 1 end -- center it if necessary local ha = _d.x + _d.p if _d.c == true then ha = _d.x + (_d.w / 2) - (#_l * 2) end local chars = {} local limit = #_l local ci = 0 -- split line in to chars while limit > 0 do local _ch = sub(_l, 1, ci + 1) add(chars, _ch) ci += 1 limit -= 1 end for c=1,#chars do print(chars[c], ha, va, _d.fg) if _d.a == true then sfx(0) end sleep(_d.ts) -- tiny delay end end else -- print normally box(_d.x, _d.y, _d.w, _d.h, _d.p / 2, _d.fg, _d.bg) for l=1,_ll do local _l = lines[l] or "" -- vertical alignment local va = _d.y + _d.p + ((l - 1) * 6) local _lld = _ll while _lld > 1 do if #lines < _lld then va += (_ch / 2) end _lld -= 1 end -- center it if necessary local ha = _d.x + _d.p if _d.c == true then ha = _d.x + (_d.w / 2) - (#_l * 2) end print(_l, ha, va, _d.fg) if _d.a == true then sfx(0) end end end if #lines > _ll then local _nl = lines -- delete strings we've used for i=1,_ll do del(_nl,lines[1]) end -- join thr remaining local str = "" if #_nl > 1 then for i=1,#_nl do str = str .. _nl[i] end else str = _nl[1] end sleep(_d.dl) -- delay before next -- call recursive local n = jt(_d, {txt=str}) dialog(n) else sleep(_d.dl) -- delay before next end end Whew that was a lot of code. Here’s how you can use it: -- short, centered, delay of 2 dialog({ txt = "hello,", c = true, dl = 2 }) -- short, centered dialog({ txt = "my name is inigo montoya", c = true }) -- short, centered dialog({ txt = "you killed my father", c = true }) -- short, centered, not typed dialog({ txt = "prepare to die!", c = true, t= false }) -- long, typed, multiple message boxes dialog({txt="this is a lot of text that will wrap on to multiple lines and hopefully use a recursive message box in order to show all of the text ♥"}) You can see how this all looks below:
2017
- My workflow for discovering new VR content
Since launching the UX of VR website last year I’ve been looking for ways to keep on top of new VR content. For most of the last year I relied solely on a mixture of Google Alerts, manually using the search functionality on websites and browsing VR blogs daily. Here’s how I improved on that process.
- The inevitable HTC Vive post
I bought an HTC Vive last November (a week before the Black Friday sales, doh!) and have spent the time since trying to get the most out of it. So here are some highlights, thoughts and other musings about the HTC Vive.
- Well that has been a crazy couple of months... Onward!
In October 2016 I spent every day of the month creating something for Google Cardboard using JavaScript — Cardboctober. Since then I’ve been preoccupied with a couple of other mostly VR-related things…
2016
- 31: Something in the Shadows
Today is the last day of Cardboctober! And less surprisingly, it’s also Halloween 🎃 - For today’s hack I’ve made something spooky. If you don’t like spiders, you should look away now.
- 30: Github Contributions
For today’s Cardboctober, having shed the burden of creating Tetris in VR, I decided to hack around with data visualisation.
- 29: Blocks out of the pram
Bit of a lame one for today. I feel like I’ve not been making much progress with the Tetris rewrite, and tackling collision detection with unexpected results is getting quite annoying, so I think I might just leave teVRis where it is for now.
- 28: Planning Revisited
I thought it’d be weird to drop a hardware post in this week as I’ve not finished the Tevris game yet, so this week’s post will continue the Tetris theme and then there will be a Cardboctober Week 4 round-up at the end.
- 27: Moving with gaze
Today’s hack is more an aside while I’m focussing on yet another Tevris rewrite.
- 26: Rotating Pieces
For today’s Cardboctober we’re still working with Tetris. The task today is to make the pieces rotate.
- 25: Creating and Moving Pieces
Today we’re going to be looking at creating Tetromino pieces and moving them around the board.
- 24: Basic Game Board
For today’s hack we’re going to start putting the teVRis game board together. We’ve already looked at positioning objects in a circle around the camera, but that doesn’t solve the whole problem.
- 23: Planning Tetrominoes in VR
I outlined the plan for the week yesterday: To make a tetromino game. So here’s how we’re going to do that.
- 22: Putting it all together
We’re starting the final (full) week of Cardboctober now, so I thought I’d do a week-long project to pull together some of the ideas from Cardboctober.
- 21: One size doesn't fit all
An important factor in whether VR works for you or not is how well the hardware fits – both in terms of fitting your head shape and being aligned with your eyes. For today’s post I’ll be looking at some of the things that affect the fit of Google Cardboard headsets.
- 20: Which way is North? Part 2
In yesterday’s post I talked about the problem of using Cardinal North with device orientation, and how Three.js’ Device Orientation Camera provides a utility function for setting the alphaOffsetAngle, today we’re going to cover building a device to use this function.
- 19: Which way is North? Part 1
When developing for Google Cardboard VR with JavaScript we’re depending on the browser’s support of device orientation to know which way the user is looking. You can’t guarantee the user is always looking in the direction you’ve oriented your scene.
- 18: Moving around in VR
Unless your VR experience is a roller coaster or some other sort of rail-based experience, you shouldn’t take movement control away from the user. Lack of movement control can cause VR sickness. Here are a couple of approaches for user-controlled movement in VR:
- 17: Displaying Pertinent Information
Some Cardboard experiences might require pertinent information to always be visible to the user. As we’re working with a stereoscopic and not particularly high resolution screen this raises a couple of challenges.
- 16: Getting in and out of Fullscreen
Today we’re looking at the task of getting in and out of Fullscreen. Fullscreen helps maintain an immersive experience. When using JavaScript as we’re confined to the capabilities of the web browser.
- 15: The Hierarchy of Needs in Quick Google Cardboard Hacks
This week I’m going to be writing about some of the UX challenges encountered while creating Cardboctober hacks. In VR experiences it’s important to consider the needs of the person using what you’ve created. We can look at the Hierarchy of Needs in Virtual Reality that Beau Cronin adapted from Maslow’s Hierarchy of Needs
- 14: Debugging your Cardboard with Chrome
Today I’m going to be talking about how I test and debug while working on this Cardboctober hacks. If you make it to the end I’ll have a recap of Week 2 of Cardboctober.
- 13: AAAAH! More Zombies
For today’s hack I’m revisiting yesterday’s zombie game again. With a full re-write to un-bodge the implementation.
- 12: AAAAH! Zombies
Today’s hack was going to be built around the gamepad API – but I utterly failed to get it working nicely with any of my USB or Bluetooth controllers. Not to mention that support seems patchy at best across all platforms/browsers. So I decided to make a little game instead.
- 11: Webaudio Beat Sequencer
For today’s hack I’ve decided to make a VR beat sequencer. Nothing says inclusive-fun like a VR experience that makes noise! So let’s get on with that.
- 10: HTML5 Video
Today we’re looking at HTML5 video – well not in the traditional sense. We’re looking at using the <video> element to create a texture and then applying it to an object in our 3D scene.
- 09: Speech recognition
Today’s hack is using the speechRecognition API. How can this be useful in Google Cardboard VR? It can add another level of input to our limited arsenal. So let’s take a closer look at that.
- 08: Playing sounds
Kicking off Cardboctober week 2 (in which I’ll be talking about using various Web APIs) today we’re looking at audio. Or more specifically how to get audio in to your VR things.
- 07: Cardboard hardware (cardware?)
Today’s hack (and each Friday for the rest of October) is going to be hardware rather than software themed. If you get all the way to the end of the post I’ll even throw in a recap of the week so far. This is totally not a cop-out.
- 06: VR Pairs Game
Todays hack is intended more as an activity to pull together various bits from the first week of Cardboctober and make a game. So here we go, it’s a “pairs” game.
- 05: Loading external models
Using a couple of Three.js plugins you can load external models in to your scene. The plugins are: OBJLoader.js MTLLoader.js OBJLoader lets you load .obj format models, and MTLLoader lets you load .mtl material definitions to apply textures to the models.
- 04: Skyboxes and generating meshes
Skyboxes There are a couple of ways you can create a skybox in Three.js Using a skydome (a textured sphere) Using a skybox (a textured cube) I’m using the textured cube approach. For this approach, our skybox texture has to be broken up into six textures.
- 03: Even better gazed based look interaction
Improving on yesterday’s Raycaster based look interaction, I’ve improved the raycasting experience. I’m using a library called vreticle.js, though I’ve tweaked it a bit.
- 02: Raycaster based look interaction
For today’s Cardboctober hack I’m doing some basic look interaction. As you look at each ghost in the circle surrounding you the ghost will cover their eyes.
- 01: Basic VR
Kicking off Cardboctober I’ve decided to go with something very simple. This is actually pretty much one of the JSCard demos that I created last November for my 20 Minutes into the Future with Google Cardboard and JavaScript talk.
- Announcing Cardboctober
Throughout October Ben and I are working on a project called Cardboctober. The challenge is hacking on something new every day using Google Cardboard.
- Summer of Hacks: Game Dev Day
As part of the JS Oxford-organised Summer of Hacks, I co-organised the “Game Dev Day” hack day in Oxford. This is the second time I’ve held an event during the Summer of Hacks (the previous event being SusHack #3 last year!).
2015
- 20 Minutes into the Future with Google Cardboard and JavaScript
I gave this talk at JS Oxford in November 2015. The talk is about using JavaScript and various web APIs to create Google Cardboard experiences.
- UX in Virtual Reality
I’ve been trying to find resources on the subject of User Experience and User Interface design in Virtual Reality a lot lately, as I’m thinking about Google Cardboard, and preparing demos for a talk about using JavaScript to build Google Cardboard experiences.
2010
- Minecraft player skins
So, since I bought Minecraft (during the Halloween update period!), I’ve been spending a lot of my free time playing it. I’ve also been creating skins for Minecraft. I’ve made a couple of skins so far: Unable to recover the image :( If you’d like to use any of these skins, you can change your skin at http://www.minecraft.net/skin/