Skip to content

Latest commit

 

History

History
888 lines (709 loc) · 14.7 KB

File metadata and controls

888 lines (709 loc) · 14.7 KB

Fantasy Console 0 - Code Examples

A collection of code patterns and examples for common game development tasks.

Table of Contents


Movement Patterns

8-Direction Movement

let player = { x: 64, y: 64, speed: 2 }

function _update() {
  let dx = 0
  let dy = 0

  if (btn(0)) dx = -1 // Left
  if (btn(1)) dx = 1 // Right
  if (btn(2)) dy = -1 // Up
  if (btn(3)) dy = 1 // Down

  // Normalize diagonal movement
  if (dx != 0 && dy != 0) {
    dx *= 0.707 // 1/sqrt(2)
    dy *= 0.707
  }

  player.x += dx * player.speed
  player.y += dy * player.speed

  // Keep on screen
  player.x = mid(4, player.x, 124)
  player.y = mid(4, player.y, 124)
}

Smooth Acceleration

let player = {
  x: 64,
  y: 64,
  vx: 0,
  vy: 0,
  accel: 0.5,
  friction: 0.85,
  maxSpeed: 4,
}

function _update() {
  // Apply acceleration
  if (btn(0)) player.vx -= player.accel
  if (btn(1)) player.vx += player.accel
  if (btn(2)) player.vy -= player.accel
  if (btn(3)) player.vy += player.accel

  // Apply friction
  player.vx *= player.friction
  player.vy *= player.friction

  // Clamp speed
  let speed = sqrt(player.vx * player.vx + player.vy * player.vy)
  if (speed > player.maxSpeed) {
    player.vx = (player.vx / speed) * player.maxSpeed
    player.vy = (player.vy / speed) * player.maxSpeed
  }

  // Apply velocity
  player.x += player.vx
  player.y += player.vy
}

Platformer Movement with Gravity

let player = {
  x: 64,
  y: 100,
  vx: 0,
  vy: 0,
  grounded: false,
}

const GRAVITY = 0.3
const JUMP_FORCE = -5
const MOVE_SPEED = 2
const FRICTION = 0.8

function _update() {
  // Horizontal movement
  if (btn(0)) player.vx = -MOVE_SPEED
  else if (btn(1)) player.vx = MOVE_SPEED
  else player.vx *= FRICTION

  // Jumping
  if (btnp(5) && player.grounded) {
    player.vy = JUMP_FORCE
    player.grounded = false
  }

  // Gravity
  player.vy += GRAVITY

  // Apply velocity
  player.x += player.vx
  player.y += player.vy

  // Simple ground collision
  if (player.y > 100) {
    player.y = 100
    player.vy = 0
    player.grounded = true
  }
}

Orbit Motion

let angle = 0
let centerX = 64
let centerY = 64
let radius = 30

function _update() {
  angle += 0.02
}

function _draw() {
  cls(0)

  // Calculate orbit position
  let x = centerX + cos(angle) * radius
  let y = centerY + sin(angle) * radius

  circfill(centerX, centerY, 4, 5) // Center
  circfill(x, y, 4, 11) // Orbiting object
}

Collision Detection

Circle vs Circle

function circleCollision(x1, y1, r1, x2, y2, r2) {
  let dx = x2 - x1
  let dy = y2 - y1
  let dist = sqrt(dx * dx + dy * dy)
  return dist < r1 + r2
}

// Usage
if (circleCollision(player.x, player.y, 4, enemy.x, enemy.y, 6)) {
  // Collision detected
}

Box vs Box (AABB)

function boxCollision(x1, y1, w1, h1, x2, y2, w2, h2) {
  return x1 < x2 + w2 && x1 + w1 > x2 && y1 < y2 + h2 && y1 + h1 > y2
}

// Usage
if (boxCollision(player.x, player.y, 8, 8, enemy.x, enemy.y, 8, 8)) {
  // Collision detected
}

Point vs Box

function pointInBox(px, py, bx, by, bw, bh) {
  return px >= bx && px < bx + bw && py >= by && py < by + bh
}

Tile-based Collision

function getTile(x, y) {
  let tileX = flr(x / 8)
  let tileY = flr(y / 8)
  return mget(tileX, tileY)
}

function isSolid(x, y) {
  let tile = getTile(x, y)
  return tile == 1 // Sprite 1 is solid
}

