基于PixiJS的小游戏广告开发

前言

这是一个完整的试玩游戏广告,最终会投放到 Moloco 广告平台进行推广,游戏不算复杂,但是也是第一次接触这个类目,记录一下 😀😀😀

如果文章对你有帮助的话,记得一键三连哟。有问题和疑惑的话也可以在评论区留言。我会第一时间回复大家,如果觉得我的文章哪里有知识点错误的话,也恳请能够告知,把错的东西理解成对的,无论在什么行业,都是致命的。

项目概览

技术栈

  • PixiJS 8.x: 2D WebGL 渲染引擎,用来画游戏画面
  • GSAP 3.x: 动画库,让元素动起来更流畅,强烈推荐
  • TypeScript: 带类型检查的 JavaScript
  • Vite: 现代化的打包工具
  • @pixi/sound: 管理音效和背景音乐

项目文件结构

plain 复制代码
src/
├── main.ts                 # 程序入口
├── types.ts               # 类型定义
├── components/            # 游戏组件
│   ├── Background.ts      # 背景层
│   ├── Character.ts       # 左侧角色
│   ├── TopBar.ts         # 顶部栏(Logo/大奖/分数)
│   ├── SlotArea.ts       # 转轮区域容器
│   ├── SlotMachine.ts    # 转轮核心逻辑
│   ├── MaskOverlay.ts    # 引导遮罩
│   ├── BottomArea.ts     # 底部按钮
│   ├── GuideHand.ts      # 引导手势
│   ├── CoinRain.ts       # 金币雨特效
│   └── DownloadOverlay.ts # 下载弹窗
├── utils/
│   ├── responsive.ts     # 响应式适配
│   ├── textures.ts       # 纹理管理
│   ├── resources.ts      # 资源加载
│   ├── audio.ts         # 音频管理
│   ├── ad.ts            # 广告跳转
│   └── time.ts          # 时间工具
└── assets/              # 资源文件

核心系统实现

程序启动流程

typescript 复制代码
// 创建 PixiJS 应用
const app = new Application();

// 初始化配置
await app.init({
  backgroundColor: 0xffffff,
  resizeTo: window, // 自动适配窗口大小
  resolution: Math.max(window.devicePixelRatio, 2), // 支持高清屏
  autoDensity: true,
});

// 初始化响应式系统(基于 375x667 设计稿)
initResponsive(app, 375, 667);

// 加载所有资源
await loadResources();

// 创建游戏场景
createGameScene();

响应式适配系统

类似 CSS 的 Rem 单位:

typescript 复制代码
// 设计稿上的像素值转换成实际屏幕像素
export function rem(px: number): number {
  const scale = getScale();
  return px * scale;
}

// 计算缩放比例
export function getScale(): number {
  const { width, height } = appInstance.screen;

  // 竖屏: 按宽度缩放
  if (height > width) {
    return width / DESIGN_WIDTH; // DESIGN_WIDTH = 375
  }
  // 横屏: 按高度缩放
  return height / DESIGN_HEIGHT; // DESIGN_HEIGHT = 667
}

使用方法:

typescript 复制代码
// 设计稿上某个元素位置是 (100, 200)
sprite.x = rem(100);
sprite.y = rem(200);

// 设计稿上字体大小是 24
text.style.fontSize = rem(24);

资源管理

纹理图集

纹理就是把很多小图片加到一个大图片里面,包含了一个 json 文件记录小图片位置,我把所有小图片打包成三张大图,这样加载更快:

typescript 复制代码
// 主资源图集 (assets.png)
// 包含 UI、背景、按钮等
await loadAssetsSheet();

// 图标图集 (assets_icons.png)
// 包含转轮上的各种图标
await loadAssetsIconsSheet();

// 宣传图集 (promo_custom.png)
// 包含最终落地页素材
await loadPromoCustomSheet();

// 使用时只需要传图片名称
const texture = getAssetsTexture("Character");
const sprite = new Sprite(texture);

图集的好处:

  • 一次请求下载多张图
  • 减少服务器压力
  • GPU 渲染更高效

音频管理

音频也是一样,用@pixi/sound 进行播放,使用单例模式管理所有音效:

typescript 复制代码
class AudioManager {
  public async init() {
    // 加载所有音效
    await sound.add("spinButton", spinButtonUrl);
    await sound.add("reelClick", reelClickUrl);
    await sound.add("backgroundMusic", backgroundMusicUrl);
  }

  public playSpinButton() {
    sound.play("spinButton");
  }

  public playBackgroundMusic() {
    sound.play("backgroundMusic", {
      loop: true, // 循环播放
      volume: 0.5, // 音量 50%
    });
  }
}

组件设计模式

所有需要适配屏幕的组件都得实现 Resizable 这个接口:

typescript 复制代码
export interface Resizable {
  resize(...args: unknown[]): void;
}

