整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

推荐10个经典小游戏开源项目(Vue、React版)

家好,我是Echa。

前段时间有00后前端程序员粉丝私信小编问道:小时候我们玩什么小游戏,用Vue和React可以开发简易的Web站点小游戏吗?这一类的问题。立马勾起了小编的小时候玩的那些经典小游戏爱的那么痴迷。

经典小游戏爱的那么痴迷

作为80后的小编,虽然秃头程序员没有头发,但是童心还是一直都在的。对于童年时玩过的那些经典游戏,我们始终难以忘怀。回忆和怀念经典最好的方式就是重新体验它们!GitHub作为程序员们的开源宝库,有着很多非常好的项目。.其中有许多可以称之为经典,像《俄罗斯方块》、《记忆翻牌》、《扫雷》、《贪吃蛇》、《坦克大战》等等陪伴我们度过了儿时快乐的时光。

小霸王学习机 一句“小霸王其乐无穷啊”能够勾起多少八零九零的集体回忆,曾经靠一副手柄就能爽快打完魂斗罗、沙罗曼蛇、雪人兄弟、忍者神龟、超级玛丽..


小霸王


借此机会,小编给大家好物分享10个经典小游戏开源项目(Vue、React版),免费送一个。希望忙碌的粉丝们劳逸结合,可以玩玩,放松放松,有利于身心健康。不过千万别成谜语游戏无法自拔的那种,玩也有一个度。

下面小编给一一介绍,看看哪款游戏是你们的最爱:你可以在试玩过程中自定义游戏关卡并学习源代码。

全文大纲

  1. react-tetris - 经典俄罗斯方块小游戏
  2. backbone-game-engine - 超级马里奥是比较经典的GBA小游戏,红帽子蓝吊带的大胡子工人陪伴着很多90后度过童年。
  3. battle-city- 使用 React 将各类素材封装为对应的组件开发的坦克大战小游戏。
  4. snake-vue3 - 基于 Vue 3.3、Vite、Vuex 实现的经典贪吃蛇小游戏。
  5. minesweeper-react - 用 CSS 复制 Win98 的风格扫雷小游戏。
  6. threejs-tetris-react - 基于 Three.js、React、TypeScript 实现的 3D 俄罗斯方块游戏
  7. habitica - 它会将你培养习惯的过程,当作一个 RPG 角色扮演游戏。
  8. game-application-using-react-and-typescript - 使用 React 和 TypeScript 构建的简单 2D 蛇游戏。
  9. 3072-是一款受流行游戏“2048”启发的数字合并游戏.
  10. 3d-chess-v2- 使用 React、Redux Toolkit、ThreeJS、React Three Fiber、ChessJS 和 ChakraUI 构建的经典国际象棋游戏。
  11. frontend-concentration-or-memory - 使用 Vue3.3、Pinia、Webpack、TypeScript 开发的一款记忆翻牌游戏。

react-tetris - 经典俄罗斯方块小游戏

Github:https://github.com/chvin/react-tetris

体验:https://chvin.github.io/react-tetris/?lan=en


react-tetris - 经典俄罗斯方块小游戏

复刻经典的俄罗斯方块,该项目采用 React+Redux+Immutable 的技术栈。这款游戏的复刻程度堪称像素级别,不仅体现在画面上,还有流畅度、玩法、音效等方面都做到了极致。

俄罗斯方块是一直各类程序语言热衷实现的经典游戏,JavaScript的实现版本也有很多,用React 做好俄罗斯方块则成了我一个目标。

backbone-game-engine - 超级马里奥是比较经典的GBA小游戏,红帽子蓝吊带的大胡子工人陪伴着很多90后度过童年。

Github:https://github.com/martindrapeau/backbone-game-engine

体验:http://martindrapeau.github.io/backbone-game-engine/super-mario-bros/index.html


backbone-game-engine - 超级马里奥


超级马里奥是比较经典的GBA游戏了,这个红帽子蓝吊带的大胡子工人陪伴着很多90后度过童年。这个游戏支持游戏自定义道具,充分回味童年的乐趣。


特性:

  • 基于Backbone。事件、模型、集合、继承和RESTful持久性。为什么要重新发明轮子?
  • 仅HTML5画布。没有jQuery,尽可能少的DOM操作。
  • 移动优化。构建为在支持透明触摸和视口的移动设备上运行。一切都针对最大每秒帧数(FPS)进行了优化。
  • 使用CocoonJS实现原生。。专为在鲁德的CocoonJS canvas+中运行而构建。在iOS和Android上部署本机。
  • 2D平台生成器。内置侧面滚动条。内置精灵类,精灵表,角色,英雄,四叉树碰撞检测,世界和编辑器。
  • 没有汇编。您不需要安装node、grunt或其他任何东西。只需编码并按F5即可运行。
  • 不需要服务器。Fork这个repo,你的Github网站就开始运行了。创建你自己的游戏,并将你的朋友指向它。返利以获取最新的引擎更新。
  • 专为移动设备打造。设想在平板电脑上运行。与妈妈分享你的URL,这样她就可以把它添加到iPad的主屏幕上。
  • 如果脱机,则Take。使用HTML5应用程序缓存,您的游戏可以离线运行。非常适合在路上或钓鱼时使用。
  • 保存状态。使用HTML5本地存储,保存您所在的位置。
  • 世界编辑。专为基于瓦片的游戏设计,配有世界编辑器。放置你的互动程序和角色,然后点击play进行尝试。点击保存来拯救你的世界。

battle-city- 使用 React 将各类素材封装为对应的组件开发的坦克大战小游戏。

Github:https://github.com/shinima/battle-city

体验:https://battle-city.js.org


坦克大战 单人打

《坦克大战》是由日本南梦宫Namco游戏公司开发的一款平面射击游戏,于1985年发售。游戏以坦克战斗及保卫基地为主题,属于策略型联机类。 这个项目在很大程度上还原了坦克大战游戏。图标、音效和界面等方面,各个细节的几乎一模一样。


坦克大战 双人打

该 GitHub 仓库的版本是经典坦克大战的复刻版本,基于原版素材,使用 React 将各类素材封装为对应的组件。素材使用 SVG 进行渲染以展现游戏的像素风,可以先调整浏览器缩放再进行游戏,1080P 屏幕下使用 200% 缩放为最佳。此游戏使用网页前端技术进行开发,主要使用 React 进行页面展现,使用 Immutable.js 作为数据结构工具库,使用 redux 管理游戏状态,以及使用 redux-saga/little-saga 处理复杂的游戏逻辑。

