运动与游戏开发
对于前端来说的运动,就是JS运动,本质来说,就是让web上的DOM元素动起来,而想要DOM动起来,就是改变其自身的属性,比如宽高,边距,透明度等等,动画的原理就是把不同的状态的物体,串联成连续的样子,不同状态的DOM,用定时器控制,就能得到动画效果,运动与动画的组合,就是最简单的运动与游戏开发。
游戏开发的最重要的元素是游戏“循环”, 从本质上讲,只要游戏一直持续,就是一个将不断重复的函数。 我们的游戏循环就像我们的动画循环,另附加一些关键事物的补充。
开发一款游戏都需要准备些什么呢?
- canvas元素(画布):用于渲染游戏画面
- audio元素(音频):用于添加音效和背景音乐
- image元素(图像):用于加载游戏图像并在canvas中显示
- 浏览器中的计时函数和循环函数:用于实现动画
一、Canvas
游戏中最重要的元素就是HTML5中的canvas元素。按照HTML5标准说法:“canvas元素为脚本提供了像素级的画布,可以实时渲染图形、游戏画面或其他虚拟图像”。
canvas元素允许我们绘制直线、圆、矩形等基本形状,以及图像和文字,而且它已经为快速绘图做过优化了。各大浏览器都支持GPU加速的2D canvas渲染,因此,使用canvas绘制出的游戏动画运行速度会很快。
绘制步骤
示例
const canvas = document.getElementById('canvas')
const context = canvas.getContext('2d')
canvas.width = 400
canvas.height = 400
context.beginPath()
context.arc(100, 100, 50, 0, Math.PI * 2, true)
context.closePath()
context.fillStyle = '#fff'
context.fill()
二、Canvas的API
1. 绘制路径
方法 | 描述 |
---|---|
fill() | 填充路径 |
stroke() | 描边 |
arc() | 创建圆弧 |
rect() | 创建矩形 |
fillRect() | 绘制矩形路径区域 |
strokeRect() | 绘制矩形路径描边 |
clearRect() | 在给定的矩形内清除指定的像素 |
arcTo() | 创建两切线之间的弧/曲线 |
beginPath() | 起始一条路径,或重置当前路径 |
moveTo() | 把路径移动到画布中的指定点,不创建线条 |
lineTo() | 添加一个新点,然后在画布中创建从该点到最后指定点的线条 |
closePath() | 创建从当前点回到起始点的路径 |
clip() | 从原始画布剪切任意形状和尺寸的区域 |
quadraticCurveTo() | 创建二次方贝塞尔曲线 |
bezierCurveTo() | 创建三次方贝塞尔曲线 |
isPointInPath() | 如果指定的点位于当前路径中,则返回 true,否则返回 false |
绘制弧/曲线
arc()
方法创建弧/曲线(用于创建圆或部分圆)
context.arc(x,y,r,sAngle,eAngle,counterclockwise);
- x:圆心的 x 坐标
- y:圆心的 y 坐标
- r:圆的半径
- sAngle:起始角,以弧度计(弧的圆形的三点钟位置是 0 度)
- eAngle:结束角,以弧度计
- counterclockwise:可选。规定应该逆时针还是顺时针绘图。false 为顺时针,true 为逆时针
2. 改变样式
属性 | 描述 |
---|---|
fillStyle | 设置或返回用于填充绘画的颜色、渐变或模式 |
strokeStyle | 设置或返回用于笔触的颜色、渐变或模式 |
shadowColor | 设置或返回用于阴影的颜色 |
shadowBlur | 设置或返回用于阴影的模糊级别 |
shadowOffsetX | 设置或返回阴影距形状的水平距离 |
shadowOffsetY | 设置或返回阴影距形状的垂直距离 |
3. 直线添加样式
样式 | 描述 |
---|---|
lineCap | 设置或返回线条的结束端点样式 |
lineJoin | 设置或返回两条线相交时,所创建的拐角类型 |
lineWidth | 设置或返回当前的线条宽度 |
miterLimit | 设置或返回最大斜接长度 |
4. 设置渐变
方法 | 描述 |
---|---|
createLinearGradient() | 创建线性渐变(用在画布内容上) |
createPattern() | 在指定的方向上重复指定的元素 |
createRadialGradient() | 创建放射状/环形的渐变(用在画布内容上) |
addColorStop() | 规定渐变对象中的颜色和停止位置 |
5. 图形转换
方法 | 描述 |
---|---|
scale() | 缩放当前绘图至更大或更小 |
rotate() | 旋转当前绘图 |
translate() | 重新映射画布上的 (0,0) 位置 |
transform() | 替换绘图的当前转换矩阵 |
setTransform() | 将当前转换重置为单位矩阵,然后运行 transform() |
7. 图像绘制
方法 | 描述 |
---|---|
drawImage() | 向画布上绘制图像、画布或视频 |
context.drawImage(img,sx,sy,swidth,sheight,x,y,width,height);
img
:规定要使用的图像、画布或视频sx
:可选。开始剪切的 x 坐标位置sy
:可选。开始剪切的 y 坐标位置swidth
:可选。被剪切图像的宽度sheight
:可选。被剪切图像的高度x
:在画布上放置图像的 x 坐标位置y
:在画布上放置图像的 y 坐标位置width
:可选。要使用的图像的宽度(伸展或缩小图像)height
:可选。要使用的图像的高度(伸展或缩小图像)
三、实例
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>太空射手</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="dialogue">
<div class="dialogue--content">
<h1>太空射手</h1>
<ul>
<li>使用<span class="key">←</span> 和<span class="key">→</span> 键盘移动</li>
<li>使用 <span class="key">空格键</span> 发射激光</li>
</ul>
<button>开始</button>
</div>
</div>
<div class="score score--hidden">分数:<span>0</span></div>
<script src="script.js"></script>
</body>
</html>
* {
margin: 0;
padding: 0;
}
body {
font-family: 'Press Start 2P', sans-serif;
overflow: hidden;
color: #fff;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
button,
.key {
display: inline-block;
padding: 10px;
border: 1px solid currentColor;
border-radius: 5px;
}
canvas {
display: block;
}
h1 {
font-size: 48px;
}
ul {
margin: 20px 0;
list-style: none;
}
li {
font-size: 14px;
margin-bottom: 10px;
color: #ff9d00;
}
button:focus,
button:hover {
transition-duration: .07s;
color: #ff9d00;
background-color: black;
}
.dialogue {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
transform: translateX(0);
text-align: center;
opacity: 1;
background-color: rgba(0, 0, 0, .85);
}
.dialogue--hidden {
transition: opacity .3s linear 0s, transform 0s linear .3s;
transform: translateX(-100vw);
opacity: 0;
}
.dialogue--content {
padding: 18px;
text-align: center;
}
.key {
margin: 0 10px;
color: #fff;
border-color: #fff;
}
.score {
font-size: 14px;
position: fixed;
top: 0;
left: 0;
display: flex;
justify-content: flex-start;
box-sizing: border-box;
width: 100%;
margin: 0;
padding: 10px;
transition: opacity .07s linear .5s;
opacity: 1;
color: #ff9d00;
}
.score--hidden {
opacity: 0;
}
const DEV_MODE = false
const stage = document.createElement('canvas')
const ctx = stage.getContext('2d')
const dialogue = document.querySelector('.dialogue')
const startBtn = dialogue.querySelector('button')
const hud = document.querySelector('.score')
const scoreNode = hud.querySelector('.score span')
let ship
let lasers = []
let enemies = []
let playing = false
let gameStarted = false
let speedMultiplier
let enemySeedFrameInterval
let score = 0
// 创建随机数
const randomBetween = function (min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min
}
// 计算分数
const calcScore = function (x) {
return Math.floor((1 / x) * 500)
}
// 创建飞船
const Ship = function (options) {
this.radius = 10
this.x = options.x || stage.width * 0.5 - this.radius - 0.5
this.y = options.y || stage.height - this.radius - 30
this.width = this.radius * 2
this.height = this.width
this.color = options.color || 'red'
this.left = false
this.right = false
this.speed = 10
this.active = true
// 监控键盘事件
document.addEventListener('keydown', this.onKeyDown.bind(this))
document.addEventListener('keyup', this.onKeyUp.bind(this))
}
Ship.prototype.update = function (x) {
this.x = x
this.y = stage.height - this.radius - 30
}
Ship.prototype.draw = function () {
ctx.save()
if (DEV_MODE) {
ctx.fillStyle = '#fc0'
ctx.fillRect(this.x, this.y, this.width, this.width)
}
ctx.fillStyle = this.color
ctx.fillRect(this.x + this.radius - 5, this.y, 10, this.radius)
ctx.fillRect(this.x, this.y + this.radius, this.width, 10)
ctx.fillRect(this.x, this.y + this.radius + 10, 10, 5)
ctx.fillRect(this.x + this.width - 10, this.y + this.radius + 10, 10, 5)
ctx.restore()
}
Ship.prototype.onKeyDown = function (e) {
if (ship.active) {
if (e.keyCode === 39) this.right = true
else if (e.keyCode === 37) this.left = true
if (e.keyCode == 32) {
const settings = {
x: this.x + this.radius - 3,
color: '#fc0'
}
const laser = new Laser(settings)
lasers.push(laser)
}
}
}
Ship.prototype.onKeyUp = function (e) {
if (e.key === 'ArrowRight') this.right = false
else if (e.key === 'ArrowLeft') this.left = false
}
// 激光
const Laser = function (options) {
this.x = options.x - 0.5
this.y = options.y || stage.height - 50
this.width = 6
this.height = 20
this.speed = 15
this.color = options.color || '#fff'
this.active = true
}
Laser.prototype.update = function (y) {
this.y = y
}
Laser.prototype.draw = function () {
ctx.save()
ctx.fillStyle = this.color
ctx.beginPath()
ctx.rect(this.x, this.y, this.width, this.height)
ctx.closePath()
ctx.fill()
ctx.restore()
}
// 创建圆点
const Enemy = function (options) {
this.radius = randomBetween(10, 40)
this.width = this.radius * 2
this.height = this.width
this.x = randomBetween(0, stage.width - this.width)
this.y = -this.radius * 2
this.color = options != undefined && options.color ? options.color : '#fff'
this.speed = 2
this.active = true
}
Enemy.prototype.update = function (x, y) {
this.x = x
this.y = y
}
Enemy.prototype.draw = function () {
if (DEV_MODE) {
ctx.fillStyle = 'skyblue'
ctx.fillRect(this.x, this.y, this.width, this.width)
}
ctx.save()
ctx.fillStyle = this.color
ctx.beginPath()
ctx.arc(
this.x + this.radius,
this.y + this.radius,
this.radius,
0,
Math.PI * 2
)
ctx.closePath()
ctx.fill()
ctx.restore()
}
// 判断击中
const hitTest = function (item1, item2) {
let collision = true
if (
item1.x > item2.x + item2.width ||
item1.y > item2.y + item2.height ||
item2.x > item1.x + item1.width ||
item2.y > item1.y + item1.height
) {
collision = false
}
return collision
}
// 击中处理
const handleLaserCollision = function () {
for (let enemy of enemies) {
for (let laser of lasers) {
let collision = hitTest(laser, enemy)
if (collision && laser.active) {
console.log('你消灭了一个敌人')
enemy.active = false
laser.active = false
// 提高速度
speedMultiplier += 0.025
if (enemySeedFrameInterval > 20) {
enemySeedFrameInterval -= 2
}
// 计算分数
score += calcScore(enemy.radius)
scoreNode.textContent = score
}
}
}
}
// 飞船处理
const handleShipCollision = function () {
if (enemies.length) {
for (let enemy of enemies) {
let collision = hitTest(ship, enemy)
if (collision) {
console.log('飞船被摧毁')
ship.active = false
setTimeout(() => {
ship.active = true
speedMultiplier = 1
enemySeedFrameInterval = 100
score = 0
scoreNode.textContent = score
}, 2000)
}
}
}
}
// 构建飞船
const drawShip = function (xPosition) {
if (ship.active) {
ship.update(xPosition)
ship.draw()
}
}
// 构建圆点
const drawEnemies = function () {
if (enemies.length) {
for (let enemy of enemies) {
if (enemy.active) {
enemy.update(enemy.x, (enemy.y += enemy.speed * speedMultiplier))
enemy.draw()
}
}
}
}
// 清除圆点
const enemyCleanup = function () {
if (enemies.length) {
enemies = enemies.filter((enemy) => {
let visible = enemy.y < stage.height + enemy.width
let active = enemy.active === true
return visible && active
})
}
}
// 构建激光
const drawLasers = function () {
if (lasers.length) {
for (let laser of lasers) {
if (laser.active) {
laser.update((laser.y -= laser.speed))
laser.draw()
}
}
}
}
// 清除激光
const laserCleanup = function () {
lasers = lasers.filter((laser) => {
let visible = laser.y > -laser.height
let active = laser.active === true
return visible && active
})
}
let tick = 0
// 构建页面
const render = function (delta) {
if (playing) {
let xPos = ship.x
// 增加新的敌人
if (tick % enemySeedFrameInterval === 0 && ship.active) {
const enemy = new Enemy()
enemies.push(enemy)
}
// 背景
ctx.save()
ctx.fillStyle = '#000'
ctx.fillRect(0, 0, stage.width, stage.height)
ctx.restore()
// 飞船移动
if (ship.left) xPos = ship.x -= ship.speed
else if (ship.right) xPos = ship.x += ship.speed
// 边界
if (gameStarted) {
if (xPos < 0) xPos = 0
else if (xPos > stage.width - ship.width) xPos = stage.width - ship.width
}
drawShip(xPos)
handleShipCollision()
handleLaserCollision()
drawLasers()
drawEnemies()
enemyCleanup()
laserCleanup()
tick++
}
requestAnimationFrame(render)
}
// 启动游戏
const startGame = function (e) {
console.log('开始游戏')
dialogue.classList.add('dialogue--hidden')
hud.classList.remove('score--hidden')
e.currentTarget.blur()
// 设置
speedMultiplier = 1
enemySeedFrameInterval = 100
ship.x = stage.width * 0.5 - ship.radius - 0.5
ship.y = stage.height - ship.radius - 30
enemies = []
gameStarted = true
}
// 重置浏览器大小
const onResize = function () {
stage.width = window.innerWidth
stage.height = window.innerHeight
}
// 点击开始游戏
startBtn.addEventListener('click', startGame)
// 监控浏览器大小
window.addEventListener('resize', onResize)
// canvas添加到body
document.body.appendChild(stage)
onResize()
// 启动
ship = new Ship({ color: '#ff9d00', x: -100, y: -100 })
playing = true
render()
拓展
利用canvas画一个机器猫