画面効果を追加

画面効果を追加

手軽な画面効果として、次のものがある

  • スクリーンシェイク
    • 画面を揺らす。揺れは徐々に収まる
  • スクリーンフラッシュ
    • 画面を明るくする。明るさは徐々に元に戻る

スクリーンシェイク

原理

  • p5.js の translate() で画面をずらすことができるので、
    ランダムな値で translate() すれば画面を揺らすことができる
  • 揺れ効果を任意のタイミングで開始できて、
    その後、揺れの強さを徐々に減衰させるような仕組みが必要

実装例

次のようなコードを用意し、

  • resetShake()
  • updateShake()
  • applyShake()

をそれぞれ適切な場所で実行する。

すると、後は setShake() を任意のタイミングで使うだけで画面を揺らすことができる

/** シェイクの現在の強さ */
let shakeMagnitude;

/** シェイクの減衰に使う係数 */
let shakeDampingFactor;

/** シェイクをリセット */
function resetShake() {
  shakeMagnitude = 0;
  shakeDampingFactor = 0.95;
}

/** シェイクを任意の強さで発動 */
function setShake(magnitude) {
  shakeMagnitude = magnitude;
}

/** シェイクを更新 */
function updateShake() {
  shakeMagnitude *= shakeDampingFactor; // シェイクの大きさを徐々に減衰
}

/** シェイクを適用。描画処理の前に実行する必要あり */
function applyShake() {
  if (shakeMagnitude < 1) return;

  // currentMagnitude の範囲内で、ランダムに画面をずらす
  translate(
    random(-shakeMagnitude, shakeMagnitude),
    random(-shakeMagnitude, shakeMagnitude)
  );
}

スクリーンフラッシュ

原理

p5.js の background() は、α値を指定すれば画面全体を半透明に塗りつぶせる。
白など明るい色でこれを行い、徐々に明るさを減衰させれば、フラッシュのようになる

実装例

構造的にはスクリーンシェイクとほぼ同様

スクリーンシェイクでは掛け算で減衰させていたが、
こちらは、徐々に減少する「残り時間」を使って明るさを決めている

/** フラッシュのα値 */
let flashAlpha;

/** フラッシュの持続時間(フレーム数) */
let flashDuration;

/** フラッシュの残り時間(フレーム数) */
let flashRemainingCount;

/** フラッシュをリセット */
function resetFlash() {
  flashAlpha = 255;
  flashDuration = 1;
  flashRemainingCount = 0;
}

/** フラッシュを、任意のα値と持続時間で発動 */
function setFlash(alpha, duration) {
  flashAlpha = alpha;
  flashDuration = duration;
  flashRemainingCount = duration;
}

/** フラッシュを更新 */
function updateFlash() {
  flashRemainingCount -= 1;
}

/** フラッシュを適用。描画処理の後に呼ぶ必要あり */
function applyFlash() {
  if (flashRemainingCount <= 0) return;

  let alphaRatio = flashRemainingCount / flashDuration;
  background(255, alphaRatio * flashAlpha);
}

使用例

▶ 動かしてみる

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
// ---- エンティティ関連の関数 ---------------------------------------------

// 全エンティティ共通

function updatePosition(entity) {
  entity.x += entity.vx;
  entity.y += entity.vy;
}

// プレイヤーエンティティ用

function createPlayer() {
  return {
    x: 200,
    y: 300,
    vx: 0,
    vy: 0
  };
}

function applyGravity(entity) {
  entity.vy += 0.15;
}

function applyJump(entity) {
  entity.vy = -5;
}

function drawPlayer(entity) {
  square(entity.x, entity.y, 40);
}

function playerIsAlive(entity) {
  // プレイヤーの位置が生存圏内なら true を返す。
  // 600 は画面の下端
  return entity.y < 600;
}

// ブロックエンティティ用

function createBlock(y) {
  return {
    x: 900,
    y,
    vx: -2,
    vy: 0
  };
}

function drawBlock(entity) {
  rect(entity.x, entity.y, 80, 400);
}

function blockIsAlive(entity) {
  // ブロックの位置が生存圏内なら true を返す。
  // -100 は適当な値(ブロックが見えなくなる位置であればよい)
  return -100 < entity.x;
}

// 複数のエンティティを処理する関数

/**
 * 2つのエンティティが衝突しているかどうかをチェックする
 *
 * @param entityA 衝突しているかどうかを確認したいエンティティ
 * @param entityB 同上
 * @param collisionXDistance 衝突しないギリギリのx距離
 * @param collisionYDistance 衝突しないギリギリのy距離
 * @returns 衝突していたら `true` そうでなければ `false` を返す
 */
function entitiesAreColliding(
  entityA,
  entityB,
  collisionXDistance,
  collisionYDistance
) {
  // xとy、いずれかの距離が十分開いていたら、衝突していないので false を返す

  let currentXDistance = abs(entityA.x - entityB.x); // 現在のx距離
  if (collisionXDistance <= currentXDistance) return false;

  let currentYDistance = abs(entityA.y - entityB.y); // 現在のy距離
  if (collisionYDistance <= currentYDistance) return false;

  return true; // ここまで来たら、x方向でもy方向でも重なっているので true
}

// ---- 画面効果 ----------------------------------------------------------

