highlight: a11y-dark
多人在线射击游戏、最强摸鱼游戏
在想体验地址====>
github地址:
开发不易,多谢大哥大姐们点个start吧,点个小爱心吧
技术栈
- canvas、socket
初始 Canvas
画布
<template>
<div class="main-wrap" id="main-wrap">
<canvas class="canvas" ref="canvasRef" id="canvas"></canvas>
</div>
</template>
<script setup lang="ts">
const canvasRef = ref();
import { onMounted, ref } from 'vue';
// 获取可是窗大小
const { innerWidth, innerHeight } = window;
onMounted(() => {
// 赋值画布大小
canvasRef.value.width = innerWidth;
canvasRef.value.height = innerHeight;
})
</script>
<style lang="scss" scoped>
.main-wrap {
height: 100%;
width: 100%;
}
</style>
定义玩家的模型
工厂模式走起,每出现一个玩家就通过创建一个实例就行了
export class Player {
public options: any;
public ctx: any;
constructor(ctx: any, options: any) {
// canvas 2d实例
this.ctx = ctx;
// 玩家属性
this.options = options;
// 渲染玩家
this.render();
}
/**
* 渲染
*/
render() {
// 玩家渲染
}
/**
* 更新位置
*/
update() {
// 玩家更新位置
}
}
这样就搞定了玩家模型了,接下来是定义子弹的模型
定义子弹的模型
同玩家模型一致
export class Bullet {
public options: any
public ctx: any
constructor(ctx: any, options: any) {
this.options = options;
this.ctx = ctx;
this.render();
}
/**
* 渲染
*/
render() {
// 子弹渲染
}
/**
* 更新位置
*/
update() {
// 子弹更新位置
}
}
工具函数 结下来会用到
/**
* 随机id
* @params length {number} 长度
* @returns id {string} 随机id
*/
export const getRandomId = (length?: number) => {
return (Math.random() + new Date().getTime()).toString(32).slice(0, length || 13);
};
/**
* 随机颜色 16进制
* @returns #cccccc
*/
export const getRandomColor = () => {
return `#${random().toString(16)}${random().toString(16)}${random().toString(16)}`;
};
/**
* 获取0 - 256 的随机数
* @returns 随机数
*/
const random = () => {
return Math.floor(Math.random() * 256);
};
创建canvas 2d画布
let ctx: CanvasRenderingContext2D;
ctx = canvasRef.value.getContext('2d');
// 设置背景颜色
ctx.fillStyle = '#ccc';
// 高宽
ctx.fillRect(0, 0, innerWidth, innerHeight);
创建玩家并渲染
// 所有玩家集合
const allPlayer = new Map();
/**
* 创建玩家
*/
const createPlayer = () => {
// 判断是否存在
if (allPlayer.has(id)) return;
const p = new Player(ctx, {
// 唯一标识
id: getRandomId(10),
// 随机出现的位置
x: Math.round(Math.random() * innerWidth),
y: Math.round(Math.random() * innerHeight),
// 初始大小
size: 20,
// 随机玩家颜色
color: getRandomColor(),
// 移动速度
speed: 20,
// 可视窗高宽
innerWidth,
innerHeight,
// 玩家名字
text: 'A'
});
allPlayer.set(p.options.id, p);
};
console.log(allPlayer);
玩家搞定了,接下来就是渲染了
把Player
类 的render
完善一下
export class Player {
......
/**
* 渲染
*/
render() {
const { x, y, size, color, text } = this.options;
const { ctx } = this;
// 开一个路径
ctx.beginPath();
// 画一个圆 ===> 为了简单,就已圆代替玩家
ctx.arc(x, y, size, 0, 2 * Math.PI, false);
// 填充颜色
ctx.fillStyle = color;
// 关闭该路径
ctx.fill();
// 设置玩家名称
if (text) {
ctx.font = '20px Arial';
ctx.fillStyle = '#000';
ctx.textAlign = 'center';
ctx.fillText(text, x+size/2-10, y+size/2-4);
}
}
......
}
接下来就是执行render
,在 new Player
的过程中就有已经执行
export class Player {
。。。。
constructor(ctx: any, options: any) {
。。。。。
this.render();
}
/** * 渲染 */
render() {
// 玩家渲染
}
}
看看效果,果然出现了
让玩家动起来
通过监听上下左右按键,分别执行不同的操作
keyCode | 方向 |
---|---|
37 | 左 |
38 | 上 |
39 | 右 |
40 | 下 |
onMounted(() => {
。。。。。。
initOperate();
。。。。。。
});
/**
* 初始化操作监听
*/
const initOperate = () => {
// 键盘事件 只控制状态值
window.onkeydown = function (e: KeyboardEvent) {
renderElements(e.keyCode);
};
};
/**
* 渲染所有的元素 子弹 玩家 。。。。
*/
const renderElements = (keyCode?: number) => {
// 清空画布
clearRect();
// 更新玩家
allPlayer.get(player?.options.id).update(keyCode);
};
接下来实现Player
中的update
export class Player {
......
/**
* 更新位置信息
* @param keyCode 键盘码值
*/
update(keyCode?: number) {
const { x, y, speed } = this.options;
// 通过keycode 改变xy的坐标信息
switch (keyCode) {
case 37:
this.options.x = x - speed;
break;
case 38:
this.options.y = y - speed;
break;
case 39:
this.options.x = x + speed;
break;
case 40:
this.options.y = y + speed;
break;
}
// 重新渲染
this.render();
}
......
}
看看效果 玩家已经动起来了
边缘计算 ,上下左右可视窗,不能超出
export class Player {
......
/**
* 边缘计算
* @param keyCode
*/
verifyPosition(keyCode: number) {
const { innerWidth, innerHeight, size, x, y, speed } = this.options;
switch (keyCode) {
case 37:
return x - speed > size;
case 38:
return y - speed > size;
case 39:
return x + speed < innerWidth;
case 40:
return y + speed < innerHeight;
default:
return false;
}
}
......
}
那在什么时候调用呢,在执行update之前就得判断是否移动外面
/**
* 初始化操作监听
*/
const initOperate = () => {
// 键盘事件 只控制状态值
window.onkeydown = function (e: KeyboardEvent) {
if (player?.verifyPosition(e.keyCode)) {
renderElements(e.keyCode);
}
};
};
搞定
玩家的移动算是搞定了
子弹
鼠标点击的方向就是子弹出来的地方
那首先就得监听鼠标点击的位置
/**
* 初始化操作监听
*/
const initOperate = () => {
。。。。。。
// 玩家点击创子弹
window.onmousedown = function (e: MouseEvent) {
createBullet(player as Player, e);
};
};
/**
* 创建 bullet
*/
const createBullet = (player: Player, e: MouseEvent) => {
const { x, y } = player.options;
// 返回原点到点的线段与x轴正方向之间的平面角度
const location = Math.atan2(e.clientY - y, e.clientX - x);
const bullet = new Bullet(ctx, {
id: getRandomId(),
x,
y,
size: 5,
color: 'red',
speed: 1,
location: {
x: Math.cos(location) * 8,
y: Math.sin(location) * 8
}
});
allBullet.set(bullet.options.id, bullet);
return bullet;
};
location计算原理
Math.atan2
api方法计算二维坐标系中任意一个点(x, y)和原点(0, 0)的连线与X轴正半轴的夹角大小。
然后根据cos
sin
计算取余弦值、正弦值正数并且 *8
(自定义) 得出移动的速度
子弹渲染render函数
export class Bullet {
。。。。。。
/**
* 渲染
*/
render() {
const { x, y, size, color } = this.options;
const { ctx } = this;
ctx.beginPath();
ctx.arc(x, y, size, 0, 2 * Math.PI, false);
ctx.fillStyle = color;
ctx.fill();
}
/**
* 更新位置
*/
update() {
const { x, y, location } = this.options;
this.options.x = x + location.x;
this.options.y = y + location.y;
this.render();
}
}
看看效果
子弹是有了,但是怎么才能让他动起来了???
requestAnimationFrame 定时器重新渲染玩家和子弹
采用requestAnimationFrame
的原因很简单,每一帧执行一次,60赫兹的话
那就是1000/60 = 16.66666
毫秒渲染一次
/**
* 定时任务
*/
const timingTask = () => {
requestAnimationFrame(timingTask);
if (!ctx || !canvasRef.value) return;
// 清空画布
clearRect();
// 重新渲染
allPlayer.forEach((pl: Player) => {
pl.render();
});
// 遍历子弹
allBullet.forEach((item: Bullet) => {
const { x, y } = item.options;
// 边缘判断 出边界线外删除
if (x >= innerWidth || x <= 0 || y >= innerHeight || y <= 0) {
allBullet.delete(item.options.id);
}
item.update();
});
};
timingTask();
看看效果怎么样
完美!!!!
下个帖子重点聊联机
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END
暂无评论内容