back to list

Playable Example

Breakout

breakout.lua ยท 640 x 480

runtime 640 x 480
Preparing Runtime

Loading game...

console stdout / stderr
 
breakout.game launch config
entry : breakout.lua
width : 640
height : 480
window_title : Breakout
background : 0xff000000
breakout.lua source
local soluna = require "soluna"
local app = require "soluna.app"
local file = require "soluna.file"
local matquad = require "soluna.material.quad"
local mattext = require "soluna.material.text"
local font = require "soluna.font"
local util = require "utils"

math.randomseed(os.time())

local args = ...
local batch = args.batch

local W = 640
local H = 480

local BRICK_ROWS = 6
local BRICK_COLS = 10
local BRICK_W = 58
local BRICK_H = 18
local BRICK_GAP = 4
local BRICK_OFFSET_X = 12
local BRICK_OFFSET_Y = 50

local KEY_ESCAPE = 256
local KEY_LEFT = 263
local KEY_RIGHT = 262
local KEY_SPACE = 32
local KEY_R = 82
local KEYSTATE_RELEASE = 0
local KEYSTATE_PRESS = 1

local COLOR_WHITE = 0xffffffff
local COLOR_DARK_GRAY = 0xff505050
local COLOR_RED = 0xffff3030
local COLOR_ORANGE = 0xffff9000
local COLOR_YELLOW = 0xffffff40
local COLOR_GREEN = 0xff30ff30
local COLOR_CYAN = 0xff00ffff
local COLOR_PURPLE = 0xffb040ff
local COLOR_GRAY = 0xff909090

local BRICK_COLORS = {
	COLOR_RED,
	COLOR_ORANGE,
	COLOR_YELLOW,
	COLOR_GREEN,
	COLOR_CYAN,
	COLOR_PURPLE,
}

local function clamp(v, min_v, max_v)
	if v < min_v then
		return min_v
	end
	if v > max_v then
		return max_v
	end
	return v
end

soluna.set_window_title "Breakout"

local fontid = util.font_init(soluna, font, file, {
	error_message = "No available system font for breakout",
})
local fontcobj = font.cobj()

local hud_block = mattext.block(fontcobj, fontid, 16, COLOR_WHITE, "LT")
local lives_block = mattext.block(fontcobj, fontid, 16, COLOR_GREEN, "LT")
local bricks_block = mattext.block(fontcobj, fontid, 16, COLOR_GRAY, "LT")
local hint_block = mattext.block(fontcobj, fontid, 16, COLOR_YELLOW, "LT")
local game_over_block = mattext.block(fontcobj, fontid, 28, COLOR_RED, "LT")
local game_win_block = mattext.block(fontcobj, fontid, 28, COLOR_GREEN, "LT")

local label = util.label_cache()
local quad = util.quad_cache(matquad)
local view = util.fixed_view(args, W, H)

local bricks = {}
local pad_w = 80
local pad_h = 12
local pad_x = 280
local pad_y = 450

local ball_x = 320.0
local ball_y = 430.0
local ball_vx = 3.0
local ball_vy = -4.0
local ball_r = 5

local score = 0
local lives = 3
local total_bricks = BRICK_ROWS * BRICK_COLS
local started = false
local game_over = false
local game_win = false
local keys_down = {}

local function reset_bricks()
	total_bricks = BRICK_ROWS * BRICK_COLS
	for r = 1, BRICK_ROWS do
		bricks[r] = bricks[r] or {}
		for c = 1, BRICK_COLS do
			bricks[r][c] = true
		end
	end
end

local function reset_game()
	reset_bricks()
	score = 0
	lives = 3
	pad_x = 280
	started = false
	game_over = false
	game_win = false
	ball_x = 320.0
	ball_y = 430.0
	ball_vx = 3.0
	ball_vy = -4.0
end

reset_game()

local function update_ball_on_paddle()
	ball_x = pad_x + pad_w / 2
	ball_y = pad_y - ball_r - 1
end

local function lose_ball()
	lives = lives - 1
	if lives <= 0 then
		game_over = true
	else
		started = false
		ball_vx = 3.0
		ball_vy = -4.0
	end
end

