导航

萌即是正义!时不时分享一些ACG活动记录与有趣代码的小站!

侧边栏
热门文章
1推文
看了一场不一样的《超时空辉夜姬》。 上周六,也就是2026年9月19日,去池袋的电影院看了 ULTRA 4DX 版《超时空辉夜姬》。 所谓的 ULTRA 4DX 就是前面三面荧幕,椅子会随着剧情发生摇晃震动等效果。 周围还会模拟刮风、下雨、打雷、爆炸烟雾等效果。 印象最深刻的就是小辉夜姬打了个喷嚏,喷了我一脸鼻水的模拟效果😂。 其他像是 VR 竞技游戏环节的椅子晃动效果非常棒! 唯一美中不足的是,官方给出的效果图里三面荧幕看起来都很亮,实际上左右两面荧幕是明显要暗很多的,而且清晰度其实也不是很高。 但是考虑到左右两面荧幕平时不是盯着看的,而是用余光来看的这么一种使用场景,也还能接受。
热度
277
2页面
友链
热度
169
3推文
去六本木吃了《穹庐下的魔女联动咖啡》的联动咖啡。 这是一家电视台里的咖啡店,本来主打的其实是4000日元的水果套餐。 联动菜单里的蒙古包我觉得是最有意思的,青青草地上的白色蒙古包蛋糕。 《原论》三明治看起来像是厚厚的书,但其实本质就是2个厚厚的三明治。
热度
167
4推文
最近下北泽把月亮请了下来呢
热度
161
5博文
《夏目友人帐》圣地巡礼之人吉 风景篇
热度
143
6推文
第一次尝试联动酒吧,《超时空辉夜姬》联动酒吧。 昨天,也就是2026年9月20日,预约了位于新宿的 HUB 酒吧。 这是一家英国风格的连锁酒吧,现在被《超时空辉夜姬》的联动包场了。 这应该是我第一次去酒吧吧。 因为本身就是连锁酒吧,酒精饮料和菜品的口味都非常有保证(炸鱼薯条好吃呢!)。 官方也很贴心的在FAQ强调,即使你不喝酒,这里的无酒精饮料和菜品也非常不错。 看起来是想借助联动,改变酒吧的刻板印象,尽可能吸引更多原本不喝酒的消费群体。 这确实让我对酒吧这样的场所有了不小的改观,有可能下次还会去消费。 联动送的特典本来以为是小卡片,结果发现是相当有质感的金属牌子! 不过人工排队去前台点餐的机制不太好,饮料是现场做完自己端到位置上,食物类则是由工作人员送到位置上,就感觉效率并不是很高。
热度
135
7博文
探访日本的废校:《轻音少女》圣地巡礼之丰乡小学校旧校舍群
热度
117
8推文
吃完联动咖啡后,去新宿逛了逛街。 意外的发现丸井百货有一大片芳文社的周边贩卖区。
热度
116
9博文
探访闭店后的高尾食堂 《时光流逝,饭菜依旧美味》圣地巡礼之高尾与西八王子
热度
78
10博文
面码找到你了!探寻超平和Busters的秘密基地——《我们仍未知道那天所看见的花的名字》秩父圣地巡礼
热度
78
最新评论
广树管理员
2026-10-04 15:00
@Ringnee:还原度非常高的月亮
广树管理员
2026-10-04 14:59
@saber酱:是呀,非常有趣
Ringnee
2026-10-04 09:49
这个做得还挺厉害的
saber酱
2026-10-04 01:12
挺有趣的呢,好玩~
广树管理员
2026-09-29 20:28
@陈X布: 怀念起了那天一起唱歌
攻略中
空之轨迹 the 1st
暂无评分
Steam空之轨迹 the 1st
2026年8月02日 12时 ~ 攻略中
已累计游玩2个月6天
因父亲卡西乌斯失踪而踏上旅程的艾丝蒂尔与约书亚, 在巡游利贝尔王国的过程中, 经历了无数与未知同伴的相遇与别离, 而在旅途终点等待着两人的,究竟会是什么……?
节奏天国 奇迹之星
暂无评分
NS节奏天国 奇迹之星
2026年7月08日 20时 ~ 攻略中
已累计游玩3个月
《节奏天国:奇迹之星》是一款轻松又充满创意的音乐节奏游戏,玩家只需跟随音乐节拍按下按键,就能完成跳跃、敲击空罐、制作料理等各种有趣的小挑战。游戏延续了《节奏天国》系列简单易上手、节奏感强、画风幽默的特点,让玩家在一个个短小精巧的关卡中感受音乐与反应力结合的乐趣,适合喜欢节奏游戏和轻松休闲玩法的玩家体验。
告别回忆 -无垢少女- 致最爱的你
暂无评分
Steam告别回忆 -无垢少女- 致最爱的你
2026年6月24日 20时 ~ 攻略中
已累计游玩3个月14天
系列作发售至今经历了19年,做为其系列最终作的《告别回忆 ~无垢少女~》(以下略称为本篇)在2018年3月发售。本次的FanDisc和至今的系列作相同,带给玩家在各结局后的短篇故事。请充分享受和各个女主角们在欢乐结局后的幸福时光。
亚路塔:狐狸狐途的面包冒险
暂无评分
Steam亚路塔:狐狸狐途的面包冒险
2026年2月12日 19时 ~ 攻略中
已累计游玩7个月26天
一场席卷全世界的面包革命现在开始! 主人公查雅阴错阳差之下,开始了在荒废的岛屿上经营面包店的生活。 出外冒险搜集食材、研发崭新面包配方、遇见性格各异的伙伴们,建造更丰富的设施,用面包香气使无人问津的广场再次热闹起来!
fault - StP - LIGHTKRAVTE
暂无评分
Steamfault - StP - LIGHTKRAVTE
2025年5月29日 20时 ~ 攻略中
已累计游玩1年4个月
全球累计销量超过50万份的“fault”系列最新作!故事的舞台是一个融合了奇幻与科幻的超前世界——卢森海德王国。本作讲述了生活在此地的一个平凡又平庸的究极普通市民——名为果子的少年的故事。
PSN奖杯卡

