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

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 现在,和我一起用 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

相关文章
|
26天前
|
安全 开发者
高效的markdown效率工具
高效的markdown效率工具
23 0
高效的markdown效率工具
|
15天前
|
前端开发 开发者
大模型代码能力体验报告之贪吃蛇小游戏《二》:OpenAI-Canvas-4o篇 - 功能简洁的文本编辑器加一点提示词语法糖功能
ChatGPT 的Canvas是一款简洁的代码辅助工具,提供快速复制、版本管理、选取提问、实时编辑、代码审查、代码转写、修复错误、添加日志和注释等功能。相较于 Claude,Canvas 更加简单易用,但缺少预览功能,适合一般开发者使用。
|
5月前
|
开发者
Markdown:解放排版,简洁高效的文字创作神器!
Markdown 是一种轻量级标记语言,以易读易写著称,常用于生成 HTML 页面。其简洁的语法加速了排版,尤其在写作、博客和文档领域广泛应用。虽然不擅长复杂排版,但能轻松实现字体大小调整、插入表格、图片和超链接等。Markdown 通过键盘快捷操作,避免了 Word 等软件的繁琐设置。本文将深入讲解 Markdown 语法,助你提升效率。Markdown 适合快速学习,兼容各种文本编辑器,支持导出多种格式,广泛应用于 GitHub 和多个在线平台。
73 0
|
机器人 图形学 Ruby
【Unity开发实战】—— 2D项目1 - Ruby‘s Adventure 游戏中动画制作(4-1)
【Unity开发实战】—— 2D项目1 - Ruby‘s Adventure 游戏中动画制作(4-1)
237 0
【Unity开发实战】—— 2D项目1 - Ruby‘s Adventure 游戏中动画制作(4-1)
|
前端开发 UED
互动应用开发p5.js——网页小游戏——贪吃蛇
贪吃蛇 一、实验内容: 基于课件改进贪吃蛇或者太空大战的小游戏,可以加入新的视觉效果,比如区分蛇头和蛇身;为食物增加特效;分数排行榜;行进改成可以循环等等; 尽可能丰富游戏的玩法。 评分标准: 游戏界面及功能 (70分) 用户体验 (10分) 代码规范(20分)
210 0
互动应用开发p5.js——网页小游戏——贪吃蛇
|
运维 应用服务中间件 nginx
在线编写Markdown
在线编写Markdown
202 0
|
JSON 前端开发 JavaScript
|
前端开发 索引
这款开源的 Markdown 编辑器,实在太好用了!
今天,小 D 给大家分享一款非常好用的微信 Markdown 编辑器 md[1]。
685 0
这款开源的 Markdown 编辑器,实在太好用了!
|
开发者
🤡公号文章排版利器 | 🐁尾汁Markdown转换工具来咯~(下)
从可定制和易用性两方面入手优化,这不第一个可用版本来咯~
170 0
|
前端开发 JavaScript 开发工具
🤡公号文章排版利器 | 🐁尾汁Markdown转换工具来咯~(上)
从可定制和易用性两方面入手优化,这不第一个可用版本来咯~
242 0