如何利用nodejs實(shí)現(xiàn)命令行游戲
本文以貪吃蛇為例, 一步一步地分析如何實(shí)現(xiàn)一個(gè)命令行游戲.

實(shí)現(xiàn)原理
命令行輸入
- 通過 process.stdin 監(jiān)聽命令行輸入的按鍵, 改變小蛇的前進(jìn)的方向
畫面渲染
- 通過 ANSI 轉(zhuǎn)義序列 擦除之前的輸出
- 通過 process.stdout 每隔一段時(shí)間將畫面幀輸出到命令行
源碼解析
監(jiān)聽按鍵事件
使用過 yarn upgrade-interactive 命令更新 npm 依賴, 或者使用過 vue-cli 等腳手架創(chuàng)建過新項(xiàng)目的同學(xué)應(yīng)該都見過: 這些工具會(huì)在命令行輸出很多選項(xiàng), 通過上下按鍵可以移動(dòng)焦點(diǎn), 通過空格鍵可以選擇
那么這些操作是如何實(shí)現(xiàn)的呢? 下面通過 readline 和 process.stdin 來實(shí)現(xiàn)命令行監(jiān)聽按鍵事件:
process.stdin 是一個(gè)可讀流, 通過 readline.emitKeypressEvents 可以給可讀流注冊 keypress 事件, 通過 keypress 事件就能獲取到按鍵的值
readline.emitKeypressEvents(process.stdin) // 注冊 keypress 事件
process.stdin.setRawMode(true) // 開啟原始模式, 使輸入的每個(gè)字符帶上各種詳細(xì)屬性
process.stdin.on('keypress', (...args) => {
console.log(args)
// 按下方向鍵會(huì)輸出
// [
// undefined,
// {
// sequence: '\u001b[A',
// name: 'up',
// ctrl: false,
// meta: false,
// shift: false,
// code: '[A'
// }
// ]
})
注意: setRawMode 會(huì)使命令行按下 ctrl + c 不再發(fā)送終止信號, 可能需要自行處理退出邏輯
繪制幀畫面
輸出到命令行的游戲畫面默認(rèn)為 30 行 x 50 列, 將其劃分為一個(gè)二維數(shù)組, 每隔一段時(shí)間將二維數(shù)組的值打印出來并擦除之前打印的值, 即完成一次幀畫面的渲染
process.stdout 是一個(gè)可寫流, 調(diào)用 process.stdout.write 可以向命令行寫入數(shù)據(jù), nodejs 中 console.log 其實(shí)就是將數(shù)據(jù)寫入到 process.stdout 并換行
通過向命令行寫入開頭為 ANSI 轉(zhuǎn)義序列 的字符串可以 光標(biāo)移動(dòng)/滾動(dòng)屏幕/擦除顯示/顏色文本 等等功能, 想要深入了解可以自行搜索關(guān)鍵字學(xué)習(xí), 本文使用 ansi-escapes npm 包實(shí)現(xiàn)擦除功能
const ansiEscapes = require('ansi-escapes')
function clear(lines) {
process.stdout.write(ansiEscapes.eraseLines(lines)) // 可以擦除指定行數(shù)的輸出
}
根據(jù)游戲畫面的寬高定義一個(gè)二維數(shù)組, 小蛇的頭和身體視為畫面中的點(diǎn), 值為非空值, 空白畫面則為空字符串
let dots = []
for (let col = 0; col < wall.height; col++) {
dots[col] = new Array(wall.width).fill(' ')
}
在每一幀中, 小蛇的頭會(huì)向前進(jìn)的方向前進(jìn)一個(gè), 頭接著的第一節(jié)身體則會(huì)移動(dòng)到上一幀頭所在的位置, 以此類推每一節(jié)身體都會(huì)移動(dòng)到前一節(jié)身體的位置上, 所以需要定義一個(gè)數(shù)據(jù)記錄之前的頭和身體的位置
const SNAKE_HEAD = '@' // 頭的符號
const SNAKE_BODY = '○' // 身體的符號
function drawFrame() {
let dots = []
for (let col = 0; col < wall.height; col++) {
dots[col] = new Array(wall.width).fill(' ')
}
let nextBody = []
let head = next(snake.body[0]) // next 方法傳入當(dāng)前點(diǎn)的 x, y 坐標(biāo), 返回向前進(jìn)方向前進(jìn)一個(gè)的 x, y 坐標(biāo)
nextBody.push(head)
dots[head.y][head.x] = SNAKE_HEAD
for (let i = 1; i < snake.length; i++) {
let body = snake.body[i - 1]
dots[body.y][body.x] = SNAKE_BODY
nextBody.push(body)
}
screen.draw(dots) // 將二維數(shù)組中的點(diǎn)輸出到命令行中
// 更新蛇的狀態(tài)
snake.body = nextBody
snake.head = snake.body[0]
}
蛇吃鳥蛋邏輯
小蛇每吃到一個(gè)鳥蛋, 身體會(huì)長一節(jié), 并在畫面中隨機(jī)生成另一個(gè)鳥蛋. 到了這一步其實(shí)就很簡單了, 隨機(jī)生成一個(gè)點(diǎn)作為鳥蛋的位置, 插入到之前的二維數(shù)組中.
function layAEgg() {
let x = ~~(wall.width * Math.random())
let y = ~~(wall.height * Math.random())
return { x, y }
}
當(dāng)小蛇的頭的位置與鳥蛋的位置相同時(shí), 則視為蛇吃到鳥蛋, 蛇的長度加一, 并在尾部增加一節(jié)上一幀蛇尾的節(jié)點(diǎn)位置
const SNAKE_HEAD = '@'
const SNAKE_BODY = '○'
const BIRD_EGG = '●'
function drawFrame() {
let dots = []
for (let col = 0; col < wall.height; col++) {
dots[col] = new Array(wall.width).fill(' ')
}
let nextBody = []
let head = next(snake.body[0])
nextBody.push(head)
dots[head.y][head.x] = SNAKE_HEAD
for (let i = 1; i < snake.length; i++) {
let body = snake.body[i - 1]
dots[body.y][body.x] = SNAKE_BODY
nextBody.push(body)
}
// 判斷蛇頭位置在上一幀中是否為鳥蛋位置, 為真視為蛇吃到鳥蛋
if (prevDots && prevDots[head.y][head.x] === BIRD_EGG) {
let body = snake.body[snake.length - 1]
dots[body.y][body.x] = SNAKE_BODY
nextBody.push(body)
snake.length += 1
egg = null
prevDots = null
}
if (!egg) {
egg = layAEgg()
while (dots[egg.y][egg.x] !== ' ') {
egg = layAEgg()
}
}
dots[egg.y][egg.x] = BIRD_EGG
prevDots = dots // 保存上一幀的數(shù)據(jù), 用于下次繪制時(shí)判斷邏輯
screen.draw(dots)
snake.body = nextBody
snake.head = snake.body[0]
}
總結(jié)
至此, 命令行貪吃蛇游戲基本邏輯都已實(shí)現(xiàn), 剩下的就是使用定時(shí)器每隔一段時(shí)間繪制一次幀畫面. 其實(shí)幾乎任何像素游戲(如俄羅斯方塊/吃豆人等)都可以按照這個(gè)流程實(shí)現(xiàn), 不同的只是幀畫面的處理邏輯而已. 如果感興趣的話, 可以去我的 github 查看該 貪吃蛇游戲源碼
到此這篇關(guān)于如何利用nodejs實(shí)現(xiàn)命令行游戲的文章就介紹到這了,更多相關(guān)nodejs實(shí)現(xiàn)命令行游戲內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
node的process以及child_process模塊學(xué)習(xí)筆記
這篇文章主要介紹了node的process以及child_process模塊學(xué)習(xí)筆記,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-03-03
Nodejs+Socket.io實(shí)現(xiàn)通訊實(shí)例代碼
本篇文章主要介紹了Nodejs+Socket.io實(shí)現(xiàn)通訊實(shí)例代碼,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-02-02
nodejs個(gè)人博客開發(fā)第六步 數(shù)據(jù)分頁
這篇文章主要為大家詳細(xì)介紹了nodejs個(gè)人博客開發(fā)的數(shù)據(jù)分頁,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-04-04
docker中編譯nodejs并使用nginx啟動(dòng)
這篇文章主要介紹了docker中編譯nodejs并使用nginx啟動(dòng)的相關(guān)資料,需要的朋友可以參考下2017-06-06
nodejs微信開發(fā)之授權(quán)登錄+獲取用戶信息
這篇文章主要介紹了nodejs微信開發(fā)之授權(quán)登錄+獲取用戶信息,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2019-03-03
gulp加批處理(.bat)實(shí)現(xiàn)ng多應(yīng)用一鍵自動(dòng)化構(gòu)建
這篇文章主要給大家介紹了利用gulp加上批處理(.bat)實(shí)現(xiàn)ng多應(yīng)用一鍵自動(dòng)化構(gòu)建的相關(guān)資料,文中介紹的很詳細(xì),需要的朋友可以參考借鑒,下面來一起看看吧。2017-02-02

