码上开火车-Three.js 3D Web 游戏案例分享

简介: 码上开火车-Three.js 3D Web 游戏案例分享

码上开火车是一款 3D 单机策略游戏。创作这款游戏主要是为了参加 AMD 和码上掘金举办的马上掘金编程挑战赛。

欢迎大家体验:code.juejin.cn/pen/7163229…

设计和开发这款游戏,一共花费了大概两周的业余时间。

现在和大家聊聊这款游戏从设计到开发这个过程的总结。


团队


游戏的作者是我的独立游戏团队,一共两个人,我和 Z 哥。我主要负责策划、程序、项目管理等工作,Z 哥负责美术、模型、音乐、素材等工作。

很多小型游戏的开发,特别是 Web 游戏,一个人都可以独立完成。不要等到团队有多少人后才开始进行开发。

维护一个游戏团队是很难的,因为一款游戏通常是需要插图、音效、模型、程序、策划、剧情、运营等多个部分组成。维持这种团队需要各种人才,成本非常之高。而依靠游戏开发赚钱又比较难。自己做游戏,很多时候还不如直接接一些外包项目来做来钱更快。

所以,在很早的时候我就想好了,如果以后要组织一个游戏团队,人一定要少,而且每个人都应该是一个六边形战士,具有极强的学习能力和抗压能力,可以身兼数职、独当一面。我的目标也不高,首先能赚些钱,然后做一些我们自己喜欢的、小而美的东西。

很幸运,我遇到了 Z 哥这个技术狂。三十多岁的年纪,仍然每天下班后仍钻研数小时技术,并以一己之力将某超大型国企的信息技术部门的技术水平提升了数个档次。

我自己也可以做一些设计方面的工作,只是没有那么多足够的经验。让专业的人负责专业的事,可以让整个团队更具有战斗力。


设计


码上开火车是一款简单的策略游戏,没有复杂的剧情,也没有管线。所以它的设计可以简单分为 UI、模型、玩法三个方面。


UI 设计


UI 设计与模型设计都属于美工的范围。

通常设计 UI 时不会从零开始设计,而是会在一些设计网站上面寻找灵感,然后模仿一些 UI 进行修改。

可以从专业的游戏网站,比如 indienova 上面找一些资源或灵感。或者从专业的设计网站上寻找图标和模型。


模型设计


通常模型也是从一些资源网站上面进行下载,然后做一些调整再进行使用。比如 clara 上面就有一大堆 3D 模型。

推荐一款 gltf 格式的模型在线查看器:techbrood.com/tool?p=gltf…。虽然网页的 UI 设计有一股上个世纪的感觉,但确实非常好用。


玩法设计


游戏规则设计其实挺简单的。

第一版草图如下:

image.png

码上跑火车本质上就是一个无限循环单机游戏。

后面又经过一系列调整,最终就是现在大家看到的效果。


开发


一款游戏采用什么技术并没有什么太大区别。特别是单机游戏,技术不是关键,可玩性才是。

但还是要简单提一下技术。

首先在模式上可以简单分为 2D 和 3D。2D 游戏在技术上比较简单,可以跳过建模这一步,当然 2D 游戏也可以建模。一些简单的 2D 游戏,只使用图片等资源就可以了。3D 游戏则必须使用模型,对应的还需要有一整套的渲染引擎、物理引擎、粒子系统等。相比较 2D 游戏会复杂很多。

市面上有一些第三方的游戏引擎,会涵盖游戏设计、开发、测试、部署全流程。而大公司一般会有自己研发的游戏引擎。

比较流行的有 unitycocosegretlayabox 等,这些游戏引擎通常都支持脚本语言的开发,比如 JavaScript/TypeScript,并且大多数都支持多平台构建,比如一套源码可以打包成移动端、HTML 或者微信小游戏。

专注于 Web 游戏开发的引擎有 createjspixijs 等。

当然如果游戏足够简单的话,可以不使用游戏引擎。比如码上开火车就没有使用任何游戏引擎。它主要使用了四个主要的库/框架:

  • three.js:负责 3D 效果呈现。
  • jquery:负责 DOM 操作。
  • preloadjs:负责资源预加载。
  • tailwindcss:CSS 框架。


基于 jQuery 的数据响应式和组件化


游戏场景主体是 canvas,UI 是 DOM。UI 的数量非常少,而且没有页面切换。所以完全没有必要使用像 React 这类框架,jQuery 反而更简单粗暴。

但是 jQuery 没有数据响应式和组件化,难免在修改状态的时候同时维护 UI。

在游戏第一版完成的时候,就是这么做的。但是继续增加功能会很累。

所以我对它进行了简单地重构,利用 jQuery 实现了简易的数据响应式和组件化。这样更新数据后不再需要关注 UI 的变化。


