初试pixijs,实现猫和老鼠小游戏(下)

286 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第12天,点击查看活动详情

一、前言

经过两天的努力,我们对pixijs有了更深刻的认知,先说一下今天的实现思路,如下:

a、开始界面,点击之后才加载场景

b、奔跑时背景滚动,造成视觉滚动

c、碰撞触发函数,提示游戏结束

改动有些大,为了避免大家伙看的云里雾里的,我尽量从头开始讲,也会在文章末尾贴一下源码。

注意: 1、本文使用ts,部分代码存在不兼容情况; 2、本文所使用的pixi版本为6.5.5。

下面就是成品啦,看看效果吧。

完整.gif

二、过程

1、基本步骤

正常的创建好我们的容器app,同时挂载到dom元素上,并加载要使用的图片,图片加载完调用我们的setup函数

// 创建我们的pixi容器
let app = new PIXI.Application({
  width: 400,         // default: 800 宽度
  height: 400,        // default: 600 高度
  antialias: true,    // default: false 反锯齿
  transparent: false, // default: false 透明度
  resolution: 1       // default: 1 分辨率
})

// 使用定时器变成异步等待标签加载完毕
setTimeout(() => {
  console.log(app, PIXI)
  document.getElementById("pixijsjs")?.appendChild(app.view)
  app.loader
  .add([
    "tom.png",
    "jerry.png",
    "bg.png",
    "gameover.jpeg"
  ])
  .load(setup) // 调用自定义函数setup
}, 400)

2、创建三个场景和声明一些会用到的变量

用Container创建开始场景,游戏场景,结束场景

// 创建三个场景,开始游戏界面,游戏主页面,和游戏结束界面
let startGame = new PIXI.Container()
let Game = new PIXI.Container()
let overGame = new PIXI.Container()
// 游戏场景的变量
let cat: PIXI.Sprite
let mouse: PIXI.Sprite
let bg: PIXI.Sprite
let state: Function
// 键盘监测
let left = keyboard("ArrowLeft")
let right = keyboard("ArrowRight")
let start = keyboard("Enter")
let catVX = 0// 设置tom的速度属性
let bgVX = 0// 设置背景的速度属性

3、在setup函数里,往3个场景里添加各自的精灵

开场,只显示开始界面,其余界面通通隐藏。

// 将三个场景加入到容器里
  app.stage.addChild(startGame)
  app.stage.addChild(Game)
  Game.visible = false
  app.stage.addChild(overGame)
  overGame.visible = false
  // 开始场景的精灵
  startScene()
  // 游戏主场景的精灵
  gameScene()
  // 结束场景的精灵
  overScene()

开始场景我们需要一点简单的文字,调用TextStyle生成下我们的开始界面文字!

   let style = new PIXI.TextStyle({
    fontFamily: "Futura",
    fontSize: 30,
    fill: "white"
  });
  let message = new PIXI.Text("按下回车键开始!", style)
  message.x = 100
  message.y = 180

  startGame.addChild(message)

结束场景也比较简单,一张图和一段文字,记得顺序是先图后文字,先加入的在下层,所以必须先图后文,不然文字会被盖住!

  let overbg = new PIXI.Sprite(app.loader.resources["gameover.jpeg"].texture)
  overbg.width = 400
  overbg.height = 400
  overGame.addChild(overbg)

  let style = new PIXI.TextStyle({
    fontFamily: "Futura",
    fontSize: 20,
    fill: "#646cffaa"
  });
  let message = new PIXI.Text("游戏结束了,Tom 气炸了!", style)
  message.x = 100
  message.y = 10

  overGame.addChild(message)

游戏场景需要加入我们的背景图,由于需要实现滚动效果,所以我们可以使用TilingSprite制作一个平铺图

   PIXI.TilingSprite(loader加载好的图, 平铺宽度, 平铺高度)

这里有个容易忽略的点,就是bg的变量也需要同步设置为1800x400,不然会出现类似overflow:hidden的情况,就完全看不到bg宽高以外的背景图。

