Ich habe die Grundlagen von ts bereits in mehreren Artikeln gelernt. Heute werden wir ts verwenden, um ein kleines Schlangenspiel abzuschließen.
Spiel-Teardown
Wir werden unsere Aufgabe kurz zerlegen und analysieren.
- Zuerst sollten wir ein Fenster haben, das wir Bildschirm nennen. Lassen Sie die Schlange sich im Inneren bewegen, also sollten wir darüber nachdenken, eine große Kiste als Karte zu entwerfen. Unter Berücksichtigung von Nahrung und Schlangenzeichnung können wir
canvas
dies erreichen. - Zweitens werden wir auch zufällig Nahrung auf der Karte platzieren (Sie können auch überlegen, ob Nahrung auf den Körperknoten der Schlange erscheinen soll, was in diesem Artikel nicht berücksichtigt wird), sodass wir höchstwahrscheinlich eine Klasse erstellen, die zum Erstellen verwendet wird ein zufälliger Block. Das ist Essen.
- Dann betrachten wir die Schlange. Die Schlange sollte am Anfang auch ein zufälliger Block sein und dann Nahrung fressen und durch Bewegung wachsen.
Code
Als Nächstes werden wir basierend auf der obigen Demontage eine detaillierte Anforderungssortierung und Codeimplementierung durchführen.
Bildschirmimplementierung
Die Bildschirmimplementierung ist die einfachste. Wir haben uns entschieden, Leinwand zum Zeichnen von Futter und Schlangen zu verwenden, sodass wir direkt ein Leinwand-Tag als Bildschirm erstellen können.
<canvas width="500" height="500"></canvas>
Lebensmittelverwirklichung
-
Als nächstes überlegen wir, wie Essen umgesetzt werden soll. Da Sie sich entschieden haben, Lebensmittel auf Leinwand zu zeichnen, ist es am einfachsten, die Lebensmittel in einem Rechteck zu zeichnen. Das Zeichnen eines Rechtecks erfordert vier Parameter, nämlich die Koordinaten des Startpunkts sowie die Breite und Höhe. Wir setzen die Breite und Höhe des Lebensmittels auf 10, sodass die einzige Unsicherheit die Koordinaten des Startpunkts sind. Diese Koordinate bestimmt, wo auf dem Bildschirm er erscheinen wird.
-
Es ist auch zu beachten, dass seine Startposition auf dem Bewegungspfad der Schlange liegen muss. Die Breite unserer Schlange beträgt beispielsweise 10. Wenn der Startpunkt Ihres Futters bei den Koordinaten (11, 11) liegt, kann er es nicht essen auf einmal. Dieses Essen.
Daher sollten die Koordinaten des Lebensmittels ein Vielfaches von 10 sein und dürfen die Grenzen des Bildschirms nicht überschreiten. -
Wir müssen auch berücksichtigen, dass Lebensmittel nach dem Verzehr automatisch verschwinden sollten, sodass die Schlangenklasse auch über eine Löschmethode verfügen sollte, um sich selbst zu löschen.
Codeanzeige
class Drop {
width: number = 10
height: number = 10
x: number
y: number
color: string
constructor(
x: number = Math.floor(Math.random() * 49) * 10,
y: number = Math.floor(Math.random() * 49) * 10,
color: string = 'black'
) {
this.color = color
this.x = x
this.y = y
}
del() {
const ctx: CanvasRenderingContext2D = canvasEle.getContext('2d')!
ctx.clearRect(this.x, this.y, this.width, this.height)
}
}
Die Implementierung von Snake
Die Implementierung von Snake ist relativ komplizierter.
- Überlegen wir uns zunächst, wie der Körper der Schlange aussehen soll. Damit sie sich flexibel drehen kann, besteht die einfachste Möglichkeit darin, dass ihr Körper aus einzeln aneinandergefügten Rechtecken besteht. In diesem Fall können wir die obige Lebensmittelklasse direkt verwenden, weshalb ich
Drop
stattdessen die obige Klasse benanntFood
und der Klasse Farben hinzugefügt habe, um Schlangen von Lebensmitteln zu unterscheiden. - Als nächstes dachten wir, dass es einen Container geben sollte, um diese Rechtecke der Reihe nach zu speichern, da die Schlange aus mehreren Rechtecken besteht, also haben wir ein Array
list
zum Speichern der Körperdaten definiert.
Wir lassen ihn im Mittelpunkt der Karte spawnen und verwenden Rot, um ihn vom Futter zu unterscheiden.class Snake { list: Array<Drop> constructor() { this.list = [new Drop(250, 250, 'red')] } }
- Als nächstes denken wir über Mobilitätsmethoden nach. Wenn sich die Schlange bewegt, muss sie zunächst die Methode bestätigen. Wir können während der Initialisierung ein Richtungsattribut und einen Standardrichtungswert festlegen. Der nächste Schritt besteht darin, sich in die Richtung zu bewegen. Wie bewegt man sich? Wenn Sie einfach die Übersetzung verwenden, werden Sie feststellen, dass sich die Schlange anscheinend nicht flexibel drehen kann und sich der Körper der Schlange nicht beugt. Zu diesem Zeitpunkt müssen wir unser Denken ändern. Da die Schlange aus Rechtecken besteht, müssen wir nur die Rechtecke im Inneren kontrollieren. Natürlich geht es nicht darum, die Verschiebung des Rechtecks im Inneren zu steuern, sondern darum, das Rechteck hinzuzufügen und zu löschen. Stellen Sie sich vor, wenn sich die Schlange um ein Raster nach oben bewegt (hier legen wir das Grundraster auf 10 x 10 Einheiten fest), bedeutet dies, dass wir 10 vom y-Wert der Startpunktkoordinate dieses Rechtecks subtrahieren, sodass wir direkt eine Schlange erstellen Kopffeld? Subtrahieren Sie 10 vom Y-Wert der Startpunktkoordinate des Felds und löschen Sie dann direkt das letzte Feld der Schlange. Kann dies als Verschieben eines Frames angesehen werden?
Natürlich müssen wir auch die Situation beim Essen berücksichtigen. In diesem Fall müssen wir das Schwanzrechteck nicht löschen. Dann wird unsere Klasse so ergänztclass Snake { list: Array<Drop> direction: string constructor(direction: string = 'ArrowUp', speed: number = 100) { this.list = [new Drop(250, 250, 'red')] this.direction = direction } move() { let newHeader = JSON.parse(JSON.stringify(this.list[0])) const { x: newHeaderX, y: newHeaderY } = newHeader const { x: foodX, y: foodY } = food let isEatFood: boolean = false if (newHeaderX === foodX && foodY === newHeaderY) { isEatFood = true } switch (this.direction) { case 'ArrowUp': newHeader.y -= 10 break case 'ArrowDown': newHeader.y += 10 break case 'ArrowLeft': newHeader.x -= 10 break case 'ArrowRight': newHeader.x += 10 break } this.addHead(newHeader) // 判断是否吃到食物 if (isEatFood) { food.del() food = new Drop() renderDorp(food) } else { this.delFooter() } } addHead(dorp: Drop) { this.list.unshift(dorp) } delFooter() { const endDrop: Drop = this.list.pop()! const { x, y, width, height } = endDrop const ctx: CanvasRenderingContext2D = canvasEle!.getContext('2d')! ctx.clearRect(x, y, width, height) } }
- Ich sollte auch einige Sondersituationen berücksichtigen, z. B. ob das Bewegen an den Bildschirmrand meinen eigenen Körper frisst. Wir fügen ein neues Statusattribut hinzu, um festzustellen, ob er draußen ist, und füllen diese Methode weiterhin aus.
class Snake { list: Array<Drop> direction: string isOut: boolean constructor(direction: string = 'ArrowUp', speed: number = 100) { this.list = [new Drop(250, 250, 'red')] this.direction = direction this.boolean = false } move() { let newHeader = JSON.parse(JSON.stringify(this.list[0])) const { x: newHeaderX, y: newHeaderY } = newHeader const { x: foodX, y: foodY } = food let isEatFood: boolean = false if (newHeaderX === foodX && foodY === newHeaderY) { isEatFood = true } if (this.direction) { } switch (this.direction) { case 'ArrowUp': newHeader.y -= 10 break case 'ArrowDown': newHeader.y += 10 break case 'ArrowLeft': newHeader.x -= 10 break case 'ArrowRight': newHeader.x += 10 break } // 是否吃到自己 const isEatSelf = this.list.some(({ x, y }) => { if (x === newHeader.x && y === newHeader.y) { return true } }) if (isEatSelf) { alert('吃到自己了!') return } this.addHead(newHeader) // 判断是否吃到食物 if (isEatFood) { food.del() food = new Drop() renderDorp(food) } else { this.delFooter() } // 判断是否达到边界 if ( newHeaderX > 500 || newHeaderY > 500 || newHeaderX < 0 || newHeaderY < 0 ) { return alert('撞墙了!') } renderDorp(this.list) } addHead(dorp: Drop) { this.list.unshift(dorp) } delFooter() { const endDrop: Drop = this.list.pop()! const { x, y, width, height } = endDrop const ctx: CanvasRenderingContext2D = canvasEle!.getContext('2d')! ctx.clearRect(x, y, width, height) } }
Schlangen und Nahrung rendern
Wir haben die Klassen für Nahrung und Schlangen geschrieben, aber wir haben sie noch nicht wirklich auf die Leinwand gezeichnet. Als nächstes verwenden wir die Überladung von ts, um die Rendering-Klasse zu zeichnen.
// 创建渲染函数
function renderDorp(dorp: Drop): void
function renderDorp(dorps: Array<Drop>): void
function renderDorp(dorps: Drop | Array<Drop>) {
if (Array.isArray(dorps)) {
dorps.forEach((element: Drop) => {
const {
x, y, width, height, color } = element
const ctx: CanvasRenderingContext2D = canvasEle!.getContext('2d')!
ctx.fillStyle = color
ctx.fillRect(x, y, width, height)
})
} else {
const {
x, y, width, height, color } = dorps
const ctx: CanvasRenderingContext2D = canvasEle!.getContext('2d')!
ctx.fillStyle = color
ctx.fillRect(x, y, width, height)
}
}
Tastaturüberwachung
Wenn wir die Pfeiltasten verwenden, um die Bewegung der Schlange zu steuern, müssen wir Tastaturereignisse überwachen. Es ist zu beachten, dass wir uns bei einer Körperlänge von 1 normalerweise nach Belieben bewegen können, z. B. direkt von rechts nach links oder von oben nach unten. Wenn die Körperlänge jedoch nicht 1 beträgt, haben wir die Definition von Kopf und Schwanz. , es sollte sich nicht beliebig nach oben und unten oder nach links und rechts bewegen. Schließlich hat er keine vordere und hintere Lokomotive wie ein Zug.
window.addEventListener('keydown', function (e) {
const {
code } = e
const keys: string[] = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight']
if (keys.includes(code)) {
if (snake.list.length === 1) {
snake.direction = code
return
}
if (snake.direction === 'ArrowUp' && code === 'ArrowDown') {
return
}
if (snake.direction === 'ArrowDown' && code === 'ArrowUp') {
return
}
if (snake.direction === 'ArrowLeft' && code === 'ArrowRight') {
return
}
if (snake.direction === 'ArrowRight' && code === 'ArrowLeft') {
return
}
snake.direction = code
}
})
Fügen Sie abschließend den vollständigen Implementierungscode hinzu
const canvasEle = document.querySelector('canvas')!
let food: Drop
let snake: Snake
class Drop {
width: number = 10
height: number = 10
x: number
y: number
color: string
constructor(
x: number = Math.floor(Math.random() * 49) * 10,
y: number = Math.floor(Math.random() * 49) * 10,
color: string = 'black'
) {
this.color = color
this.x = x
this.y = y
}
del() {
const ctx: CanvasRenderingContext2D = canvasEle.getContext('2d')!
ctx.clearRect(this.x, this.y, this.width, this.height)
}
}
class Snake {
list: Array<Drop>
direction: string
isOut: boolean
constructor(direction: string = 'ArrowUp', speed: number = 100) {
this.list = [new Drop(250, 250, 'red')]
this.direction = direction
this.isOut = false
}
move() {
let newHeader = JSON.parse(JSON.stringify(this.list[0]))
const {
x: newHeaderX, y: newHeaderY } = newHeader
const {
x: foodX, y: foodY } = food
let isEatFood: boolean = false
if (newHeaderX === foodX && foodY === newHeaderY) {
isEatFood = true
}
if (this.direction) {
}
switch (this.direction) {
case 'ArrowUp':
newHeader.y -= 10
break
case 'ArrowDown':
newHeader.y += 10
break
case 'ArrowLeft':
newHeader.x -= 10
break
case 'ArrowRight':
newHeader.x += 10
break
}
// 是否吃到自己
const isEatSelf = this.list.some(({
x, y }) => {
if (x === newHeader.x && y === newHeader.y) {
return true
}
})
if (isEatSelf) {
this.isOut = true
return alert('吃到自己了!')
}
this.addHead(newHeader)
// 判断是否吃到食物
if (isEatFood) {
food.del()
food = new Drop()
renderDorp(food)
} else {
this.delFooter()
}
// 判断是否达到边界
if (
newHeaderX > 500 ||
newHeaderY > 500 ||
newHeaderX < 0 ||
newHeaderY < 0
) {
this.isOut = true
return alert('撞墙了!')
}
renderDorp(this.list)
}
addHead(dorp: Drop) {
this.list.unshift(dorp)
}
delFooter() {
const endDrop: Drop = this.list.pop()!
const {
x, y, width, height } = endDrop
const ctx: CanvasRenderingContext2D = canvasEle!.getContext('2d')!
ctx.clearRect(x, y, width, height)
}
}
// 创建渲染函数
function renderDorp(dorp: Drop): void
function renderDorp(dorps: Array<Drop>): void
function renderDorp(dorps: Drop | Array<Drop>) {
if (Array.isArray(dorps)) {
dorps.forEach((element: Drop) => {
const {
x, y, width, height, color } = element
const ctx: CanvasRenderingContext2D = canvasEle!.getContext('2d')!
ctx.fillStyle = color
ctx.fillRect(x, y, width, height)
})
} else {
const {
x, y, width, height, color } = dorps
const ctx: CanvasRenderingContext2D = canvasEle!.getContext('2d')!
ctx.fillStyle = color
ctx.fillRect(x, y, width, height)
}
}
;(function () {
food = new Drop()
snake = new Snake()
renderDorp(food)
let timer = setInterval(() => {
snake.move()
if (snake.isOut) {
clearInterval(timer)
}
}, 100)
window.addEventListener('keydown', function (e) {
const {
code } = e
const keys: string[] = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight']
if (keys.includes(code)) {
if (snake.list.length !== 1) {
if (snake.direction === 'ArrowUp' && code === 'ArrowDown') {
return
}
if (snake.direction === 'ArrowDown' && code === 'ArrowUp') {
return
}
if (snake.direction === 'ArrowLeft' && code === 'ArrowRight') {
return
}
if (snake.direction === 'ArrowRight' && code === 'ArrowLeft') {
return
}
}
snake.direction = code
}
})
})()
Dies ist nur eine einfache Version des Schlangeneffekts. Es wurde nicht gründlich getestet. Es wird definitiv Fehler geben. Ich hoffe, Sie können eine Nachricht zur Kommunikation hinterlassen!
Ich werde eine weitere erweiterte Komponentenbibliothek meines eigenen Plug-Ins element-ui starten, das noch verbessert wird. Ich hoffe, Sie werden es unterstützen.