// Check before moving
function movePlayer(dx, dy) {
  let newX = player.x + dx
  let newY = player.y + dy

  // Check corners
  if (
    !isSolid(newX, newY) &&
    !isSolid(newX + 7, newY) &&
    !isSolid(newX, newY + 7) &&
    !isSolid(newX + 7, newY + 7)
  ) {
    player.x = newX
    player.y = newY
  }
}

Animation

Frame-based Animation

let player = {
  x: 64,
  y: 64,
  frame: 0,
  animTimer: 0,
  sprites: [0, 1, 2, 1], // Sprite indices
}

function _update() {
  player.animTimer += 1
  if (player.animTimer >= 8) {
    // Change frame every 8 ticks
    player.animTimer = 0
    player.frame = (player.frame + 1) % player.sprites.length
  }
}

function _draw() {
  cls(0)
  spr(player.sprites[player.frame], player.x, player.y)
}

State-based Animation

let player = {
  x: 64,
  y: 64,
  state: 'idle',
  frame: 0,
  timer: 0,
}

let animations = {
  idle: { sprites: [0, 1], speed: 15 },
  walk: { sprites: [2, 3, 4, 3], speed: 6 },
  jump: { sprites: [5], speed: 1 },
}

function _update() {
  // Determine state
  if (!player.grounded) {
    player.state = 'jump'
  } else if (btn(0) || btn(1)) {
    player.state = 'walk'
  } else {
    player.state = 'idle'
  }

  // Animate
  let anim = animations[player.state]
  player.timer += 1
  if (player.timer >= anim.speed) {
    player.timer = 0
    player.frame = (player.frame + 1) % anim.sprites.length
  }
}

function _draw() {
  cls(0)
  let anim = animations[player.state]
  spr(anim.sprites[player.frame], player.x, player.y)
}

Squash and Stretch

let ball = {
  x: 64,
  y: 64,
  vy: 0,
  scaleX: 1,
  scaleY: 1,
}

function _update() {
  ball.vy += 0.2 // Gravity
  ball.y += ball.vy

  // Bounce
  if (ball.y > 100) {
    ball.y = 100
    ball.vy = -ball.vy * 0.8

    // Squash on impact
    ball.scaleX = 1.4
    ball.scaleY = 0.6
  }

  // Stretch when falling fast
  if (ball.vy > 2) {
    ball.scaleX = 0.8
    ball.scaleY = 1.3
  }

  // Return to normal
  ball.scaleX += (1 - ball.scaleX) * 0.2
  ball.scaleY += (1 - ball.scaleY) * 0.2
}

function _draw() {
  cls(0)
  let w = 8 * ball.scaleX
  let h = 8 * ball.scaleY
  ovalfill(ball.x - w / 2, ball.y - h / 2, ball.x + w / 2, ball.y + h / 2, 8)
}

Particle Systems

Simple Particles

let particles = []

function spawnParticle(x, y) {
  add(particles, {
    x: x,
    y: y,
    vx: rnd(2) - 1,
    vy: rnd(2) - 1,
    life: 30,
    col: flr(rnd(3)) + 8,
  })
}

function _update() {
  // Spawn on button press
  if (btnp(5)) {
    for (let i = 0; i < 10; i++) {
      spawnParticle(64, 64)
    }
  }

  // Update particles
  for (let i = particles.length - 1; i >= 0; i--) {
    let p = particles[i]
    p.x += p.vx
    p.y += p.vy
    p.vy += 0.1 // Gravity
    p.life -= 1

    if (p.life <= 0) {
      particles.splice(i, 1)
    }
  }
}

function _draw() {
  cls(0)
  for (let p of all(particles)) {
    pset(p.x, p.y, p.col)
  }
}

Explosion Effect

function explode(x, y, count) {
  for (let i = 0; i < count; i++) {
    let angle = rnd(1)
    let speed = rnd(3) + 1
    add(particles, {
      x: x,
      y: y,
      vx: cos(angle) * speed,
      vy: sin(angle) * speed,
      life: 20 + flr(rnd(20)),
      col: flr(rnd(3)) + 8, // Red, orange, yellow
    })
  }
}

Trail Effect

let trail = []
let maxTrail = 20

function _update() {
  // Player movement
  if (btn(0)) player.x -= 2
  if (btn(1)) player.x += 2
  if (btn(2)) player.y -= 2
  if (btn(3)) player.y += 2

  // Add current position to trail
  add(trail, { x: player.x, y: player.y }, 0)

  // Remove old positions
  while (trail.length > maxTrail) {
    trail.shift()
  }
}

