这篇文章主要介绍“p5.js怎么实现声音控制警察抓小偷游戏”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“p5.js怎么实现声音控制警察抓小偷游戏”文章能帮助大家解决问题。
一、游戏介绍
之前一直用原生canvas写小游戏,很多逻辑都需要自己一点点封装,最近看了下p5.js,哇哦,好的很嘞!就用它开发了一款名为“警察抓小偷”的游戏。这是一款非常有趣的游戏,玩家扮演警察追捕小偷,通过大声喊话来控制警察的速度,抓住小偷。游戏中,小偷会在棋盘上移动,而警察则会向小偷靠近。如果警察追上小偷,就算警察胜利;如果小偷逃脱到棋盘的最下方,那么小偷胜利。
玩法指南
点击开始游戏按钮小偷会在棋盘上移动,游戏会调用麦克风,玩家通过大声狂呼乱叫,此时警察则会向小偷靠近,你喊得越大声,警察移动速度越快,当警察追上小偷,就算警察胜利;如果小偷跑到的右下方出口处逃脱,那么小偷胜利。
二、用到的JS库
p5.js库基于 Processing,是一个 JavaScript 库,可以轻松地创建交互式图形和动态图像。它的核心是使用 HTML5 Canvas 元素来创建图形。p5.js 提供了简单易用的 API,让编写和理解代码变得更加容易。
p5.sound.min.js是 p5.js 库的音频处理插件,它基于 Web Audio API,可以加载、播放和操纵音频文件。Web Audio API 是 HTML5 中的一项标准,提供了低延迟、高品质的音频处理能力,可以通过 JavaScript 对音频进行混合、变形、过滤等操作。
三、游戏开发思路
设计游戏规则和界面
使用p5.js绘制游戏界面和元素,这比自己用原生canvas写迅速多了。
实现小偷和警察的移动逻辑
实现通过音量控制警察的速度
实现胜利逻辑和动画效果
添加开始游戏和重新开始的按钮
预加载音乐和图片资源
四、核心功能点
核心功能点主要包括绘制棋盘、小偷的移动逻辑、警察的移动逻辑以及胜利逻辑函数。大家着重理解这些开发思路。
1. 绘制棋盘
在 p5.js 的
draw()函数中,调用
rect()方法来绘制20X15的游戏棋盘,
image()方法来绘制小偷、警察元素,并实时绘制小偷和警察的新位置和动态更新页面上玩家的麦克风音量刻度条,通过
whoWin变量检查是否胜利,以及执行相应的文案绘制效果。
//循环体 function draw() { //..... // 绘制格子盘 for (let i = 1; i < col + 1; i++) { for (let j = 0; j < row; j++) { fill('pink') rect(i * gridW, j * gridH, gridW, gridH - 1); // 将高度改为gridH-1 } } // 绘制小偷和警察 image(thiefImg, thiefXPos, thiefYPos - 5, gridW, gridH + 5); image(policeImg, policeXPos, policeYPos - 20, gridW, gridH + 25); if (whoWin === 'police') { fill(255, 0, 0); textSize(30) text("恭喜你抓住小偷啦,警察胜利喽!", col * gridW / 6, 60); } if (whoWin === 'thief') { fill(255, 0, 0); textSize(30) text("小样,还想抓住我,哈哈哈", col * gridW / 5, 60); } //..... // 显示当前玩家数据 thiefVoiceValNode.innerText = thiefProgressNode.value = policeDir / policeStepLen; policeValNode.innerText = policeProgressNode.value = Math.abs(policeDir); }
2. 小偷的移动逻辑
小偷的移动逻辑是每隔一定时间在 X 轴方向上以固定的步长移动,步长由
stepLen常量控制,在到达棋盘边界
if (thiefXPos >= (col + 1) * gridW)改变运动方向
dir = -stepLen,并在 Y 轴方向上增加一个棋盘格子高
thiefYPos += gridH。小偷的移动步长可以通过调整
stepLen的值来改变,值越大,小偷移动的越快。
3. 警察的移动逻辑
警察的移动逻辑比较复杂,需要根据下面代码中
getVoiceSize()获取的音量值来动态调整警察的速度
policeDir = Math.floor(voiceSize * 100) / 10 * policeStepLen。通过
setInterval()函数来定时改变警察的横坐标的位置
policeXPos += policeDir;,并根据警察的位置和小偷的位置来判断是否胜利。
//获取音量大小 async function getVoiceSize() { try { const stream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true } });// echoCancellation: true以减少回音和噪声的影响 const audioContext = new AudioContext(); const source = audioContext.createMediaStreamSource(stream); const processor = audioContext.createScriptProcessor(1024, 1, 1); source.connect(processor); processor.connect(audioContext.destination); processor.onaudioprocess = function (event) { const buffer = event.inputBuffer.getChannelData(0); const sum = buffer.reduce((acc, val) => acc + val ** 2, 0);//计算音量大小 voiceSize = Math.sqrt(sum / buffer.length); }; } catch (err) { console.log('getUserMedia error: ', err); } }
4. 胜利逻辑
在开始游戏点击事件的回调函数中,判断游戏胜利的条件并播放胜利音乐。在
draw()函数中通过
whoWin来判断游戏胜利的条件绘制页面上的文案。当游戏胜利时,页面上绘制对应文案并播放胜利音乐。
//开始游戏事件 startBtn.addEventListener('click', () => { //....省略其它逻辑代码 //贼胜利逻辑 if (thiefYPos >= row * gridH) { winLogicFunc('thief') } //警察胜利逻辑 if (policeYPos === thiefYPos && (policeStepLen > 0 && policeXPos >= thiefXPos || policeStepLen < 0 && Math.abs(policeXPos) <= thiefXPos)) { winLogicFunc('police') } }, moveTime); });
五、待实现的创意玩法
为了增加游戏的趣味性,可以考虑增加更多的道具和障碍物。
道具:可以设置一些可以帮助小偷逃脱警察追捕的道具,例如加速药水、隐身衣、烟雾弹等。这些道具可以增加小偷的移动速度、暂时让小偷隐身躲避警察的视线、或者释放烟雾干扰警察的追踪。
障碍物,可以设置一些可以对小偷和警察造成影响的障碍物,例如路障、警车、人群等。这些障碍物可以阻碍小偷的逃脱,或者让警察无法前进,从而给小偷争取时间。
除了增加道具和障碍物,我们还可以增加多种不同的游戏模式,例如多人模式、计时模式等等。
多人模式:可以让两个玩家分别扮演小偷和警察,进行对抗。
计时模式中,可以设置一个时间限制,让警察在规定时间内抓住小偷。
还有可以美化页面还有音效、动效等,让游戏更加丰富多彩,让玩家有更多的选择和挑战。
六、代码实现
为了避免文章过于臃肿,这里只放js的实现代码,对于页面布局代码可以到我的github上去阅读代码,代码里都有丰富的注释,相信大家可以快速读懂代码,如果我用原生canvas开发,估计工作量会大很多。不过,代码在封装方面还有待提高,除此之外,玩法也还有更多创意有待实现。
各位如果更好玩的实现玩法,不妨把代码
fork过去实现,记得评论区告诉我,我期待你的实现。
1. js代码
const gridW = 30; //棋盘格子宽 const gridH = 30; //棋盘格子高 const row = 15; //棋盘行数 const col = 20; //棋盘列数 let sone = null;//胜利音乐 let whoWin = ''; //谁胜利可选值'police|thief' let interval = null;//循环句柄 const moveTime = 16;//循环时间 let thiefImg = null;//小偷图片 let thiefXPos = 40;//小偷横坐标 let thiefYPos = 0;//小偷纵坐标 const stepLen = 2; //小偷移动步长(速度) let policeImg = null;//警察图片 let policeXPos = 0;//警察横坐标 let policeYPos = 0;//警察纵坐标 let policeStepLen = 2;//警察移动步长基数 let policeDir = 0; //警察动态移动步长(速度) let voiceSize = 0; //获取DOM节点 const thiefProgressNode = document.getElementById('progress'); const thiefVoiceValNode = document.getElementById('voiceVal'); const policeProgressNode = document.getElementById('policeProgress'); const policeValNode = document.getElementById('policeVal'); const startBtn = document.getElementById('start-btn'); const restartBtn = document.getElementById('restart-btn'); const thiefWinGifNode = document.getElementById('thiefGif'); //小偷胜利动图 const policeWinGifNode = document.getElementById('policeGif'); //警察胜利动图 //预加载图片 function preload() { song = loadSound('./resources/胜利配乐.mp3'); thiefImg = loadImage('./resources/thief.png'); policeImg = loadImage('./resources/police.png'); } //初次调用 function setup() { createCanvas(col * gridW + 60, row * gridH).parent('gameCanvas') document.querySelector('.page').style.cssText = `width:${(col + 2) * gridW}px` } //循环体 function draw() { noStroke(); background('#fff'); // 绘制格子盘 for (let i = 1; i < col + 1; i++) { for (let j = 0; j < row; j++) { fill('pink') rect(i * gridW, j * gridH, gridW, gridH - 1); // 将高度改为gridH-1 } } // 绘制小偷和警察 image(thiefImg, thiefXPos, thiefYPos - 5, gridW, gridH + 5); image(policeImg, policeXPos, policeYPos - 20, gridW, gridH + 25); if (whoWin === 'police') { fill(255, 0, 0); textSize(30) text("恭喜你抓住小偷啦,警察胜利喽!", col * gridW / 6, 60); } if (whoWin === 'thief') { fill(255, 0, 0); textSize(30) text("小样,还想抓住我,哈哈哈", col * gridW / 5, 60); } //绘制通道 for (let i = 1; i < row; i++) { fill('pink'); (i % 2 === 0) ? rect((col + 1) * gridW, gridH * i - 1, gridW, 3) : rect(0, gridH * i - 1, gridW, 3); } //绘制出口文字 fill(0, 0, 0); textSize(12) text("出口", (col + 1) * gridW + 2, row * gridH - 10); // 显示当前玩家数据 thiefVoiceValNode.innerText = thiefProgressNode.value = policeDir / policeStepLen; policeValNode.innerText = policeProgressNode.value = Math.abs(policeDir); } //获取音量大小 async function getVoiceSize() { try { const stream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true } });// echoCancellation: true以减少回音和噪声的影响 const audioContext = new AudioContext(); const source = audioContext.createMediaStreamSource(stream); const processor = audioContext.createScriptProcessor(1024, 1, 1); source.connect(processor); processor.connect(audioContext.destination); processor.onaudioprocess = function (event) { const buffer = event.inputBuffer.getChannelData(0); const sum = buffer.reduce((acc, val) => acc + val ** 2, 0);//计算音量大小 voiceSize = Math.sqrt(sum / buffer.length); }; } catch (err) { console.log('getUserMedia error: ', err); } } //胜利逻辑函数 function winLogicFunc(who) { if (who == 'thief' || who == 'police') { clearInterval(interval); song.play(); whoWin = who noLoop(); (who === 'thief' ? thiefWinGifNode : policeWinGifNode).style.visibility = 'visible'; } } //开始游戏事件 startBtn.addEventListener('click', () => { getVoiceSize() startBtn.disabled = true; let dir = stepLen; // policeDir = Math.floor(voiceSize * 100) / 10 * policeStepLen; //小偷、警察运动循环体 interval = setInterval(() => { policeDir = Math.floor(voiceSize * 100) / 10 * policeStepLen thiefXPos += dir; policeXPos += policeDir; //小偷的改变方向逻辑 if (thiefXPos >= (col + 1) * gridW) { thiefXPos = (col + 1) * gridW - stepLen dir = -stepLen thiefYPos += gridH } else if (thiefXPos < 0) { thiefXPos = -stepLen dir = stepLen thiefYPos += gridH } //警察的改变方向逻辑 if (policeXPos >= (col + 1) * gridW) { policeXPos = (col + 1) * gridW - stepLen policeStepLen = -policeStepLen policeYPos += gridH } else if (policeXPos < 0) { policeXPos = 1 policeStepLen = -policeStepLen policeYPos += gridH } //贼胜利逻辑 if (thiefYPos >= row * gridH) { winLogicFunc('thief') } //警察胜利逻辑 if (policeYPos === thiefYPos && (policeStepLen > 0 && policeXPos >= thiefXPos || policeStepLen < 0 && Math.abs(policeXPos) <= thiefXPos)) { winLogicFunc('police') } }, moveTime); }); //重新开始游戏 restartBtn.addEventListener('click', () => { thiefWinGifNode.style.visibility = 'hidden'; policeWinGifNode.style.visibility = 'hidden'; startBtn.disabled = false; // 重新开始游戏 clearInterval(interval); thiefXPos = 40; thiefYPos = 0; policeXPos = 0; policeYPos = 0; whoWin = ''; loop(); });
2.页面布局和样式
页面的布局结构相对简单,大家可以到github页面上去阅读源码。页面布局每个人的审美不同,实现的各有千秋,大家着重理js核心代码,页面实现那是三下五除二的。