snake-vue3 - 基于 Vue 3.3、Vite、Vuex 实现的经典贪吃蛇小游戏。

Github:https://github.com/ekinkaradag/snake-vue3


经典贪吃蛇小游戏

snake-vue3 基于 Vue 3.3、Vite、Vuex 实现的经典贪吃蛇游戏。

minesweeper-react - 用 CSS 复制 Win98 的风格扫雷小游戏。

Github:https://github.com/laoqiu233/minesweeper-react

Win98 的风格扫雷小游戏

一个扫雷游戏,作者尝试使用老式字体和经典的 Win98 图标,用 CSS 复制 Win98 的风格,使这个项目尽可能真实。该项目使用的技术栈包括:TypeScript、Webpack、React、Redux、React Router。

threejs-tetris-react - 基于 Three.js、React、TypeScript 实现的 3D 俄罗斯方块游戏

Github:https://github.com/RylanBot/threejs-tetris-react

3D 俄罗斯方块游戏

基于 Three.js、React、TypeScript 实现的 3D 俄罗斯方块游戏,可以拖动旋转页面进行观察。

habitica - 它会将你培养习惯的过程,当作一个 RPG 角色扮演游戏。

Github:https://github.com/HabitRPG/habitica


RPG 角色扮演游戏


这是一个培养习惯的开源应用,那它为什么会出现在游戏集合里呢?因为它会将你培养习惯的过程,当作一个 RPG 角色扮演游戏。

你需要根据设定的习惯,创建对应现实中需要完成的任务,当你完成一个任务时会获得相应的经验和金币,这些东西可以用来提升虚拟人物的等级以及购买装备。但当任务失败时,对应的将失去血量作为惩罚。随着你的等级提升,将会开启更多的玩法,比如:孵化宠物、职业、专属技能、组队打副本等。

game-application-using-react-and-typescript - 使用 React 和 TypeScript 构建的简单 2D 蛇游戏。

Github:https://github.com/Aklilu-Mandefro/game-application-using-react-and-typescript

简单 2D 蛇游戏

使用 React 和 TypeScript 构建的简单 2D 蛇游戏。可以使用 w、a、s 和 d 键来移动蛇。当吃掉水果时,得分和蛇的长度会动态增加,使用 canvas 元素构建。其用到的技术包括:React、Chakra-UI、Redux、Redux-saga。

3072-是一款受流行游戏“2048”启发的数字合并游戏.

Github:https://github.com/WeiChongDevelops/3072

体验:https://3072.vercel.app/

3072 数字合并游戏

3072 是一款受流行游戏“2048”启发的数字合并游戏,但游戏玩法与2048截然不同,使用的是 3 的倍数而不是 2,这真的是一种非常深刻和令人振奋的用户体验改变。这个项目使用 TypeScript、React 和 Tailwind CSS 构建,确保高性能的交互性和令人惊艳的响应式设计。

3d-chess-v2- 使用 React、Redux Toolkit、ThreeJS、React Three Fiber、ChessJS 和 ChakraUI 构建的经典国际象棋游戏。

Github:https://github.com/Kirill2603/3d-chess-v2

经典国际象棋游戏

使用 React、Redux Toolkit、ThreeJS、React Three Fiber、ChessJS 和 ChakraUI 构建的经典国际象棋游戏。

frontend-concentration-or-memory - 使用 Vue3.3、Pinia、Webpack、TypeScript 开发的一款记忆翻牌游戏。

Github:https://github.com/LAxBANDA/frontend-concentration-or-memory#concentration-or-memory-game

记忆翻牌游戏

使用 Vue3.3、Pinia、Webpack、TypeScript 开发的一款记忆翻牌游戏。

最后

粉丝们,有没有勾起你们儿童对回忆?喜欢哪款经典小游戏呢?

欢迎在评论区分享讨论。

一台电脑,一个键盘,尽情挥洒智慧的人生;

几行数字,几个字母,认真编写生活的美好;

一 个灵感,一段程序,推动科技进步,促进社会发展。

创作不易,喜欢的老铁们加个关注,点个赞,打个赏,后面会不定期更新干货和技术相关的资讯,速速收藏,谢谢!你们的一个小小举动就是对小编的认可,更是创作的动力。

创作文章的初心是:沉淀、分享和利他。既想写给现在的你,也想贪心写给 10 年、20 年后的工程师们,现在的你站在浪潮之巅,面对魔幻的互联网世界,很容易把一条河流看成整片大海。未来的读者已经知道了这段技术的发展历史,但难免会忽略一些细节。如果未来的工程师们真的创造出了时间旅行机器,可以让你回到现在。那么小编的创作就是你和当年工程师们的接头暗号,你能感知到他们在这个时代的键盘上留下的余温。

ameShell是一款由中国团队打造的开源化掌机产品,早在2017年的11月份就在众筹网站Kickstarter上开展众筹,正式的项目名称为“clockwork”。在设备上线仅13个小时之后,众筹金额就已经达到了预定目标,最终得到了30万美元左右的众筹,并在今年一月中旬正式开卖。

这款GameShell最大的亮点就是它是一款模块化掌机产品,每个部分都是独立并且可以自由搭配,主板也是采用了树莓派标准;除此之外还搭载了经定制的clockworkpi OS,可以实现主机编程,可以说是一款可玩性非常高的开源掌机。

首先要了解一下这款机器的参数,以便于了解这款主机的基本性能,下面是具体参数:

主板:经定制的 clockworkPi V3.1(树莓派标准)

CPU:4核 ARM Cortex-A7 CPU,Mali-400 GPU

网络: WI-FI & Bluetooth 模块,

内存:1GB DDR3

接口: Micro HDMI 输出,Micro SD卡槽,支持PMU电源管理。

屏幕:2.7 英寸 IPS RGB@60fps,分辨率 320*240

Keypad 键盘模块/Arduino兼容开发板:支持经典的 D-Pad 物理按钮和布局,支持 12 颗独立 IO 的按钮。可编程的键盘模块开发板完全兼容 Arduino 生态。基于 ATmega168P MPU@20MIPS MPU,同时包括 USB 调试接口、两个 15PIN GPIOs 扩展接口和一个 6PIN ISP 烧录接口。一个7PIN 扩展口用于支持 Lightkey 模块。

电池:1200mAh 锂电池

系统:clockwork OS, 支持 Linux Kernel 4.2x 内核或更高内核版本。

