phaser 3 实践-简单的跳格子(大富翁)

569 阅读3分钟

vue3+ts+phaser3+vite 类似大富翁。实现内容:路径排列+玩家跳跃 效果如下

001.png

平台图片

1732537181376995917.png

具体实现

// phaserScene.ts
import Phaser from "phaser";

interface customParamType {
  zoomRatio: number;
  gameWidth: number;
  gameHeight: number;
}

export class GameScene extends Phaser.Scene {
  private tileWidth = 66;  // 方块宽
  private tileHeight = 50;  // 方块高
  private prizeH = 24;  // 礼物宽
  private prizeW = 24;  // 礼物高
  public zoomRatio: number
  public tiles: Phaser.GameObjects.Image[]
  public rewards: { index: number, object: Phaser.GameObjects.Image}[]
  public player!: Phaser.GameObjects.Image
  public currentTileIndex: number // 当前玩家所在位置

  constructor(config: Phaser.Types.Scenes.SettingsConfig, customParam: customParamType) {
    super(config); // 调用父类构造函数

    this.zoomRatio = customParam.zoomRatio
    this.tileWidth = this.tileWidth * this.zoomRatio
    this.tileHeight = this.tileHeight * this.zoomRatio
    this.prizeH = this.prizeH * this.zoomRatio
    this.prizeW = this.prizeW * this.zoomRatio

    this.tiles = []
    this.rewards = []

    this.currentTileIndex = 0
  }

  preload(): void {
    this.load.image("tile", "/image/1732537181376995917.png");
    this.load.image("player", "/image/1732537181376317481.png");
    this.load.image("reward", "/image/1732537181376495211.png");
  }

  create(): void {
    // 路径
    const pathData = [
      { sX: 242, sY: 464, angle: 210, reduce: -1, itemList: [
          { x: 0, y: 0 , depth: -1, reward: false },
          { x: 0, y: 0 , depth: -2, reward: true },
          { x: 0, y: 0 , depth: -3, reward: false },
          { x: 0, y: 0 , depth: -4, reward: true },
          { x: 0, y: 0 , depth: -5, reward: false },
        ] 
      },
      { sX: 80, sY: 340, angle: -30, reduce: 1, itemList: [
          { x: 0, y: 0 , depth: -6, reward: true },
          { x: 0, y: 0 , depth: -7, reward: true },
          { x: 0, y: 0 , depth: -8, reward: false },
          { x: 0, y: 0 , depth: -9, reward: true },
          { x: 0, y: 0 , depth: -10, reward: false },
          { x: 0, y: 0 , depth: -11, reward: true },
        ] 
      },
      { sX: 247, sY: 217, angle: 0, reduce: -1, itemList: [
          { x: 0, y: 0 , depth: -13, reward: false }
        ] 
      },
      { sX: 200, sY: 233, angle: 210, reduce: -1, itemList: [
          { x: 0, y: 0 , depth: -12, reward: false },
          { x: 0, y: 0 , depth: -14, reward: true },
          { x: 0, y: 0 , depth: -15, reward: false },
          { x: 0, y: 0 , depth: -16, reward: true },
          { x: 0, y: 0 , depth: -17, reward: false },
        ] 
      },
      { sX: 102, sY: 131, angle: -30, reduce: 1, itemList: [
          { x: 0, y: 0 , depth: -18, reward: true },
          { x: 0, y: 0 , depth: -19, reward: false },
          { x: 0, y: 0 , depth: -20, reward: true },
          { x: 0, y: 0 , depth: -21, reward: true },
        ] 
      },
      { sX: 222, sY: 40, angle: 210, reduce: -1, itemList: [
          { x: 0, y: 0 , depth: -22, reward: false },
          { x: 0, y: 0 , depth: -23, reward: true }
        ] 
      }
    ]

    pathData.forEach((pItem, i) => {
      const angle = Phaser.Math.DegToRad(pItem.angle); // 倾斜角度(30 度)
      const stepX = 120; // 每个 Tile 的水平间距
      const stepY = Math.tan(angle) * stepX; // 根据角度计算垂直偏移

      pItem.itemList.forEach((tItem, j) => {
        const x = pItem.sX * this.zoomRatio + j * stepX * pItem.reduce
        const y = pItem.sY * this.zoomRatio + j * stepY * pItem.reduce
        const tile = this.add.image(x, y, 'tile')
        tile.setDisplaySize(this.tileWidth, this.tileHeight)
        tile.setDepth(tItem.depth)
        // tiles.push({x, y, obj: tile})
        this.tiles.push(tile)

        if (tItem.reward) {
          const rx = pItem.sX * this.zoomRatio + j * stepX * pItem.reduce
          const ry = (pItem.sY - 14) * this.zoomRatio + j * stepY * pItem.reduce
          const reward = this.add.image(rx, ry, "reward");
          reward.setDisplaySize(this.prizeW, this.prizeH)
          this.rewards.push({ index: i, object: reward });
        }
      })
    })

    // 创建玩家
    this.player = this.add.image(242 * this.zoomRatio, 464 * this.zoomRatio, "player");
    this.player.setDisplaySize(80, 80)
    this.player.setOrigin(0.5, 1)
  }

  update(): void {
    // TODO
  }

  // 玩家移动逻辑
  public movePlayer = (steps: number) => {
    const nextTileIndex = Math.min(this.currentTileIndex + steps, this.tiles.length - 1);

    this.jumpStepByStep(this.player, this.tiles, this.currentTileIndex, nextTileIndex, () => {
      this.currentTileIndex = nextTileIndex; // 更新玩家位置
      console.log(`Player reached platform ${this.currentTileIndex}`);
    });
  };

