The materials and render script are included in the project zip file, but I guess I might as well paste what I can here too. The materials are just duplicates of the builtin sprite material and use the builtin vertex and fragment shaders. The case assets are positioned and aligned in an initial 90x160 collection and the relevant data from that is cached on startup. The render script looks like this:
g_virtual_width = 0
g_virtual_height = 0
g_actual_width = 0
g_actual_height = 0
g_viewport_size = 0
g_viewport_left = 0
g_viewport_bottom = 0
local function update_window_size(self)
self.ever_updated_window_size = true
g_virtual_width = render.get_width()
g_virtual_height = render.get_height()
local prev_width = g_actual_width
local prev_height = g_actual_height
g_actual_width = render.get_window_width()
g_actual_height = render.get_window_height()
if prev_width ~= g_actual_width or prev_height ~= g_actual_height then
msg.post("default:/case#case", "on_window_resized")
end
end
function init(self)
self.pre_game_predicate = render.predicate({"pre"})
self.game_predicate = render.predicate({"tile"})
self.post_game_predicate = render.predicate({"post"})
self.view = vmath.matrix4()
self.ever_updated_window_size = false
self.load_delay = 0.05
self.startup_time = socket.gettime()
end
function update(self)
-- setup render state
render.set_stencil_mask(0xFF)
render.set_depth_mask(false)
render.disable_state(render.STATE_DEPTH_TEST)
render.disable_state(render.STATE_STENCIL_TEST)
render.disable_state(render.STATE_CULL_FACE)
render.enable_state(render.STATE_BLEND)
render.set_blend_func(render.BLEND_SRC_ALPHA, render.BLEND_ONE_MINUS_SRC_ALPHA)
render.clear({
[render.BUFFER_COLOR_BIT] = col(g_palettes[g_save_data.palette].background),
[render.BUFFER_DEPTH_BIT] = 1,
[render.BUFFER_STENCIL_BIT] = 0
})
-- don't render anything until the case and viewport have been set up
if not self.ever_updated_window_size then
return
elseif self.load_delay > 0 then
if socket.gettime() - self.startup_time >= self.load_delay then
self.load_delay = 0
else
return
end
end
-- draw pre-game case elements
local case_projection_matrix = vmath.matrix4_orthographic(
0, g_actual_width, -- left, right
0, g_actual_height, -- bottom, top
-1, 1) --near, far
render.set_projection(case_projection_matrix)
render.set_viewport(0, 0, g_actual_width, g_actual_height)
render.set_view(vmath.matrix4())
render.draw(self.pre_game_predicate)
-- draw game
render.set_projection(vmath.matrix4_orthographic(0, GAME_SIZE, 0, GAME_SIZE, -1, 1))
render.set_viewport(g_viewport_left, g_viewport_bottom, g_viewport_size, g_viewport_size)
render.set_view(self.view)
render.draw(self.game_predicate)
--draw post-game case elements
render.set_projection(case_projection_matrix)
render.set_viewport(0, 0, g_actual_width, g_actual_height)
render.set_view(vmath.matrix4())
render.draw(self.post_game_predicate)
end
function on_message(self, message_id, message)
if message_id == hash("set_view_projection") then
self.view = message.view
elseif message_id == hash("window_resized") or message_id == hash("update_window_size") then
update_window_size(self)
end
end
And the case resizing script looks like this:
-- NOTE: this script represents just the relevant parts from several scripts in a much
-- larger project with many more assets and case parts. Excuse the mess.
g_save_data = {}
g_save_data.palette = 1
local colors = {
[0] = vmath.vector4( 0/255, 0/255, 0/255, 1),
[1] = vmath.vector4( 29/255, 43/255, 83/255, 1),
[2] = vmath.vector4(126/255, 37/255, 83/255, 1),
[3] = vmath.vector4( 0/255, 135/255, 81/255, 1),
[4] = vmath.vector4(171/255, 82/255, 54/255, 1),
[5] = vmath.vector4( 95/255, 87/255, 79/255, 1),
[6] = vmath.vector4(194/255, 195/255, 199/255, 1),
[7] = vmath.vector4(255/255, 241/255, 232/255, 1),
[8] = vmath.vector4(255/255, 0/255, 77/255, 1),
[9] = vmath.vector4(255/255, 163/255, 0/255, 1),
[10] = vmath.vector4(255/255, 240/255, 36/255, 1),
[11] = vmath.vector4( 0/255, 231/255, 86/255, 1),
[12] = vmath.vector4( 41/255, 173/255, 255/255, 1),
[13] = vmath.vector4(131/255, 118/255, 156/255, 1),
[14] = vmath.vector4(255/255, 119/255, 168/255, 1),
[15] = vmath.vector4(255/255, 204/255, 170/255, 1)
}
function col(n)
return colors[n % 16]
end
g_palettes = {}
g_palettes[1] = {
case = 9,
shadow = 4,
highlight = 10,
outline = 0,
background = 15,
screen_surround = 0,
button_enabled_top = 7,
button_enabled_bottom = 2,
button_disabled_top = 6,
button_disabled_bottom = 1,
button_down_top = 5,
button_down_bottom = 1,
button_icon_disabled = 5,
button_icon_confirm = 11,
button_icon_undo = 12,
button_icon_back = 8,
button_icon_arrows = 9
}
-- case constants
GAME_SIZE = 128
local MIN_CASE_BORDER = 24
local MAX_SCREEN_HEIGHT_GAME_CAN_OCCUPY = 0.55
local VIEWPORT_CENTRE = 2 / 3 -- how far up the case to centre the viewport
local CASE_WIDTH = 90
local CASE_HEIGHT = 160
local MAX_ASPECT_RATIO = 16 / 9
local BOUNDARY_ASPECT_RATIO = 4 / 3
local SURROUND_SIZE = 2
local case_bounds = {}
local case_parts = { -- from back to front
{ name = "case", palette = "case" },
{ name = "highlight", palette = "highlight" },
{ name = "screen_surround", palette = "screen_surround" },
{ name = "s_fill", palette = "shadow" },
{ name = "w_outline", palette = "outline" },
{ name = "n_outline", palette = "outline" },
{ name = "e_outline", palette = "outline" },
{ name = "s_outline", palette = "outline" },
{ name = "s_edge_outline", palette = "outline" },
{ name = "btn_down_bottom", palette = "button_enabled_bottom" },
{ name = "btn_down_top", palette = "button_enabled_top" },
{ name = "btn_down_icon", palette = "button_icon_arrows" },
{ name = "btn_left_bottom", palette = "button_enabled_bottom" },
{ name = "btn_left_top", palette = "button_enabled_top" },
{ name = "btn_left_icon", palette = "button_icon_arrows" },
{ name = "btn_up_bottom", palette = "button_enabled_bottom" },
{ name = "btn_up_top", palette = "button_enabled_top" },
{ name = "btn_up_icon", palette = "button_icon_arrows" },
{ name = "btn_right_bottom", palette = "button_enabled_bottom" },
{ name = "btn_right_top", palette = "button_enabled_top" },
{ name = "btn_right_icon", palette = "button_icon_arrows" },
{ name = "btn_back_bottom", palette = "button_enabled_bottom" },
{ name = "btn_back_top", palette = "button_enabled_top" },
{ name = "btn_back_icon", palette = "button_icon_back" },
{ name = "btn_confirm_bottom", palette = "button_enabled_bottom" },
{ name = "btn_confirm_top", palette = "button_enabled_top" },
{ name = "btn_confirm_icon", palette = "button_icon_confirm" },
{ name = "btn_undo_bottom", palette = "button_enabled_bottom" },
{ name = "btn_undo_top", palette = "button_enabled_top" },
{ name = "btn_undo_icon", palette = "button_icon_undo" },
-- etc
}
local function calculate_viewport()
-- work out the largest multiple of GAME_SIZE we can fit in the window while obeying MIN_CASE_BORDER size
local best_size = GAME_SIZE * math.max(1, (math.floor(case_bounds.width / GAME_SIZE)))
if case_bounds.width - best_size < 2 * MIN_CASE_BORDER then
best_size = math.max(GAME_SIZE, best_size - GAME_SIZE)
end
-- don't let the size of the game occupy too much vertical space as we need room for the buttons
while best_size / case_bounds.height > MAX_SCREEN_HEIGHT_GAME_CAN_OCCUPY and best_size > GAME_SIZE do
best_size = math.max(GAME_SIZE, best_size - GAME_SIZE)
end
-- calculate viewport
g_viewport_size = best_size
local border = (case_bounds.width - g_viewport_size) / 2
g_viewport_left = case_bounds.x + border
g_viewport_bottom = case_bounds.y + (VIEWPORT_CENTRE * case_bounds.height) - (g_viewport_size / 2)
g_viewport_bottom = math.floor(0.5 + g_viewport_bottom)
print("viewport: size "..g_viewport_size..", left "..g_viewport_left..", bottom "..g_viewport_bottom)
end
local function cache_case()
for i = 1, #case_parts do
local id = go.get_id(case_parts[i].name)
case_parts[i].id = id
case_parts[i].original_position = vmath.vector3(go.get_position(id))
case_parts[i].original_scale = vmath.vector3(go.get_scale(id))
end
end
local function resize_part(part, z)
local pos = vmath.vector3(part.original_position)
local scale = vmath.vector3(part.original_scale)
local size = go.get(part.name.."#sprite", "size")
local width = size.x * scale.x
local height = size.y * scale.y
-- normalize position then map to new bounds
pos.x = case_bounds.x + (pos.x / CASE_WIDTH) * case_bounds.width
pos.y = case_bounds.y + (pos.y / CASE_HEIGHT) * case_bounds.height
pos.z = z
-- calculate scale to fit desired dimensions
scale.x = (width / size.x) * case_bounds.pixel_scale
scale.y = (height / size.y) * case_bounds.pixel_scale
if width == CASE_WIDTH then
scale.x = case_bounds.width / size.x
end
if height == CASE_HEIGHT then
scale.y = case_bounds.height / size.y
end
go.set_position(pos, part.id)
go.set_scale(scale, part.id)
print(part.name.." pos ("..pos.x..", "..pos.y..", "..pos.z.."), scale ("..scale.x..", "
..scale.y..", "..scale.z.."), size ("..size.x..", "..size.y..")")
end
local function resize_case()
-- letterboxing
case_bounds.width = g_actual_width
case_bounds.height = g_actual_height
local aspect = case_bounds.width / case_bounds.height
if aspect <= BOUNDARY_ASPECT_RATIO then
-- portrait
aspect = math.min(aspect, 1 / MAX_ASPECT_RATIO)
case_bounds.width = case_bounds.height * aspect
aspect = math.max(aspect, 1 / MAX_ASPECT_RATIO)
case_bounds.height = case_bounds.width / aspect
else
-- landscape (TODO)
aspect = math.max(aspect, MAX_ASPECT_RATIO)
case_bounds.height = case_bounds.width / aspect
aspect = math.min(aspect, MAX_ASPECT_RATIO)
case_bounds.width = case_bounds.height * aspect
end
case_bounds.x = (g_actual_width - case_bounds.width) / 2
case_bounds.y = (g_actual_height - case_bounds.height) / 2
print("\n*\n**\n***\n**\n*\nresizing: window "..g_actual_width.."x"..g_actual_height.." ("
..(g_actual_width / g_actual_height).."), bounds "..case_bounds.width.."x"..case_bounds.height
.." ("..aspect..") @ ("..case_bounds.x..", "..case_bounds.y..")")
local x_scale = math.floor(case_bounds.width / CASE_WIDTH)
local y_scale = math.floor(case_bounds.height / CASE_HEIGHT)
case_bounds.pixel_scale = math.max(1, math.min(x_scale, y_scale))
print("pixel_scale "..case_bounds.pixel_scale)
for i = 1, #case_parts do
resize_part(case_parts[i], (i - 1) / #case_parts)
end
end
local function resize_case_after_viewport()
local surround = nil
for i = 1, #case_parts do
if case_parts[i].name == "screen_surround" then
surround = case_parts[i]
break
end
end
if surround ~= nil then
local pos = go.get_position(surround.id)
pos.x = case_bounds.x + case_bounds.width / 2
pos.y = case_bounds.y + case_bounds.height * VIEWPORT_CENTRE
go.set_position(pos, surround.id)
local size = go.get(surround.name.."#sprite", "size")
local scale = go.get_scale(surround.id)
scale.x = (g_viewport_size + 2 * SURROUND_SIZE * case_bounds.pixel_scale) / size.x
scale.y = (g_viewport_size + 2 * SURROUND_SIZE * case_bounds.pixel_scale) / size.y
go.set_scale(scale, surround.id)
end
end
local function tint_case()
print("tinting case: palette "..g_save_data.palette)
local pal = g_palettes[g_save_data.palette]
for i = 1, #case_parts do
sprite.set_constant(case_parts[i].name, "tint", col(pal[case_parts[i].palette]))
end
end
function init(self)
msg.post(".", "acquire_input_focus")
cache_case()
tint_case()
msg.post("@render:", "update_window_size")
end
function update(self, dt)
end
function on_message(self, message_id, message)
if message_id == hash("on_window_resized") then
resize_case()
calculate_viewport()
resize_case_after_viewport()
elseif message_id == hash("on_palette_changed") then
tint_case()
end
end
function on_input(self, action_id, action)
if action_id == hash("quit") then
msg.post("@system:", "exit", { code = 0 })
end
end