You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
import{Trait}from'../Entity.js'/*extends keyword can be used to inherit all the properties and methods. */exportdefaultclassGoextendsTrait{constructor(){/*super keyword in here means the father class's constructor of this class. */super('go');this.dir=0;this.acceleration=400;this.deceleration=300;this.dragFactor=1/5000;this.distance=0;this.heading=1;}update(entity,{ deltaTime }){constabsX=Math.abs(entity.vel.x);if(this.dir!==0){entity.vel.x+=this.acceleration*deltaTime*this.dir;if(entity.jump){if(!entity.jump.falling){this.heading=this.dir;}}else{this.heading=this.dir;}}elseif(entity.vel.x!==0){constdecel=Math.min(absX,this.deceleration*deltaTime);entity.vel.x+=entity.vel.x>0 ? -decel : decel;}else{this.distance=0;}constdrag=this.dragFactor*entity.vel.x*absX;entity.vel.x-=drag;this.distance+=absX*deltaTime;}}
exportdefaultclassTimer{constructor(deltaTime=1/60){this.animationFrameID=nullletaccumulatedTime=0;letlastTime=0;this.updateProxy=(time)=>{accumulatedTime+=(time-lastTime)/1000;if(accumulatedTime>1){/* A hack to Solve the time accumulate * when it is running background. * So that our computer wont be slow down by this, * after long time of running this in background.*/accumulatedTime=1;}while(accumulatedTime>deltaTime){this.update(deltaTime);accumulatedTime-=deltaTime;}lastTime=time;this.enqueue();}}enqueue(){this.animationFrameID=requestAnimationFrame(this.updateProxy);}start(){this.enqueue();}stop(){if(this.animationFrameID){window.cancelAnimationFrame(this.animationFrameID)}}}
constaudioContext=newwindow.AudioContext()exportdefaultclassAudioBoard{constructor(){// this.context = context// not hardcore audioContext, but send it as a param below,// so than we can change the context easily.this.buffers=newMap()}addAudio(name,buffer){this.buffers.set(name,buffer)}playAudio(name,audioContext){constsource=audioContext.createBufferSource()// TODO Global Volume Setting// const gainNode = audioContext.createGain();// gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);// source.connect(gainNode)// gainNode.connect(audioContext.destination);source.connect(audioContext.destination)source.buffer=this.buffers.get(name)source.start(0)}}
The text was updated successfully, but these errors were encountered:
《 Canvas 2D游戏开发分享——以超级马力欧为例》
零、DEMO试玩
源码:https://github.com/JuniorTour/es6-mario
「闲聊一会~」
一、序
超级马力欧的游戏细节:
简介
《超级马力欧兄弟》是最初发布于1985年的 Nintendo Family Computer(Famicom)游戏机平台的一款平台跳跃类游戏。
风靡全球,售出了超过4000万份,游戏的设定、理念时至今日仍然为我们津津乐道。
体积优化
承载这款游戏数据的载体:卡带,空间非常有限,只能容纳 256kb 的代码和64kb的精灵图。
所以游戏的开发者在许多方面做了优化,以满足这些限制。
例如:
复用精灵图:
拼接精灵图:
虽然今天的软硬件性能都已今非昔比,很少需要开发者主动优化软件的性能、体积,但是这些优化所展现的思路,仍然很值得我们学习,也非常有趣。
游戏指引
进入游戏后的前30秒、第一个场景,显然经过了精心的设计,既引起人的好奇心,又非常符合直觉,让玩家无需指引,就带着好奇心快速上手游玩:
这个场景:
利用游玩者的好奇心做内容指引。
二、2D游戏常见基础概念
帧(Frame)
2D游戏的原理和视频类似,连贯的画面,是由一幅幅静止不动的图片”快速连续交替“形成的,
如上动图所示,动图中的每一张纸就是一”帧”。
通常用 每秒帧数(FPS,Frame Per Second),来计量帧数的高低,电影电视一般是24FPS;游戏也会以一定的的帧数绘制画面、运行。
(也有把“帧”称为“张”的叫法,个人认为,“张”作为帧的单位,确实更容易理解)
层(Layer)
浏览器中有文档流的概念,块级元素、行内元素默认都在文档流中,从上到下、从左到右(可能受语言设置影响)排列,
如果给元素声明了float: left; position: fixed; 等特殊属性,会使元素脱离文档流,视觉上的表现就是这些元素会“浮”在文档流中的元素之上,仿佛有2个层次、2张带有透明度的纸叠在了一起一样。
2D游戏为了营造丰富的视觉效果,也会把游戏内的图像区分为不同的层次,通过层叠(composite)各个层次,渲染出最终呈现在的玩家面前的游戏画面。
以此次介绍的超级马力欧游戏DEMO为例,区分出了:
3个层次,请看DEMO:
2D游戏中也经常见到「视差移动」效果,把不同层次的画面,以不同的运动速度,呈现在玩家面前,营造出近似于3D的效果:https://youtube.com/watch?v=MGHudLM7W5U
类似的效果在浏览器中也可以实现,效果也十分两眼。
精灵图(Sprite)
前端也曾有过精灵图(雪碧图)的概念,早年间,客户端网络带宽较小,为了节约网络资源,会把页面所需的图片资源拼接成一张图片,
一次请求,获取到所有的图片资源,再利用CSS的图片定位功能,把不同的图标呈现在对应的位置。
这个思路和2D游戏异曲同工,2D游戏很久很久以前就一直是这样做的,
基于这种实现方式,还可以把所需的图像切割、重组,实现之前在游戏细节章节所说的体积优化等特殊处理。
下图是超级马力欧 DEMO 中所使用的精灵图:
原版游戏不是这样的,可能是直接用二进制数据储存的,
因为在当时的平台上还没有图片文件这一概念,更没有 jpeg, png 等文件格式(都是90s的产物),
此外为了节约卡带的内存空间,也不会区分这么多颜色,布局也会更紧凑。
镜头(Camera)、横向卷轴移动
游戏中的视角多种多样,现在流行的赛车、枪战等3D游戏往往会有第一人称、第三人称等多重视角,不同的视角会产生截然不同的体验。
游戏视角一般是由镜头(Camera)决定的。
2D游戏的视角在游戏过程中通常是全程固定的,在横版卷轴游戏中,游戏的画面会像卷轴一样徐徐展开,呈现出游戏中的世界。
超级马力欧兄弟就是一个典型的横版卷轴2D游戏。
Demo 中构造了一个 Camera 对象,来记录当前视角所处的二维坐标系位置。
在每一帧中,根据 Camera 当前的位置以及提前配置好的该位置对应图像,绘制出一帧画面、实现卷轴滚动的效果。
边界盒(BoundingBox)、碰撞检测(Collision)
2D游戏通常会有自己的坐标体系,用来更方便、高效的构建游戏世界。
DEMO中构建了一个以16px * 16px为一单元格,共计有16*15格的世界,砖块、马力欧、板栗仔都占据一单元格。
当2个实体(entity)所处的单元格重叠时,就会在主循环中,绘制当前帧时,遍历每个实体,进行碰撞检测,根据实体各自的属性,判断、计算将要产生的结果。
以下图为例,马力欧所处的红色边线单元格,与板栗仔所处的红色单元格,发生了重叠,主循环在绘制当前帧、进行碰撞检测是,
会根据双方所处的坐标系位置,判断出马力欧“踩在了”板栗仔的头部,板栗仔将被打倒。
物理效果、手感
游戏通常都有自己独特的的世界观,但又常常要和现实世界对齐。
很多游戏都有模仿现实世界的物理效果,用来改善游戏的手感,增强趣味性。
以超级马力欧这款游戏为例,
DEMO中,通过在人物行走时,计算位移的逻辑中增加加速度系数( acceleration)、摩擦系数 ( dragFactor ),
模拟了奔跑时逐渐加速;快速奔跑后、惯性运动一段距离的物理效果。
三、源码实现介绍
面向对象:封装 - 万物皆对象
游戏这样复杂度非常高的项目,很适合用面向对象的思路来构建,DEMO也是这样实现的,大量运用了基于类封装、继承的思路。
DEMO中把 马力欧(Mario.js)、板栗仔(Goomba.js)、行走能力(Go.js)、甚至一整个关卡(Level.js)都视为一个“对象“,封装、抽象出一个类来构建这些实例。
实例各自拥有特殊的属性、方法,用来储存、更新自己的状态(update())。
以玩家操控的人物马力欧为例:
用 createMario() 创建马力欧,每个马力欧都是继承自 Class Entity 的实例,
拥有 pos(位置), vel(速度), traits(特性) 等等属性,用来记录实例的位置、速度等状态。
以及 addTrait , collides, update 等方法,用来调用实例的特性,从而更新状态。
主循环
游戏“动起来”靠的就是主循环。
DEMO中的实现是用一个基于
requestAnimationFrame
API 的 while 循环:当代码运行积累的时间( accumulatedTime)大于设定的更新一帧的时间(默认是 1/60 === 0.016667秒),
就会调用 level.update 等方法,更新当前关卡的状态、移动镜头,让画面动起来。
电子游戏之所以让人着迷,核心原因是它有即时的正向反馈。
按下按键就会有华丽的动画效果、生动的声音特效,立刻就能得到引人入胜的反馈。
不像现实世界,很多事情的结果和反馈不以人的主观意志为转移,例如学习。
Canvas相关API
游戏的主要反馈就是丰富多彩的画面图像的变化。
DEMO中主要用了 Canvas 2d 上下文的 API 来绘制画面:
折叠源码
音效相关API
声音在现实生活中也潜移默化的影响着我们,例如吸尘器吹风机以及汽车故意制造的轰鸣声、薯片的弧度以便产生酥脆的感觉。
在游戏中音效也是游戏反馈的核心组成。
DEMO里借助浏览器平台的
window.AudioContext()
API 来控制音效的播放。把异步加载的 .ogg 文件二进制数据,预先储存在内存里的 Map 结构(this.buffers)中,需要播放时从内存中取出即可。
AudioContext 还提供了非常多的特性,可以基于这些特性实现 空间立体声音效、音频裁剪 等功能,
市面上已经有了很多成熟的浏览器平台音效管理框架,可以参考:https://howlerjs.com/
The text was updated successfully, but these errors were encountered: