前言
我觉得应该很多人都想做一款属于自己的游戏🌟,但是无奈不知道如何上手开发,加上之前人生重开模拟器爆火,我就想到其实看似页面简单的游戏也可以十分有趣,于是我也打算动手开发一款游戏,可是,就连那样的游戏我也嫌麻烦,就想着是否可以用 markdown
这种我们记录内容或者撰写文章常用的格式来完成一款游戏的制作呢?
所以,既然我这么懒,要不就做个“游戏引擎”吧!一个可以用 markdown
开发游戏的”游戏引擎“~
本文内容极其简单,请放心阅读~
引擎设计
上图中的内容尚未完全实现,且最后实现情况与此存在差异
既然已经确立了目标,那么下一步就是去把这个目标实现出来,首先我们需要一个确定的语法规则以对 markdown
的内容加以限制方便解析,在最开始去做一个功能的时候我们可以先一切从简,比如我们现在只包括以下内容:
- 唯一标识 id
- 名称 name
- 字体颜色 font-color
- 游戏节点内容 text
- 游戏的下一节点 next
- 背景颜色 background-color
- 选项/分支 option
之后我们不可能开发的时候要把所有的游戏单元写在一个 markdown
文件里,所以还设计多文件的管理,以及如何解析为一个 html
文件。
游戏单元:我这里的专有名词,上文中的
markdown
格式也是一个游戏单元的内容格式,对应着文字冒险游戏一个单页节点的全部内容。
还有很多细节需要处理,包括:
- 用图的方式可视化展示游戏单元的关联关系
- 辅助代码段,方便开发者快速开发不需要记住
markdown
格式 - 判断
markdown
的哪一处存在问题,报错提示
还会涉及文字动效
等交互相关问题,但是这都是细节,暂时不多介绍。
代码实现
关于如何新建一个 vscode 插件项目我已经在之前的「教你用十分钟开发一款提升工作体验的vscode插件🌿 」console, debugger一键删除|自定义代码模板中介绍过了,本篇我们直接开始讲业务逻辑的细节。
代码段
首先我们要做的第一件事就是去配置一个代码段,以优化开发者的体验。
而代码段的配置在 vscode
插件中十分的容易,首先在 package.json
里面进行这样的配置:
"categories": [ "Snippets" ], "contributes": { "snippets": [ { "language": "markdown", "path": "./snippets.json" } ], }
之后我们就可以去 snippets.json
去设置代码片段了:
{ "文字冒险游戏单元": { "prefix": "gameunit", "body": [ "@unitStart ", "id: ${1: 唯一标识(当id为0代表为初始节点,id不能重复且初始节点必须存在)} ", "name: ${2: 名称} ", "font-color: ${3: 字体颜色(默认白色)} ", "text: ${4: 文字内容} ", "next: ${5: 可选(代表本画面没有分支)} ", "background-color: ${6: 画面背景颜色(默认黑色)} ", "@unitOptionStart ", "text: ${8:可多个,本选项的描述} ", "next: ${9:可多个,本选项的下一个节点} ", "text: ${10:可多个,本选项的描述} ", "next: ${11:可多个,本选项的下一个节点} ", "@unitOptionEnd " ], "description": "文字冒险游戏单元" } }
代码片段我如此设计,基本上是依照上文中的思维导图。
命令入口
同样的,我们用上面的代码段辅助完成了一个 markdown
的编写,下一步就需要对这个 markdown
进行一大堆操作,使其可以变成一个可以运行的 html
文件,那么执行这些操作需要一个入口,监听用户的操作,并完成指定行为。
首先我们还是在 package.json
里面进行配置:
"activationEvents": [ "onCommand:text-game-maker.build" ], "main": "./extension.js", "categories": [ "Snippets" ], "contributes": { "snippets": [ { "language": "markdown", "path": "./snippets.json" } ], "commands": [ { "command": "text-game-maker.build", "title": "打包游戏" } ], "menus": { "explorer/context": [ { "command": "text-game-maker.build", "when": "filesExplorerFocus", "group": "navigation@1" } ] }
还是我们熟悉的味道,鼠标右键点击文件或者目录触发,文案是“打包游戏”,之后在 extension.js
中要完成指令的注册:
const vscode = require('vscode'); function activate(context) { let disposable = vscode.commands.registerCommand('text-game-maker.build', function (params) {}); context.subscriptions.push(disposable); } function deactivate() {} module.exports = { activate, deactivate }
注意,
params
含有右键的文件路径信息,我们会在后续的操作中用到✨
读取 markdown 文件
按照我们之前说的,我们为了方便维护,其实 markdown
文件可能不止一个,于是我们在读取文件信息的时候需要:
- 如果选择的文件是
markdown
文件,则只读取该文件内容 - 如果选择的是目录,则读取该目录下所有的
markdown
文件
具体代码如下,完成对全部 markdown
文件的读取🔥
function isDir(path) { const stat = lstatSync(path); return stat.isDirectory(); } const readContent = (path) => { let resContent = ''; if(isDir(path)) { let files = readdirSync(path); for(const file of files) { resContent += readContent(join(path, file)); } } else if(extname(path) == '.md') { const content = readFileSync(path, { encoding: 'utf-8' }); resContent += content; } return resContent; }
解析 markdown 文件
我们现在拿到 markdown
文件,现在我们需要把 markdown
解析成我们可以使用的数据结构,其实就是分成了两步:
- 按照
@unitOptionStart
分割字符串,并存入数组
现在可以说数组中的每一项都是包含完整的单个游戏单元的字符串
- 解析每个游戏单元字符串,形成对象
解析字符串,输出的是对象,对象包含游戏单元信息
const parseContent = (content) => { const unitLinesList = content.split('@unitStart').filter(item => item).map(item => item.split('\n')).map(list => { let res = []; for(const item of list) { const str = item.replace(/\s*/g,""); if(str) { res.push(str); } } return res; }) // const unitMap = new Map(); const unitList = []; for(const unitLines of unitLinesList) { const unit = {}; let isInOption = false; for(const line of unitLines){ if(!isInOption) { if(line != '@unitOptionStart') { const lineContent = line.split(":"); unit[lineContent[0]] = lineContent[1]; } else { isInOption = true; unit['unit-option'] = []; } } else { if(line != '@unitOptionEnd') { const lineContent = line.split(":"); const len = unit['unit-option'].length; if (len == 0){ unit['unit-option'][0] = {}; unit['unit-option'][0][lineContent[0]] = lineContent[1]; } else if (unit['unit-option'][len - 1][lineContent[0]]) { unit['unit-option'][len] = {}; unit['unit-option'][len][lineContent[0]] = lineContent[1]; } else if(!unit['unit-option'][len - 1][lineContent[0]]) { unit['unit-option'][len - 1][lineContent[0]] = lineContent[1]; } } else { isInOption = false; } } } // unitMap.set(unit['id'], unit); unitList.push(unit); } // return unitMap; return unitList; }
生成 html 文件
既然我们已经拿到数据结构,我们如何去用这个去生成一个 html
文件呢,我的思路是先写出一个 html
模板:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Game</title> <style> * { margin: 0; padding: 0; } body { color: #ffffff; font-weight: 600; font-size: 16px; background-color: black; overflow: hidden; } #container { margin: 5vh auto; height: 92vh; width: 82vw; overflow: hidden; border-radius: 10px; border: #ffffff 1px dashed; } #text-content { width: 100%; height: 40vh; text-align: center; padding-top: 20vh; } #option-container { width: 80vw; margin: 0 auto; height: 30vh; border: #ffffff 1px solid; border-radius: 10px; overflow: hidden; } #name { border: #ffffff 1px solid; transform: skewX(-45deg); padding-left: 5vw; padding-right: 3vw; height: 5vh; display: inline-block; line-height: 6vh; margin-left: -2vw; margin-top: -1vh; } #name>div { transform: skewX(45deg); } #options { margin-top: 5vh; display: flex; flex-wrap: wrap; } #options .option { line-height: 5vh; height: 5vh; width: 49%; cursor: pointer; text-align: center; transition: all 0.5s ease; } #options .end { line-height: 10vh; height: 10vh; font-size: 24px; width: 100%; cursor: pointer; text-align: center; } #options .option:hover { font-size: 25px; } #options .option:hover:before { content: "🌟"; } #goback-begin { cursor: pointer; position: absolute; right: 10vw; } </style> </head> <body> <div id="container"> <div id="goback-begin">从头开始🌿</div> <div id="text-content"> </div> <div id="option-container"> <div id="name"></div> <div id="options"></div> </div> </div> <script> let currentUnit; const unitList = [{ "id": "0", "name": "寒草", "font-color": "red", "text": "你是谁", "unit-option": [{ "text": "a", "next": "1" }, { "text": "b", "next": "2" }] }, { "id": "1", "name": "小辛", "font-color": "red", "text": "你是谁", "next": "2", "background-color": "white" }, { "id": "2", "name": "xxx", "text": "好的" }]; const unitMap = new Map(); for (const unit of unitList) { unitMap.set(unit.id, unit); } const containerDom = document.getElementById('container'); const textContentDom = document.getElementById('text-content'); const nameDom = document.getElementById('name'); const optionContainerDom = document.getElementById('option-container'); const optionDom = document.getElementById('options'); const resetDom = document.getElementById('goback-begin'); resetDom.onclick = function () { init(); } function init() { currentUnit = unitMap.get("0"); update(); } function update() { // name if (!currentUnit.name) { nameDom.style.opacity = 0; } else { nameDom.style.opacity = 1; nameDom.innerHTML = '<div>'+ currentUnit.name +'</div>'; } // text textContentDom.innerText = currentUnit.text; // font-color document.body.style.color = currentUnit['font-color'] || '#fff'; containerDom.style.borderColor = currentUnit['font-color'] || '#fff'; optionContainerDom.style.borderColor = currentUnit['font-color'] || '#fff'; nameDom.style.borderColor = currentUnit['font-color'] || '#fff'; // background-color document.body.style.background = currentUnit['background-color'] || '#000'; // options if (!currentUnit.next && !currentUnit['unit-option']) { optionDom.innerHTML = '<div class="end">End</div>'; } else if (currentUnit.next) { optionDom.innerHTML = '<div class="option">Next</div>'; document.getElementsByClassName('option')[0].onclick = function () { currentUnit = unitMap.get(currentUnit.next); update(); } } else { let domStr = ''; for (const option of currentUnit['unit-option']) { domStr += '<div class="option">' + option.text + '</div>'; } optionDom.innerHTML = domStr; const optionDomList = document.getElementsByClassName('option'); Array.from(optionDomList).forEach((optionDom, index) => { optionDom.onclick = function () { currentUnit = unitMap.get(currentUnit['unit-option'][index].next); update(); } }) } } init(); </script> </body> </html>
其中后面的 js
代码会根据 unitList
的内容动态改变页面内容,那我们这个思路就有了,无非是动态改变 html
文件中 unitList
的内容,之后把文件写入到指定文件中:
writeFileSync(join(dirPath, 'text-game.html'), ` html 模板A ${JSON.stringify(unitList)} html 模板B ` , { encoding: 'utf-8' });
结果展示
最后的页面大概就是这个样子,后续还会加入更多的细节和动画效果,还包括音频或者视频引入~
结束语
这篇文章就接近了尾声,其实整体思路比较直白,就是解析 markdown
文件,并动态生成 html
,未来寒草还会继续与大家一起用技术创造快乐🌟
如果大家喜欢我的文章,点赞➕关注就是最大的支持,感谢各位伙伴了哦🌿
写在最后
山海的浩瀚
宇宙的浪漫
都在我内心翻腾
在推着我前进
—To Be Continued—