// Package main implements Brkout - a classic Breakout game for Dreamcast.
// This is a port of the arcade classic using Go and KOS.
// Ported from https://3e8.org/hacks/brkout/
package main
import "kos"
// Screen dimensions
const (
ScreenWidth = 640
ScreenHeight = 480
)
// Playing field configuration
const (
FieldX = 10
FieldY = 40
FieldWidth = 500
FieldHeight = 420
)
// Brick grid configuration
const (
BrickColumns = 20
BrickRows = 15
BrickWidth = 25
BrickHeight = 12
)
// Paddle configuration
const (
PaddleWidth = 70
PaddleHeight = 10
PaddleSpeed = 6.0
PaddleY = FieldHeight - 30
)
// Ball configuration
const (
BallRadius = 6
BallInitialSpeed = 3.0
BallMaxSpeed = 7.0
BallSpeedStep = 0.5
)
// Input configuration
const (
JoystickDeadzone = 15
JoystickSensitivity = 18.0
)
// Game rules
const (
InitialLives = 3
PointsPerBrick = 10
)
// Audio settings
const (
SoundVolume = 160
SoundCenter = 128
)
// Textures loaded from romdisk
var (
titleTexture *kos.PlxTexture
fontTexture *kos.PlxTexture
fieldTexture *kos.PlxTexture
)
// Sound effects
var (
clickSound kos.SfxHandle
bounceSound kos.SfxHandle
hitSound kos.SfxHandle
loseSound kos.SfxHandle
winSound kos.SfxHandle
)
// Controller state
var (
currentButtons uint32
previousButtons uint32
joystickX int32
)
// Game state
var (
paddleX float32
ballX float32
ballY float32
ballSpeedX float32
ballSpeedY float32
bricks [BrickColumns * BrickRows]int
bricksLeft int
lives int
score int
level int
)
// Brick color palette
var brickColors []uint32
// ----------------------------------------------------------------------------
// Math helpers
// ----------------------------------------------------------------------------
func abs(x float32) float32 {
if x < 0 {
return -x
}
return x
}
// ----------------------------------------------------------------------------
// Input handling
// ----------------------------------------------------------------------------
func readController() {
previousButtons = currentButtons
controller := kos.MapleEnumType(0, kos.MAPLE_FUNC_CONTROLLER)
if controller == nil {
currentButtons, joystickX = 0, 0
return
}
state := controller.ContState()
if state == nil {
currentButtons, joystickX = 0, 0
return
}
currentButtons = state.Buttons
joystickX = state.Joyx
}
func buttonPressed(button uint32) bool {
wasUp := (previousButtons & button) == 0
isDown := (currentButtons & button) != 0
return wasUp && isDown
}
func buttonHeld(button uint32) bool {
return (currentButtons & button) != 0
}
func getJoystickMovement() float32 {
if joystickX > -JoystickDeadzone && joystickX < JoystickDeadzone {
return 0
}
return float32(joystickX) / JoystickSensitivity
}
// ----------------------------------------------------------------------------
// Drawing primitives (using Plx library like other examples)
// ----------------------------------------------------------------------------
func drawColoredQuad(x, y, width, height, z float32, color uint32) {
kos.PlxVertInp(kos.PLX_VERT, x, y+height, z, color)
kos.PlxVertInp(kos.PLX_VERT, x, y, z, color)
kos.PlxVertInp(kos.PLX_VERT, x+width, y+height, z, color)
kos.PlxVertInp(kos.PLX_VERT_EOS, x+width, y, z, color)
}
func drawTexturedQuad(x, y, width, height, z float32, color uint32, u1, v1, u2, v2 float32) {
kos.PlxVertIfp(kos.PLX_VERT, x, y+height, z, color, u1, v2)
kos.PlxVertIfp(kos.PLX_VERT, x, y, z, color, u1, v1)
kos.PlxVertIfp(kos.PLX_VERT, x+width, y+height, z, color, u2, v2)
kos.PlxVertIfp(kos.PLX_VERT_EOS, x+width, y, z, color, u2, v1)
}
func setupColoredContext(list uint32) {
kos.PlxCxtInit()
kos.PlxCxtTexture(nil)
kos.PlxCxtCulling(kos.PLX_CULL_NONE)
kos.PlxCxtSend(int32(list))
}
func setupTexturedContext(list uint32, texture *kos.PlxTexture) {
kos.PlxCxtInit()
kos.PlxCxtTexture(texture.Ptr())
kos.PlxCxtCulling(kos.PLX_CULL_NONE)
kos.PlxCxtSend(int32(list))
}
// ----------------------------------------------------------------------------
// Text rendering (bitmap font from texture)
// ----------------------------------------------------------------------------
const (
CharWidth = 12
CharHeight = 24
FontSize = 256.0
)
func drawString(x, y, z float32, color uint32, text string) {
for i, char := range text {
if char < 32 {
continue
}
column := int(char) % 16
row := int(char) / 16
u1 := float32(column*CharWidth) / FontSize
v1 := float32(row*CharHeight) / FontSize
u2 := u1 + CharWidth/FontSize
v2 := v1 + CharHeight/FontSize
drawTexturedQuad(x+float32(i*CharWidth), y, CharWidth, CharHeight, z, color, u1, v1, u2, v2)
}
}
func drawInteger(x, y, z float32, color uint32, value int) {
if value == 0 {
drawString(x, y, z, color, "0")
return
}
text := ""
for value > 0 {
digit := byte(value % 10)
text = string('0'+digit) + text
value = value / 10
}
drawString(x, y, z, color, text)
}
// ----------------------------------------------------------------------------
// Asset loading
// ----------------------------------------------------------------------------
func loadAssets() {
kos.PvrInitDefaults()
// Load textures from romdisk
titleTexture = kos.PlxTxrLoad("/rd/brkout.png", false, 0)
fontTexture = kos.PlxTxrLoad("/rd/font.png", true, 0)
fieldTexture = kos.PlxTxrLoad("/rd/field.png", false, 0)
// Load sound effects
kos.SndStreamInit()
clickSound = kos.SndSfxLoad("/rd/click.wav")
bounceSound = kos.SndSfxLoad("/rd/bounce.wav")
hitSound = kos.SndSfxLoad("/rd/toggled.wav")
loseSound = kos.SndSfxLoad("/rd/failure.wav")
winSound = kos.SndSfxLoad("/rd/success.wav")
// Initialize brick color palette (rainbow gradient)
brickColors = []uint32{
kos.PlxPackColor(1, 1.0, 0.2, 0.2), // Red
kos.PlxPackColor(1, 1.0, 0.6, 0.2), // Orange
kos.PlxPackColor(1, 1.0, 1.0, 0.2), // Yellow
kos.PlxPackColor(1, 0.2, 1.0, 0.2), // Green
kos.PlxPackColor(1, 0.2, 1.0, 1.0), // Cyan
kos.PlxPackColor(1, 0.2, 0.2, 1.0), // Blue
kos.PlxPackColor(1, 0.8, 0.2, 1.0), // Purple
}
}
// ----------------------------------------------------------------------------
// Colors (pre-computed for performance)
// ----------------------------------------------------------------------------
func colorWhite() uint32 { return kos.PlxPackColor(1, 1, 1, 1) }
func colorGray() uint32 { return kos.PlxPackColor(1, 0.5, 0.5, 0.5) }
func colorBackground() uint32 { return kos.PlxPackColor(1, 0.05, 0.05, 0.1) }
func colorWall() uint32 { return kos.PlxPackColor(1, 0.4, 0.4, 0.6) }
func colorPaddle() uint32 { return kos.PlxPackColor(1, 0.4, 0.4, 0.7) }
func colorShadow() uint32 { return kos.PlxPackColor(0.7, 0, 0, 0) }
func colorGameOverBg() uint32 { return kos.PlxPackColor(1, 0.1, 0.1, 0.15) }
// ----------------------------------------------------------------------------
// Title Menu
// ----------------------------------------------------------------------------
func showTitleMenu() bool {
kos.PvrSetBgColor(0, 0, 0)
selection := 0
for {
readController()
// Navigate menu
if buttonPressed(kos.CONT_DPAD_UP) || buttonPressed(kos.CONT_DPAD_DOWN) {
selection = 1 - selection
kos.SndSfxPlay(clickSound, SoundVolume, SoundCenter)
}
// Confirm selection
if buttonPressed(kos.CONT_A) || buttonPressed(kos.CONT_START) {
kos.SndSfxPlay(hitSound, SoundVolume, SoundCenter)
fadeOutMenu(selection)
return selection == 1 // true = quit
}
renderTitleMenu(selection, 1.0)
}
}
func fadeOutMenu(selection int) {
for frame := 30; frame >= 0; frame-- {
alpha := float32(frame) / 30.0
renderTitleMenu(selection, alpha)
}
}
func renderTitleMenu(selection int, alpha float32) {
kos.PvrWaitReady()
kos.PvrSceneBegin()
kos.PvrListBegin(kos.PVR_LIST_OP_POLY)
// Title screen background
if titleTexture != nil {
setupTexturedContext(kos.PVR_LIST_OP_POLY, titleTexture)
tintColor := kos.PlxPackColor(1, alpha, alpha, alpha)
drawTexturedQuad(0, 0, ScreenWidth, ScreenHeight, 100, tintColor,
0, 0, 640.0/1024.0, 480.0/512.0)
}
kos.PvrListFinish()
// Menu cursor
kos.PvrListBegin(kos.PVR_LIST_TR_POLY)
setupColoredContext(kos.PVR_LIST_TR_POLY)
cursorColor := kos.PlxPackColor(alpha, 1, 1, 1)
cursorY := float32(314 + selection*50)
drawColoredQuad(250, cursorY, 20, 20, 200, cursorColor)
kos.PvrListFinish()
kos.PvrSceneFinish()
}
// ----------------------------------------------------------------------------
// Game Logic
// ----------------------------------------------------------------------------
func startNewGame() {
lives = InitialLives
score = 0
level = 1
setupLevel()
}
func setupLevel() {
// Center paddle
paddleX = float32(FieldWidth-PaddleWidth) / 2.0
resetBall()
// Create brick grid (more rows at higher levels)
rows := 3 + level
if rows > BrickRows {
rows = BrickRows
}
bricksLeft = 0
colorCount := len(brickColors)
for row := 0; row < BrickRows; row++ {
for col := 0; col < BrickColumns; col++ {
index := row*BrickColumns + col
if row < rows {
bricks[index] = (row % colorCount) + 1
bricksLeft++
} else {
bricks[index] = 0
}
}
}
}
func resetBall() {
ballX = paddleX + float32(PaddleWidth)/2.0 - float32(BallRadius)
ballY = float32(FieldHeight) - 50.0
speed := BallInitialSpeed + float32(level)*BallSpeedStep
if speed > BallMaxSpeed {
speed = BallMaxSpeed
}
ballSpeedX = speed * 0.6
ballSpeedY = -speed
}
func runGameLoop() {
kos.PvrSetBgColor(0.05, 0.05, 0.1)
startNewGame()
for lives > 0 {
readController()
// Exit to menu on START+B
if buttonHeld(kos.CONT_START) && buttonHeld(kos.CONT_B) {
return
}
updatePaddle()
updateBall()
renderGameFrame()
}
}
func updatePaddle() {
// D-pad movement
if buttonHeld(kos.CONT_DPAD_LEFT) {
paddleX -= PaddleSpeed
}
if buttonHeld(kos.CONT_DPAD_RIGHT) {
paddleX += PaddleSpeed
}
// Analog stick movement
paddleX += getJoystickMovement()
// Clamp to field bounds
if paddleX < 0 {
paddleX = 0
}
maxX := float32(FieldWidth - PaddleWidth)
if paddleX > maxX {
paddleX = maxX
}
}
func updateBall() {
ballX += ballSpeedX
ballY += ballSpeedY
handleWallCollisions()
handlePaddleCollision()
handleBrickCollisions()
handleBallLost()
handleLevelComplete()
}
func handleWallCollisions() {
// Left wall
if ballX < 0 {
ballX = 0
ballSpeedX = -ballSpeedX
kos.SndSfxPlay(bounceSound, 140, 64)
}
// Right wall
maxX := float32(FieldWidth - BallRadius*2)
if ballX > maxX {
ballX = maxX
ballSpeedX = -ballSpeedX
kos.SndSfxPlay(bounceSound, 140, 192)
}
// Top wall
if ballY < 0 {
ballY = 0
ballSpeedY = -ballSpeedY
kos.SndSfxPlay(bounceSound, 140, SoundCenter)
}
}
func handlePaddleCollision() {
paddleTop := float32(PaddleY)
ballBottom := ballY + float32(BallRadius*2)
ballCenterX := ballX + float32(BallRadius)
// Check if ball is at paddle height
if ballBottom < paddleTop || ballY >= paddleTop+float32(PaddleHeight) {
return
}
// Check if ball is horizontally aligned with paddle
if ballCenterX < paddleX || ballCenterX >= paddleX+float32(PaddleWidth) {
return
}
// Bounce off paddle
ballY = paddleTop - float32(BallRadius*2)
ballSpeedY = -abs(ballSpeedY)
// Adjust horizontal speed based on hit position
hitPosition := (ballCenterX - paddleX) / float32(PaddleWidth)
ballSpeedX = (hitPosition - 0.5) * 8.0
kos.SndSfxPlay(bounceSound, SoundVolume, SoundCenter)
}
func handleBrickCollisions() {
ballCenterX := ballX + float32(BallRadius)
ballCenterY := ballY + float32(BallRadius)
// Convert ball position to brick grid coordinates
brickCol := int(ballCenterX) / BrickWidth
brickRow := int(ballCenterY) / BrickHeight
// Check bounds
if brickCol < 0 || brickCol >= BrickColumns || brickRow < 0 || brickRow >= BrickRows {
return
}
index := brickRow*BrickColumns + brickCol
if bricks[index] == 0 {
return
}
// Destroy brick
bricks[index] = 0
bricksLeft--
score += PointsPerBrick * level
// Determine bounce direction
brickCenterX := float32(brickCol*BrickWidth) + float32(BrickWidth)/2.0
brickCenterY := float32(brickRow*BrickHeight) + float32(BrickHeight)/2.0
horizontalOverlap := abs(ballCenterX-brickCenterX) / float32(BrickWidth)
verticalOverlap := abs(ballCenterY-brickCenterY) / float32(BrickHeight)
if horizontalOverlap > verticalOverlap {
ballSpeedX = -ballSpeedX
} else {
ballSpeedY = -ballSpeedY
}
kos.SndSfxPlay(hitSound, 150, SoundCenter)
}
func handleBallLost() {
if ballY <= float32(FieldHeight) {
return
}
lives--
kos.SndSfxPlay(loseSound, SoundVolume, SoundCenter)
if lives <= 0 {
showGameOver()
return
}
resetBall()
pauseFrames(60)
}
func handleLevelComplete() {
if bricksLeft > 0 {
return
}
kos.SndSfxPlay(winSound, SoundVolume, SoundCenter)
level++
setupLevel()
pauseFrames(60)
}
func pauseFrames(count int) {
for i := 0; i < count; i++ {
renderGameFrame()
}
}
// ----------------------------------------------------------------------------
// Game Over Screen
// ----------------------------------------------------------------------------
func showGameOver() {
for frame := 0; frame < 180; frame++ {
kos.PvrWaitReady()
kos.PvrSceneBegin()
// Background
kos.PvrListBegin(kos.PVR_LIST_OP_POLY)
setupColoredContext(kos.PVR_LIST_OP_POLY)
drawColoredQuad(0, 0, ScreenWidth, ScreenHeight, 1, colorGameOverBg())
kos.PvrListFinish()
// Text
kos.PvrListBegin(kos.PVR_LIST_TR_POLY)
if fontTexture != nil {
setupTexturedContext(kos.PVR_LIST_TR_POLY, fontTexture)
drawString(220, 180, 100, colorWhite(), "GAME OVER")
drawString(220, 230, 100, colorGray(), "Score:")
drawInteger(320, 230, 100, colorWhite(), score)
}
kos.PvrListFinish()
kos.PvrSceneFinish()
readController()
if buttonPressed(kos.CONT_A) {
return
}
}
}
// ----------------------------------------------------------------------------
// Game Rendering
// ----------------------------------------------------------------------------
func renderGameFrame() {
kos.PvrWaitReady()
kos.PvrSceneBegin()
// Opaque geometry
kos.PvrListBegin(kos.PVR_LIST_OP_POLY)
renderBackground()
renderField()
renderWalls()
renderBricks()
renderPaddle()
renderBall()
kos.PvrListFinish()
// Translucent geometry (HUD text)
kos.PvrListBegin(kos.PVR_LIST_TR_POLY)
renderHUD()
kos.PvrListFinish()
kos.PvrSceneFinish()
}
func renderBackground() {
setupColoredContext(kos.PVR_LIST_OP_POLY)
drawColoredQuad(0, 0, ScreenWidth, ScreenHeight, 1, colorBackground())
}
func renderField() {
if fieldTexture == nil {
return
}
setupTexturedContext(kos.PVR_LIST_OP_POLY, fieldTexture)
drawTexturedQuad(FieldX, FieldY, FieldWidth, FieldHeight, 50, colorWhite(),
0, 0, float32(FieldWidth)/512.0, float32(FieldHeight)/512.0)
}
func renderWalls() {
setupColoredContext(kos.PVR_LIST_OP_POLY)
wall := colorWall()
drawColoredQuad(FieldX-5, FieldY, 5, FieldHeight, 100, wall) // Left
drawColoredQuad(FieldX+FieldWidth, FieldY, 5, FieldHeight, 100, wall) // Right
drawColoredQuad(FieldX-5, FieldY-5, FieldWidth+10, 5, 100, wall) // Top
}
func renderBricks() {
shadow := colorShadow()
colorCount := len(brickColors)
for row := 0; row < BrickRows; row++ {
for col := 0; col < BrickColumns; col++ {
colorIndex := bricks[row*BrickColumns+col]
if colorIndex == 0 {
continue
}
x := float32(FieldX + col*BrickWidth)
y := float32(FieldY + row*BrickHeight)
// Shadow
drawColoredQuad(x+2, y+2, BrickWidth-2, BrickHeight-2, 99, shadow)
// Brick
drawColoredQuad(x, y, BrickWidth-2, BrickHeight-2, 100,
brickColors[(colorIndex-1)%colorCount])
}
}
}
func renderPaddle() {
x := float32(FieldX) + paddleX
y := float32(FieldY + PaddleY)
drawColoredQuad(x+3, y+3, PaddleWidth, PaddleHeight, 99, colorShadow())
drawColoredQuad(x, y, PaddleWidth, PaddleHeight, 100, colorPaddle())
}
func renderBall() {
x := float32(FieldX) + ballX
y := float32(FieldY) + ballY
drawColoredQuad(x, y, BallRadius*2, BallRadius*2, 100, colorWhite())
}
func renderHUD() {
if fontTexture == nil {
return
}
setupTexturedContext(kos.PVR_LIST_TR_POLY, fontTexture)
hudX := float32(FieldX + FieldWidth + 15)
white := colorWhite()
gray := colorGray()
drawString(hudX, 50, 200, white, "BRKOUT")
drawString(hudX, 100, 200, gray, "Level")
drawInteger(hudX, 125, 200, white, level)
drawString(hudX, 170, 200, gray, "Score")
drawInteger(hudX, 195, 200, white, score)
drawString(hudX, 250, 200, gray, "Lives")
drawInteger(hudX, 275, 200, white, lives)
}
// ----------------------------------------------------------------------------
// Main Entry Point
// ----------------------------------------------------------------------------
func main() {
loadAssets()
for {
if showTitleMenu() {
return // User selected Quit
}
runGameLoop()
}
}