PSN奖杯卡

归档
赞助商广告

2D WebGL renderer Pixi.js v4 入门【最终回】

2018-09-12 13:47:31
JavaScript

前情回顾

上回链接:《2D WebGL renderer Pixi.js v4 入门【第五回】》

上回学习了WebGL的各种图像与文字的绘制。


检测碰撞

上图为理想状态。

左边的猫能够使用键盘操作,右边的块是无法移动的,然后上面的信息提示状态。


在基于<第三回>的键盘控制所写的代码中,

改造和追加了一部分代码。

var 猫, 块, 消息, 状态;
function setup(){
  放入猫;
  制作块;
  消息初始化;
  设置键盘事件的移动数值;
  将状态设定为play;
  gameLoop();
}

function play(){
  接收键盘的移动数值;
  if(碰撞){
    变化块的颜色和消息;
  }else{
    回滚块的颜色和消息;
  }
}

function 碰撞(r1, r2){
  查找每个元素的中心点;
  计算每个元素一半的高和一半的宽;
  r1的一半高度和r2的一半高度相加;
  r1的一半宽度和r2的一半宽度相加;

  if(r1,r2的中心点距离 < r1,r2 相加的距离){
    碰撞;
  }
}


翻译成代码就是这样

var cat, box, message, state;

function setup() {
  box = new PIXI.Graphics();
  box.beginFill(0xCCFF99);
  box.drawRect(0, 0, 64, 64);
  box.endFill();
  box.x = 120;
  box.y = 96;
  stage.addChild(box);

  cat = new Sprite(resources["images/cat.png"].texture);
  cat.x = 16;
  cat.y = 96; 
  cat.vx = 0;
  cat.vy = 0;
  stage.addChild(cat);

  var left = keyboard(37),
      up = keyboard(38),
      right = keyboard(39),
      down = keyboard(40);

  left.press = function() {
    cat.vx = -5;
    cat.vy = 0;
  };
  left.release = function() {
    if (!right.isDown && cat.vy === 0) {
      cat.vx = 0;
    }
  };
  up.press = function() {
    cat.vy = -5;
    cat.vx = 0;
  };
  up.release = function() {
    if (!down.isDown && cat.vx === 0) {
      cat.vy = 0;
    }
  };
  right.press = function() {
    cat.vx = 5;
    cat.vy = 0;
  };
  right.release = function() {
    if (!left.isDown && cat.vy === 0) {
      cat.vx = 0;
    }
  };
  down.press = function() {
    cat.vy = 5;
    cat.vx = 0;
  };
  down.release = function() {
    if (!up.isDown && cat.vx === 0) {
      cat.vy = 0;
    }
  };

  message = new PIXI.Text(
    "No collision...", 
    {font: "18px sans-serif", fill: "white"}
  );
  message.position.set(8, 8);
  stage.addChild(message);

  state = play;
 
  gameLoop();
}

function play() {
  cat.x += cat.vx;
  cat.y += cat.vy;

  if (hitTestRectangle(cat, box)) {
    message.text = "hit!";
    box.tint = 0xff3300;
  } else {
    message.text = "No collision...";
    box.tint = 0xccff99;
  }
}