组件的生命周期:

  1. 构造: 创建精灵、设置初始位置
  2. Resize: 窗口变化时重新计算布局
  3. 交互: 处理用户点击、拖动等
  4. 销毁: 清理资源防止内存泄漏

游戏流程设计

场景层级

页面中各个区块层级不一样,使用 zIndex 管理不同层级,数字越大越靠前:

plain 复制代码
Stage
├── Background (zIndex: 0) - 背景最底层
├── MaskOverlay (zIndex: 1) - 引导遮罩
├── Character (zIndex: 2) - 角色
├── BottomArea (zIndex: 3) - 按钮
├── SlotArea (zIndex: 4) - 转轮
├── TopBar (zIndex: 5) - 顶部UI
└── DownloadOverlay (zIndex: 100) - 弹窗最上层

游戏流程图

plain 复制代码
用户进入
    ↓
[初始场景]
- 显示角色和引导
- Play 按钮 + 手势动画
- 播放背景音乐
    ↓
用户点击 Play
    ↓
[淡出动画]
- 角色淡出 (0.8秒)
- 遮罩淡出
- 按钮淡出
    ↓
[转轮旋转]
- 播放旋转音效
- 3个转轮依次启动
- 每个转 2 秒
- 依次停止
    ↓
[判断结果]
- 检查是否三个图标一样
- 分数增长动画
    ↓
[金币雨]
- 开始掉金币
- 金币下落+旋转+翻转
- 播放中奖音效
    ↓
[3秒后]
    ↓
[显示下载弹窗]
- 弹窗淡入
- 金币雨逐渐停止

核心功能实现

游戏转轮

状态机管理

我用状态机来管理转轮状态,避免出现点两次按钮之类的 bug:

typescript 复制代码
enum SlotMachineState {
  IDLE = "IDLE", // 空闲,可以点击
  SPINNING = "SPINNING", // 正在转
  STOPPING = "STOPPING", // 正在停止
  STOPPED = "STOPPED", // 已停止
}

class SlotMachineStateMachine {
  canSpin(): boolean {
    // 只有空闲和已停止状态才能再次旋转
    return [SlotMachineState.IDLE, SlotMachineState.STOPPED].includes(
      this.currentState
    );
  }
}

转轮动画

typescript 复制代码
class Reel {
  async spin(
    delay: number, // 延迟启动
    targetIcon: number, // 目标图标
    speed: number, // 速度
    duration: number // 持续时间
  ): Promise<void> {
    // 1. 延迟启动(让3个转轮错开)
    await sleep(delay * 1000);

    // 2. 加速阶段
    gsap.to(this, {
      y: `+=${speed * 3}`,
      duration: 0.3,
      ease: "power2.in",
    });

    // 3. 匀速旋转
    gsap.to(this, {
      y: `+=${speed * duration}`,
      duration: duration,
      ease: "none",
    });

    // 4. 减速停在目标图标
    await this.stopAtIcon(targetIcon);
  }
}

预设结果

因为是试玩游戏,为了控制体验,直接预设几个游戏结果,随机抽取:

typescript 复制代码
const PRESET_RESULTS = [
  [0, 0, 0], // 三个相同 - 中奖
  [1, 1, 1],
  [2, 2, 2],
  // ...
  [0, 1, 2], // 不同 - 不中奖
];

// 随机选一个
const resultIndex = Math.floor(Math.random() * PRESET_RESULTS.length);
const resultData = PRESET_RESULTS[resultIndex];

金币雨特效

游戏结束时金币下落效果

金币的属性

typescript 复制代码
interface CoinSprite extends Sprite {
  speedY: number; // 下落速度
  speedX: number; // 水平漂移
  speedR: number; // 旋转速度
  flipSpeed: number; // 翻转速度(实现3D效果)
  flipPhase: number; // 翻转相位
  initialScale: number; // 初始大小
}

创建金币

typescript 复制代码
private createCoin(randomY: boolean = false) {
  const coin = new Sprite(this.texture) as CoinSprite;

  // 随机位置
  coin.x = Math.random() * this.app.screen.width;
  coin.y = randomY
    ? (Math.random() * this.app.screen.height) / 2 - rem(200)
    : rem(-50); // 从顶部开始

  // 随机大小 (0.3 - 0.6)
  const scale = (0.3 + Math.random() * 0.3) * getScale();
  coin.scale.set(scale);
  coin.initialScale = scale;

  // 随机物理属性
  coin.speedY = this.speedMin + Math.random() * (this.speedMax - this.speedMin);
  coin.speedX = (Math.random() - 0.5) * 1;      // 左右漂移
  coin.speedR = (Math.random() - 0.5) * 0.1;    // 旋转
  coin.flipSpeed = 0.02 + Math.random() * 0.05; // 翻转快慢
  coin.flipPhase = Math.random() * Math.PI * 2; // 初始相位

  this.coins.push(coin);
}

动画更新