local function update_game()
	if game_over or game_win then
		return
	end

	if keys_down[KEY_LEFT] then
		pad_x = pad_x - 6
	end
	if keys_down[KEY_RIGHT] then
		pad_x = pad_x + 6
	end
	pad_x = clamp(pad_x, 0, W - pad_w)

	if not started then
		update_ball_on_paddle()
		return
	end

	ball_x = ball_x + ball_vx
	ball_y = ball_y + ball_vy

	if ball_x - ball_r < 0 then
		ball_x = ball_r
		ball_vx = -ball_vx
	end
	if ball_x + ball_r > W then
		ball_x = W - ball_r
		ball_vx = -ball_vx
	end
	if ball_y - ball_r < 0 then
		ball_y = ball_r
		ball_vy = -ball_vy
	end
	if ball_y + ball_r > H then
		lose_ball()
		return
	end

	if ball_vy > 0
		and ball_x + ball_r > pad_x
		and ball_x - ball_r < pad_x + pad_w
		and ball_y + ball_r >= pad_y
		and ball_y + ball_r <= pad_y + pad_h + 4 then
		ball_vy = -ball_vy
		ball_y = pad_y - ball_r
		local hit_pos = (ball_x - pad_x) / pad_w
		ball_vx = (hit_pos - 0.5) * 8.0
	end

	for r = 1, BRICK_ROWS do
		for c = 1, BRICK_COLS do
			if bricks[r][c] then
				local bx = BRICK_OFFSET_X + (c - 1) * (BRICK_W + BRICK_GAP)
				local by = BRICK_OFFSET_Y + (r - 1) * (BRICK_H + BRICK_GAP)
				if ball_x + ball_r > bx and ball_x - ball_r < bx + BRICK_W
					and ball_y + ball_r > by and ball_y - ball_r < by + BRICK_H then
					bricks[r][c] = false
					total_bricks = total_bricks - 1
					score = score + 10 * (BRICK_ROWS - (r - 1))

					local overlap_left = (ball_x + ball_r) - bx
					local overlap_right = (bx + BRICK_W) - (ball_x - ball_r)
					local overlap_top = (ball_y + ball_r) - by
					local overlap_bottom = (by + BRICK_H) - (ball_y - ball_r)
					local min_overlap_x = math.min(overlap_left, overlap_right)
					local min_overlap_y = math.min(overlap_top, overlap_bottom)

					if min_overlap_x < min_overlap_y then
						ball_vx = -ball_vx
					else
						ball_vy = -ball_vy
					end

					if total_bricks <= 0 then
						game_win = true
					end
					return
				end
			end
		end
	end
end

local function draw_bricks()
	for r = 1, BRICK_ROWS do
		for c = 1, BRICK_COLS do
			if bricks[r][c] then
				local bx = BRICK_OFFSET_X + (c - 1) * (BRICK_W + BRICK_GAP)
				local by = BRICK_OFFSET_Y + (r - 1) * (BRICK_H + BRICK_GAP)
				local color = BRICK_COLORS[r]
				batch:add(quad { width = BRICK_W, height = BRICK_H, color = color }, bx, by)
				batch:add(quad { width = BRICK_W, height = 2, color = COLOR_WHITE }, bx, by)
				batch:add(quad { width = BRICK_W, height = 2, color = COLOR_WHITE }, bx, by + BRICK_H - 2)
				batch:add(quad { width = 2, height = BRICK_H, color = COLOR_WHITE }, bx, by)
				batch:add(quad { width = 2, height = BRICK_H, color = COLOR_WHITE }, bx + BRICK_W - 2, by)
			end
		end
	end
end

local function draw_ball()
	local d = ball_r * 2 + 1
	for dy = -ball_r, ball_r do
		for dx = -ball_r, ball_r do
			if dx * dx + dy * dy <= ball_r * ball_r then
				batch:add(quad { width = 1, height = 1, color = COLOR_WHITE }, ball_x + dx, ball_y + dy)
			end
		end
	end
end

local function draw_hud()
	batch:add(label { block = hud_block, text = string.format("Score: %d", score), width = 140, height = 20 }, 10, 10)
	batch:add(label { block = lives_block, text = string.format("Lives: %d", lives), width = 120, height = 20 }, 10, 28)
	batch:add(label { block = bricks_block, text = string.format("Bricks: %d", total_bricks), width = 120, height = 20 },
		W - 130, 10)
end

local function draw_launch_hint()
	if not started and not game_over and not game_win then
		batch:add(label { block = hint_block, text = "SPACE to launch", width = 140, height = 20 }, 240, 420)
	end
end

local function draw_result_panel()
	if not game_over and not game_win then
		return
	end

	local x = 200
	local y = 200
	batch:add(quad { width = 240, height = 80, color = COLOR_DARK_GRAY }, x, y)
	batch:add(quad { width = 240, height = 2, color = COLOR_WHITE }, x, y)
	batch:add(quad { width = 240, height = 2, color = COLOR_WHITE }, x, y + 78)
	batch:add(quad { width = 2, height = 80, color = COLOR_WHITE }, x, y)
	batch:add(quad { width = 2, height = 80, color = COLOR_WHITE }, x + 238, y)

	if game_over then
		batch:add(label { block = game_over_block, text = "GAME OVER", width = 170, height = 30 }, 230, 210)
	else
		batch:add(label { block = game_win_block, text = "YOU WIN!", width = 170, height = 30 }, 240, 210)
	end
	batch:add(label { block = hud_block, text = string.format("Score: %d", score), width = 100, height = 20 }, 260, 245)
	batch:add(label { block = hint_block, text = "R to restart", width = 100, height = 20 }, 245, 262)
end

local callback = {}

function callback.frame()
	view.begin(batch)
	update_game()
	draw_hud()
	draw_bricks()
	batch:add(quad { width = pad_w, height = pad_h, color = COLOR_WHITE }, pad_x, pad_y)
	draw_ball()
	draw_launch_hint()
	draw_result_panel()
	view.finish(batch)
end

function callback.key(keycode, state)
	if state == KEYSTATE_RELEASE then
		keys_down[keycode] = false
		return
	end

	keys_down[keycode] = true

	if state == KEYSTATE_PRESS then
		if keycode == KEY_ESCAPE then
			app.quit()
		elseif (game_over or game_win) and keycode == KEY_R then
			reset_game()
		elseif not started and not game_over and not game_win and keycode == KEY_SPACE then
			started = true
			ball_vx = 3.0
			ball_vy = -4.0
		end
	end
end

function callback.window_resize(w, h)
	view.resize(w, h)
end

return callback