从以上参数上来看,Cortex-A7架构的CPU曾经用于28nm制程的移动设备,大概在12年左右的中端智能机上应用,内存容量也将够用。另外就是QVGA分辨率(320*240)的2.7英寸屏幕,由于主机显示主要在这块屏幕上,所以还是比较关键的,从屏幕分辨率的规格上可以看出这款主机的性能还是可以接受的。

开箱/拼装:

GameShell的外观设计更多体现在团队对于掌机的理解,每个人心中都有一些经典机型的原型,而GameShell团队中抽象出一些历史上一些标志性的掌机特征,并经历了至少500次修改之后才达到了现在的这个外观。

几乎每一个喜欢过掌机的人看过这台掌机之后都会喜欢上它,经典的十字键与矩形排列的按钮,横向的椭圆形的按键以及横版的屏幕全都是十分经典游戏机上的元素,再加上工程塑料一般质感的外壳以及两端的旋钮,都有着上个世纪九十年代最单纯的游戏怀旧情怀实在是非常撩人。

当然这款GameShell并不只是掌机而已,这款掌机的乐趣也并不只是打游戏,还在于拼装。提到拼装,相比大家都会想到乐高或者高达,又或者去年十分火爆的Labo的纸盒子。相比其它拼装,GameShell不需要使用复杂的工具,也没让人眼花缭乱的拼装指南,大家打开盒子,只需简单了解下各个组件,就可以轻松组装。接下来就给大家详细介绍一下。

首先是外包装,外壳是淡黄色的纸盒右边是GameShell的结构图,线路的透视非常漂亮。右侧则是主机的一些介绍与参数。

在套装里分别有几个部分,包括主板模块、键盘模块、声音模块、显示模块、电池模块、Lightkey 模块、1个前壳 & 2个后壳(透明简洁后壳 + 兼容乐高积木插口的可拓展后壳) + 6个模块外壳。 一张16GB的MicroSD Card内存卡(已内置 clockwork OS)、5根线缆(40PIN FPC、4PIN x 2、2PIN、7PIN)+ 14PIN 调试线缆。 以及 GameShell 安装指引 & 贴纸与开放的外壳3D打印模型文件 & 电路原理图。

主要的部件就是这些,电路板、屏幕以及电池都被封装好,其他的部件是固定用的塑料盒子以及按钮,是不是有种高达零件的感觉!

整体拼装并不复杂,只是需要把细节做好,部件之间的水口要清理干净,否则会有线缆链接的时候出现问题,只要按照拼装指南一步一步拼好就行。

一共有六个部分,分别是屏幕部分,主板部分,按键部分,电池部分,音响部分以及Lightkey 模块,这个模块是提供模拟肩部按键功能的,可以选择性的装,如果要装这个需要兼容乐高积木插口的可拓展后壳。

拼装各部件还算简单,但是把线缆连接起来并合在一处就相对复杂了一些,屏幕和主板在最上,键盘和电池在中间,音箱在最下方,Lightkey 模块在后壳之外。期间需要注意走线,如果让后背没有飞线的话需要将线缆都夹在双模块之间,不过安装的时候会有少许模块的移动,不过拼完之后会更美观一些。

在拼好之后,咱们就看到了这台掌机的真容,从这台掌机的按钮上可以进行一些经典元素的结构,首先十字键,是任天堂第一代主机FC上手柄的方向键,十分之经典。而YXAB的排列则是索尼第一代主机PS1手柄上的排列方式,但YXAB这四个数字则是XBOX主机上的元素,上边四个按MENU、与Select与Start键想不想最早FC手柄上的复位和暂停按键。

但是在2017年FC曾推出过一款复刻版手柄与现在的按键布局十分相似。

无论如何,看到都十分经典。

曾经任天堂推出过一款GAME BOY Color其中的透明塑料的原始质感是不是与今天介绍的GameShell十分相似啊。

左右两个旋钮并不是调节用,而是将掌机合住的卡扣,下边的就是Lightkey模块。

顶端则是3.5mm耳机插口、mini HDMI插口以及Micro USB插口与开关键,Lightkey模块上共有五个按键,用户可根据自己的需求进行自定义编程,一般用于模拟器中掌机部分无法涉及到的按键。

总结:

GameShell作为一款开源的模块化掌机,在掌机的外观上继承了诸多经典元素,拼接完之后无论是工程塑料的感觉还是按键的设计以及透明背壳都十分有复古的怀旧气息。而拼装的DIY元素则是让这款掌机除了游戏之外的另外一个乐趣。拼接过程虽然简单但细节之处还是需要打磨,完全拼完大概需要一个小时左右的时间。

clockwork OS 的模拟器游戏以及开源

clockwork OS 基于 Debian 9 ARMhf 和 Linux 4.2x或更高版本的Linux 内核构建。支持包括 C、C++、Python、Lua、Golang、 JavaScript、LISP、JAVA 等各种主流语言及脚本,您可以轻易移植或创建各种属于您的独立游戏和应用程序。完美运行 PICO-8, TIC80, LOVE2D, PyGame, Phaser.io, Libretro 等各种游戏引擎。

当然这些都只是支持,咱们没有办法一一测试,所以就先来看一下这款掌机的系统部分。

进入主界面,可以看到最左到最右分别是设置、模拟器游戏、独立游戏、一些系统自带的游戏以及各种编程软件,如PICO-8、TIC-80、Love2D、音乐、网络传输与关机等按键。界面非常简单,当然这个界面你也可以通过编程来自己定制,在设置之内有一个独立的主题选择。

在设置界面,有些类似手机中的布局,飞行模式、WIFI、音量、背景亮度、语言、蓝牙、界面选择,网关设置、主题管理等等,日常大家需要用到的功能都可以在这里找到。其中网络部分由于主板空间的限制,wifi天线增益不是很高,所以在传输游戏或者音乐的时候需要在一个网络较好的地方。

关于数据传输就要给大家做一个比较详细的介绍。

连接分成两种情况,分别为wifi连接与USB连接,由于上述的情况,比较建议大家进行USB连接,会更加稳定一些。

首先是wifi连接,需要与传输的设备在同一个网络环境之下,然后进入掌机的Tiny Cloud中查看所在的网络ip。

然后在文件管理器中输入IP地址就能进入设备内部。

USB连接则更容易一些,MAC系统要在文件管理器中CMD键+K进入连接服务器,然后输入USB-Ethernet中的地址,然后输入ID与Key就能进入了。Win系统同理,在文件管理器地址上输入IP地址,即可访问机器中相应的文件夹,如上图所示。

