使多人绘图游戏画布具有响应性

Make multiplayer drawing game canvas responsive

提问人:septimus 提问时间:11/17/2023 最后编辑:septimus 更新时间:11/17/2023 访问量:15

问:

我目前正在使用 react(客户端)、nestjs(后端)和 WebSockets(就像 https://skribbl.io/)构建一个多人绘图游戏。只要画布元素保持在固定的高度 (800x600) 上,我的绘图逻辑就可以正常工作。 但我希望能够使其响应,以便纵横比仍然是 800x600 的纵横比,但如果需要,或者如果有人调整窗口大小或拥有小型设备,画布可以更大/更小。

这会导致我的绘图逻辑出现问题,如果画布的大小不是 800x600,我的鼠标指针就不是我的线条绘制的位置。

我制作了一个小视频,在 youtube 上展示了这个问题:https://youtu.be/DhLzhbDCDII

我的代码目前是这个(前端,React)DrawingCanvas.tsx:

import React, { useEffect, useState } from 'react';
import { useDraw } from '../hooks/useDraw';
import {
  Draw,
  DrawingCanvasProps,
  DrawLineProps,
} from '../interfaces/draw.interface';
import { ChromePicker } from 'react-color';
import { Button } from 'react-bootstrap';
import { drawLine } from '../utils/drawLine';
import {
  SOCKET_EVENT_CANVAS_STATE,
  SOCKET_EVENT_CANVAS_STATE_FROM_SERVER,
  SOCKET_EVENT_CLEAR_CANVAS,
  SOCKET_EVENT_CLIENT_READY,
  SOCKET_EVENT_DRAW_LINE,
  SOCKET_EVENT_GET_CANVAS_STATE,
} from '../config/socket.config';

export const DrawingCanvas: React.FC<DrawingCanvasProps> = ({ socket }) => {
  const { canvasRef, onMouseDown, clearCanvas } = useDraw(createLine);
  const [color, setColor] = useState<string>('#000');

  useEffect(() => {
    const ctx = canvasRef.current?.getContext('2d');

    socket.emit(SOCKET_EVENT_CLIENT_READY);
    socket.on(SOCKET_EVENT_GET_CANVAS_STATE, () => {
      if (!canvasRef.current?.toDataURL()) {
        return;
      }
      socket.emit(SOCKET_EVENT_CANVAS_STATE, canvasRef.current.toDataURL());
    });

    socket.on(SOCKET_EVENT_CANVAS_STATE_FROM_SERVER, (state: string) => {
      const img = new Image();
      img.src = state;
      img.onload = () => {
        ctx?.drawImage(img, 0, 0);
      };
    });

    socket.on(
      SOCKET_EVENT_DRAW_LINE,
      ({ previousPoint, currentPoint, color }: DrawLineProps) => {
        if (!ctx) {
          return;
        }
        drawLine({ previousPoint, currentPoint, ctx, color });
      },
    );

    socket.on(SOCKET_EVENT_CLEAR_CANVAS, clearCanvas);

    // cleanup like with event listeners
    return () => {
      socket.off(SOCKET_EVENT_GET_CANVAS_STATE);
      socket.off(SOCKET_EVENT_CANVAS_STATE_FROM_SERVER);
      socket.off(SOCKET_EVENT_DRAW_LINE);
      socket.off(SOCKET_EVENT_CLEAR_CANVAS);
    };
  }, [canvasRef, socket]);

  function createLine({ previousPoint, currentPoint, ctx }: Draw) {
    socket.emit('draw-line', { previousPoint, currentPoint, color });
    drawLine({ previousPoint, currentPoint, ctx, color });
  }

  return (
    <div className="container d-flex justify-content-center align-items-center">
      <div className="d-flex flex-column gap-4 px-4">
        <ChromePicker color={color} onChange={(e) => setColor(e.hex)} />
        <Button
          onClick={() => socket.emit(SOCKET_EVENT_CLEAR_CANVAS)}
          className="p-2 rounded border border-black"
          variant="secondary"
        >
          Reset
        </Button>
      </div>
      <canvas
        style={{ width: '100%', height: 'auto' }} // make it responsive
        width={800}
        height={600}
        className="border border-black rounded"
        ref={canvasRef}
        onMouseDown={onMouseDown}
      ></canvas>
    </div>
  );
};

我还有一个用于绘图的自定义钩子:UseDraw.ts

import { useEffect, useRef, useState } from 'react';
import { Draw, Point } from '../interfaces/draw.interface';

export const useDraw = (
  onDraw: ({ ctx, currentPoint, previousPoint }: Draw) => void,
) => {
  const [mouseDown, setMouseDown] = useState(false);
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const previousPoint = useRef<null | Point>(null);

  const onMouseDown = () => setMouseDown(true);

  const clearCanvas = () => {
    const canvas = canvasRef.current;
    if (!canvas) {
      return;
    }

    const ctx = canvas.getContext('2d');
    if (!ctx) {
      return;
    }

    ctx.clearRect(0, 0, canvas.width, canvas.height);
  };

  useEffect(() => {
    const handler = (e: MouseEvent) => {
      if (!mouseDown) {
        return;
      }

      const currentPoint = computePointInCanvas(e);
      const ctx = canvasRef.current?.getContext('2d');
      if (!ctx || !currentPoint) {
        return;
      }

      onDraw({ ctx, currentPoint, previousPoint: previousPoint.current });
      previousPoint.current = currentPoint;
    };

    const computePointInCanvas = (e: MouseEvent) => {
      const canvas = canvasRef.current;
      if (!canvas) {
        return;
      }

      const rect = canvas.getBoundingClientRect();
      const x = e.clientX - rect.left;
      const y = e.clientY - rect.top;

      return { x, y };
    };

    const mouseUpHandler = () => {
      setMouseDown(false);
      previousPoint.current = null; // reset the previous point
    };

    // add event listeners
    canvasRef.current?.addEventListener('mousemove', handler);
    window.addEventListener('mouseup', mouseUpHandler);

    // remove event listeners
    return () => {
      canvasRef.current?.removeEventListener('mousemove', handler);
      window.removeEventListener('mouseup', mouseUpHandler);
    };
  }, [onDraw]);

  return { canvasRef, onMouseDown, clearCanvas };
};

还有一个小的辅助函数:drawLine.ts

import { DrawLineProps } from '../interfaces/draw.interface';

export const drawLine = ({
  previousPoint,
  currentPoint,
  ctx,
  color,
}: DrawLineProps) => {
  const { x: currentX, y: currentY } = currentPoint;
  const lineColor = color;
  const lineWidth = 5;

  let startPoint = previousPoint ?? currentPoint;
  ctx.beginPath();
  ctx.lineWidth = lineWidth;
  ctx.strokeStyle = lineColor;
  ctx.moveTo(startPoint.x, startPoint.y);
  ctx.lineTo(currentX, currentY);
  ctx.stroke();

  ctx.fillStyle = lineColor;
  ctx.beginPath();
  ctx.arc(startPoint.x, startPoint.y, 2, 0, 2 * Math.PI);
  ctx.fill();
};

在后端,我在 nestjs 中定义了一个网关,用于向我的客户端发送绘图坐标:

@SubscribeMessage('draw-line')
  handleDrawLine(client: Socket, data: DrawLine): void {
    const player = this.roomService.getPlayerById(client.id);
    if (player) {
      const room = this.roomService
        .getRooms()
        .find((r) => r.players.includes(player));
      if (room) {
        this.emitToRoomClients(room, 'draw-line', data);
      }
    }
  }

DrawLine - 接口包含以下属性:

export interface DrawLine {
  previousPoint: Point | null;
  currentPoint: Point;
  color: string;
}

export interface Point {
  x: number;
  y: number;
}

我已经尝试通过调整当前画布大小和高度的点来解决问题,但这不起作用。似乎没有任何效果,几天来我一直在努力让它发挥作用。

ReactJS Canvas WebSocket 响应式设计 绘图

评论


答: 暂无答案