function _draw() {
  cls(0)

  // Draw trail (older = darker)
  for (let i = 0; i < trail.length; i++) {
    let t = trail[i]
    let col = flr((i / trail.length) * 7) + 1
    circfill(t.x, t.y, 2, col)
  }

  // Draw player
  circfill(player.x, player.y, 4, 7)
}

Camera Techniques

Follow Player

function _draw() {
  // Center camera on player
  camera(player.x - 64, player.y - 64)

  // Draw world
  map(0, 0, 0, 0, 32, 32)
  spr(0, player.x, player.y)

  // Draw HUD (reset camera)
  camera()
  print('HP: ' + player.hp, 2, 2, 7)
}

Smooth Camera Follow

let cam = { x: 0, y: 0 }
let camSpeed = 0.1

function _update() {
  // Target position (center on player)
  let targetX = player.x - 64
  let targetY = player.y - 64

  // Smooth interpolation
  cam.x += (targetX - cam.x) * camSpeed
  cam.y += (targetY - cam.y) * camSpeed
}

function _draw() {
  camera(cam.x, cam.y)
  // ... draw world
}

Camera Bounds

function updateCamera() {
  let targetX = player.x - 64
  let targetY = player.y - 64

  // Clamp to world bounds
  let worldWidth = 256 // World is 256 pixels wide
  let worldHeight = 256

  targetX = mid(0, targetX, worldWidth - 128)
  targetY = mid(0, targetY, worldHeight - 128)

  cam.x += (targetX - cam.x) * 0.1
  cam.y += (targetY - cam.y) * 0.1
}

Screen Shake

let shake = 0

function doShake(intensity) {
  shake = intensity
}

function _draw() {
  // Apply shake
  let shakeX = 0
  let shakeY = 0
  if (shake > 0) {
    shakeX = rnd(shake * 2) - shake
    shakeY = rnd(shake * 2) - shake
    shake *= 0.9 // Decay
    if (shake < 0.5) shake = 0
  }

  camera(cam.x + shakeX, cam.y + shakeY)
  // ... draw world
}

Map Scrolling

Infinite Horizontal Scroll

let scrollX = 0
let scrollSpeed = 1

function _update() {
  scrollX += scrollSpeed
}

function _draw() {
  cls(0)

  // Draw two copies for seamless scrolling
  let mapWidth = 128 * 8 // 128 tiles * 8 pixels

  let offset = scrollX % mapWidth
  camera(offset, 0)
  map(0, 0, 0, 0, 128, 16)

  camera(offset - mapWidth, 0)
  map(0, 0, 0, 0, 128, 16)

  camera()
}

Parallax Scrolling

let layers = [
  { speed: 0.2, y: 80 }, // Far background
  { speed: 0.5, y: 60 }, // Mid background
  { speed: 1.0, y: 0 }, // Foreground
]

function _draw() {
  cls(1)

  for (let layer of all(layers)) {
    let offset = flr(cam.x * layer.speed)
    // Draw layer at offset
    // (assumes each layer has its own map region)
  }
}

Procedural Generation

Random Dungeon Rooms

function generateRoom(x, y, w, h) {
  // Floor
  for (let ty = y; ty < y + h; ty++) {
    for (let tx = x; tx < x + w; tx++) {
      mset(tx, ty, 0) // Floor tile
    }
  }

  // Walls
  for (let tx = x; tx < x + w; tx++) {
    mset(tx, y, 1) // Top wall
    mset(tx, y + h - 1, 1) // Bottom wall
  }
  for (let ty = y; ty < y + h; ty++) {
    mset(x, ty, 1) // Left wall
    mset(x + w - 1, ty, 1) // Right wall
  }
}

function _init() {
  srand(12345) // Reproducible

  // Clear map
  for (let y = 0; y < 64; y++) {
    for (let x = 0; x < 128; x++) {
      mset(x, y, 1) // All walls
    }
  }

  // Generate random rooms
  for (let i = 0; i < 10; i++) {
    let rx = flr(rnd(100)) + 5
    let ry = flr(rnd(50)) + 5
    let rw = flr(rnd(8)) + 4
    let rh = flr(rnd(6)) + 4
    generateRoom(rx, ry, rw, rh)
  }
}

Terrain Noise

// Simple 1D noise for terrain height
function noise(x) {
  // Using sin for simple pseudo-noise
  return sin(x * 0.1) * 0.5 + sin(x * 0.05) * 0.3 + sin(x * 0.02) * 0.2
}