  public jumpStepByStep = (player: Phaser.GameObjects.Image, platforms: Phaser.GameObjects.Image[], startIndex: number, targetIndex: number, onComplete: Function) => {
    if (startIndex >= targetIndex) {
      onComplete && onComplete();
      return;
    }

    const nextIndex = startIndex + 1;
    const nextPlatform = platforms[nextIndex];

    this.jumpToPlatform(player, nextPlatform, () => {
      // 方块缩放效果
      this.applyPlatformEffect(nextPlatform);

      // 递归跳到下一个平台
      this.jumpStepByStep(player, platforms, nextIndex, targetIndex, onComplete);
    });
  }

  // 跳跃动画函数
  public jumpToPlatform = (player: Phaser.GameObjects.Image, targetPlatform: Phaser.GameObjects.Image, onComplete: Function) => {
    const startX = player.x;
    const startY = player.y;
    const targetX = targetPlatform.x;
    const targetY = targetPlatform.y;
    // console.log(startX, startY, targetX, targetY)

    // 计算跳跃曲线高度(与距离成比例)
    const distance = Phaser.Math.Distance.Between(startX, startY, targetX, targetY);
    const jumpHeight = distance / 2; // 跳跃高度可以根据需求调整

    // 自定义路径模拟抛物线
    const topP = new Phaser.Math.Vector2(targetX, targetY - jumpHeight)
    const topP2 = new Phaser.Math.Vector2(targetX, targetY)
    const curve = new Phaser.Curves.Path(startX, startY);
    curve.splineTo([topP, topP2]);

    // 用路径控制跳跃动画
    const duration = 500; // 动画时长
    let progress = 0;
    const curveGraphics = player.scene.add.graphics();

    const tween = player.scene.tweens.add({
      targets: { t: 0 },
      t: 1,
      duration,
      ease: 'Linear',
      onUpdate: (tween, target) => {
        progress = target.t;
        const point = curve.getPoint(progress);
        player.setPosition(point.x, point.y);

        // 可选:绘制路径调试
        curveGraphics.clear();
        curveGraphics.lineStyle(2, 0xff0000, 1);
        curve.draw(curveGraphics, 64);
      },
      onComplete: () => {
        curveGraphics.clear(); // 清除调试路径
        player.setY(targetY - 10); // 确保玩家落在方块上
        onComplete && onComplete();
      },
    });
  }

  // 平台效果(缩放动画)
  public applyPlatformEffect = (platform: Phaser.GameObjects.Image) => {
    platform.scene.tweens.add({
      targets: platform,
      scale: 0.06,
      duration: 100,
      yoyo: true,
      ease: 'Quad.easeInOut',
    });
  }
}

使用

<template>
  <div ref="gameContainer" class="game-container"></div>
  <button @click="rollDice">掷骰子</button>
</template>

<script setup lang="ts">
// https://juejin.cn/post/7385098943941673014
// https://github.com/phaserjs/custom-build
// https://juejin.cn/post/7140833400835244045
  import Phaser from 'phaser'
  import { onMounted, onBeforeUnmount, ref } from "vue";
  import { GameScene } from './phaserScene'

  const gameContainer = ref<HTMLElement>(); // 绑定 Phaser 容器
  let game: Phaser.Game; // Phaser 游戏实例
  let gameWidth = 0;
  let gameHeight = 0;
  let zoomRatio = 1; // 缩放比

  // 初始化 Phaser 游戏
  const initGame = () => {
    const config: Phaser.Types.Core.GameConfig = {
      type: Phaser.CANVAS,
      parent: gameContainer.value,
      width: gameWidth,
      height: gameHeight,
      canvasStyle: 'width:100%;height:100%',
      backgroundColor: 0x291F61,
      physics: {
        default: "arcade",
      },
      scene: [
        new GameScene({key: "GameScene"}, { zoomRatio, gameWidth, gameHeight })
      ],
      loader: {
        baseURL: 'https://xxx',  // 设置自定义域名路径
      }
    };
    game = new Phaser.Game(config);
    game.input.enabled = false
  };

  // 掷骰子
  const rollDice = () => {
    const steps = Math.floor(Math.random() * 6) + 1; // 随机生成 1-6 的步数
    const gameScene = game.scene.keys["GameScene"] as GameScene; // 获取场景实例
    gameScene.movePlayer(steps)
  };

  // 初始化游戏
  onMounted(() => {
    if (gameContainer.value) {
      // 获取游戏屏幕高度
      gameHeight = gameContainer.value.clientHeight || 542
      gameWidth = gameContainer.value.clientWidth || 351

      // https://segmentfault.com/q/1010000021068970
      gameHeight = gameHeight * window.devicePixelRatio
      gameWidth = gameWidth * window.devicePixelRatio

      zoomRatio = Number((gameHeight/542).toFixed(4))
      console.log(gameHeight, gameWidth, zoomRatio)
    }
    initGame()
  });

  // 销毁游戏实例
  onBeforeUnmount(() => {
    if (game) {
      game.destroy(true);
    }
  });
</script>

<style>
.game-container {
  width: 351px;
  height: 542px;
  margin: 0 auto;
  border: 1px solid #ccc;
  position: relative;
}
button {
  margin-top: 10px;
}
</style>

欢迎批评指正

参考内容: 都在代码里