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

简介: 现在,和我一起用 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

相关文章
语雀的markdown常用语法
语雀的markdown常用语法
3921 0
语雀的markdown常用语法
|
Python
Markdown 拓展-Docsify 主题美化
docsify-themeable - A delightfully simple theme system for docsify.js https://jhildenbiddle.github.io/docsify-themeable/#/
1128 0
|
3天前
|
JavaScript 搜索推荐 前端开发
《VitePress 简易速速上手小册》第2章:Markdown 与页面创建(2024 最新版)
《VitePress 简易速速上手小册》第2章:Markdown 与页面创建(2024 最新版)
52 0
|
6月前
|
移动开发 前端开发 JavaScript
入坑吗?说说几个富文本编辑器
入坑吗?说说几个富文本编辑器
31 1
|
Linux Windows
Marp —用Markdown编写PPT
Marp —用Markdown编写PPT
130 0
Marp —用Markdown编写PPT
|
运维 应用服务中间件 nginx
在线编写Markdown
在线编写Markdown
164 0
|
机器人 图形学 Ruby
【Unity开发实战】—— 2D项目1 - Ruby‘s Adventure 游戏中动画制作(4-1)
【Unity开发实战】—— 2D项目1 - Ruby‘s Adventure 游戏中动画制作(4-1)
205 0
【Unity开发实战】—— 2D项目1 - Ruby‘s Adventure 游戏中动画制作(4-1)
|
前端开发 UED
互动应用开发p5.js——网页小游戏——贪吃蛇
贪吃蛇 一、实验内容: 基于课件改进贪吃蛇或者太空大战的小游戏,可以加入新的视觉效果,比如区分蛇头和蛇身;为食物增加特效;分数排行榜;行进改成可以循环等等; 尽可能丰富游戏的玩法。 评分标准: 游戏界面及功能 (70分) 用户体验 (10分) 代码规范(20分)
160 0
互动应用开发p5.js——网页小游戏——贪吃蛇
|
前端开发 索引
这款开源的 Markdown 编辑器,实在太好用了!
今天,小 D 给大家分享一款非常好用的微信 Markdown 编辑器 md[1]。
608 0
这款开源的 Markdown 编辑器,实在太好用了!
|
前端开发 JavaScript 开发工具
🤡公号文章排版利器 | 🐁尾汁Markdown转换工具来咯~(上)
从可定制和易用性两方面入手优化,这不第一个可用版本来咯~
202 0