如何利用nodejs实现命令行游戏


Posted in NodeJs onNovember 24, 2020

本文以贪吃蛇为例, 一步一步地分析如何实现一个命令行游戏.

如何利用nodejs实现命令行游戏

实现原理

命令行输入

  • 通过 process.stdin 监听命令行输入的按键, 改变小蛇的前进的方向

画面渲染

  • 通过 ANSI 转义序列 擦除之前的输出
  • 通过 process.stdout 每隔一段时间将画面帧输出到命令行

源码解析

监听按键事件

使用过 yarn upgrade-interactive 命令更新 npm 依赖, 或者使用过 vue-cli 等脚手架创建过新项目的同学应该都见过: 这些工具会在命令行输出很多选项, 通过上下按键可以移动焦点, 通过空格键可以选择

那么这些操作是如何实现的呢? 下面通过 readline 和 process.stdin 来实现命令行监听按键事件:

process.stdin 是一个可读流, 通过 readline.emitKeypressEvents 可以给可读流注册 keypress 事件, 通过 keypress 事件就能获取到按键的值

readline.emitKeypressEvents(process.stdin) // 注册 keypress 事件

process.stdin.setRawMode(true) // 开启原始模式, 使输入的每个字符带上各种详细属性

process.stdin.on('keypress', (...args) => {
 console.log(args)
 // 按下方向键会输出
 // [
 //  undefined,
 //  {
 //   sequence: '\u001b[A',
 //   name: 'up',
 //   ctrl: false,
 //   meta: false,
 //   shift: false,
 //   code: '[A'
 //  }
 // ]
})

注意: setRawMode 会使命令行按下 ctrl + c 不再发送终止信号, 可能需要自行处理退出逻辑

绘制帧画面

输出到命令行的游戏画面默认为 30 行 x 50 列, 将其划分为一个二维数组, 每隔一段时间将二维数组的值打印出来并擦除之前打印的值, 即完成一次帧画面的渲染

process.stdout 是一个可写流, 调用 process.stdout.write 可以向命令行写入数据, nodejs 中 console.log 其实就是将数据写入到 process.stdout 并换行

通过向命令行写入开头为 ANSI 转义序列 的字符串可以 光标移动/滚动屏幕/擦除显示/颜色文本 等等功能, 想要深入了解可以自行搜索关键字学习, 本文使用 ansi-escapes npm 包实现擦除功能

const ansiEscapes = require('ansi-escapes')

function clear(lines) {
 process.stdout.write(ansiEscapes.eraseLines(lines)) // 可以擦除指定行数的输出
}

根据游戏画面的宽高定义一个二维数组, 小蛇的头和身体视为画面中的点, 值为非空值, 空白画面则为空字符串

let dots = []
for (let col = 0; col < wall.height; col++) {
 dots[col] = new Array(wall.width).fill(' ')
}

在每一帧中, 小蛇的头会向前进的方向前进一个, 头接着的第一节身体则会移动到上一帧头所在的位置, 以此类推每一节身体都会移动到前一节身体的位置上, 所以需要定义一个数据记录之前的头和身体的位置

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 方法传入当前点的 x, y 坐标, 返回向前进方向前进一个的 x, y 坐标
 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) // 将二维数组中的点输出到命令行中
 
 // 更新蛇的状态
 snake.body = nextBody
 snake.head = snake.body[0]
}

蛇吃鸟蛋逻辑

小蛇每吃到一个鸟蛋, 身体会长一节, 并在画面中随机生成另一个鸟蛋. 到了这一步其实就很简单了, 随机生成一个点作为鸟蛋的位置, 插入到之前的二维数组中.

function layAEgg() {
 let x = ~~(wall.width * Math.random())
 let y = ~~(wall.height * Math.random())
 return { x, y }
}

当小蛇的头的位置与鸟蛋的位置相同时, 则视为蛇吃到鸟蛋, 蛇的长度加一, 并在尾部增加一节上一帧蛇尾的节点位置

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 // 保存上一帧的数据, 用于下次绘制时判断逻辑

 screen.draw(dots)
 snake.body = nextBody
 snake.head = snake.body[0]
}

总结

至此, 命令行贪吃蛇游戏基本逻辑都已实现, 剩下的就是使用定时器每隔一段时间绘制一次帧画面. 其实几乎任何像素游戏(如俄罗斯方块/吃豆人等)都可以按照这个流程实现, 不同的只是帧画面的处理逻辑而已. 如果感兴趣的话, 可以去我的 github 查看该 贪吃蛇游戏源码

到此这篇关于如何利用nodejs实现命令行游戏的文章就介绍到这了,更多相关nodejs实现命令行游戏内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