function generateTerrain() {
  for (let x = 0; x < 128; x++) {
    let height = 32 + flr(noise(x) * 20)

    for (let y = 0; y < 64; y++) {
      if (y >= height) {
        mset(x, y, 1) // Ground
      } else {
        mset(x, y, 0) // Air
      }
    }
  }
}

State Machines

Game State Manager

let state = 'title'

function _update() {
  if (state == 'title') updateTitle()
  else if (state == 'game') updateGame()
  else if (state == 'gameover') updateGameOver()
}

function _draw() {
  if (state == 'title') drawTitle()
  else if (state == 'game') drawGame()
  else if (state == 'gameover') drawGameOver()
}

function updateTitle() {
  if (btnp(5)) {
    state = 'game'
    initGame()
  }
}

function drawTitle() {
  cls(1)
  print('PRESS Z TO START', 20, 60, 7)
}

function updateGame() {
  // ... game logic
  if (player.hp <= 0) {
    state = 'gameover'
  }
}

function updateGameOver() {
  if (btnp(5)) {
    state = 'title'
  }
}

Entity States

let enemy = {
  x: 64,
  y: 64,
  state: 'patrol',
  stateTimer: 0,
  patrolDir: 1,
}

function updateEnemy() {
  enemy.stateTimer += 1

  if (enemy.state == 'patrol') {
    enemy.x += enemy.patrolDir

    // Turn around at edges
    if (enemy.x < 20 || enemy.x > 108) {
      enemy.patrolDir = -enemy.patrolDir
    }

    // Check for player
    if (abs(player.x - enemy.x) < 30) {
      enemy.state = 'chase'
      enemy.stateTimer = 0
    }
  } else if (enemy.state == 'chase') {
    // Move toward player
    let dir = sgn(player.x - enemy.x)
    enemy.x += dir * 1.5

    // Give up after a while
    if (enemy.stateTimer > 120) {
      enemy.state = 'patrol'
      enemy.stateTimer = 0
    }
  }
}

Object Pools

Bullet Pool

let bullets = []
let maxBullets = 20

function _init() {
  // Pre-create bullets
  for (let i = 0; i < maxBullets; i++) {
    add(bullets, { active: false, x: 0, y: 0, vx: 0, vy: 0 })
  }
}

function fireBullet(x, y, vx, vy) {
  // Find inactive bullet
  for (let b of all(bullets)) {
    if (!b.active) {
      b.active = true
      b.x = x
      b.y = y
      b.vx = vx
      b.vy = vy
      return
    }
  }
}

function _update() {
  // Fire on button press
  if (btnp(5)) {
    fireBullet(player.x, player.y, 0, -4)
  }

  // Update active bullets
  for (let b of all(bullets)) {
    if (b.active) {
      b.x += b.vx
      b.y += b.vy

      // Deactivate if off screen
      if (b.y < -8 || b.y > 136 || b.x < -8 || b.x > 136) {
        b.active = false
      }
    }
  }
}

function _draw() {
  cls(0)

  // Draw active bullets
  for (let b of all(bullets)) {
    if (b.active) {
      circfill(b.x, b.y, 2, 10)
    }
  }
}

Useful Patterns

Flash Effect

let flashTimer = 0

function flash() {
  flashTimer = 5
}

function _draw() {
  cls(0)
  // ... draw game

  // Flash overlay
  if (flashTimer > 0) {
    flashTimer -= 1
    rectfill(0, 0, 128, 128, 7)
  }
}

Fade Transition

let fadeLevel = 0 // 0 = normal, 15 = full black

function fadeOut() {
  fadeLevel = min(15, fadeLevel + 1)
}

function fadeIn() {
  fadeLevel = max(0, fadeLevel - 1)
}

function _draw() {
  // Draw game normally
  cls(0)
  // ...

  // Draw fade overlay using screen palette
  if (fadeLevel > 0) {
    // Darken all colors
    for (let i = 0; i < 16; i++) {
      let darkIndex = max(0, i - fadeLevel)
      pal(i, darkIndex, 1) // Screen palette
    }
  }
}

Timer Utility

let timers = []

function setTimer(frames, callback) {
  add(timers, { time: frames, fn: callback })
}

function updateTimers() {
  for (let i = timers.length - 1; i >= 0; i--) {
    timers[i].time -= 1
    if (timers[i].time <= 0) {
      timers[i].fn()
      timers.splice(i, 1)
    }
  }
}

// Usage
setTimer(60, function () {
  spawnEnemy()
})