function hitTestRectangle(r1, r2) {
  var hit, combinedHalfWidths, combinedHalfHeights, vx, vy;
  hit = false;

  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; 

  r1.halfWidth = r1.width / 2;
  r1.halfHeight = r1.height / 2;
  r2.halfWidth = r2.width / 2;
  r2.halfHeight = r2.height / 2;

  vx = r1.centerX - r2.centerX;
  vy = r1.centerY - r2.centerY;

  combinedHalfWidths = r1.halfWidth + r2.halfWidth;
  combinedHalfHeights = r1.halfHeight + r2.halfHeight;

  if (Math.abs(vx) < combinedHalfWidths) {
    if (Math.abs(vy) < combinedHalfHeights) {
      hit = true;
    } else {
      hit = false;
    }
  } else {
    hit = false;
  }

  return hit;
};


分析之后,发现还挺简单的。

如果没问题的话就进入正题吧。


寻宝游戏

游戏原型大致是这样:避开怪物,获取宝物,从门那里出去后成功。如果碰到怪物的话减少HP。减完了则判断为输。

代码设计

// pixi.js的初始化以及图像的读取

function setup() {
  // 初始化,然后游戏状态切换为play。
  // gameLoop();
}

function gameLoop() {
  // 描绘,并将精灵传递到render。
}

function play() {
  // 游戏所有的逻辑写这里。
}

function end() {
  // 游戏结束后。
}

// 其他必要的helper functions:
// `keyboard`, `hitTestRectangle`, `contain` and `randomInt`


首先将游戏场景和游戏结束场景分开。(不同的Container)

gameScene = new Container();
stage.addChild(gameScene);

gameOverScene = new Container();
stage.addChild(gameOverScene);

然后,先将gameOverScene隐藏。

gameOverScene.visible = false;


游戏场景

首先将简单的洞窟背景、门、勇者、宝箱描绘出来。

id = resources["images/treasureHunter.json"].textures;

//洞窟
dungeon = new Sprite(id["dungeon.png"]);
gameScene.addChild(dungeon);

//门
door = new Sprite(id["door.png"]);
door.position.set(32, 0);
gameScene.addChild(door);

//勇者
explorer = new Sprite(id["explorer.png"]);
explorer.x = 68;
explorer.y = gameScene.height / 2 - explorer.height / 2;
explorer.vx = 0;
explorer.vy = 0;
gameScene.addChild(explorer);

//宝箱
treasure = new Sprite(id["treasure.png"]);
treasure.x = gameScene.width - treasure.width - 48;
treasure.y = gameScene.height / 2 - treasure.height / 2;
gameScene.addChild(treasure);

接下来制作随机移动的怪物。

// 设定怪物数量

var numberOfBlobs = 6,

    // 怪物与怪物之间的间距

    spacing = 48,

    // 从左边开始最初的空隙

    xOffset = 150,

    // 速度

    speed = 2,

    // 方向

    direction = 1;



// 储存怪物的数组

blobs = [];



// 按照设定的数值描绘怪物

for (var i = 0; i < numberOfBlobs; i++) {



  // 怪物出生(?)

  var blob = new Sprite(id["blob.png"]);



  // 空出沿x轴一定距离的空间

  var x = spacing * i + xOffset;



  // y坐标(随机)

  var y = randomInt(0, stage.height - blob.height);



  // 给与坐标

  blob.x = x;

  blob.y = y;



  // 方向(direction)为1的话向下,-1的话向上

  // 方向 乘以 速度

  blob.vy = speed * direction;



  // 下一只怪物为反方向

  direction *= -1;



  // 配置到数组中

  blobs.push(blob);



  gameScene.addChild(blob);

}

然后是血条

healthBar = new PIXI.DisplayObjectContainer();
healthBar.position.set(stage.width - 170, 6)
gameScene.addChild(healthBar);

// HP血条的空间
var innerBar = new PIXI.Graphics();
innerBar.beginFill(0x000000);
innerBar.drawRect(0, 0, 128, 8);
innerBar.endFill();
healthBar.addChild(innerBar);

// HP血条的HP值
var outerBar = new PIXI.Graphics();
outerBar.beginFill(0xFF3300);
outerBar.drawRect(0, 0, 128, 8);
outerBar.endFill();
healthBar.addChild(outerBar);

healthBar.outer = outerBar;

这里出现的DisplayObjectContainer和Container和ParticleContainer的差异非常明显呢。

另外,outer的意义现在还不是很清楚,等知道了补充。


游戏结束场景

准备好游戏结束后的生理和失败的消息。

// 写在之前隐藏gameOverScene代码的后面

  message = new Text(
    "The End!", 
    {font: "64px Futura", fill: "white"}
  );
  message.x = 120;
  message.y = stage.height / 2 - 32;
  gameOverScene.addChild(message);


play function

已经准备好登场人物和怪物了,现在将游戏时要做什么列了一张表。


