Skip to content

Rendering teks dalam game web

Teks ada di mana-mana dalam game—skor, dialog, menu, angka kerusakan. Tutorial ini membahas berbagai pendekatan dan kapan sebaiknya masing-masing digunakan.

1) Dasar-dasar teks Canvas

Pendekatan paling sederhana:

js
ctx.font = '24px Arial'
ctx.fillStyle = '#ffffff'
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
ctx.fillText('Skor: 1000', canvas.width / 2, 50)

Jika Anda memerlukan jarak yang lebih rapat atau renggang untuk judul dan label HUD, konteks canvas kini mendukung letterSpacing dan wordSpacing secara langsung. Keduanya menerima string panjang CSS dan mencapai Baseline lintas peramban pada 2025, sehingga Anda tidak perlu lagi menggambar glif satu per satu untuk meniru kerning:

js
ctx.font = '24px sans-serif'
ctx.letterSpacing = '2px'
ctx.wordSpacing = '6px'
ctx.fillText('GAME OVER', 100, 100)

Atur ulang keduanya ke '0px' setelah selesai karena nilainya tetap tersimpan dalam konteks seperti font dan fillStyle.

2) Teks dengan garis luar

js
function drawTextWithOutline(ctx, text, x, y, fillColor, outlineColor, outlineWidth = 2) {
  ctx.font = '24px Arial'
  ctx.textAlign = 'center'
  ctx.textBaseline = 'middle'
  
  ctx.strokeStyle = outlineColor
  ctx.lineWidth = outlineWidth
  ctx.strokeText(text, x, y)
  
  ctx.fillStyle = fillColor
  ctx.fillText(text, x, y)
}

drawTextWithOutline(ctx, 'GAME OVER', 400, 300, '#fff', '#000', 3)

3) Teks dengan bayangan

js
function drawTextWithShadow(ctx, text, x, y, color, shadowOffset = 2) {
  ctx.font = '32px sans-serif'
  ctx.textAlign = 'left'
  ctx.textBaseline = 'top'
  
  // Bayangan
  ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'
  ctx.fillText(text, x + shadowOffset, y + shadowOffset)
  
  // Teks utama
  ctx.fillStyle = color
  ctx.fillText(text, x, y)
}

4) Rendering font bitmap

Untuk game seni piksel atau tampilan yang konsisten di berbagai platform:

js
class BitmapFont {
  constructor(image, charWidth, charHeight, chars) {
    this.image = image
    this.charWidth = charWidth
    this.charHeight = charHeight
    this.chars = chars
  }
  
  draw(ctx, text, x, y, scale = 1) {
    const w = this.charWidth * scale
    const h = this.charHeight * scale
    
    for (let i = 0; i < text.length; i++) {
      const charIndex = this.chars.indexOf(text[i].toUpperCase())
      if (charIndex === -1) continue
      
      const sx = (charIndex % 16) * this.charWidth
      const sy = Math.floor(charIndex / 16) * this.charHeight
      
      ctx.drawImage(
        this.image,
        sx, sy, this.charWidth, this.charHeight,
        Math.round(x + i * w), Math.round(y), w, h
      )
    }
  }
  
  measureText(text, scale = 1) {
    return text.length * this.charWidth * scale
  }
}

// Penggunaan
const font = new BitmapFont(fontImage, 8, 8, 
  'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.,!?-+:; '
)
font.draw(ctx, 'SKOR: 1000', 10, 10, 2)

5) Teks yang dirender sebelumnya untuk performa

Untuk teks statis, render sekali lalu gunakan kembali:

js
function createTextImage(text, font, color) {
  const tempCanvas = document.createElement('canvas')
  const tempCtx = tempCanvas.getContext('2d')
  
  tempCtx.font = font
  const metrics = tempCtx.measureText(text)
  
  tempCanvas.width = Math.ceil(metrics.width)
  tempCanvas.height = parseInt(font) * 1.5
  
  tempCtx.font = font
  tempCtx.fillStyle = color
  tempCtx.textBaseline = 'top'
  tempCtx.fillText(text, 0, 0)
  
  return tempCanvas
}

// Buat sekali
const titleImage = createTextImage('GAME SAYA', '48px Impact', '#fff')

// Gambar berkali-kali (cepat)
ctx.drawImage(titleImage, x, y)

6) Efek mesin tik

js
class TypewriterText {
  constructor(text, x, y, charsPerSecond = 20) {
    this.fullText = text
    this.x = x
    this.y = y
    this.speed = 1 / charsPerSecond
    this.timer = 0
    this.charIndex = 0
  }
  
  update(dt) {
    this.timer += dt
    while (this.timer >= this.speed && this.charIndex < this.fullText.length) {
      this.charIndex++
      this.timer -= this.speed
    }
  }
  
  draw(ctx) {
    ctx.font = '18px monospace'
    ctx.fillStyle = '#fff'
    ctx.fillText(this.fullText.slice(0, this.charIndex), this.x, this.y)
  }
  
  isComplete() {
    return this.charIndex >= this.fullText.length
  }
  
  skip() {
    this.charIndex = this.fullText.length
  }
}

7) Teks dengan pembungkusan kata