(因为用的pixi是6.5.5,写法上和5.x版本的PIXI.extras.TilingSprite存在差别,我一度以为是不起效了,结果是我bg宽度没同步设成1800)

  // 设置背景
  // bg = new PIXI.Sprite(app.loader.resources["bg.jpeg"].texture)
  bg = new PIXI.TilingSprite(app.loader.resources["bg.png"].texture, 1800, 400) // 替换为这一行代码
  bg.x = 0
  bg.y = 0
  bg.width = 1800
  bg.height = 400
  Game.addChild(bg)
  

然后是我们的主角tom和jerry,这里有个小点就是,我的素材里,tom的方向刚好是反了,所以我就稍微查了一下在pixi里如何实现sprite镜像水平翻转,就是anchor设置好scale缩放的中心点,然后设置scale乘以-1。

  cat = new PIXI.Sprite(app.loader.resources["tom.png"].texture)
  //  调整汤姆的大小和位置
  //Change the sprite's position
  cat.x = 40
  cat.y = 190
  // 用cat.scale.set(0.5, 0.5)也可以,缩放大小为原图的一半
  //Change the sprite's size
  cat.width = 80
  cat.height = 240
  // 实现sprite水平镜像反转
  // 设置anchor  0left 0.5center 1right
  cat.anchor.x = 0.5
  // 水平缩放比例为-1
  cat.scale.x *= -1
  Game.addChild(cat)
  mouse = new PIXI.Sprite(app.loader.resources["jerry.png"].texture)
  //  调整杰瑞的大小和位置
  //Change the sprite's position
  mouse.x = 300
  mouse.y = 280
  //Change the sprite's size
  mouse.width = 40
  mouse.height = 60

  // 杰瑞应该比汤姆小只
  Game.addChild(mouse)
  

4、先监听键盘事件,实现对应的函数

引入完官网给的自定义键盘识别函数keyboard ,我就直接放最后了。

目前是需要监听← →和enter键

// 完善一下left和right的press和release函数
left.press = () => {
  // console.log(cat)
  catVX = -3
  bgVX = 1
}
left.release = () => {
  if (!right.isDown) {
    catVX = 0
    bgVX = 0
  }
}
right.press = () => {
  // console.log(cat)
  catVX = 3
  bgVX = -1
}
right.release = () => {
  if (!left.isDown) {
    catVX = 0
    bgVX = 0
  }
}
// 按回车键开始
start.press = () => {
  console.log(cat)
  startGame.visible = false
  Game.visible = true
  overGame.visible = false
}

5、碰撞检测逻辑

同样引入完hitTestRectangle函数后,在 游戏循环函数--具体处理逻辑 【play】函数里,添加好逻辑。

// 游戏循环函数--具体处理逻辑
function play(delta: number) {
  cat.x += catVX
  bg.x += bgVX
  // 如果相撞了
  if (hitTestRectangle(cat, mouse)) {
    //There's a collision
    // 隐藏开始场景和游戏场景,开启游戏结束场景
    startGame.visible = false
    Game.visible = false
    overGame.visible = true
  } else if (!hitTestRectangle(cat, mouse)) {
  }
}

我们的项目就完成啦!

6、完整源码

