最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 从0到1进阶P6~P7水平前端进阶篇 - 运动与游戏开发

    正文概述 掘金(尊享小栈)   2020-12-30   414

    运动与游戏开发

    对于前端来说的运动,就是JS运动,本质来说,就是让web上的DOM元素动起来,而想要DOM动起来,就是改变其自身的属性,比如宽高,边距,透明度等等,动画的原理就是把不同的状态的物体,串联成连续的样子,不同状态的DOM,用定时器控制,就能得到动画效果,运动与动画的组合,就是最简单的运动与游戏开发。
    游戏开发的最重要的元素是游戏“循环”, 从本质上讲,只要游戏一直持续,就是一个将不断重复的函数。 我们的游戏循环就像我们的动画循环,另附加一些关键事物的补充。

    开发一款游戏都需要准备些什么呢?

    • canvas元素(画布):用于渲染游戏画面
    • audio元素(音频):用于添加音效和背景音乐
    • image元素(图像):用于加载游戏图像并在canvas中显示
    • 浏览器中的计时函数和循环函数:用于实现动画

    一、Canvas

    游戏中最重要的元素就是HTML5中的canvas元素。按照HTML5标准说法:“canvas元素为脚本提供了像素级的画布,可以实时渲染图形、游戏画面或其他虚拟图像”。
    canvas元素允许我们绘制直线、圆、矩形等基本形状,以及图像和文字,而且它已经为快速绘图做过优化了。各大浏览器都支持GPU加速的2D canvas渲染,因此,使用canvas绘制出的游戏动画运行速度会很快。

    绘制步骤

    从0到1进阶P6~P7水平前端进阶篇 - 运动与游戏开发

    示例

    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 为逆时针


    从0到1进阶P6~P7水平前端进阶篇 - 运动与游戏开发

    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:可选。要使用的图像的高度(伸展或缩小图像)

    三、实例

    从0到1进阶P6~P7水平前端进阶篇 - 运动与游戏开发 从0到1进阶P6~P7水平前端进阶篇 - 运动与游戏开发

    <!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">&#8592;</span> 和<span class="key">&#8594;</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画一个机器猫
    从0到1进阶P6~P7水平前端进阶篇 - 运动与游戏开发


    起源地下载网 » 从0到1进阶P6~P7水平前端进阶篇 - 运动与游戏开发

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元