这是整个特效的核心,每帧都会执行:

typescript 复制代码
private update = (ticker: Ticker) => {
  // 1. 生成新金币(如果没有停止)
  if (!this.isStopping && this.coins.length < 300) {
    if (Math.random() < this.density) { // density = 0.2
      this.createCoin();
    }
  }

  // 2. 更新每个金币
  for (let i = this.coins.length - 1; i >= 0; i--) {
    const coin = this.coins[i];

    // 位置更新
    coin.y += coin.speedY * ticker.deltaTime;
    coin.x += coin.speedX * ticker.deltaTime;
    coin.rotation += coin.speedR * ticker.deltaTime;

    // 3D翻转效果(通过缩放 x 轴模拟)
    coin.flipPhase += coin.flipSpeed * ticker.deltaTime;
    coin.scale.x = coin.initialScale * Math.sin(coin.flipPhase);

    // 边界检查
    if (coin.y > this.app.screen.height + rem(50)) {
      if (this.isStopping) {
        // 停止阶段: 销毁掉出去的金币
        coin.destroy();
        this.coins.splice(i, 1);
      } else {
        // 正常阶段: 循环利用,从顶部重新掉下来
        coin.y = rem(-50);
        coin.x = Math.random() * this.app.screen.width;
      }
    }
  }

  // 3. 如果停止且没有金币了,清理资源
  if (this.isStopping && this.coins.length === 0) {
    this.cleanup();
  }
};

性能优化技巧

  1. 对象池思想: 金币掉出屏幕后不销毁,而是重置位置继续用
  2. 数量限制: 最多 300 个金币,避免内存爆炸
  3. 软停止 : 调用 stop() 后不立即清理,让现有金币自然落完
  4. 禁用交互 : coin.eventMode = "none" 避免不必要的事件检测

引导手势动画

使用 GSAP Timeline 做循环动画:

typescript 复制代码
private startAnimation() {
  this.timeline = gsap.timeline({ repeat: -1 }); // 无限循环

  this.timeline
    // 手指下压 + 旋转
    .to(this.mcPointer, {
      y: rem(10),
      rotation: -0.1,
      duration: 0.3,
      ease: "power2.out"
    })
    // 同时播放水波纹扩散
    .to(this.mcRing, {
      scale: 1.5,
      alpha: 0,      // 淡出
      duration: 0.6,
      ease: "power2.out"
    }, "<")          // "<" 表示和上一个动画同时开始
    // 手指复位
    .to(this.mcPointer, {
      y: 0,
      rotation: 0,
      duration: 0.3,
      ease: "power2.in"
    })
    // 暂停 0.5 秒
    .to({}, { duration: 0.5 });
}

Moloco集成

这个项目最终要在 Moloco 广告平台上运行,按他的需求给下载按钮添加以下代码:

typescript 复制代码
export function handleCTA() {
  // 检查广告平台注入的全局对象
  if (typeof FbPlayableAd !== "undefined" && FbPlayableAd.onCTAClick) {
    FbPlayableAd.onCTAClick(); // 调用平台的跳转方法
  } else {
    console.log("【本地测试】模拟跳转");
  }
}

打包配置

Moloco要求产物是一个单独的HTML文件,要改一下 Vite 的打包配置:

typescript 复制代码
// vite.config.ts
export default defineConfig({
  plugins: [viteSingleFile()],
  build: {
    assetsInlineLimit: 100000000, // 所有资源内联
    cssCodeSplit: false, // CSS 不分割
    minify: "terser", // 压缩代码
  },
});

运行 npm run build 后得到一个 HTML 文件, HTML 文件包含所有代码、图片、音频

总结

尽管游戏比较简单,但整个开发过程让我对游戏开发有了初步的了解。这个项目没有涉及到碰撞检测、路由、骨骼动画以及资产包管理(AssetPack)等技术,未来有机会再去学习强化。

相关推荐
哆啦A梦158837 分钟前
62 对接支付宝沙箱
前端·javascript·vue.js·node.js
用户8168694747251 小时前
Lane 优先级模型与时间切片调度
前端·react.js
虎头金猫1 小时前
MateChat赋能电商行业智能导购:基于DevUI的技术实践
前端·前端框架·aigc·ai编程·ai写作·华为snap·devui
LiuMingXin1 小时前
CESIUM JS 学习笔记 (持续更新)
前端·cesium
豆苗学前端1 小时前
面试复盘:谈谈你对 原型、原型链、构造函数、实例、继承的理解
前端·javascript·面试
Crystal3281 小时前
Git 基础:生成版本、撤消操作、版本重置、忽略文件
前端·git·github
lichenyang4531 小时前
React 组件通讯全案例解析:从 Context 到 Ref 的实战应用
前端
姓王者1 小时前
chen-er 专为Chen式ER图打造的npm包
前端·javascript
青莲8431 小时前
Android Jetpack - 2 ViewModel
android·前端