Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- // 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()
- }
- }
Advertisement