<script setup lang="ts">
import * as  PIXI from 'pixi.js'
defineProps<{ msg: string }>()
// 创建我们的pixi容器
let app = new PIXI.Application({
 width: 400,         // default: 800 宽度
 height: 400,        // default: 600 高度
 antialias: true,    // default: false 反锯齿
 transparent: false, // default: false 透明度
 resolution: 1       // default: 1 分辨率
})
// 创建三个场景,开始游戏界面,游戏主页面,和游戏结束界面
let startGame = new PIXI.Container()
let Game = new PIXI.Container()
let overGame = new PIXI.Container()
// 使用定时器变成异步等待标签加载完毕
setTimeout(() => {
 console.log(app, PIXI)
 document.getElementById("pixijsjs")?.appendChild(app.view)
 app.loader
 .add([
   "tom.png",
   "jerry.png",
   "bg.png",
   "gameover.jpeg"
 ])
 .load(setup)
}, 400)
let cat: PIXI.Sprite
let mouse: PIXI.Sprite
let bg: PIXI.Sprite
let state: Function
function setup() {
 // 加载完图片后
 // 将三个场景加入到容器里
 app.stage.addChild(startGame)
 app.stage.addChild(Game)
 Game.visible = false
 app.stage.addChild(overGame)
 overGame.visible = false
 // 开始场景的精灵
 startScene()
 // 游戏主场景的精灵
 gameScene()
 // 结束场景的精灵
 overScene()
 state = play
 app.ticker.add(delta => gameLoop(delta))
}
// 游戏循环函数
function gameLoop(delta: number){
 // console.log(delta, cat)
 // //Move the cat 1 pixel 
 // cat.x -= 1
 state(delta)
}
// 游戏循环函数--具体处理逻辑
function play(delta: number) {
 cat.x += catVX
 bg.x += bgVX
 // 如果相撞了
 if (hitTestRectangle(cat, mouse)) {
   // 隐藏开始场景和游戏场景,开启游戏结束场景
   startGame.visible = false
   Game.visible = false
   overGame.visible = true
 } else if (!hitTestRectangle(cat, mouse)) {
 }
}
// 碰撞函数
function hitTestRectangle(r1: PIXI.Sprite, r2: PIXI.Sprite) {

//Define the variables we'll need to calculate
let hit, combinedHalfWidths, combinedHalfHeights, vx, vy;

//hit will determine whether there's a collision
hit = false;

//Find the center points of each sprite
r1.centerX = r1.x + r1.width / 2;
r1.centerY = r1.y + r1.height / 2;
r2.centerX = r2.x + r2.width / 2;
r2.centerY = r2.y + r2.height / 2;

//Find the half-widths and half-heights of each sprite
r1.halfWidth = r1.width / 2;
r1.halfHeight = r1.height / 2;
r2.halfWidth = r2.width / 2;
r2.halfHeight = r2.height / 2
//Calculate the distance vector between the sprites
vx = r1.centerX - r2.centerX
vy = r1.centerY - r2.centerY

//Figure out the combined half-widths and half-heights
combinedHalfWidths = r1.halfWidth + r2.halfWidth
combinedHalfHeights = r1.halfHeight + r2.halfHeight

//Check for a collision on the x axis
if (Math.abs(vx) < combinedHalfWidths) {

 //A collision might be occurring. Check for a collision on the y axis
 if (Math.abs(vy) < combinedHalfHeights) {

   //There's definitely a collision happening
   hit = true
 } else {

   //There's no collision on the y axis
   hit = false
 }
} else {

 //There's no collision on the x axis
 hit = false
}

//`hit` will be either `true` or `false`
return hit
}