class Reactivity {
  constructor({ state, updateCallback } = {}) {
    this.state = state;
    this.updateState = this.updateState.bind(this)
    this.updateCallback = updateCallback
    this.initUpdate()
    this.bindEvents()
  }
  updateState(key, value) {
    if (typeof value === 'function') {
      $(this).trigger(`state.change`, [key, value(this.state)]);
      return
    }
    $(this).trigger(`state.change`, [key, value]);
  }
  bindEvents() {
    $(this).on(`state.change`, this.updateCallback);
  }
  initUpdate() {
    Object.keys(this.state).forEach((key) => {
      this.updateCallback(null, key, this.state[key]);
    });
  }
}

对,只用了 27 行,就实现了 jQuery 数据响应式。

核心代码就是 .trigger和.trigger 和 .trigger.on 这两个 API。

使用方式如下:


const { updateState } = new Reactivity({
  state: {
    'key': 'value'
  },
  updateCallback: (event, key, value) => {
  }
})
updateState('key', 'hello')

组件化是基于 Reactivity 进行封装的,实现了事件绑定、指令、动态组件、静态组件、ref 等,代码不多,有 100 多行。


class Component {
  constructor({ template, state, methods } = {}) {
    this.refs = {}
    this.template = template || ``
    this.state = state || {}
    this.methods = methods || {}
    this.type = typeof this.template === 'string' ? 'static' : 'dynamic'
    this.el = typeof this.template === 'string' ? $(this.template) : $(this.template(this.state))
    this.mount()
    this.#bindEvents()
    this.#bindRef()
    this.updateState = new Reactivity({
      state: this.state,
      updateCallback: this.#updateElements.bind(this)
    }).updateState
  }
  mount() {
    $('body').append(this.el)
  }
  unmount() {
    this.el.remove()
  }
  #bindEvents() {
    const events = [
      'click',
      'change',
      'input',
      'blur',
      'focus',
      'keydown',
      'keyup',
      'keypress',
      'mouseenter',
      'mouseleave',
      'mouseover',
      'mouseout',
      'mousedown',
      'mouseup',
      'touchstart',
      'touchend',
      'touchmove',
      'touchcancel',
      'wheel',
      'scroll',
      'resize',
      'load',
      'unload',
      'abort',
      'error',
      'select',
      'contextmenu',
      'dblclick',
      'drag',
      'dragend',
      'dragenter',
      'dragleave',
      'dragover',
      'dragstart',
      'drop',
      'copy',
      'cut',
      'paste',
      'reset',
      'submit',
      'focusin',
      'focusout',
      'animationstart',
      'animationend',
      'animationiteration',
      'transitionend',
      'transitionstart',
      'transitioncancel',
      'transitionrun',
      'mousewheel'
    ]
    events.forEach(evt => {
      Array.from([this.el, ...this.el.find(`[on-${evt}]`)]).forEach(el => {
        const methodName = $(el).attr(`on-${evt}`)
        if (methodName) {
          $(el).on(evt, this.methods[methodName])
        }
      })
    })
  }
  #bindRef() {
    Array.from([this.el, ...this.el.find('[ref]')]).forEach(el => {
      const refName = $(el).attr('ref')
      if (refName) {
        this.refs[refName] = $(el)
      }
    })
  }
  #updateElements(evt, key, value) {
    this.state[key] = value
    if (this.type === 'dynamic') {
      this.unmount()
      this.el = $(this.template(this.state))
      this.mount()
      this.#bindEvents()
      this.#bindRef()
    }
    this.#render(this.el, key, value)
  }
  #render(el, key, value) {
    // data-class
    Array.from([el, ...el.find(`*[data-class*='${key}']`)]).forEach(el => {
      const reg = new RegExp(`{${key}}`, 'g')
      const classTemp = $(el).attr('data-class')
      if (classTemp) {
        const classRaw = classTemp.replaceAll(reg, value)
        $(el).attr('class', (i, val) => `${val} ${classRaw}`)
      }
    })
    // data-style
    Array.from([el, ...el.find(`*[data-style*=${key}]`)]).forEach(el => {
      const styleTemp = $(el).data('style')
      if (styleTemp) {
        const reg = new RegExp(`{${key}}`, 'g')
        const styleRaw = styleTemp.replaceAll(reg, value)
        $(el).attr('style', (i, val) => `${val || ''} ${styleRaw} `)
      }
    })
    // data-bind 
    Array.from([el, ...el.find(`[data-bind=${key}]`)]).forEach(el => {
      const reg = new RegExp(`{${key}}`, 'g')
      // data-temp
      const temp = $(el).data('temp')
      if (temp) {
        $(el).text(temp.replaceAll(reg, value))
      }
    })
    // data-show
    const dataShowEls = Array.from(el.find(`[data-show=${key}]`))
    const show = el.data('show')
    if (show) {
      dataShowEls.push(el)
    }
    dataShowEls.forEach(el => {
      if (value) {
        $(el).show()
      } else {
        $(el).hide()
      }
    })
  }
}