在目录中有响应的文件夹,包括编程软件的文件夹,与模拟器游戏的文件夹,放入对应格式的游戏即可。

小编使用了GB Studio制作了一段简单剧情的GB游戏,然后放入对应文件夹,在掌机中亲测可以使用。

玩自己制作的小游戏还是十分带感的,同样这款掌机完美运行 PICO-8, TIC80, LOVE2D, PyGame, Phaser.io, Libretro 等各种 游戏引擎。支持包括 C、C++、Python、Lua、Golang、 JavaScript、LISP、JAVA 等各种主流语言及脚本,如果你是个游戏制作人员的话可以非常轻易的移植独立游戏与应用程序进入到掌机中。

下面是模拟器游戏,掌机支持包括 Atari、 GameBoy系列、NES、SNES、MAME街机系列、MD、PS1 等15种历史上各种著名的游戏主机,无论是自己制作小游戏,还是以模拟器游戏进行游玩都不失为一个功能非常全面的掌机。

测试游戏为SFC版本《最终幻想6》

游戏在读取的速度非常快,屏幕分辨率为320*240,复古的游戏分辨率大部分都被囊括,所以不用在意无法点对点模拟的问题。而且掌机中已经支持GPU驱动,在进行模拟游戏的时候非常顺滑,只是在玩PS1等较大的3D游戏情况下较为吃力。对一般经典的模拟复古游戏来讲一点问题都没有。

在按键手感方面,由于是自行拼装的,所以对按键拼装的精准度有一些要求,如果对歪点的话,需要拆开重新在组装一次,大部分是没有问题的。按键反馈十分灵敏,背后的肩键模块的十分牢固,在游戏过程中没有出现松动的迹象。

掌机中有9档屏幕亮度调节,与音量调节,可以应付大部分使用场景,Shift键与Select与Start可以进行组合调节音量大小,也是很方便的。

影音娱乐

GameShell虽然是一个掌机的形态,但其实是一台小型的电脑,所以在影音方面也是没有问题的,只是系统与硬件上的限制无法浏览大型电影。GameShell机身自带HDMI接口,也可以将掌机端的显示内容投射到显示器端口,让这台掌机化身为一代电脑。

音乐方面比较容易,只要连接到手机之后考入手机就可以进行音乐欣赏,GameShell支持播放各种音乐格式,包括各 种无损格式。机身自带3.5mm耳机接口,所以可以直接当这台掌机为一个前端。不过机身原生自带的播放器不支持在音乐播放界面的操作,只能在列表中进行切换。

但是播放视频就会有一些技术上的麻烦,GameShell本身由于硬件限制所以系统并没有自带播放器,但是我们可以通过GameShell的社区中寻找玩家的教程,在其中有Kodi播放器的安装教程。

但是其硬件屏幕只能支持320*240分辨率,而且在系统层面上,GameShell的系统为clockwork OS 基于 Debian 9 ARMhf 和 Linux 4.2x 或更高版本的 Linux 内核构建。需要有一定的Linux系统使用基础,并且需要了解一些代码基础才好操作,总体来说是需要一定的技术门槛。

在经过漫长的安装过程之后终于把播放器装上了,但是由于没有低分辨率的UI所以在这个屏幕上看不清文字,在播放上面,超低分辨率的本地视频是没有问题的,但是超过QVGA分辨率的视频就会出现卡顿的问题,在安装流媒体插件之后可以观看CNET等网络电视,但是优于硬件性能的问题同样会有卡顿的问题,不过声音很流畅。

当然在影音部分“折腾”的乐趣远比实际效果要重要很多,2.7英寸的屏幕用来看视频本身也不够舒适,但是完成安装播放器并让这款掌机播放视频的乐趣却无以言表,而这也是这款GameShell的真谛,它不仅是掌机,更是一款开源的口袋大小的全功能 Linux 个人电脑。

总结:

GameShell在本质上并不是一台专业的游戏掌机,而是一个基于Linux的开源中端设备,而在这台掌机上你可以获得多种乐趣,可以拼装,可以玩复古的模拟器游戏,可以将它作为一个编程的测试设备,而由于它又有着掌机的外形,对于动手能力强的玩家来说又可以通过学习简单的编程自己也可以制作属于自己的游戏。对于这款产品很难用简单的一个掌机,或者一个游戏终端来概括,完全看他对你而言可以做到什么。

个月前,JS1k游戏制作节(JS1K game jam)传出不再举办消息后,许多游戏迷开始哀嚎。

Frank Force 也是其中一位,但他还有另一层身份——一位德克萨斯州奥斯汀的独立游戏设计师。Frank Force 在游戏行业工作了20年,参与过9款主流游戏、47个独立游戏的设计。在听到这个消息后,他马上和其他开发朋友讨论了这个问题,并决定做点什么为此纪念。

在此期间,他们受到三重因素的启发。一是赛车游戏,包括怀旧向的80年代赛车游戏,他们在非常早期的硬件上推动实时 3D 图形,所以作者沿用了相同的技术,用纯 JavaScript 从头开始实现做 3D 图形和物理引擎;还有一些现代赛车游戏带来了视觉设计的灵感,比如《Distance》和《Lonely Mountains: Downhill》;二是之前 Jake Gordon 用 JavaScript 创建一个虚拟3D赛车的项目,并分享了代码;三是 Chris Glover 曾经做过一款小到只有 1KB 的 JS1k 赛车游戏《Moto1kross by Chris Glover》。

于是 Frank 和他的朋友们决定做一个压缩后只有 2KB 的 3D 赛车游戏。2KB 到底有多小呢?提供一个参考,一个3.5英寸软盘可以容纳700多个这样的游戏。

他给这个游戏取名 Hue Jumper。关于名字的由来,Frank 表示,游戏的核心操作是移动。当玩家通过一个关卡时,游戏世界就会换一个颜色色调。“在我想象中,每通过过一个关卡,玩家都会跳转到另一个维度,有着完全不同的色调。”

做完这个游戏后,Frank 将包含了游戏的全部 JavaScript 代码都发布在他的个人博客上,其中用到的软件主要也是免费或开源软件的。游戏代码发布在CodePen,可以在 iframe 中试玩,有兴趣的朋友可以去看看。

以下是原博内容,AI源创评论进行了不改变原意的编译:

确定最高目标

因为严格的大小限制,我需要非常仔细对待我的程序。我的总体策略是尽可能保持一切简单,为最终目标服务。

为了帮助压缩代码,我使用了 Google Closure Compiler,它删除了所有空格,将变量重命名为1个字母字符,并进行了一些轻量级优化。

