现在,和我一起用 markdown 开发一款游戏🌿

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
简介: 现在,和我一起用 markdown 开发一款游戏🌿

前言


我觉得应该很多人都想做一款属于自己的游戏🌟,但是无奈不知道如何上手开发,加上之前人生重开模拟器爆火,我就想到其实看似页面简单的游戏也可以十分有趣,于是我也打算动手开发一款游戏,可是,就连那样的游戏我也嫌麻烦,就想着是否可以用 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

相关文章
|
7月前
|
存储 JavaScript 前端开发
使用JavaScript制作一个在线记事本
使用JavaScript制作一个在线记事本
|
Python
Markdown 拓展-Docsify 主题美化
docsify-themeable - A delightfully simple theme system for docsify.js https://jhildenbiddle.github.io/docsify-themeable/#/
1278 0
|
25天前
|
人工智能 移动开发 前端开发
Markdown-to-Image:开源的在线 Markdown 转海报编辑器
Markdown-to-Image 是一款开源的在线 Markdown 转海报编辑器,能够将 Markdown 文本内容转换为图像,适用于创建社交媒体帖子、海报和其他视觉内容。该工具支持多种输出格式,并允许用户自定义样式,适用于多种应用场景。
67 4
Markdown-to-Image:开源的在线 Markdown 转海报编辑器
|
7天前
|
存储 安全 Linux
全平台免费的在线笔记本(支持markdown、mermaid)
StackEdit是一款基于浏览器的Markdown编辑器,支持跨平台使用,无需安装,可将笔记存储在gitee、github等平台上。其优势包括内容安全免费、多平台同步、离线可用、支持UML图和流程图绘制等。通过简单的步骤即可完成注册、登录和笔记创作,并能轻松实现在线共享。
27 0
|
2月前
|
前端开发 开发者
大模型代码能力体验报告之贪吃蛇小游戏《二》:OpenAI-Canvas-4o篇 - 功能简洁的文本编辑器加一点提示词语法糖功能
ChatGPT 的Canvas是一款简洁的代码辅助工具,提供快速复制、版本管理、选取提问、实时编辑、代码审查、代码转写、修复错误、添加日志和注释等功能。相较于 Claude,Canvas 更加简单易用,但缺少预览功能,适合一般开发者使用。
|
7月前
|
Web App开发 移动开发 搜索推荐
常见的Markdown在线编辑器
在线Markdown编辑器提供了更加稳定和流畅的用户体验。用户无需下载安装任何软件,只需打开浏览器,即可在任何设备上轻松使用这款编辑器,实现随时随地的写作。基于HTML5的在线Markdown编辑器可实现即时的编辑和预览功能
116 2
MarkDown编辑器-MarkText使用文档
1.Why MarkText typora要收费使用了,🤔我们可以使用免费的开源软件MarkText来编写MarkDown文档 MarkText官方承认,将会永远免费开源此软件 MarkText 是一个带有各种降价扩展的降价实时预览编辑器。您可以简单地编写和编辑文本 安装地址: MarkText安装
1357 0
MarkDown编辑器-MarkText使用文档
|
运维 应用服务中间件 nginx
在线编写Markdown
在线编写Markdown
208 0
|
前端开发 JavaScript 数据建模
JavaScript 使用 Markdown 制作 PPT
markdown 对于开发者来说是一个熟悉的文档格式,编写文档或者博客首选的格式。markdown 文档可以转换为HTML进行展示。在文章《2021 年 6 个GitHub推荐前端项目》中介绍了一个将 markdown 转换为幻灯片的脚本库 Slidev 。
372 0
JavaScript 使用 Markdown 制作 PPT
|
前端开发 索引
这款开源的 Markdown 编辑器,实在太好用了!
今天,小 D 给大家分享一款非常好用的微信 Markdown 编辑器 md[1]。
699 0
这款开源的 Markdown 编辑器,实在太好用了!