如何用 HTML 模拟一个滚动条?


游戏中有一个帮助面板,右侧的滚动条 UI 是高度定制的。

image.png

实现原理比较简单,左侧内容区使用一个容器元素包裹真正的内容元素。容器元素设置溢出隐藏,内容元素通过 translate-y 进行位置调整。

监听滚动条按钮的 mousedown 事件,然后在其中再监听 mousemove 事件,计算移动的 x 距离,然后设置按钮的 top 属性,然后计算按钮高度和整个滚动条高度的比例,将这个比例同步给左侧内容区,进行偏移调整。

最后监听 mouseup 事件,取消 mousedown 和 mousemove 的监听。

除了 mousedown 事件外,还需要处理 mousewheel 事件,也就是滚轮事件。处理逻辑基本与上述相同。


预加载的原理


为什么需要预加载呢?

因为游戏中通常会包含大量的图片、音频、字体包等资源。如果不预加载,那么在游戏的过程中容易出现各种卡顿或者页面某个部分空白的情况。而且现代浏览器一般会对 network 进行限制,最多只能同时发出 3 个请求,超过这个数量会出现取消请求的现象。

预加载就是维护了一个资源下载池,保证永远同时最多只有 3 个资源请求,每当有资源请求成功后,再发起下一个请求。


合作


关于码上开火车这款游戏的开发就聊到这里了。

如果你对游戏开发感兴趣,或者需要开发一些 3D 模型展示、3D 游戏、元宇宙等项目,欢迎联系我。



相关文章
|
1月前
|
XML JavaScript 前端开发
JavaScript控制台:提升Web开发技能的秘密武器
作为Web开发人员,掌握JavaScript控制台中的各种方法至关重要。本文介绍了22种实用的console方法,从基本的log()到高级的profile()和memory,每种方法都配有示例和说明,帮助开发者更高效地调试和记录信息。通过了解这些工具,您可以优化代码、提高开发速度,减少错误,使编码过程更加顺畅愉快。
40 1
JavaScript控制台:提升Web开发技能的秘密武器
|
26天前
|
JavaScript 前端开发 持续交付
构建现代Web应用:Vue.js与Node.js的完美结合
【10月更文挑战第22天】随着互联网技术的快速发展,Web应用已经成为了人们日常生活和工作的重要组成部分。前端技术和后端技术的不断创新,为Web应用的构建提供了更多可能。在本篇文章中,我们将探讨Vue.js和Node.js这两大热门技术如何完美结合,构建现代Web应用。
24 4
|
1月前
|
存储 JavaScript 前端开发
深入探索 Vue.js:构建现代 Web 应用的利器
【10月更文挑战第11天】深入探索 Vue.js:构建现代 Web 应用的利器
21 1
|
1月前
|
JavaScript 前端开发 网络架构
如何使用Vue.js构建响应式Web应用
【10月更文挑战第9天】如何使用Vue.js构建响应式Web应用
|
1月前
|
JavaScript 前端开发
如何使用Vue.js构建响应式Web应用程序
【10月更文挑战第9天】如何使用Vue.js构建响应式Web应用程序
|
1月前
|
JavaScript 前端开发 开发者
前端开发趋势:从Web Components到Vue.js
【10月更文挑战第9天】前端开发趋势:从Web Components到Vue.js
|
1月前
|
Web App开发 前端开发 JavaScript
JavaScript Web Full Stack 全栈开发者路线及内容推荐
本文详细介绍了一条全面的JavaScript全栈开发者学习路径,涵盖基础知识、前端和后端开发、数据库与API、MERN Stack与React Native、工程化与部署、安全与测试、未来趋势等方面。推荐了HTML5、CSS3、JavaScript(ES6+)、Node.js、React.js、Vue.js、Svelte、Tailwind CSS、Web Components等关键技术,并提供了丰富的书籍、博主和在线资源。此外,还回顾了JavaScript的历史,并推荐了多个活跃的社区和平台,帮助开发者紧跟技术前沿。
|
1月前
|
JavaScript 应用服务中间件 Apache
Node.js Web 模块
10月更文挑战第7天
30 0
|
JavaScript 前端开发
杨老师课堂之JavaScript案例全选、全不选、及反选
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/kese7952/article/details/79906084      JavaScript案例之全选、全不选、及反选 效果图: 思路:     1.
1038 0
|
4月前
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp的客户关系管理系统附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp的客户关系管理系统附带文章源码部署视频讲解等
97 2