用户可以通过 Google Closure Compiler 官网在线跑代码。不幸的是,Closure Compiler 做了一些没有帮助的事情,比如替换模板字符串、默认参数和其他帮助节省空间的ES6特性。所以我需要手动撤销其中一些事情,并执行一些更“危险”的压缩技术来挤出最后一个字节空间。在压缩方面,这不算很成功,大部分挤出的空间来自代码本身的结构优化。

代码需要压缩到2KB。如果不是非要这么做不可,有一个类似的但功能没那么强的工具叫做 RegPack 。

无论哪种方式,策略都是一样的:尽最大可能重复代码,然后用压缩工具压缩。最好的例子是 c.width,c.height和 Math。因此,在阅读这段代码时,请记住,你经常会看到我不断重复一些东西,最终目的就是为了压缩。

HTML

其实我的游戏很少使用 html ,因为它主要用到的是 JavaScript 。但这是创建全屏画布 Canvas ,也能将画布 Canvas 设为窗口内部大小的代码最小方法。我不知道为什么在 CodePen 上有必要添加 overflow:hiddento the body,当直接打开时按理说也可以运行。

我将 JavaScript 封装在一个 onload 调用,得到了一个更小的最终版本… 但是,在开发过程中,我不喜欢用这个压缩设置,因为代码存储在一个字符串中,所以编辑器不能正确地高亮显示语法。

常量

有许多常量在各方面控制着游戏。当代码被 Google Closure 这样的工具缩小时,这些常量将被替换,就像 C++ 中的 #define 一样,把它们放在第一位会加快游戏微调的过程。

// draw settings

const context = c.getContext`2d`; // canvas context

const drawDistance = 800; // how far ahead to draw

const cameraDepth = 1; // FOV of camera

const segmentLength = 100; // length of each road segment

const roadWidth = 500; // how wide is road

const curbWidth = 150; // with of warning track

const dashLineWidth = 9; // width of the dashed line

const maxPlayerX = 2e3; // limit player offset

const mountainCount = 30; // how many mountains are there

const timeDelta = 1/60; // inverse frame rate

const PI = Math.PI; // shorthand for Math.PI

// player settings

const height = 150; // high of player above ground

const maxSpeed = 300; // limit max player speed

const playerAccel = 1; // player forward acceleration

const playerBrake = -3; // player breaking acceleration

const turnControl = .2; // player turning rate

const jumpAccel = 25; // z speed added for jump

const springConstant = .01; // spring players pitch

const collisionSlow = .1; // slow down from collisions

const pitchLerp = .1; // rate camera pitch changes

const pitchSpringDamp = .9; // dampen the pitch spring

const elasticity = 1.2; // bounce elasticity

const centrifugal = .002; // how much turns pull player

const forwardDamp = .999; // dampen player z speed

const lateralDamp = .7; // dampen player x speed

const offRoadDamp = .98; // more damping when off road

const gravity = -1; // gravity to apply in y axis

const cameraTurnScale = 2; // how much to rotate camera

const worldRotateScale = .00005; // how much to rotate world

// level settings

const maxTime = 20; // time to start

const checkPointTime = 10; // add time at checkpoints

const checkPointDistance = 1e5; // how far between checkpoints

const maxDifficultySegment = 9e3; // how far until max difficulty

const roadEnd = 1e4; // how far until end of road

鼠标控制

鼠标是唯一的输入系统。通过这段代码,我们可以跟踪鼠标点击和光标位置,位置显示为-1到1之间的值。

双击是通过 mouseUpFrames 实现的。mousePressed 变量只在玩家第一次点击开始游戏时使用这么一次。

mouseDown =

mousePressed =

mouseUpFrames =

mouseX = 0;

onmouseup =e=> mouseDown = 0;

onmousedown =e=> mousePressed ? mouseDown = 1 : mousePressed = 1;

onmousemove =e=> mouseX = e.x/window.innerWidth*2 - 1;

数学函数

这个游戏使用了一些函数来简化代码和减少重复,一些标准的数学函数用于 Clamp 和 Lerp 值。 ClampAngle 是有用的,因为它在 -PI 和 PI 之间 wrap angles,在许多游戏中已经广泛应用。

R函数就像个魔术师,因为它生成随机数,通过取当前随机数种子的正弦,乘以一个大数字,然后看分数部分来实现的。其实有很多方法可以做到,但这是最小的方法之一,而且对我们来说也是足够随机。

我们将使用这个随机生成器来创建各种程序,且不需要保存任何数据。例如,山脉、岩石和树木的变化不用存到内存。在这种情况下,目标不是减少内存,而是去除存储和检索数据所需的代码。

因为这是一个“真正的3D”游戏,所以有一个 3D vector class 非常有用,它也能减少代码量。这个 class 只包含这个游戏必需的基本元素,一个带有加法和乘法函数的 constructor 可以接受标量或向量参数。为了确定标量是否被传入,我们只需检查它是否小于一个大数。更正确的方法是使用 isNan 或者检查它的类型是否是 Vec3,但是这需要更多的存储。

Clamp =(v, a, b) => Math.min(Math.max(v, a), b);

