HTML・CSS・JavaScriptだけで、ブラウザで動く本格シューティングゲームを完成させるチュートリアルです。
ボス戦・パワーアップ・コンボシステムまで実装して、アーケードゲーム級の作品に仕上げよう。
⏱ 読了:約30分 🎯 対象:プログラミング初心者〜
📋 もくじ
- 完成するとどうなる?
- 準備するもの
- ファイル構成を作ろう
- HTMLを書こう
- CSSでデザインしよう
- JavaScriptの基本構造を書こう
- ゲームループを作ろう
- 自機を動かそう
- 弾を撃とう(オブジェクトプール)
- 敵を出現させよう
- 当たり判定を作ろう
- ボス戦を実装しよう
- エフェクトを追加しよう
- サウンドをコードだけで作ろう
- レベル&コンボシステムを作ろう
- 完成コード全文
- 動かしてみよう
- 次のステップ
完成するとどうなる?
この記事を最後まで読むと、ブラウザ上で動く本格シューティングゲームが完成します。ただのシューティングじゃなくて、こんな機能がついています:
- 自機が上下左右に動いて自動連射する
- ザコ敵・中型敵・高速突進敵の3種類の敵が出現
- スコアが3000点たまるたびにボス戦が始まる(WARNING!演出つき)
- ボスには全方位弾幕・レーザー照射・ザコ召喚の3つの攻撃パターン
- 敵を連続で倒すとコンボがつながってスコアがどんどん増える
- 3WAY弾・スピードアップ・ボムのパワーアップアイテム
- 爆発パーティクル・画面シェイク・ヒットストップのジューシーな演出
- 音声ファイルなし!Web Audio APIでコードだけで効果音を生成
使うのは HTML・CSS・JavaScript の3つだけ。特別なライブラリやフレームワークは使いません。約2,900行でアーケードゲーム級のゲームが作れます!
準備するもの
以下の2つだけ用意してください。
- テキストエディタ(Web版のVS Codeだとインストール不要!)
- Webブラウザ(Chrome、Edge、Safariなど何でもOK)
ファイル構成を作ろう
パソコンのどこかにフォルダを1つ作ります。名前は shooting にしましょう。その中に3つのファイルを作ります。
shooting/
index.html ─ 画面の構造
style.css ─ 見た目のデザイン
script.js ─ ゲームの動き(約2,900行)
Webページは基本的にこの3つの役割分担でできています。HTMLが骨組み、CSSが見た目、JavaScriptが動き。テトリスの記事でもやった構成と同じです。
HTMLを書こう
index.html を開いて、以下のコードを書いてください。
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>シューティングゲーム</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<canvas id="gameCanvas"></canvas>
<script src="script.js"></script>
</body>
</html>
HTMLのポイント解説
テトリスの時と比べてかなりシンプルです。<canvas> タグが1つあるだけ。スコア表示もHP表示も、すべてJavaScriptのCanvas描画で行うので、HTML側には余計な要素を置きません。
<canvas> は「自由に絵が描ける画用紙」みたいなもの。ここにJavaScriptから宇宙船や敵やレーザーを描いていきます。
CSSでデザインしよう
style.css にゲームの見た目を書きます。
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-color: #000;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
canvas {
display: block;
box-shadow: 0 0 40px rgba(0, 200, 255, 0.12),
0 0 80px rgba(0, 100, 200, 0.06);
border: 1px solid rgba(0, 200, 255, 0.08);
}
CSSのポイント解説
背景を真っ黒にして、Canvasを画面のど真ん中に配置しています。box-shadow でCanvasの周りにシアン色のうっすら光るエフェクトをつけて、ネオン風の雰囲気を出しています。
ゲームの描画はすべてJavaScript側で行うので、CSSは最小限。これがCanvas系ゲームの特徴です。
JavaScriptの基本構造を書こう
ここからがメイン! script.js に書いていくコードを、セクションごとに解説していきます。
Canvasの初期化
const CANVAS_WIDTH = 400;
const CANVAS_HEIGHT = 700;
const canvas = document.getElementById("gameCanvas");
const ctx = canvas.getContext("2d");
canvas.width = CANVAS_WIDTH;
canvas.height = CANVAS_HEIGHT;
getContext("2d") で、2Dの絵を描く「ペン」を手に入れます。テトリスでは context.scale(20, 20) で座標を拡大しましたが、今回はピクセル単位でそのまま描画します。
ゲーム状態の管理(ステートマシン)
const STATE = {
TITLE: "title",
PLAYING: "playing",
GAMEOVER: "gameover",
};
let gameState = STATE.TITLE;
ゲームには「タイトル画面」「プレイ中」「ゲームオーバー」の3つの状態があります。gameState 変数を切り替えることで、同じ画面でまったく違う動きを実現しています。これをステートマシン(状態機械)と呼びます。ゲーム開発ではど定番のパターンです。
ゲームループを作ろう
ゲームの心臓部、ゲームループを作ります。
let lastTime = 0;
function gameLoop(timestamp) {
// 前フレームからの経過時間を秒に変換
const rawDelta = lastTime === 0 ? 0 : (timestamp - lastTime) / 1000;
lastTime = timestamp;
const deltaTime = rawDelta;
// --- 更新 ---
switch (gameState) {
case STATE.TITLE:
updateTitle(deltaTime);
break;
case STATE.PLAYING:
updatePlaying(deltaTime);
break;
case STATE.GAMEOVER:
updateGameOver(deltaTime);
break;
}
// --- 描画 ---
drawBackground(deltaTime);
switch (gameState) {
case STATE.TITLE:
drawTitle();
break;
case STATE.PLAYING:
drawPlaying();
break;
case STATE.GAMEOVER:
drawGameOver();
break;
}
// 次のフレームを予約
requestAnimationFrame(gameLoop);
}
// ゲーム開始!
requestAnimationFrame(gameLoop);
ここで何をしてる?
requestAnimationFrame は「次の画面更新のタイミングで関数を呼んでね」とブラウザにお願いする関数です。これを繰り返すことで、毎秒約60回画面が更新されるループが完成します。
deltaTime(デルタタイム)は「前のフレームから何秒経ったか」を表す数値です。60FPSなら約 0.016秒。なぜこれが必要か? パソコンの性能によってFPSが変わっても、同じ速度でゲームが動くようにするためです。移動量を 速度 × deltaTime で計算すれば、高性能PC(120FPS)でも低性能PC(30FPS)でも同じ体験になります。
switch 文でゲーム状態ごとに違う処理を呼び分けているのもポイント。タイトル画面ではタイトルを描画し、プレイ中ではゲームロジックを走らせ、ゲームオーバーではスコアを表示……と、状態ごとに完全に分離できます。
自機を動かそう
キー入力の管理
const keys = {
ArrowLeft: false,
ArrowRight: false,
ArrowUp: false,
ArrowDown: false,
};
document.addEventListener("keydown", (e) => {
if (e.key in keys) {
keys[e.key] = true;
e.preventDefault(); // 矢印キーでページがスクロールしないように
}
});
document.addEventListener("keyup", (e) => {
if (e.key in keys) keys[e.key] = false;
});
keydown でキーの状態を true に、keyup で false に戻します。こうすると「キーが押されているかどうか」を毎フレーム確認できるので、押しっぱなしでスムーズに移動できます。
自機の移動
const PLAYER_SPEED = 300; // px/秒
const player = {
x: CANVAS_WIDTH / 2,
y: CANVAS_HEIGHT - 80,
width: 32,
height: 36,
hp: 5,
tilt: 0, // 左右移動時の傾き
};
function updatePlayer(deltaTime) {
let dx = 0;
let dy = 0;
if (keys.ArrowLeft) dx -= 1;
if (keys.ArrowRight) dx += 1;
if (keys.ArrowUp) dy -= 1;
if (keys.ArrowDown) dy += 1;
player.x += dx * PLAYER_SPEED * deltaTime;
player.y += dy * PLAYER_SPEED * deltaTime;
// 画面外に出ないよう制限
const hw = player.width / 2;
player.x = Math.max(hw, Math.min(CANVAS_WIDTH - hw, player.x));
player.y = Math.max(minY, Math.min(CANVAS_HEIGHT - player.height / 2, player.y));
// 傾き演出:左右移動で機体が傾く
const targetTilt = dx * (8 * Math.PI / 180);
player.tilt += (targetTilt - player.tilt) * Math.min(1, 10 * deltaTime);
}
移動量は 方向 × 速度 × deltaTime で計算しています。Math.max / Math.min で画面端からはみ出さないようにクランプ(制限)しているのもポイント。
自機の描画
function drawPlayer() {
const { x, y, width, height, tilt } = player;
ctx.save();
ctx.translate(x, y);
ctx.rotate(tilt);
// グロウエフェクト
ctx.shadowColor = "#00ffff";
ctx.shadowBlur = 18;
// 三角形の宇宙船
ctx.beginPath();
ctx.moveTo(0, -height / 2); // 機首(上)
ctx.lineTo(-width / 2, height / 2); // 左翼端
ctx.lineTo(0, height / 2 - 8); // くびれ
ctx.lineTo(width / 2, height / 2); // 右翼端
ctx.closePath();
ctx.fillStyle = "rgba(0, 200, 220, 0.15)";
ctx.fill();
ctx.strokeStyle = "#00ffff";
ctx.lineWidth = 2;
ctx.stroke();
ctx.restore();
}
ctx.translate() と ctx.rotate() で座標系を自機の位置に移動してから描画しています。こうすると、自機の位置が変わっても描画コードは常に「原点を基準」に書けるので楽になります。
shadowColor と shadowBlur でネオンのような発光エフェクトをつけています。影の色をシアンにして、ぼかしを強くすると光って見えるんです!
弾を撃とう(オブジェクトプール)
🔑 このゲーム最大の設計ポイント:オブジェクトプール
シューティングゲームでは弾が大量に飛び交います。弾を撃つたびに new Object() で作って、画面外に出たら捨てる……とやると、JavaScriptの**ガベージコレクション(GC)**が頻繁に動いてカクつきます。
そこで使うのがオブジェクトプールパターン。最初にまとめて作っておいて、active フラグで使い回します。
const BULLET_POOL_SIZE = 80;
const BULLET_SPEED = 600;
const SHOT_INTERVAL = 0.12; // 自動連射の間隔(秒)
// 80個分の弾を事前に確保
const bulletPool = Array.from({ length: BULLET_POOL_SIZE }, () => ({
active: false,
x: 0,
y: 0,
vx: 0,
vy: 0,
}));
// 空いている弾を1つ探す
function acquireFromPool(pool) {
for (const item of pool) {
if (!item.active) return item;
}
return null; // 全部使用中ならスキップ
}
// 弾を発射
function fireBullet(x, y, vx = 0, vy = -BULLET_SPEED) {
const b = acquireFromPool(bulletPool);
if (!b) return;
b.active = true;
b.x = x;
b.y = y;
b.vx = vx;
b.vy = vy;
}
プールなし vs プールあり
プールなし | プールあり | |
|---|---|---|
弾を撃つとき |
|
|
弾が消えるとき | ゴミ箱(GC)が回収 |
|
パフォーマンス | GCでカクつく | GCが起きない! |
このパターンは弾だけでなく、敵・パーティクル・敵弾にも使っています。ゲーム開発の鉄則です。
自動連射の仕組み
let shotTimer = 0;
function updateBullets(deltaTime) {
shotTimer += deltaTime;
while (shotTimer >= SHOT_INTERVAL) {
shotTimer -= SHOT_INTERVAL;
fireBullet(player.x, player.y - player.height / 2);
playShootSound();
}
// 弾の移動&画面外判定
for (const b of bulletPool) {
if (!b.active) continue;
b.x += b.vx * deltaTime;
b.y += b.vy * deltaTime;
if (b.y < -15 || b.x < -50 || b.x > CANVAS_WIDTH + 50) {
b.active = false;
}
}
}
shotTimer で時間を計って、0.12秒ごとに弾を発射しています。プレイヤーが何もしなくても自動で撃ち続けるので、操作は移動に集中できます。
敵を出現させよう
このゲームには3種類の敵がいます。
ザコ敵(ひし形・赤)
const ENEMY_POOL_SIZE = 30;
const ENEMY_SPEED_Y = 150;
const ENEMY_SINE_AMPLITUDE = 30; // 左右揺れの幅
const enemyPool = Array.from({ length: ENEMY_POOL_SIZE }, () => ({
active: false,
x: 0, y: 0, baseX: 0,
time: 0, phase: 0,
hp: 1,
radius: 10,
}));
ザコ敵はサイン波で左右に揺れながら降りてきます。Math.sin() を使って波状の動きを作るのは、ゲームプログラミングの定番テクニックです。
// サイン波で左右に揺れる
e.x = e.baseX + Math.sin(e.time / 2 * Math.PI * 2 + e.phase) * 30;
編隊出現もあります。5〜8体を横一列に並べて、少しずつ位相をずらすことでウェーブ状の動きになります。
中型敵(八角形・オレンジ)
HP4で、降下→停止→プレイヤーに向けて扇状に3発発射→再降下という行動パターン。撃破するとパワーアップアイテムをドロップすることがあります。
突進敵(三角形・緑)
画面横から高速で斜めに突っ込んでくるタイプ。残像エフェクトつき。残像はリングバッファ(固定サイズの配列を循環して使う)で実装しています。
当たり判定を作ろう
📐 数学の出番!円形の当たり判定
見た目は三角形やひし形でも、当たり判定は円で行います。2つの円の中心同士の距離が、半径の合計より小さければ「当たり」。
const BULLET_RADIUS = 1.5; // 弾の半径
function checkCollisions() {
for (const b of bulletPool) {
if (!b.active) continue;
for (const e of enemyPool) {
if (!e.active) continue;
const dx = b.x - e.x;
const dy = b.y - e.y;
// 2点間の距離 < 半径の合計 → ヒット!
if (Math.hypot(dx, dy) < BULLET_RADIUS + e.radius) {
b.active = false;
e.hp -= 1;
if (e.hp <= 0) {
e.active = false;
spawnExplosion(e.x, e.y); // 爆発エフェクト
score += onKill(100); // コンボ付きスコア
playDestroySound();
}
}
}
}
}
💡 Math.hypot() = 三平方の定理
Math.hypot(dx, dy) は √(dx² + dy²) を計算する関数で、**三平方の定理(ピタゴラスの定理)**そのものです。中学数学で習う公式がゲーム開発にそのまま使われています。三角関数(Math.sin, Math.cos, Math.atan2)も敵弾の角度計算やレーザーの方向決定で大活躍します。
ボス戦を実装しよう
スコアが3000点に達するたびにボスが出現します。
ボス出現の演出
function checkBossSpawn() {
if (score >= nextBossScore) {
bossIntro.phase = "warning"; // WARNING!表示開始
}
}
演出は3段階で進みます。
- WARNING! が赤く点滅(2秒間)
- 画面が暗転(0.3秒)
- ボスが上から降りてくる
出現時には低音BGM(60Hz × 8パルス)が鳴って、緊張感を演出します。
3つの攻撃パターン
ボスは3秒ごとにパターンをローテーションします。
パターンA:全方位弾幕
function fireBossOmni() {
for (let i = 0; i < 16; i++) {
const angle = (i / 16) * Math.PI * 2;
const b = acquireFromPool(enemyBulletPool);
if (!b) continue;
b.active = true;
b.x = boss.x;
b.y = boss.y;
b.vx = Math.cos(angle) * bulletSpeed;
b.vy = Math.sin(angle) * bulletSpeed;
}
}
パターンB:レーザー照射
プレイヤーの方向を1秒間追尾する予告線を表示してから、太いレーザーを0.5秒間照射します。当たるとダメージ!
パターンC:ザコ召喚
ボスの周りにザコ敵3体を呼び出します。ボスを攻撃しつつザコも処理しなければなりません。
ヒットストップ演出
ボスを倒した瞬間、ゲーム全体が0.3秒間スローモーションになります。
// ヒットストップ中はゲーム時間を1/10に
if (hitStopTimer > 0) hitStopTimer = Math.max(0, hitStopTimer - rawDelta);
const deltaTime = hitStopTimer > 0 ? rawDelta * 0.1 : rawDelta;
たった2行のコードで「倒した瞬間の重み」を表現できます。格闘ゲームやアクションゲームでもよく使われるテクニックです。
エフェクトを追加しよう
このゲームの「すごいところ」は演出です。6つのエフェクトでゲームをリッチにしています。
背景の星(パララックス)
// 遠い星(50個)と近い星(30個)を2層で配置
const starsFar = Array.from({ length: 50 }, () => createStar("far"));
const starsNear = Array.from({ length: 30 }, () => createStar("near"));
遠い星は小さくゆっくり流れ、近い星は大きく速く流れます。2つの層を重ねることで**奥行き感(パララックス効果)**が生まれます。
爆発パーティクル
function spawnExplosion(x, y, count = 20, baseHue = 0) {
for (let i = 0; i < count; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = 60 + Math.random() * 200;
const p = acquireFromPool(explosionPool);
p.active = true;
p.x = x;
p.y = y;
p.vx = Math.cos(angle) * speed;
p.vy = Math.sin(angle) * speed;
p.hue = baseHue + Math.random() * 30;
// ...
}
}
撃破時に20〜30個のパーティクルが放射状に飛び散ります。HSLカラーで色相(hue)を変えることで、ザコは赤、中型はオレンジ、突進は緑……と敵ごとに爆発の色を変えています。
画面シェイク
if (screenShake.timer > 0) {
const intensity = screenShake.intensity * (screenShake.timer / screenShake.duration);
ctx.translate(
(Math.random() * 2 - 1) * intensity,
(Math.random() * 2 - 1) * intensity
);
}
被弾やボス撃破の瞬間に ctx.translate() でキャンバス全体をランダムにずらして揺れを表現しています。intensity を時間とともに減衰させることで、揺れが徐々に収まります。
その他のエフェクト
- 流れ星:3〜5秒ごとにランダムに出現。グラデーショントレイルで尾を引く
- ネオングリッド:画面左右にレトロ風のグリッドライン。奥ほど細く暗くなる(パースペクティブ)
- スクリーンフラッシュ:被弾時に画面全体が一瞬赤く光る
- コンボ表示:コンボ5以上で画面端がレインボーに、10以上でテキストもレインボーに
サウンドをコードだけで作ろう
このゲームの特徴の1つが、音声ファイルを一切使わないこと。すべての効果音をWeb Audio APIでリアルタイム生成しています。
let audioCtx = null;
// ユーザーが最初にキーを押した瞬間に初期化
function initAudio() {
if (audioCtx) return;
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
// 発射音:800Hz のサイン波、50ms
function playShootSound() {
if (!audioCtx) return;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = "sine";
osc.frequency.setValueAtTime(800, audioCtx.currentTime);
gain.gain.setValueAtTime(0.1, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.05);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + 0.05);
}
音の作り方
効果音 | 波形 | 周波数 | 長さ |
|---|---|---|---|
発射音 | サイン波 | 800Hz | 50ms |
撃破音 | サイン波 + ノイズ | 200Hz | 150ms |
被弾音 | のこぎり波 800→100Hz | スイープ | 100ms |
アイテム取得 | C→E→G→C アルペジオ | 261〜523Hz | 200ms |
ボス登場 | サイン波パルス | 60Hz × 8回 | 1秒 |
レベルUP | C+E 和音 | 261+329Hz | 200ms |
createOscillator() で波形を作り、createGain() で音量を制御、exponentialRampToValueAtTime() で減衰させる。たったこれだけで様々な効果音が作れます!
ブラウザの制限に注意
ブラウザのセキュリティ上、ユーザーが操作する前に音を鳴らすことはできません。だから initAudio() はSPACEキーが押されたタイミングで呼んでいます。
レベル&コンボシステムを作ろう
レベルシステム
const LEVEL_UP_INTERVAL = 30; // 30秒ごと
const MAX_LEVEL = 10;
// Lv.1〜10 の間を線形補間
function levelLerp(minVal, maxVal) {
const t = (gameLevel - 1) / (MAX_LEVEL - 1);
return minVal + (maxVal - minVal) * t;
}
function getEnemySpawnInterval() {
return levelLerp(1.5, 0.4); // Lv.1: 1.5秒 → Lv.10: 0.4秒
}
30秒ごとにレベルアップし、敵の出現間隔・編隊確率・敵弾速度がすべて levelLerp() で滑らかに上昇します。線形補間(lerp)は、ゲームの難易度バランスを取るときの必須テクニックです。
コンボシステム
const COMBO_TIMEOUT = 1.0; // 1秒以内に次を倒せばコンボ継続
function onKill(baseScore) {
comboCount++;
comboTimer = COMBO_TIMEOUT;
return Math.round(baseScore * (1 + comboCount * 0.1));
}
敵を1秒以内に連続撃破するとコンボが加算され、スコアにボーナス倍率がかかります。コンボ数に応じて演出も豪華になります。
コンボ数 | 演出 |
|---|---|
2〜4 | コンボ数テキスト表示(白) |
5〜9 | 画面端がレインボーに光る + テキスト黄色 |
10〜 | テキストもレインボーに! |
パワーアップアイテム
中型敵を倒すと30%の確率でパワーアップアイテムが落ちてきます。
- W(黄色):3WAY弾 — 3方向に弾を撃つ(15秒間)
- S(緑色):スピードUP — 移動速度1.5倍(15秒間)
- B(赤色):ボム — 画面上の敵を全滅+敵弾消し
アイテムは六角形で描画され、虹色にキラキラ回転しながら降りてきます。
完成コード全文
以下が3ファイルの完成コードです。そのままコピペして使ってOK! HTMLとCSSは上で書いた通り。JavaScriptは約2,900行あるので、ファイルに直接コピーしてください。
script.js(完成版)
コード全文が非常に長い(約2,900行)ため、以下のリンクからダウンロードしてください。
全コードをコピーして script.js という名前で保存すればOKです。
記事の中で解説した各セクションが、コメント付きで整理されています:
script.js の構成
├── 定数・設定(1-13行)
├── Canvas初期化(17-21行)
├── 背景演出:星・流れ星・グリッド(42-201行)
├── プレイヤー:移動・描画・パーティクル(204-431行)
├── サウンドシステム(434-488行)
├── 弾:プール・発射・描画(490-640行)
├── 敵:ザコ・中型・突進(642-1141行)
├── 爆発パーティクル(1152-1241行)
├── コンボ・スコアポップアップ(1244-1402行)
├── パワーアップ&ボム(1404-1614行)
├── ボス戦(1617-2068行)
├── 衝突判定(2071-2405行)
├── レベルシステム(2152-2267行)
└── UI表示・演出・メインループ(2270-2880行)
動かしてみよう
3つのファイルを保存したら、index.html をダブルクリックしてブラウザで開いてみましょう。「PRESS SPACE TO START」と表示されたらスペースキーを押してゲーム開始!
←→↑↓:自機の移動(弾は自動連射)
もし画面が真っ黒のままだったり、エラーが出る場合は以下をチェックしてみてください:
- 3つのファイルが同じフォルダに入っているか?
- ファイル名は
index.html、style.css、script.jsになっているか? - ブラウザの開発者ツール(F12キー)のConsoleタブにエラーが出ていないか?
次のステップ
シューティングゲームが動いたら、次はカスタマイズに挑戦してみましょう!自分でコードをいじって遊ぶのが、プログラミング上達の一番の近道です。
チャレンジアイデア
- 弾の速度や色を変える:
BULLET_SPEEDやfillStyleを好きな値に変えてみる - 新しい敵の種類を追加する:既存の敵のコードを参考に、動きの違う敵を作ってみる
- ハイスコアを保存する:
localStorageを使えばブラウザを閉じてもスコアが残る - 自機のデザインを変えてみる:
drawPlayer()の描画コードを書き換えて自分だけの機体に - 新しいパワーアップを考えて実装:シールド?ホーミング弾?レーザー?
- BGMを追加する:Web Audio APIのオシレーターを組み合わせてメロディーを作ってみる
わからないことがあったら「JavaScript Canvas ○○」で検索してみよう。Canvasの描画APIはWebのゲーム開発で超よく使うので、覚えておくと他のゲームも作れるようになるよ!