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:
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:
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
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
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:
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:
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
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
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
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
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:
// 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 SDFKhusus 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
- Loop game Canvas 2D
- Rendering seni piksel
- Dasar-dasar WebGL
- Canvas game responsif — menskalakan teks sesuai resolusi canvas dan DPI
- Tempat Menemukan Aset Game Gratis — sumber font dan aset UI