ClampAngle=(a) => (a+PI) % (2*PI) + (a+PILerp =(p, a, b) => a + Clamp(p, 0, 1) * (b-a);

R =(a=1, b=0) => Lerp((Math.sin(++randSeed)+1)*1e5%1,a,b);

class Vec3 // 3d vector class

{

constructor(x=0, y=0, z=0) {this.x = x; this.y = y; this.z = z;}

Add=(v)=>(

v = v new Vec3( this.x + v.x, this.y + v.y, this.z + v.z ));

Multiply=(v)=>(

v = v new Vec3( this.x * v.x, this.y * v.y, this.z * v.z ));

}

Render Functions渲染函数

LSHA 通过模板字符串生成一组标准的 HSLA (色调、饱和度、亮度、alpha)颜色,并且刚刚被重新排序,所以更常用的 component 排在第一位。每过一关换一个整体色调也是通过这设置的。

DrawPoly 绘制一个梯形形状,用于渲染场景中的一切。使用 |0 将 Ycomponent 转换为整数,以确保每段多边形道路都能无缝连接,不然路段之间就会有一条细线。

DrawText 则用于显示时间、距离和游戏标题等文本渲染。

LSHA=(l,s=0,h=0,a=1)=>`hsl(${h+hueShift},${s}%,${l}%,${a})`;

// draw a trapazoid shaped poly

DrawPoly=(x1, y1, w1, x2, y2, w2, fillStyle)=>

{

context.beginPath(context.fillStyle = fillStyle);

context.lineTo(x1-w1, y1|0);

context.lineTo(x1+w1, y1|0);

context.lineTo(x2+w2, y2|0);

context.lineTo(x2-w2, y2|0);

context.fill;

}

// draw outlined hud text

DrawText=(text, posX)=>

{

context.font = '9em impact'; // set font size

context.fillStyle = LSHA(99,0,0,.5); // set font color

context.fillText(text, posX, 129); // fill text

context.lineWidth = 3; // line width

context.strokeText(text, posX, 129); // outline text

}

设计轨道

首先,我们必须生成完整的轨道,而且准备做到每次游戏轨道都是不同的。如何做呢?我们建立了一个道路段列表,存储道路在轨道上每一关卡的位置和宽度。轨道生成器是非常基础的操作,不同频率、振幅和宽度的道路都会逐渐变窄,沿着跑道的距离决定这一段路有多难。

atan2 函数可以用来计算道路俯仰角,据此来设计物理运动和光线。

roadGenLengthMax = // end of section

roadGenLength = // distance left

roadGenTaper = // length of taper

roadGenFreqX = // X wave frequency

roadGenFreqY = // Y wave frequency

roadGenScaleX = // X wave amplitude

roadGenScaleY = 0; // Y wave amplitude

roadGenWidth = roadWidth; // starting road width

startRandSeed = randSeed = Date.now; // set random seed

road = ; // clear road

// generate the road

for( i = 0; i {

if (roadGenLength++ > roadGenLengthMax) // is end of section?

{

// calculate difficulty percent

d = Math.min(1, i/maxDifficultySegment);

// randomize road settings

roadGenWidth = roadWidth*R(1-d*.7,3-2*d); // road width

roadGenFreqX = R(Lerp(d,.01,.02)); // X curves

roadGenFreqY = R(Lerp(d,.01,.03)); // Y bumps

roadGenScaleX = i>roadEnd ? 0 : R(Lerp(d,.2,.6));// X scale

roadGenScaleY = R(Lerp(d,1e3,2e3)); // Y scale

// apply taper and move back

roadGenTaper = R(99, 1e3)|0; // random taper

roadGenLengthMax = roadGenTaper + R(99,1e3); // random length

roadGenLength = 0; // reset length

i -= roadGenTaper; // subtract taper

}

// make a wavy road

x = Math.sin(i*roadGenFreqX) * roadGenScaleX;

y = Math.sin(i*roadGenFreqY) * roadGenScaleY;

road[i] = road[i]? road[i] : {x:x, y:y, w:roadGenWidth};

// apply taper from last section and lerp values

p = Clamp(roadGenLength / roadGenTaper, 0, 1);

road[i].x = Lerp(p, road[i].x, x);

road[i].y = Lerp(p, road[i].y, y);

road[i].w = i > roadEnd ? 0 : Lerp(p, road[i].w, roadGenWidth);

// calculate road pitch angle

road[i].a = road[i-1] ?

Math.atan2(road[i-1].y-road[i].y, segmentLength) : 0;

}

启动游戏

现在跑道就绪,我们只需要预置一些变量就可以开始游戏了。

// reset everything

velocity = new Vec3

( pitchSpring = pitchSpringSpeed = pitchRoad = hueShift = 0 );

position = new Vec3(0, height); // set player start pos

nextCheckPoint = checkPointDistance; // init next checkpoint

time = maxTime; // set the start time

heading = randSeed; // random world heading

更新玩家

这是主要的更新功能,用来更新和渲染游戏中的一切!一般来说,如果你的代码中有一个很大的函数,这不是好事,为了更简洁易懂,我们会把它分几个成子函数。

首先,我们需要得到一些玩家所在位置的道路信息。为了使物理和渲染感觉平滑,需要在当前和下一个路段之间插入一些数值。

玩家的位置和速度是 3D 向量,并受重力、dampening 和其他因素等影响更新。如果玩家跑在地面上时,会受到加速度影响;当他离开这段路时,摄像机还会抖动。另外,在对游戏测试后,我决定让玩家在空中时仍然可以跑。

接下来要处理输入指令,涉及加速、刹车、跳跃和转弯等操作。双击通过 mouseUpFrames 测试。还有一些代码是来跟踪玩家在空中停留了多少帧,如果时间很短,游戏允许玩家还可以跳跃。

当玩家加速、刹车和跳跃时,我通过spring system展示相机的俯仰角以给玩家动态运动的感觉。此外,当玩家驾车翻越山丘或跳跃时,相机还会随着道路倾斜而倾斜。

Update==>

{

// get player road segment

s = position.z / segmentLength | 0; // current road segment

p = position.z / segmentLength % 1; // percent along segment

// get lerped values between last and current road segment

roadX = Lerp(p, road[s].x, road[s+1].x);

roadY = Lerp(p, road[s].y, road[s+1].y) + height;

roadA = Lerp(p, road[s].a, road[s+1].a);

// update player velocity

lastVelocity = velocity.Add(0);

velocity.y += gravity;

velocity.x *= lateralDamp;

velocity.z = Math.max(0, time?forwardDamp*velocity.z:0);

// add velocity to position

position = position.Add(velocity);

// limit player x position (how far off road)

position.x = Clamp(position.x, -maxPlayerX, maxPlayerX);

// check if on ground

if (position.y {

position.y = roadY; // match y to ground plane

airFrame = 0; // reset air frames

// get the dot product of the ground normal and the velocity

dp = Math.cos(roadA)*velocity.y + Math.sin(roadA)*velocity.z;

// bounce velocity against ground normal

velocity = new Vec3(0, Math.cos(roadA), Math.sin(roadA))

.Multiply(-elasticity * dp).Add(velocity);

// apply player brake and accel

velocity.z +=

mouseDown? playerBrake :

Lerp(velocity.z/maxSpeed, mousePressed*playerAccel, 0);

// check if off road

if (Math.abs(position.x) > road[s].w)

{

velocity.z *= offRoadDamp; // slow down

pitchSpring += Math.sin(position.z/99)**4/99; // rumble

}

}

// update player turning and apply centrifugal force

turn = Lerp(velocity.z/maxSpeed, mouseX * turnControl, 0);

velocity.x +=

velocity.z * turn -

velocity.z ** 2 * centrifugal * roadX;

// update jump

if (airFrame++ && mouseDown && mouseUpFrames && mouseUpFrames{

velocity.y += jumpAccel; // apply jump velocity

airFrame = 9; // prevent jumping again

}

mouseUpFrames = mouseDown? 0 : mouseUpFrames+1;

// pitch down with vertical velocity when in air

airPercent = (position.y-roadY) / 99;

pitchSpringSpeed += Lerp(airPercent, 0, velocity.y/4e4);

// update player pitch spring

pitchSpringSpeed += (velocity.z - lastVelocity.z)/2e3;

pitchSpringSpeed -= pitchSpring * springConstant;

pitchSpringSpeed *= pitchSpringDamp;

pitchSpring += pitchSpringSpeed;

pitchRoad = Lerp(pitchLerp, pitchRoad, Lerp(airPercent,-roadA,0));

playerPitch = pitchSpring + pitchRoad;

// update heading

heading = ClampAngle(heading + velocity.z*roadX*worldRotateScale);

cameraHeading = turn * cameraTurnScale;

// was checkpoint crossed?

if (position.z > nextCheckPoint)

{

time += checkPointTime; // add more time

nextCheckPoint += checkPointDistance; // set next checkpoint

hueShift += 36; // shift hue

}

预渲染

在渲染之前,canvas 每当高度或宽度被重设时,画布内容就会被清空。这也适用于自适应窗口的画布。

我们还计算了将世界点转换到画布的投影比例。cameraDepth 值代表摄像机的视场(FOV)。这个游戏是90度。计算结果是 1/Math.tan(fovRadians/2) ,FOV 是90度的时候,计算结果正好是1。另外为了保持屏幕长宽比,投影按 c.width 缩放。

// clear the screen and set size

c.width = window.innerWidth, c.height = window.innerHeight;

// calculate projection scale, flip y

projectScale = (new Vec3(1,-1,1)).Multiply(c.width/2/cameraDepth);

给世界画上天空、太阳和月亮

空气背景是用全屏的 linear gradient (径向渐变)绘制的,它还会根据太阳的位置改变颜色。

为了节省存储空间,太阳和月亮在同一个循环中,使用了一个带有透明度的全屏 radial gradient(线性渐变)。

线性和径向渐变相结合,形成一个完全包围场景的天空背景。

// get horizon, offset, and light amount

horizon = c.height/2 - Math.tan(playerPitch)*projectScale.y;

backgroundOffset = Math.sin(cameraHeading)/2;

light = Math.cos(heading);

// create linear gradient for sky

g = context.createLinearGradient(0,horizon-c.height/2,0,horizon);

g.addColorStop(0,LSHA(39+light*25,49+light*19,230-light*19));

g.addColorStop(1,LSHA(5,79,250-light*9));

// draw sky as full screen poly

DrawPoly(c.width/2,0,c.width/2,c.width/2,c.height,c.width/2,g);

// draw sun and moon (0=sun, 1=moon)

for( i = 2 ; i--; )

{

// create radial gradient

g = context.createRadialGradient(

x = c.width*(.5+Lerp(

(heading/PI/2+.5+i/2)%1,

4, -4)-backgroundOffset),

y = horizon - c.width/5,

c.width/25,

x, y, i?c.width/23:c.width);

g.addColorStop(0, LSHA(i?70:99));

g.addColorStop(1, LSHA(0,0,0,0));

// draw full screen poly

DrawPoly(c.width/2,0,c.width/2,c.width/2,c.height,c.width/2,g);

}

给世界画上山峰、地平线

山脉是通过在地平线上画50个三角形,然后根据程序自己生成的。

因为用了光线照明,山脉在面对太阳时会更暗,因为它们处于阴影中。此外,越近的山脉颜色越暗,我想以此来模拟雾气。这里我有个诀窍,就是微调大小和颜色的随机值。

背景的最后一部分是绘制地平线,再用纯绿填充画布的底部。

// set random seed for mountains

randSeed = startRandSeed;

// draw mountains

for( i = mountainCount; i--; )

{

angle = ClampAngle(heading+R(19));

light = Math.cos(angle-heading);

DrawPoly(

x = c.width*(.5+Lerp(angle/PI/2+.5,4,-4)-backgroundOffset),

y = horizon,

w = R(.2,.8)**2*c.width/2,

x + w*R(-.5,.5),

y - R(.5,.8)*w, 0,

LSHA(R(15,25)+i/3-light*9, i/2+R(19), R(220,230)));

}

// draw horizon

DrawPoly(

c.width/2, horizon, c.width/2, c.width/2, c.height, c.width/2,

LSHA(25, 30, 95));

将路段投影到画布空间

在渲染道路之前,我们必须首先获得投影的道路点。第一部分有点棘手,因为我们的道路的 x 值需要转换成世界空间位置。为了使道路看起来蜿蜒曲折,我们把x值作为二阶导数。这就是为什么有奇怪的代码“x+=w+=”出现的原因。由于这种工作方式,路段没有固定的世界空间位置,每一帧都是根据玩家的位置重新计算。

一旦我们有了世界空间位置,我们就可以从道路位置中知道玩家的位置,从而得到本地摄像机空间位置。代码的其余部分,首先通过旋转标题、俯仰角来应用变换,然后通过投影变换,做到近大远小的效果,最后将其移动到画布空间。

for( x = w = i = 0; i {

p = new Vec3(x+=w+=road[s+i].x, // sum local road offsets

road[s+i].y, (s+i)*segmentLength) // road y and z pos

.Add(position.Multiply(-1)); // get local camera space

// apply camera heading

p.x = p.x*Math.cos(cameraHeading) - p.z*Math.sin(cameraHeading);

// tilt camera pitch and invert z

z = 1/(p.z*Math.cos(playerPitch) - p.y*Math.sin(playerPitch));

p.y = p.y*Math.cos(playerPitch) - p.z*Math.sin(playerPitch);

p.z = z;

// project road segment to canvas space

road[s+i++].p = // projected road point

p.Multiply(new Vec3(z, z, 1)) // projection

.Multiply(projectScale) // scale

.Add(new Vec3(c.width/2,c.height/2)); // center on canvas

}

绘制路段

现在我们有了每个路段的画布空间点,渲染就相当简单了。我们需要从后向前画出每一个路段,或者更具体地说,连接上一路段的梯形多边形。

为了创建道路,这里有4层渲染:地面,条纹路边缘,道路本身和白色虚线。每一个都是基于路段的俯仰角和方向来加阴影,并且根据该层的表现还有一些额外的逻辑。

有必要检查路段是在近还是远剪辑范围,以防止渲染出现 bug 。此外,还有一个很好的优化方法是,当道路变得很窄时,可以通过 distance 来减小道路的分辨率。如此,不仅减少了 draw count 一半以上,而且没有明显的质量损失,这是一次性能胜利。

let segment2 = road[s+drawDistance]; // store the last segment

for( i = drawDistance; i--; ) // iterate in reverse

{

// get projected road points

segment1 = road[s+i];

p1 = segment1.p;

p2 = segment2.p;

// random seed and lighting

randSeed = startRandSeed + s + i;

light = Math.sin(segment1.a) * Math.cos(heading) * 99;

// check near and far clip

if (p1.z 0)

{

// fade in road resolution over distance

if (i % (Lerp(i/drawDistance,1,9)|0) == 0)

{

// ground

DrawPoly(c.width/2, p1.y, c.width/2,

c.width/2, p2.y, c.width/2,

LSHA(25 + light, 30, 95));

// curb if wide enough

if (segment1.w > 400)

DrawPoly(p1.x, p1.y, p1.z*(segment1.w+curbWidth),

p2.x, p2.y, p2.z*(segment2.w+curbWidth),

LSHA(((s+i)%19

// road and checkpoint marker

DrawPoly(p1.x, p1.y, p1.z*segment1.w,

p2.x, p2.y, p2.z*segment2.w,

LSHA(((s+i)*segmentLength%checkPointDistance 70 : 7) + light));

// dashed lines if wide and close enough

if ((segment1.w > 300) && (s+i)%9==0 && i DrawPoly(p1.x, p1.y, p1.z*dashLineWidth,

p2.x, p2.y, p2.z*dashLineWidth,

LSHA(70 + light));

// save this segment

segment2 = segment1;

}

绘制路边的树和石头

游戏有两种不同类型的物体:树和石头。首先,我们通过使用 R 函数来确定是否加一个对象。这是随机数和随机数种子特别有意思的地方。我们还将使用 R 为对象随机添加不同的形状和颜色。

最初我还想涉及其他车型,但为了达到 2KB 的要求,必须要进行特别多的削减,因此我最后放弃了这个想法,用风景作为障碍。这些位置是随机的,也比较靠近道路,不然它们太稀疏,就很容易行驶。为了节省空间,对象高度还决定了对象的类型。

这是通过比较玩家和物体在 3D 空间中的位置来检查它们之间的碰撞位置。当玩家撞到一个物体时,玩家减速,该物体被标记为“ hit ”,这样它就可以安全通过。

为了防止对象突然出现在地平线上,透明度会随着距离的接近而削弱。梯形绘图函数定义物体的形状和颜色,另外随机函数会改变这两个属性。

if (R
{
// player object collision check
x = 2*roadWidth * R(10,-10) * R(9); // choose object pos
const objectHeight = (R(2)|0) * 400; // choose tree or rock
if (!segment1.h // dont hit same object
&& Math.abs(position.x-x) && Math.abs(position.z-(s+i)*segmentLength) && position.y-height {
// slow player and mark object as hit
velocity = velocity.Multiply(segment1.h = collisionSlow);
}

// draw road object
const alpha = Lerp(i/drawDistance, 4, 0); // fade in object
if (objectHeight)
{
// tree trunk
DrawPoly(x = p1.x+p1.z * x, p1.y, p1.z*29,
x, p1.y-99*p1.z, p1.z*29,
LSHA(5+R(9), 50+R(9), 29+R(9), alpha));

// tree leaves
DrawPoly(x, p1.y-R(50,99)*p1.z, p1.z*R(199,250),
x, p1.y-R(600,800)*p1.z, 0,
LSHA(25+R(9), 80+R(9), 9+R(29), alpha));
}
else
{
// rock
DrawPoly(x = p1.x+p1.z*x, p1.y, p1.z*R(200,250),
x+p1.z*(R(99,-99)), p1.y-R(200,250)*p1.z, p1.z*R(99),
LSHA(50+R(19), 25+R(19), 209+R(9), alpha));
}
}
}
}

画上 HUD,更新时间,请求下一次更新

游戏的标题、时间和距离是用一个非常基础的字体渲染系统显示出来的,就是之前设置的 DrawText 函数。在玩家点击鼠标之前,它会在屏幕中央显示标题。

按下鼠标后,游戏开始,然后 HUD 会显示剩余时间和当前距离。时间也在这块更新,玩过此类游戏的都知道,时间只在比赛开始后减少。

在这个 massive Update function 结束后,它调用 requestAnimationFrame (Update) 来触发下一次更新。

if (mousePressed)

{

time = Clamp(time - timeDelta, 0, maxTime); // update time

DrawText(Math.ceil(time), 9); // show time

context.textAlign = 'right'; // right alignment

DrawText(0|position.z/1e3, c.width-9); // show distance

}

else

{

context.textAlign = 'center'; // center alignment

DrawText('HUE JUMPER', c.width/2); // draw title text

}

requestAnimationFrame(Update); // kick off next frame

} // end of update function

代码的最后一位

HTML 需要一个结束脚本标签来让所有的代码能够跑起来。

Update; // kick off update loop

压缩

这就是整个游戏啦!下方的一小段代码就是压缩后的最终结果,我用不同的颜色标注了不同的部分。完成所有这些工作后,你能感受到我在2KB内就做完了整个游戏是多么让我满意了吗?而这还是在zip之前的工作,zip还可以进一步压缩大小。

警告 Caveats

当然,还有很多其他 3D 渲染方法可以同时保证性能和视觉效果。如果我有更多的可用空间,我会更倾向于使用一个 WebGL API 比如 three.js ,我在去年制作的一个类似游戏“Bogus Roads”中用过这个框架。此外,因为它使用的是 requestAnimationFrame ,所以需要一些额外的代码来确保帧速率不超过60 fps,增强版本中我会这么用,尽管我更喜欢使用 requestAnimationFrame 而不是 setInterval ,因为它是垂直同期的(VSyn,VerticalSynchronization),所以渲染更丝滑。这种代码的一个主要好处是它非常兼容,可以在任何设备上运行,尽管在我旧 iPhone 上运行有点慢。

游戏代码被我放到了 GitHub 上的 GPL-3.0 下(https://github.com/KilledByAPixel/HueJumper2k),所以你可以在自己的项目中自由使用它。该库中还包含 2KB 版本的游戏,准确说是2031字节!欢迎你添加一些其他的功能,比如音乐和音效到“增强”版本中。

后记

雷锋网