勇者移动,移动范围限制。
怪物的移动,移动范围限制,碰撞到墙壁时的反馈。
勇者和怪物碰撞了吗?
勇者和宝箱碰撞了吗?(获得宝物)
勇者和门碰撞了吗?(成功逃出)
游戏成功与失败的判定


然后,整理一下吧!

作为helper function集中起来由play function调用比较好的有


范围限制(判断是否在边界)
碰撞判断


那么,改变代码吧

// 范围限制(判断是否在边界)
// 这里放入的值是能够移动、移动中的精灵(这里为勇者和怪物)和可移动范围
function contain(sprite, container) {
  var collision = undefined;
  // 左
  if (sprite.x < container.x) {
    sprite.x = container.x;
    collision = "left";
  }
  // 上
  if (sprite.y < container.y) {
    sprite.y = container.y;
    collision = "top";
  }
  // 右
  if (sprite.x + sprite.width > container.width) {
    sprite.x = container.width - sprite.width;
    collision = "right";
  }
  // 下
  if (sprite.y + sprite.height > container.height) {
    sprite.y = container.height - sprite.height;
    collision = "bottom";
  }
  // 返回值
  return collision;
}
// 判断是否碰撞的函数
function hitTestRectangle(r1, r2) {
  var hit, combinedHalfWidths, combinedHalfHeights, vx, vy;

  hit = false;

  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; 

  r1.halfWidth = r1.width / 2;
  r1.halfHeight = r1.height / 2;
  r2.halfWidth = r2.width / 2;
  r2.halfHeight = r2.height / 2;

  vx = r1.centerX - r2.centerX;
  vy = r1.centerY - r2.centerY;

  combinedHalfWidths = r1.halfWidth + r2.halfWidth;
  combinedHalfHeights = r1.halfHeight + r2.halfHeight;

  if (Math.abs(vx) < combinedHalfWidths) {
    if (Math.abs(vy) < combinedHalfHeights) {
      hit = true;
    } else {
      hit = false;
    }
  } else {
    hit = false;
  }

  return hit;
};
// 虽然感觉没必要独立出来,但还是和教程一样分开写吧
function randomInt(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

放入play function

function play() {

  // 用键盘事件中获取的数值来移动
  explorer.x += explorer.vx;
  explorer.y += explorer.vy;

  // 勇者的移动范围
  contain(explorer, {x: 28, y: 10, width: 488, height: 480});
  //contain(explorer, stage);

  // 给勇者一个没有碰撞的初始设定
  var explorerHit = false;

  // 使用怪物的分布
  blobs.forEach(function(blob) {

    // 使其移动
    blob.y += blob.vy;

    // 给怪物一个移动范围的限制,捕捉返回的值
    var blobHitsWall = contain(blob, {x: 28, y: 10, width: 488, height: 480});

    // 判断返回的值,将方向反向
    if (blobHitsWall === "top" || blobHitsWall === "bottom") {
      blob.vy *= -1;
    }

    // 如果和勇者碰上了,就改变刚才碰撞的值
    if(hitTestRectangle(explorer, blob)) {
      explorerHit = true;
    }
  });

  // 如果勇者和怪物碰上了
  if(explorerHit) {

    // 一瞬间半透明
    explorer.alpha = 0.5;

    // HP削减
    healthBar.outer.width -= 1;

  } else {

    // 从半透明恢复
    explorer.alpha = 1;
  }

  // 如果勇者和宝箱碰撞
  if (hitTestRectangle(explorer, treasure)) {

    // 做一个手持宝箱的样子(宝箱的位置一直跟随在勇者右下角)
    treasure.x = explorer.x + 8;
    treasure.y = explorer.y + 8;
  }

  // HP归零则游戏结束,弹出失败信息
  if (healthBar.outer.width < 0) {
    state = end;
    message.text = "You lost!";
  }

  // 宝箱(和勇者)和门碰撞,游戏结束,弹出成功信息
  if (hitTestRectangle(treasure, door)) {
    state = end;
    message.text = "You won!";
  } 
}

然后用end function切换场景

function end() {
  gameScene.visible = false;
  gameOverScene.visible = true;
}

游戏完成

<点击预览游戏效果>


补充:关于精灵

精灵除了坐标、visible、旋转之外还有各种设置,详情可以在官方文档中查找。

连接如下

Class: Sprite


基本上精灵是遵循继承规则的

DisplayObject > Container > Sprite

就此教程结束。

各位看官辛苦了。


本回的代码:进入git


本文翻译自:2D WebGL renderer Pixi.js v4【連載第六回】、2D WebGL renderer Pixi.js v4【連載最終回】
翻译者:广树
转载请注明出处!


收款.png


file_000000008460720687635c008459256b.png