let left = keyboard("ArrowLeft")
let right = keyboard("ArrowRight")
let start = keyboard("Enter")
let catVX = 0// 设置tom的速度属性
let bgVX = 0// 设置背景的速度属性
// 完善一下left和right的press和release函数
left.press = () => {
 // console.log(cat)
 catVX = -3
 bgVX = 1
}
left.release = () => {
 if (!right.isDown) {
   catVX = 0
   bgVX = 0
 }
}
right.press = () => {
 // console.log(cat)
 catVX = 3
 bgVX = -1
}
right.release = () => {
 if (!left.isDown) {
   catVX = 0
   bgVX = 0
 }
}
// 按回车键开始
start.press = () => {
 console.log(cat)
 startGame.visible = false
 Game.visible = true
 overGame.visible = false
}
// 自定义键盘识别函数
function keyboard(value: string) {
 let key: { value: string; isDown: boolean; isUp: boolean; press: (() => void) | undefined; release: (() => void) | undefined; downHandler: { (event: any): void; bind?: any; }; upHandler: { (event: any): void; bind?: any; }; unsubscribe: () => void; };
 key = {}
 key.value = value;
 key.isDown = false;
 key.isUp = true;
 key.press = undefined;
 key.release = undefined;
 //The `downHandler`
 key.downHandler = event => {
   if (event.key === key.value) {
     if (key.isUp && key.press) key.press();
     key.isDown = true;
     key.isUp = false;
     event.preventDefault();
   }
 };

 //The `upHandler`
 key.upHandler = event => {
   if (event.key === key.value) {
     if (key.isDown && key.release) key.release();
     key.isDown = false;
     key.isUp = true;
     event.preventDefault();
   }
 };

 //Attach event listeners
 const downListener = key.downHandler.bind(key);
 const upListener = key.upHandler.bind(key);
 
 window.addEventListener(
   "keydown", downListener, false
 );
 window.addEventListener(
   "keyup", upListener, false
 );
 
 // Detach event listeners
 key.unsubscribe = () => {
   window.removeEventListener("keydown", downListener);
   window.removeEventListener("keyup", upListener);
 };
 
 return key;
}
// 开始场景的精灵
function startScene(): void {
 let style = new PIXI.TextStyle({
   fontFamily: "Futura",
   fontSize: 30,
   fill: "white"
 });
 let message = new PIXI.Text("按下回车键开始!", style)
 message.x = 100
 message.y = 180

 startGame.addChild(message)
}
// 游戏场景的精灵
function gameScene(): void {
 // 设置背景
 // bg = new PIXI.Sprite(app.loader.resources["bg.jpeg"].texture)
 bg = new PIXI.TilingSprite(app.loader.resources["bg.png"].texture, 1800, 400) // 替换为这一行代码
 bg.x = 0
 bg.y = 0
 bg.width = 1800
 bg.height = 400
 Game.addChild(bg)
 cat = new PIXI.Sprite(app.loader.resources["tom.png"].texture)
 //  调整汤姆的大小和位置
 //Change the sprite's position
 cat.x = 40
 cat.y = 190
 // 用cat.scale.set(0.5, 0.5)也可以,缩放大小为原图的一半
 //Change the sprite's size
 cat.width = 80
 cat.height = 240
 // 实现sprite水平镜像反转
 // 设置anchor  0left 0.5center 1right
 cat.anchor.x = 0.5
 // 水平缩放比例为-1
 cat.scale.x *= -1
 Game.addChild(cat)
 mouse = new PIXI.Sprite(app.loader.resources["jerry.png"].texture)
 //  调整杰瑞的大小和位置
 //Change the sprite's position
 mouse.x = 300
 mouse.y = 280
 //Change the sprite's size
 mouse.width = 40
 mouse.height = 60
 
 // 杰瑞应该比汤姆小只
 Game.addChild(mouse)
}
// 结束场景的精灵
function overScene(): void {
 let overbg = new PIXI.Sprite(app.loader.resources["gameover.jpeg"].texture)
 overbg.width = 400
 overbg.height = 400
 overGame.addChild(overbg)

 let style = new PIXI.TextStyle({
   fontFamily: "Futura",
   fontSize: 20,
   fill: "#646cffaa"
 });
 let message = new PIXI.Text("游戏结束了,Tom 气炸了!", style)
 message.x = 100
 message.y = 10

 overGame.addChild(message)

 
}
</script>

<template>
 <p class="pixilabel"> pixi盒子</p>
 <div id="pixijsjs" class="pixibox">
 </div>
</template>

<style scoped>
.pixilabel {
 position: absolute;
 top: 20%;
 left: 50%;
 transform: translate(-50%, -50%);
}
.pixibox {
 width: 400px;
 height: 400px;
 position: absolute;
 top: 50%;
 left: 50%;
 transform: translate(-50%, -50%);
}
</style>

三、 总结

经过这三天对pixijs的尝试,从零到现在完成一个小demo,也算是有点熟悉了,其实今天是打算实现精灵动画的,不过加班导致我没啥时间去钻研,而且感觉有些许复杂,加上手头没比较好的素材,就没加上tom的奔跑效果,等我琢磨一下再出一期讲精灵动画如何实现的。

ps: 我是地霊殿__三無,希望能和你一起进步!

微信图片_20221001074313.jpg