Web oyunlarında metin oluşturma
Metinler oyunların her yerindedir: skorlar, diyaloglar, menüler ve hasar sayıları. Bu eğitimde farklı yaklaşımları ve her birinin ne zaman kullanılacağını ele alacağız.
1) Canvas metninin temelleri
En basit yaklaşım:
ctx.font = '24px Arial'
ctx.fillStyle = '#ffffff'
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
ctx.fillText('Score: 1000', canvas.width / 2, 50)Başlıklar ve HUD etiketleri için daha dar veya geniş aralıklara ihtiyacınız varsa Canvas bağlamı artık letterSpacing ve wordSpacing özelliklerini doğrudan destekliyor. Her ikisi de CSS uzunluk dizeleri kabul eder ve 2025'te tarayıcılar arası Baseline desteğine ulaştı. Bu nedenle karakter aralığını taklit etmek için glifleri artık tek tek çizmeniz gerekmez:
ctx.font = '24px sans-serif'
ctx.letterSpacing = '2px'
ctx.wordSpacing = '6px'
ctx.fillText('GAME OVER', 100, 100)Bu değerler font ve fillStyle gibi bağlamda kalıcı olduğundan işiniz bittiğinde ikisini de '0px' değerine sıfırlayın.
2) Konturlu metin
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) Gölgeli metin
function drawTextWithShadow(ctx, text, x, y, color, shadowOffset = 2) {
ctx.font = '32px sans-serif'
ctx.textAlign = 'left'
ctx.textBaseline = 'top'
// Shadow
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'
ctx.fillText(text, x + shadowOffset, y + shadowOffset)
// Main text
ctx.fillStyle = color
ctx.fillText(text, x, y)
}4) Bitmap yazı tipi oluşturma
Piksel sanatlı oyunlar veya platformlar arasında tutarlı bir görünüm için:
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
}
}
// Usage
const font = new BitmapFont(fontImage, 8, 8,
'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.,!?-+:; '
)
font.draw(ctx, 'SCORE: 1000', 10, 10, 2)5) Performans için önceden oluşturulmuş metin
Sabit metinleri bir kez oluşturup yeniden kullanın:
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
}
// Create once
const titleImage = createTextImage('MY GAME', '48px Impact', '#fff')
// Draw many times (fast)
ctx.drawImage(titleImage, x, y)6) Daktilo efekti
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) Sözcük kaydırmalı metin
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) Diyalog kutusu sistemi
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) {
// Box background
ctx.fillStyle = 'rgba(0, 0, 0, 0.8)'
ctx.fillRect(this.x, this.y, this.width, this.height)
// Border
ctx.strokeStyle = '#fff'
ctx.lineWidth = 2
ctx.strokeRect(this.x, this.y, this.width, this.height)
// Text
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
)
}
// Continue indicator
if (this.isComplete()) {
ctx.fillText('▼', this.x + this.width - 24, this.y + this.height - 24)
}
}
}9) Havada süzülen hasar sayıları
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
}
}
// Manager
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) WebGL metni (SDF yazı tipleri)
WebGL oyunlarında Signed Distance Field yazı tipleri ölçeklendirildiğinde netliğini korur:
// SDF shader fragment
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);
}
`
// Libraries like msdf-bmfont-xml can generate SDF font atlasesÖzellikle three.js sahneleri için troika-three-text iyi bir seçenektir. Önceden bir atlas hazırlamak yerine .ttf/.otf/.woff dosyalarını ayrıştırır, SDF gliflerini bir web worker içinde anında oluşturur ve karakter aralığını, bitişik harfleri ve sağdan sola yazılan alfabeleri sizin yerinize işler.
Çoğu 2D Canvas oyunu için bitmap yazı tipleri veya önceden oluşturulmuş Canvas metinleri iyi sonuç verir.
İlgili içerikler
- Canvas 2D oyun döngüsü
- Piksel sanat oluşturma
- WebGL'in temelleri
- Duyarlı oyun canvas'ı — metni canvas çözünürlüğü ve DPI ile ölçeklendirme
- Ücretsiz oyun varlıkları nerede bulunur? — yazı tipi ve kullanıcı arayüzü varlık kaynakları