// BRKOUT - A Breakout Clone for Sega Dreamcast // Ported from: https://3e8.org/hacks/brkout/ // // This example demonstrates how to build a complete game using Go on the // Dreamcast. It showcases: // - Loading textures and sounds from the romdisk filesystem // - Reading controller input (D-pad and analog stick) // - Rendering 2D graphics using the PowerVR hardware // - Simple collision detection and game physics // - Game state management with menus and screens // // Controls: // D-Pad / Analog Stick - Move paddle // A or START - Select menu option // START + B - Return to menu during gameplay package main import "kos" // ============================================================================= // GAME CONFIGURATION // ============================================================================= // // All game parameters are defined as constants, making them easy to tweak. // Try changing these values to see how they affect gameplay! // Screen size (Dreamcast outputs 640x480 by default) const ( ScreenWidth = 640 ScreenHeight = 480 ) // The playing field is where the action happens const ( FieldLeft = 10 // Pixels from left edge FieldTop = 40 // Pixels from top edge FieldWidth = 500 // Width in pixels FieldHeight = 420 // Height in pixels ) // Brick grid - the targets to destroy const ( BrickCols = 20 // Columns of bricks BrickRows = 15 // Maximum rows of bricks BrickWidth = 25 // Width of each brick BrickHeight = 12 // Height of each brick ) // Paddle - the player's bat const ( PaddleWidth = 70 // Width in pixels PaddleHeight = 10 // Height in pixels PaddleSpeed = 6.0 // Movement speed PaddleY = FieldHeight - 30 // Distance from field bottom ) // Ball physics const ( BallSize = 12 // Ball diameter BallRadius = BallSize / 2 // Ball radius StartSpeed = 3.0 // Initial ball speed MaxSpeed = 7.0 // Maximum ball speed SpeedIncrease = 0.5 // Speed boost per level ) // Input sensitivity const ( StickDeadzone = 15 // Ignore small joystick movements StickScale = 18.0 // How fast joystick moves paddle ) // Game rules const ( StartingLives = 3 // Lives at game start PointsPerBrick = 10 // Base score for each brick ) // ============================================================================= // COLOR PALETTE // ============================================================================= // // Colors are packed into 32-bit values: Alpha, Red, Green, Blue (0.0 to 1.0) // We use helper functions to make the code more readable. func white() uint32 { return kos.PlxPackColor(1, 1, 1, 1) } func gray() uint32 { return kos.PlxPackColor(1, 0.5, 0.5, 0.5) } func darkBlue() uint32 { return kos.PlxPackColor(1, 0.05, 0.05, 0.1) } func wallBlue() uint32 { return kos.PlxPackColor(1, 0.4, 0.4, 0.6) } func paddleBlue() uint32 { return kos.PlxPackColor(1, 0.4, 0.4, 0.7) } func shadow() uint32 { return kos.PlxPackColor(0.7, 0, 0, 0) } // Rainbow colors for bricks - each row gets a different color! var 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 } // ============================================================================= // GAME ASSETS // ============================================================================= // // Textures and sounds loaded from the romdisk (virtual filesystem compiled // into the game executable). var ( titleImage *kos.PlxTexture // Title screen background fontImage *kos.PlxTexture // Bitmap font for text fieldImage *kos.PlxTexture // Playing field background soundClick kos.SfxHandle // Menu navigation soundBounce kos.SfxHandle // Ball bouncing soundHit kos.SfxHandle // Brick destroyed soundLose kos.SfxHandle // Lost a life soundWin kos.SfxHandle // Level complete ) func loadAssets() { // Initialize the PowerVR graphics chip kos.PvrInitDefaults() // Load images from the /rd (romdisk) filesystem titleImage = kos.PlxTxrLoad("/rd/brkout.png", false, 0) fontImage = kos.PlxTxrLoad("/rd/font.png", true, 0) // true = has transparency fieldImage = kos.PlxTxrLoad("/rd/field.png", false, 0) // Initialize sound system and load effects kos.SndStreamInit() soundClick = kos.SndSfxLoad("/rd/click.wav") soundBounce = kos.SndSfxLoad("/rd/bounce.wav") soundHit = kos.SndSfxLoad("/rd/toggled.wav") soundLose = kos.SndSfxLoad("/rd/failure.wav") soundWin = kos.SndSfxLoad("/rd/success.wav") } // ============================================================================= // CONTROLLER INPUT // ============================================================================= // // The Dreamcast controller has a D-pad, analog stick, and buttons. // We track the previous frame's buttons to detect new presses. var ( buttonsNow uint32 // Buttons held this frame buttonsPrev uint32 // Buttons held last frame stickX int32 // Analog stick X position (-128 to 127) ) func readController() { buttonsPrev = buttonsNow // Find the first controller plugged in controller := kos.MapleEnumType(0, kos.MAPLE_FUNC_CONTROLLER) if controller == nil { buttonsNow, stickX = 0, 0 return } // Read its current state state := controller.ContState() if state == nil { buttonsNow, stickX = 0, 0 return } buttonsNow = state.Buttons stickX = state.Joyx } // justPressed returns true only on the frame when a button is first pressed func justPressed(button uint32) bool { wasUp := (buttonsPrev & button) == 0 isDown := (buttonsNow & button) != 0 return wasUp && isDown } // isHeld returns true every frame while a button is held down func isHeld(button uint32) bool { return (buttonsNow & button) != 0 } // getStickMovement returns paddle movement from the analog stick func getStickMovement() float32 { // Ignore tiny movements (deadzone prevents drift) if stickX > -StickDeadzone && stickX < StickDeadzone { return 0 } return float32(stickX) / StickScale } // ============================================================================= // GAME STATE // ============================================================================= // // All the variables that track what's happening in the game. var ( // Paddle position (X coordinate within the field) paddleX float32 // Ball position and velocity ballX, ballY float32 ballSpeedX, speedY float32 // Brick grid: 0 = empty, 1-7 = brick color bricks [BrickCols * BrickRows]int bricksLeft int // Player progress lives int score int level int ) // ============================================================================= // DRAWING HELPERS // ============================================================================= // // The Dreamcast's PowerVR chip renders 3D polygons. For 2D games, we draw // rectangles (quads) at a fixed Z depth. The PLX library makes this easy! // setupColors prepares for drawing solid-colored shapes func setupColors(list uint32) { kos.PlxCxtInit() kos.PlxCxtTexture(nil) // No texture, just colors kos.PlxCxtCulling(kos.PLX_CULL_NONE) kos.PlxCxtSend(int32(list)) } // setupTexture prepares for drawing textured shapes func setupTexture(list uint32, texture *kos.PlxTexture) { kos.PlxCxtInit() kos.PlxCxtTexture(texture.Ptr()) kos.PlxCxtCulling(kos.PLX_CULL_NONE) kos.PlxCxtSend(int32(list)) } // drawRect draws a solid colored rectangle func drawRect(x, y, w, h, z float32, color uint32) { // A quad needs 4 vertices, last one marked as "end of strip" kos.PlxVertInp(kos.PLX_VERT, x, y+h, z, color) kos.PlxVertInp(kos.PLX_VERT, x, y, z, color) kos.PlxVertInp(kos.PLX_VERT, x+w, y+h, z, color) kos.PlxVertInp(kos.PLX_VERT_EOS, x+w, y, z, color) } // drawImage draws a textured rectangle with UV coordinates func drawImage(x, y, w, h, z float32, color uint32, u1, v1, u2, v2 float32) { kos.PlxVertIfp(kos.PLX_VERT, x, y+h, z, color, u1, v2) kos.PlxVertIfp(kos.PLX_VERT, x, y, z, color, u1, v1) kos.PlxVertIfp(kos.PLX_VERT, x+w, y+h, z, color, u2, v2) kos.PlxVertIfp(kos.PLX_VERT_EOS, x+w, y, z, color, u2, v1) } // ============================================================================= // TEXT RENDERING // ============================================================================= // // We use a bitmap font stored in a texture. Each character is 12x24 pixels, // arranged in a 16x16 grid (256 characters total, ASCII order). const ( charW = 12 // Character width charH = 24 // Character height fontSize = 256.0 // Texture size ) // drawText draws a string at the given position func drawText(x, y, z float32, color uint32, text string) { for i, ch := range text { if ch < 32 { continue // Skip control characters } // Calculate UV coordinates for this character col := int(ch) % 16 row := int(ch) / 16 u1 := float32(col*charW) / fontSize v1 := float32(row*charH) / fontSize u2 := u1 + charW/fontSize v2 := v1 + charH/fontSize // Draw the character drawImage(x+float32(i*charW), y, charW, charH, z, color, u1, v1, u2, v2) } } // drawNumber draws an integer as text func drawNumber(x, y, z float32, color uint32, n int) { if n == 0 { drawText(x, y, z, color, "0") return } // Build string from digits (right to left) text := "" for n > 0 { digit := byte(n % 10) text = string('0'+digit) + text n = n / 10 } drawText(x, y, z, color, text) } // ============================================================================= // SOUND EFFECTS // ============================================================================= // // Simple wrappers for playing sounds with panning (left/center/right) func playSound(sound kos.SfxHandle) { kos.SndSfxPlay(sound, 160, 128) } func playSoundLeft(sound kos.SfxHandle) { kos.SndSfxPlay(sound, 140, 64) } func playSoundRight(sound kos.SfxHandle) { kos.SndSfxPlay(sound, 140, 192) } // ============================================================================= // MATH HELPERS // ============================================================================= func abs(x float32) float32 { if x < 0 { return -x } return x } // ============================================================================= // TITLE MENU // ============================================================================= func showMenu() bool { kos.PvrSetBgColor(0, 0, 0) // Black background choice := 0 // 0 = Play, 1 = Quit for { readController() // Navigate with D-pad if justPressed(kos.CONT_DPAD_UP) || justPressed(kos.CONT_DPAD_DOWN) { choice = 1 - choice // Toggle between 0 and 1 playSound(soundClick) } // Select with A or START if justPressed(kos.CONT_A) || justPressed(kos.CONT_START) { playSound(soundHit) fadeOutMenu(choice) return choice == 1 // true = quit } renderMenu(choice, 1.0) } } func fadeOutMenu(choice int) { for frame := 30; frame >= 0; frame-- { renderMenu(choice, float32(frame)/30.0) } } func renderMenu(choice int, alpha float32) { kos.PvrWaitReady() kos.PvrSceneBegin() // Draw title image (opaque layer) kos.PvrListBegin(kos.PVR_LIST_OP_POLY) if titleImage != nil { setupTexture(kos.PVR_LIST_OP_POLY, titleImage) tint := kos.PlxPackColor(1, alpha, alpha, alpha) drawImage(0, 0, ScreenWidth, ScreenHeight, 100, tint, 0, 0, 640.0/1024.0, 480.0/512.0) } kos.PvrListFinish() // Draw menu cursor (translucent layer) kos.PvrListBegin(kos.PVR_LIST_TR_POLY) setupColors(kos.PVR_LIST_TR_POLY) cursorY := float32(314 + choice*50) drawRect(250, cursorY, 20, 20, 200, kos.PlxPackColor(alpha, 1, 1, 1)) kos.PvrListFinish() kos.PvrSceneFinish() } // ============================================================================= // GAME INITIALIZATION // ============================================================================= func startNewGame() { lives = StartingLives score = 0 level = 1 setupLevel() } func setupLevel() { // Center the paddle paddleX = float32(FieldWidth-PaddleWidth) / 2.0 // Reset the ball resetBall() // Fill brick grid (more rows at higher levels) rows := 3 + level if rows > BrickRows { rows = BrickRows } bricksLeft = 0 numColors := len(brickColors) for row := 0; row < BrickRows; row++ { for col := 0; col < BrickCols; col++ { i := row*BrickCols + col if row < rows { bricks[i] = (row % numColors) + 1 // Color index 1-7 bricksLeft++ } else { bricks[i] = 0 // Empty } } } } func resetBall() { // Position ball above paddle ballX = paddleX + PaddleWidth/2.0 - BallRadius ballY = FieldHeight - 50.0 // Calculate speed for current level speed := StartSpeed + float32(level)*SpeedIncrease if speed > MaxSpeed { speed = MaxSpeed } // Launch at an angle ballSpeedX = speed * 0.6 speedY = -speed } // ============================================================================= // GAME LOOP // ============================================================================= func playGame() { kos.PvrSetBgColor(0.05, 0.05, 0.1) // Dark blue background startNewGame() for lives > 0 { readController() // Return to menu with START+B if isHeld(kos.CONT_START) && isHeld(kos.CONT_B) { return } updatePaddle() updateBall() renderGame() } } // ============================================================================= // PADDLE MOVEMENT // ============================================================================= func updatePaddle() { // D-pad control if isHeld(kos.CONT_DPAD_LEFT) { paddleX -= PaddleSpeed } if isHeld(kos.CONT_DPAD_RIGHT) { paddleX += PaddleSpeed } // Analog stick control paddleX += getStickMovement() // Keep paddle inside the field if paddleX < 0 { paddleX = 0 } if paddleX > FieldWidth-PaddleWidth { paddleX = FieldWidth - PaddleWidth } } // ============================================================================= // BALL PHYSICS // ============================================================================= func updateBall() { // Move ball ballX += ballSpeedX ballY += speedY // Check all collisions checkWalls() checkPaddle() checkBricks() checkLost() checkWin() } func checkWalls() { // Left wall if ballX < 0 { ballX = 0 ballSpeedX = -ballSpeedX playSoundLeft(soundBounce) } // Right wall if ballX > FieldWidth-BallSize { ballX = FieldWidth - BallSize ballSpeedX = -ballSpeedX playSoundRight(soundBounce) } // Top wall if ballY < 0 { ballY = 0 speedY = -speedY playSound(soundBounce) } } func checkPaddle() { // Ball center coordinates ballCenterX := ballX + BallRadius ballBottom := ballY + BallSize // Is ball at paddle height? if ballBottom < PaddleY || ballY > PaddleY+PaddleHeight { return } // Is ball above paddle? if ballCenterX < paddleX || ballCenterX > paddleX+PaddleWidth { return } // Bounce! ballY = PaddleY - BallSize speedY = -abs(speedY) // Angle depends on where ball hit the paddle // Hit left = go left, hit right = go right hitPoint := (ballCenterX - paddleX) / PaddleWidth // 0.0 to 1.0 ballSpeedX = (hitPoint - 0.5) * 8.0 // -4.0 to +4.0 playSound(soundBounce) } func checkBricks() { // Ball center coordinates ballCenterX := ballX + BallRadius ballCenterY := ballY + BallRadius // Which brick is the ball in? col := int(ballCenterX) / BrickWidth row := int(ballCenterY) / BrickHeight // Out of bounds? if col < 0 || col >= BrickCols || row < 0 || row >= BrickRows { return } // Is there a brick here? i := row*BrickCols + col if bricks[i] == 0 { return } // Destroy the brick! bricks[i] = 0 bricksLeft-- score += PointsPerBrick * level // Bounce off brick (figure out which side we hit) brickCenterX := float32(col*BrickWidth) + BrickWidth/2.0 brickCenterY := float32(row*BrickHeight) + BrickHeight/2.0 horizDist := abs(ballCenterX-brickCenterX) / BrickWidth vertDist := abs(ballCenterY-brickCenterY) / BrickHeight if horizDist > vertDist { ballSpeedX = -ballSpeedX // Hit side } else { speedY = -speedY // Hit top/bottom } playSound(soundHit) } func checkLost() { // Ball fell below the field? if ballY <= FieldHeight { return } lives-- playSound(soundLose) if lives <= 0 { showGameOver() return } // Continue with new ball resetBall() waitFrames(60) // Brief pause } func checkWin() { // All bricks destroyed? if bricksLeft > 0 { return } playSound(soundWin) level++ setupLevel() waitFrames(60) // Brief pause } func waitFrames(n int) { for i := 0; i < n; i++ { renderGame() } } // ============================================================================= // GAME OVER SCREEN // ============================================================================= func showGameOver() { bg := kos.PlxPackColor(1, 0.1, 0.1, 0.15) for frame := 0; frame < 180; frame++ { // 3 seconds kos.PvrWaitReady() kos.PvrSceneBegin() // Background kos.PvrListBegin(kos.PVR_LIST_OP_POLY) setupColors(kos.PVR_LIST_OP_POLY) drawRect(0, 0, ScreenWidth, ScreenHeight, 1, bg) kos.PvrListFinish() // Text kos.PvrListBegin(kos.PVR_LIST_TR_POLY) if fontImage != nil { setupTexture(kos.PVR_LIST_TR_POLY, fontImage) drawText(220, 180, 100, white(), "GAME OVER") drawText(220, 230, 100, gray(), "Score:") drawNumber(320, 230, 100, white(), score) } kos.PvrListFinish() kos.PvrSceneFinish() // Skip with A button readController() if justPressed(kos.CONT_A) { return } } } // ============================================================================= // GAME RENDERING // ============================================================================= func renderGame() { kos.PvrWaitReady() kos.PvrSceneBegin() // === OPAQUE LAYER === // Draw solid objects that don't need transparency kos.PvrListBegin(kos.PVR_LIST_OP_POLY) drawBackground() drawField() drawWalls() drawBricks() drawPaddle() drawBall() kos.PvrListFinish() // === TRANSLUCENT LAYER === // Draw text (font has transparent background) kos.PvrListBegin(kos.PVR_LIST_TR_POLY) drawHUD() kos.PvrListFinish() kos.PvrSceneFinish() } func drawBackground() { setupColors(kos.PVR_LIST_OP_POLY) drawRect(0, 0, ScreenWidth, ScreenHeight, 1, darkBlue()) } func drawField() { if fieldImage == nil { return } setupTexture(kos.PVR_LIST_OP_POLY, fieldImage) drawImage(FieldLeft, FieldTop, FieldWidth, FieldHeight, 50, white(), 0, 0, float32(FieldWidth)/512.0, float32(FieldHeight)/512.0) } func drawWalls() { setupColors(kos.PVR_LIST_OP_POLY) color := wallBlue() drawRect(FieldLeft-5, FieldTop, 5, FieldHeight, 100, color) // Left drawRect(FieldLeft+FieldWidth, FieldTop, 5, FieldHeight, 100, color) // Right drawRect(FieldLeft-5, FieldTop-5, FieldWidth+10, 5, 100, color) // Top } func drawBricks() { numColors := len(brickColors) for row := 0; row < BrickRows; row++ { for col := 0; col < BrickCols; col++ { colorIndex := bricks[row*BrickCols+col] if colorIndex == 0 { continue // Empty space } // Calculate screen position x := float32(FieldLeft + col*BrickWidth) y := float32(FieldTop + row*BrickHeight) // Draw shadow first (slightly offset) drawRect(x+2, y+2, BrickWidth-2, BrickHeight-2, 99, shadow()) // Draw brick on top color := brickColors[(colorIndex-1)%numColors] drawRect(x, y, BrickWidth-2, BrickHeight-2, 100, color) } } } func drawPaddle() { x := float32(FieldLeft) + paddleX y := float32(FieldTop + PaddleY) // Shadow drawRect(x+3, y+3, PaddleWidth, PaddleHeight, 99, shadow()) // Paddle drawRect(x, y, PaddleWidth, PaddleHeight, 100, paddleBlue()) } func drawBall() { x := float32(FieldLeft) + ballX y := float32(FieldTop) + ballY drawRect(x, y, BallSize, BallSize, 100, white()) } func drawHUD() { if fontImage == nil { return } setupTexture(kos.PVR_LIST_TR_POLY, fontImage) // Position HUD to the right of the playing field x := float32(FieldLeft + FieldWidth + 15) drawText(x, 50, 200, white(), "BRKOUT") drawText(x, 100, 200, gray(), "Level") drawNumber(x, 125, 200, white(), level) drawText(x, 170, 200, gray(), "Score") drawNumber(x, 195, 200, white(), score) drawText(x, 250, 200, gray(), "Lives") drawNumber(x, 275, 200, white(), lives) } // ============================================================================= // MAIN ENTRY POINT // ============================================================================= func main() { // Load all game assets (textures, sounds) loadAssets() // Main game loop - keeps returning to menu after each game for { if showMenu() { return // User chose "Quit" } playGame() } }