js
function wrapText(ctx, text, maxWidth) {
  const words = text.split(' ')
  const lines = []
  let currentLine = ''
  
  for (const word of words) {
    const testLine = currentLine ? currentLine + ' ' + word : word
    const metrics = ctx.measureText(testLine)
    
    if (metrics.width > maxWidth && currentLine) {
      lines.push(currentLine)
      currentLine = word
    } else {
      currentLine = testLine
    }
  }
  
  if (currentLine) lines.push(currentLine)
  return lines
}

function drawWrappedText(ctx, text, x, y, maxWidth, lineHeight) {
  const lines = wrapText(ctx, text, maxWidth)
  for (let i = 0; i < lines.length; i++) {
    ctx.fillText(lines[i], x, y + i * lineHeight)
  }
}

8) Sistem kotak dialog

js
class DialogueBox {
  constructor(x, y, width, height) {
    this.x = x
    this.y = y
    this.width = width
    this.height = height
    this.padding = 16
    this.lineHeight = 24
    this.text = ''
    this.displayedChars = 0
    this.charsPerSecond = 30
    this.timer = 0
  }
  
  show(text) {
    this.text = text
    this.displayedChars = 0
    this.timer = 0
  }
  
  update(dt) {
    if (this.displayedChars < this.text.length) {
      this.timer += dt
      const charsToAdd = Math.floor(this.timer * this.charsPerSecond)
      this.displayedChars = Math.min(this.displayedChars + charsToAdd, this.text.length)
      this.timer = this.timer % (1 / this.charsPerSecond)
    }
  }
  
  skip() {
    this.displayedChars = this.text.length
  }
  
  isComplete() {
    return this.displayedChars >= this.text.length
  }
  
  draw(ctx) {
    // Latar belakang kotak
    ctx.fillStyle = 'rgba(0, 0, 0, 0.8)'
    ctx.fillRect(this.x, this.y, this.width, this.height)
    
    // Bingkai
    ctx.strokeStyle = '#fff'
    ctx.lineWidth = 2
    ctx.strokeRect(this.x, this.y, this.width, this.height)
    
    // Teks
    ctx.font = '18px sans-serif'
    ctx.fillStyle = '#fff'
    ctx.textBaseline = 'top'
    
    const displayText = this.text.slice(0, this.displayedChars)
    const maxWidth = this.width - this.padding * 2
    const lines = wrapText(ctx, displayText, maxWidth)
    
    for (let i = 0; i < lines.length; i++) {
      ctx.fillText(
        lines[i],
        this.x + this.padding,
        this.y + this.padding + i * this.lineHeight
      )
    }
    
    // Indikator lanjut
    if (this.isComplete()) {
      ctx.fillText('▼', this.x + this.width - 24, this.y + this.height - 24)
    }
  }
}

9) Angka kerusakan melayang

js
class FloatingText {
  constructor(text, x, y, color = '#fff') {
    this.text = text
    this.x = x
    this.y = y
    this.color = color
    this.vy = -60
    this.lifetime = 1
    this.age = 0
  }
  
  update(dt) {
    this.y += this.vy * dt
    this.vy *= 0.95
    this.age += dt
  }
  
  draw(ctx) {
    const alpha = 1 - (this.age / this.lifetime)
    ctx.font = 'bold 20px sans-serif'
    ctx.textAlign = 'center'
    ctx.fillStyle = this.color
    ctx.globalAlpha = alpha
    ctx.fillText(this.text, Math.round(this.x), Math.round(this.y))
    ctx.globalAlpha = 1
  }
  
  isExpired() {
    return this.age >= this.lifetime
  }
}

// Pengelola
const floatingTexts = []

function spawnDamageNumber(damage, x, y) {
  const color = damage > 50 ? '#ff4444' : '#ffff44'
  floatingTexts.push(new FloatingText(damage.toString(), x, y, color))
}

function updateFloatingTexts(dt) {
  for (let i = floatingTexts.length - 1; i >= 0; i--) {
    floatingTexts[i].update(dt)
    if (floatingTexts[i].isExpired()) {
      floatingTexts.splice(i, 1)
    }
  }
}

10) Teks WebGL (font SDF)

Untuk game WebGL, font Signed Distance Field dapat diskalakan dengan tajam:

js
// Fragmen shader SDF
const sdfFragmentShader = `
  precision mediump float;
  
  uniform sampler2D u_texture;
  uniform vec4 u_color;
  varying vec2 v_texCoord;
  
  void main() {
    float distance = texture2D(u_texture, v_texCoord).a;
    float alpha = smoothstep(0.4, 0.6, distance);
    gl_FragColor = vec4(u_color.rgb, u_color.a * alpha);
  }
`

// Pustaka seperti msdf-bmfont-xml dapat menghasilkan atlas font SDF

Khusus untuk scene three.js, troika-three-text adalah pilihan yang bagus. Alih-alih membuat atlas terlebih dahulu, pustaka ini mengurai file .ttf/.otf/.woff dan menghasilkan glif SDF secara dinamis di web worker. Pustaka ini juga menangani kerning, ligatur, dan aksara kanan-ke-kiri untuk Anda.

Untuk sebagian besar game Canvas 2D, font bitmap atau teks Canvas yang dirender sebelumnya dapat digunakan dengan baik.

Terkait