NodeJs 相关文章推荐
NodeJS制作爬虫全过程
Dec 22 NodeJs
NodeJS学习笔记之Connect中间件应用实例
Jan 27 NodeJs
使用DNode实现php和nodejs之间通信的简单实例
Jul 06 NodeJs
NodeJS与HTML5相结合实现拖拽多个文件上传到服务器的实现方法
Jul 26 NodeJs
基于NodeJS+MongoDB+AngularJS+Bootstrap开发书店案例分析
Jan 12 NodeJs
手把手教你把nodejs部署到linux上跑出hello world
Jun 19 NodeJs
NodeJs中express框架的send()方法简介
Jun 20 NodeJs
Nodejs中使用phantom将html转为pdf或图片格式的方法
Sep 18 NodeJs
基于nodejs res.end和res.send的区别
May 14 NodeJs
nodejs微信开发之接入指南
Mar 17 NodeJs
详解nodejs http请求相关总结
Mar 31 NodeJs
nodejs实现百度舆情接口应用示例
Feb 07 NodeJs
NodeJS模块Buffer原理及使用方法解析
Nov 11 #NodeJs
nodejs中内置模块fs,path常见的用法说明
Nov 07 #NodeJs
Nodejs + sequelize 实现增删改查操作
Nov 07 #NodeJs
nodejs+koa2 实现模仿springMVC框架
Oct 21 #NodeJs
nodejs使用Sequelize框架操作数据库的实现
Oct 21 #NodeJs
用Nodejs实现在终端中炒股的实现
Oct 18 #NodeJs
Nodejs在局域网配置https访问的实现方法
Oct 17 #NodeJs
You might like
解决MySQL中文输出变成问号的问题
2008/06/05 PHP
php str_pad() 将字符串填充成指定长度的字符串
2010/02/23 PHP
php图片处理函数获取类型及扩展名实例
2014/11/19 PHP
phpStudy访问速度慢和启动失败的解决办法
2015/11/19 PHP
PHP实现基于图的深度优先遍历输出1,2,3...n的全排列功能
2017/11/10 PHP
彻底搞懂JS无缝滚动代码
2007/01/03 Javascript
JavaScript Undefined,Null类型和NaN值区别
2008/10/22 Javascript
jQuery(1.6.3) 中css方法对浮动的实现缺陷分析
2011/09/09 Javascript
js实现屏蔽默认快捷键调用自定义事件示例
2013/06/18 Javascript
JS简单实现登陆验证附效果图
2013/11/19 Javascript
js取float型小数点后两位数的方法
2014/01/18 Javascript
在页面中输出当前客户端时间javascript实例代码
2016/03/02 Javascript
js 判断一个数字是不是2的n次方幂的实例
2017/11/26 Javascript
vue+mousemove实现鼠标拖动功能(拖动过快失效问题解决方法)
2018/08/24 Javascript
Vue2.0实现组件之间数据交互和通信操作示例
2019/05/16 Javascript
微信小程序websocket实现即时聊天功能
2019/05/21 Javascript
JavaScript中的ES6 Proxy的具体使用
2019/06/16 Javascript
[01:06:25]Secret vs Liquid 2018国际邀请赛淘汰赛BO3 第一场 8.25
2018/08/29 DOTA
[55:26]DOTA2-DPC中国联赛 正赛 Aster vs LBZS BO3 第一场 2月23日
2021/03/11 DOTA
python ansible服务及剧本编写
2017/12/29 Python
python实现名片管理系统项目
2019/04/26 Python
docker-py 用Python调用Docker接口的方法
2019/08/30 Python
解决pandas展示数据输出时列名不能对齐的问题
2019/11/18 Python
python修改linux中文件(文件夹)的权限属性操作
2020/03/05 Python
Django-xadmin+rule对象级权限的实现方式
2020/03/30 Python
CSS3径向渐变之大鱼吃小鱼之孤单的大鱼
2016/04/26 HTML / CSS
OPPO手机官方商城:中国手机市场出货量第一品牌
2017/10/18 全球购物
Lulu Guinness露露·吉尼斯官网:红唇包
2019/02/03 全球购物
Java程序员面试90题
2013/10/19 面试题
财务会计专业毕业生自荐信
2013/10/02 职场文书
股权收购意向书
2014/04/01 职场文书
公司租房协议书
2014/10/14 职场文书
小学教师学习党的群众路线教育实践活动心得体会
2014/10/31 职场文书
求职推荐信范文
2015/03/27 职场文书
Python读取和写入Excel数据
2022/04/20 Python
Python自动化实战之接口请求的实现
2022/05/30 Python