上班最强摸鱼游戏-多人联机小游戏 (一)


highlight: a11y-dark

image.png
多人在线射击游戏、最强摸鱼游戏
在想体验地址====>

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);

image.png
玩家搞定了,接下来就是渲染了

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() { 
        // 玩家渲染
    }
}

看看效果,果然出现了

image.png

让玩家动起来

通过监听上下左右按键,分别执行不同的操作

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();
  }
  ......
}

看看效果 玩家已经动起来了

2022-11-29 17-27-36.2022-11-29 17_38_16.gif

边缘计算 ,上下左右可视窗,不能超出

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);
    }
  };
};

搞定

2022-11-29 17-35-48.2022-11-29 17_37_38.gif

玩家的移动算是搞定了

子弹

鼠标点击的方向就是子弹出来的地方

那首先就得监听鼠标点击的位置

/**
 * 初始化操作监听
 */
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轴正半轴的夹角大小。

image.png

image.png

然后根据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();
  }

}

看看效果

2022-11-29 18-12-00.2022-11-29 18_12_08.gif
子弹是有了,但是怎么才能让他动起来了???

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();

看看效果怎么样

2022-11-29 18-18-18.2022-11-29 18_18_30.gif
完美!!!!

下个帖子重点聊联机

© 版权声明
THE END
喜欢就支持一下吧
点赞6 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容