// スクリーンシェイク

/** シェイクの現在の強さ */
let shakeMagnitude;

/** シェイクの減衰に使う係数 */
let shakeDampingFactor;

/** シェイクをリセット */
function resetShake() {
  shakeMagnitude = 0;
  shakeDampingFactor = 0.95;
}

/** シェイクを任意の強さで発動 */
function setShake(magnitude) {
  shakeMagnitude = magnitude;
}

/** シェイクを更新 */
function updateShake() {
  shakeMagnitude *= shakeDampingFactor; // シェイクの大きさを徐々に減衰
}

/** シェイクを適用。描画処理の前に実行する必要あり */
function applyShake() {
  if (shakeMagnitude < 1) return;

  // currentMagnitude の範囲内で、ランダムに画面をずらす
  translate(
    random(-shakeMagnitude, shakeMagnitude),
    random(-shakeMagnitude, shakeMagnitude)
  );
}

// スクリーンフラッシュ

/** フラッシュのα値 */
let flashAlpha;

/** フラッシュの持続時間(フレーム数) */
let flashDuration;

/** フラッシュの残り時間(フレーム数) */
let flashRemainingCount;

/** フラッシュをリセット */
function resetFlash() {
  flashAlpha = 255;
  flashDuration = 1;
  flashRemainingCount = 0;
}

/** フラッシュを、任意のα値と持続時間で発動 */
function setFlash(alpha, duration) {
  flashAlpha = alpha;
  flashDuration = duration;
  flashRemainingCount = duration;
}

/** フラッシュを更新 */
function updateFlash() {
  flashRemainingCount -= 1;
}

/** フラッシュを適用。描画処理の後に呼ぶ必要あり */
function applyFlash() {
  if (flashRemainingCount <= 0) return;

  let alphaRatio = flashRemainingCount / flashDuration;
  background(255, alphaRatio * flashAlpha);
}

// ---- ゲーム全体に関わる部分 --------------------------------------------

/** プレイヤーエンティティ */
let player;

/** ブロックエンティティの配列 */
let blocks;

/** ゲームの状態。"play" か "gameover" を入れるものとする */
let gameState;

/** ブロックを上下ペアで作成し、`blocks` に追加する */
function addBlockPair() {
  let y = random(-100, 100);
  blocks.push(createBlock(y)); // 上のブロック
  blocks.push(createBlock(y + 600)); // 下のブロック
}

/** ゲームオーバー画面を表示する */
function drawGameoverScreen() {
  background(0, 192); // 透明度 192 の黒
  fill(255);
  textSize(64);
  textAlign(CENTER, CENTER); // 横に中央揃え & 縦にも中央揃え
  text("GAME OVER", width / 2, height / 2); // 画面中央にテキスト表示
}

/** ゲームのリセット */
function resetGame() {
  // 状態をリセット
  gameState = "play";

  // プレイヤーを作成
  player = createPlayer();

  // ブロックの配列準備
  blocks = [];

  // 画面効果をリセット
  resetShake();
  resetFlash();
}

function setGameOver() {
  gameState = "gameover";
  setShake(300);
  setFlash(128, 60);
}

/** ゲームの更新 */
function updateGame() {
  // 画面効果を更新
  updateShake();
  updateFlash();

  // ゲームオーバーなら更新しない
  if (gameState === "gameover") return;

  // ブロックの追加と削除
  if (frameCount % 120 === 1) addBlockPair(blocks); // 一定間隔で追加
  blocks = blocks.filter(blockIsAlive); // 生きているブロックだけ残す

  // 全エンティティの位置を更新
  updatePosition(player);
  for (let block of blocks) updatePosition(block);

  // プレイヤーに重力を適用
  applyGravity(player);

  // プレイヤーが死んでいたらゲームオーバー
  if (!playerIsAlive(player)) {
    setGameOver();
    return;
  }

  // 衝突判定
  for (let block of blocks) {
    if (entitiesAreColliding(player, block, 20 + 40, 20 + 200)) {
      setGameOver();
      break;
    }
  }
}

/** ゲームの描画 */
function drawGame() {
  // スクリーンシェイクを適用
  applyShake();

  // 全エンティティを描画
  background(0);
  drawPlayer(player);
  for (let block of blocks) drawBlock(block);

  // ゲームオーバー状態なら、それ用の画面を表示
  if (gameState === "gameover") drawGameoverScreen();

  // スクリーンフラッシュを適用
  applyFlash();
}

/** マウスボタンが押されたときのゲームへの影響 */
function onMousePress() {
  switch (gameState) {
    case "play":
      // プレイ中の状態ならプレイヤーをジャンプさせる
      applyJump(player);
      break;
    case "gameover":
      // ゲームオーバー状態ならリセット
      resetGame();
      break;
  }
}

// ---- setup/draw 他 --------------------------------------------------

function setup() {
  createCanvas(800, 600);
  rectMode(CENTER);

  resetGame();
}

function draw() {
  updateGame();
  drawGame();
}

function mousePressed() {
  onMousePress();
}

改造中のコードがあったらそれに組み込んでみて、
本当にちゃんと機能しているか、実行して確かめてみましょう。

たとえばスクリーンシェイクを縦だけ・横だけにするなど、
カスタマイズしてみる余地もあるかもしれません。