This page publishes the complete source code of "Retro Alien Shooter", the game running on HinaTech Games. This is not a rewritten teaching version — these are the real files that power the very game you can play right now.
In this game you shoot down a swarm of aliens that march toward you in formation. The highlights of the code are the system that keeps many enemies lined up and moves them all at once, and the "pixel art" technique that draws each alien as a cluster of tiny squares instead of a single dot. You'll see exactly how the classic arcade shooters were built.
Feel free to copy the code and run it on your own computer. Change the number of enemies or their speed to tune the difficulty just the way you like.
Retro Alien Shooter runs on the files below, each with its own job. We will walk through every one of them.
| File | Role |
|---|---|
index.html | The skeleton of the game screen. Lays out the board, score display and buttons in HTML |
style.css | The look and feel: colors, sizes, layout and animations |
game.js | The brain that runs the game rules: input handling, logic and scoring |
This split — HTML for structure, CSS for looks, JavaScript for behavior — is how almost every web page and browser game is built.
index.html is the file the browser loads first. It only lays out the "parts" of the game in HTML — none of the actual behavior lives here.
The star of this game is a single <canvas> tag. The player's ship, the aliens, bullets, shields, the UFO, even the starry sky — JavaScript draws everything onto this one board. Besides that, there is only the score-area that shows your score and best, the lives-display that shows remaining ships, and the start and game-over overlays.
Because the playfield lives inside the <canvas>, you won't find a single alien anywhere in the HTML.
* The code below omits the ad and analytics tags (the first few lines of the page), which have nothing to do with how the game works.
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>インベーダーゲーム|無料ブラウザシューティング|ひなテックGames</title>
<meta name="description" content="インベーダーゲームを無料で遊ぼう!迫りくるエイリアンの大群を撃ち落とせ。PC・スマホ対応の無料ブラウザシューティングゲーム。登録不要ですぐ遊べる!">
<link rel="canonical" href="https://hinata-ya.tech/games/games/invader/">
<!-- I18N:hreflang -->
<link rel="alternate" hreflang="ja" href="https://hinata-ya.tech/games/games/invader/">
<link rel="alternate" hreflang="en" href="https://hinata-ya.tech/games/en/games/invader/">
<link rel="alternate" hreflang="x-default" href="https://hinata-ya.tech/games/games/invader/">
<!-- /I18N:hreflang -->
<meta property="og:title" content="インベーダーゲーム|無料ブラウザシューティング|ひなテックGames">
<meta property="og:description" content="インベーダーゲームを無料で遊ぼう!迫りくるエイリアンの大群を撃ち落とせ。PC・スマホ対応の無料ブラウザシューティングゲーム。">
<meta property="og:type" content="website">
<meta property="og:url" content="https://hinata-ya.tech/games/games/invader/">
<meta property="og:site_name" content="ひなテックGames">
<meta property="og:locale" content="ja_JP">
<!-- SEO:meta-extra -->
<meta property="og:image" content="https://hinata-ya.tech/games/images/og/invader.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="インベーダーゲーム">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="インベーダーゲーム|無料ブラウザシューティング|ひなテックGames">
<meta name="twitter:description" content="インベーダーゲームを無料で遊ぼう!迫りくるエイリアンの大群を撃ち落とせ。PC・スマホ対応の無料ブラウザシューティングゲーム。登録不要ですぐ遊べる!">
<meta name="twitter:image" content="https://hinata-ya.tech/games/images/og/invader.png">
<!-- /SEO:meta-extra -->
<link rel="stylesheet" href="../../css/style.css">
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="../../css/leaderboard.css">
<!-- SEO:ld+json -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "VideoGame",
"name": "インベーダーゲーム",
"description": "インベーダーゲームを無料で遊ぼう!迫りくるエイリアンの大群を撃ち落とせ。PC・スマホ対応の無料ブラウザシューティングゲーム。登録不要ですぐ遊べる!",
"url": "https://hinata-ya.tech/games/games/invader/",
"image": "https://hinata-ya.tech/games/images/og/invader.png",
"inLanguage": "ja",
"genre": [
"シューティング",
"クラシック"
],
"gamePlatform": [
"Web Browser"
],
"applicationCategory": "GameApplication",
"operatingSystem": [
"Windows",
"macOS",
"iOS",
"Android",
"ChromeOS"
],
"browserRequirements": "Requires JavaScript. Requires HTML5.",
"isAccessibleForFree": true,
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "JPY",
"availability": "https://schema.org/InStock"
},
"publisher": {
"@type": "Organization",
"name": "ひなテック",
"url": "https://hinata-ya.tech"
},
"author": {
"@type": "Organization",
"name": "ひなテック",
"url": "https://hinata-ya.tech"
}
}
</script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "ホーム",
"item": "https://hinata-ya.tech/games/"
},
{
"@type": "ListItem",
"position": 2,
"name": "インベーダーゲーム",
"item": "https://hinata-ya.tech/games/games/invader/"
}
]
}
</script>
<!-- /SEO:ld+json -->
</head>
<body>
<div id="header"></div>
<div class="game-page">
<h1 class="game-page-title">インベーダーゲーム</h1>
<div class="game-page-tags">
<span class="tag">シューティング</span>
<span class="tag">クラシック</span>
</div>
<div class="game-container">
<div class="score-area">
<div class="score-box"><span class="score-label">スコア</span><span class="score-value" id="score">0</span></div>
<div class="score-box"><span class="score-label">ベスト</span><span class="score-value" id="best-score">0</span></div>
<button class="btn-new-game" id="btn-new-game">New Game</button>
</div>
<div class="board-wrapper">
<canvas id="game-canvas"></canvas>
<div class="lives-display" id="lives-display"></div>
<div class="game-overlay" id="game-over-overlay">
<div class="overlay-content">
<h2>ゲームオーバー</h2>
<p>スコア: <span id="final-score">0</span></p>
<p class="best-result" id="best-result"></p>
<button class="btn-primary" id="btn-retry">もう一度遊ぶ</button>
</div>
</div>
<div class="game-overlay active" id="game-start-overlay">
<div class="overlay-content">
<h2>インベーダーゲーム</h2>
<p>エイリアンの大群を<br>撃ち落とせ!</p>
<button class="btn-primary" id="btn-start">ゲームスタート</button>
</div>
</div>
</div>
</div>
<div class="game-instructions">
<h3>遊び方</h3>
<ul>
<li><strong>PC</strong>: 左右の矢印キーで移動、スペースキーで弾を発射</li>
<li><strong>スマホ</strong>: 画面の左半分タッチで左移動、右半分タッチで右移動(弾は自動発射)</li>
<li>隊列を組んで迫りくるエイリアンを全滅させよう!</li>
<li>エイリアンは端に到達すると一段降りてくる</li>
<li>上段のエイリアンほど高得点(10〜30点)</li>
<li>バリアで敵の弾を防げるが、ダメージを受けると壊れる</li>
<li>全滅させるとウェーブクリア!次はさらに速く!</li>
</ul>
</div>
<div class="game-promo">
<h3>このゲーム、自分でも作れるよ!</h3>
<p>インベーダーゲームはCanvas APIと<br>配列操作・当たり判定で作られています。<br><br>ひなテックでは、こうしたゲームの<br>作り方を楽しく学べます!</p>
<div class="promo-links">
<a href="https://hinata-ya.tech/contact/" class="btn-primary">体験レッスンに申し込む →</a>
<a href="source/" class="btn-secondary">ソースコードを見る →</a>
</div>
</div>
<div class="other-games"><a href="../../index.html">← 他のゲームも遊ぶ</a></div>
</div>
<div id="footer"></div>
<script src="../../js/common.js?v=20260613c" data-base="../../"></script>
<script src="../../js/leaderboard.js"></script>
<script src="game.js?v=20260610a"></script>
</body>
</html>
style.css styles everything around the game screen. The ship and enemies themselves are drawn on the Canvas by JavaScript, so what this file mainly builds is the "frame" of the screen. The background is set to a dark color, #0a0a1a, to create a deep-space mood.
.board-wrapper uses aspect-ratio: 2 / 3 so the game keeps its proportions no matter how the screen size changes. touch-action: none is an important rule that stops the page from scrolling while you play on a phone.
The remaining ships are shown as green dots with .life-icon; when you lose a ship, the .lost class fades it out with opacity. @keyframes overlay-appear makes the game-over screen fade in softly.
/* インベーダーゲーム - 固有スタイル */
.score-area {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1rem;
}
.score-box {
background: #BBADA0;
border-radius: 8px;
padding: 0.5rem 1rem;
text-align: center;
min-width: 70px;
}
.score-label {
display: block;
font-size: 0.7rem;
color: #EEE4DA;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.score-value {
display: block;
font-size: 1.25rem;
font-weight: 700;
color: #fff;
}
.btn-new-game {
margin-left: auto;
background: #8F7A66;
color: #fff;
border: none;
border-radius: 8px;
padding: 0.5rem 1.25rem;
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
.btn-new-game:hover {
background: #7A6658;
}
.board-wrapper {
position: relative;
width: 100%;
max-width: 400px;
margin: 0 auto;
aspect-ratio: 2 / 3;
touch-action: none;
}
#game-canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
background: #0a0a1a;
border-radius: 8px;
display: block;
}
.lives-display {
position: absolute;
bottom: 8px;
right: 12px;
display: flex;
gap: 4px;
z-index: 5;
}
.life-icon {
width: 12px;
height: 12px;
background: #33ff33;
border-radius: 50%;
opacity: 0.9;
}
.life-icon.lost {
opacity: 0.25;
}
.game-overlay {
position: absolute;
inset: 0;
background: rgba(238, 228, 218, 0.85);
border-radius: 8px;
display: none;
align-items: center;
justify-content: center;
z-index: 10;
animation: overlay-appear 0.3s ease;
}
.game-overlay.active {
display: flex;
}
@keyframes overlay-appear {
0% { opacity: 0; }
100% { opacity: 1; }
}
.overlay-content {
text-align: center;
padding: 1.5rem;
}
.overlay-content h2 {
font-size: 1.75rem;
color: #776E65;
margin-bottom: 0.5rem;
}
.overlay-content p {
font-size: 1.1rem;
color: #776E65;
margin-bottom: 0.75rem;
}
.overlay-content .best-result {
font-size: 0.95rem;
color: #33ff33;
font-weight: 600;
margin-bottom: 1rem;
}
.overlay-content .btn-primary {
margin-bottom: 0.5rem;
}
@media (max-width: 500px) {
.board-wrapper { max-width: 100%; }
}
@media (max-width: 360px) {
.score-area { gap: 0.5rem; }
.score-box { padding: 0.4rem 0.75rem; min-width: 55px; }
.score-value { font-size: 1rem; }
.score-label { font-size: 0.6rem; }
}
game.js is the heart of this game. The whole file is wrapped in (function () { ... })(); — an "immediately invoked function" — so the game's variables don't clash with any other code. At the center is the game loop, gameLoop(), repeated by requestAnimationFrame, which runs "update, then draw" every time.
What makes this game special is the alien formation movement. initInvaders() lines up 40 aliens (5 rows × 8 columns) in the invaders array. updateInvaders() moves them: the enemies step sideways together while getGridBounds() checks whether the outermost edge of the formation has reached the side of the screen. When it does, needDrop is set, everyone drops down one row, and the marching direction flips. That famous "step-step sideways, then drop at the edge" motion is built exactly like this.
The enemies speeding up is another key flavor of the game. Inside updateInvaders(), the fewer aliens remain alive, the bigger the speed multiplier speedMult becomes. The more you defeat, the faster the rest move — and the last alien races around at incredible speed, just like the arcade originals.
Functions like drawSmallInvader() draw the aliens. Notice how each alien is shaped out of many small squares drawn with ctx.fillRect — that is pixel art. A frame value swaps between two poses with different arm and leg positions, which makes the aliens look like they are wriggling along.
Collision detection uses overlapping rectangles (rectsOverlap). The player's bullet is checked against aliens, the UFO, and the shields; enemy bullets are checked against the player and the shields. Each shield (shields) is a cluster of small blocks, and only the block that gets hit has its alive flag set to false, so shields crumble away bit by bit as they get shot. When you wipe out every enemy, checkWaveClear() kicks in and the next wave moves even faster. Your best score is saved in localStorage.
// ============================================
// インベーダーゲーム ゲームロジック
// ============================================
(function () {
'use strict';
// 言語別テキスト(英語版ページが window.GAME_TEXT を定義して上書きする。docs/i18n.md 参照)
var TEXT = window.GAME_TEXT || {};
// ============================================
// 定数
// ============================================
var GRID_ROWS = 5;
var GRID_COLS = 8;
var MAX_LIVES = 3;
var PLAYER_SPEED_RATIO = 0.012;
var PLAYER_BULLET_SPEED_RATIO = 0.018;
var ENEMY_BULLET_SPEED_RATIO = 0.004;
var ENEMY_BASE_STEP_RATIO = 0.004;
var ENEMY_DROP_RATIO = 0.025;
var INVADER_WIDTH_RATIO = 0.055;
var INVADER_HEIGHT_RATIO = 0.032;
var INVADER_SPACING_X_RATIO = 0.075;
var INVADER_SPACING_Y_RATIO = 0.05;
var GRID_TOP_RATIO = 0.08;
var GRID_MARGIN_RATIO = 0.04;
var PLAYER_Y_RATIO = 0.90;
var SHIELD_Y_RATIO = 0.78;
var SHIELD_BLOCK_RATIO = 0.012;
var UFO_SPEED_RATIO = 0.004;
var UFO_WIDTH_RATIO = 0.08;
var UFO_HEIGHT_RATIO = 0.03;
var FIRE_COOLDOWN = 250;
var ENEMY_FIRE_BASE_INTERVAL = 1800;
var ENEMY_FIRE_INITIAL_DELAY = 2000;
var UFO_MIN_INTERVAL = 15000;
var UFO_MAX_INTERVAL = 25000;
var WAVE_CLEAR_DURATION = 1500;
var INVINCIBLE_DURATION = 1500;
var MOVE_STEPS_PER_ANIM = 10;
// ポイント値
var ROW_POINTS = [30, 20, 20, 10, 10];
var ROW_COLORS = ['#ff3333', '#33ccff', '#33ccff', '#33ff33', '#33ff33'];
var UFO_POINTS = [100, 150, 200, 300];
// ============================================
// 状態変数
// ============================================
var canvas, ctx;
var W, H;
var dpr;
// プレイヤー
var playerX, playerW, playerH, playerSpeed;
var playerBullet = null;
var playerBulletSpeed;
var lastFireTime = 0;
var lives = MAX_LIVES;
var invincibleUntil = 0;
// 敵グリッド
var invaders = [];
var enemyDir = 1;
var enemyStepX, enemyDropY;
var enemyMoveTimer = 0;
var enemyMoveInterval;
var enemyAnimFrame = 0;
var enemyAnimStepCount = 0;
var totalEnemies = GRID_ROWS * GRID_COLS;
// 敵弾
var enemyBullets = [];
var enemyBulletSpeed;
var lastEnemyFireTime = 0;
var enemyFireInterval;
var gameStartTime = 0;
// シールド
var shields = [];
// UFO
var ufo = null;
var nextUfoTime = 0;
// パーティクル・エフェクト
var particles = [];
var floatingTexts = [];
// 背景の星
var stars = [];
// ゲーム状態
var score = 0;
var bestScore = parseInt(localStorage.getItem('bestInvader') || '0', 10);
var wave = 1;
var gameRunning = false;
var gameOverFlag = false;
var waveClearFlag = false;
var waveClearTime = 0;
var animFrameId = null;
var lastTime = 0;
// 入力状態
var leftPressed = false;
var rightPressed = false;
var spacePressed = false;
var touchSide = null;
var touchAutoFireTimer = 0;
// ============================================
// DOM要素
// ============================================
var scoreEl = document.getElementById('score');
var bestScoreEl = document.getElementById('best-score');
var livesDisplay = document.getElementById('lives-display');
var gameOverOverlay = document.getElementById('game-over-overlay');
var gameStartOverlay = document.getElementById('game-start-overlay');
var finalScoreEl = document.getElementById('final-score');
var bestResultEl = document.getElementById('best-result');
// ============================================
// キャンバスの初期化
// ============================================
canvas = document.getElementById('game-canvas');
ctx = canvas.getContext('2d');
function resizeCanvas() {
var wrapper = canvas.parentElement;
var rect = wrapper.getBoundingClientRect();
dpr = window.devicePixelRatio || 1;
W = rect.width;
H = rect.height;
canvas.width = W * dpr;
canvas.height = H * dpr;
canvas.style.width = W + 'px';
canvas.style.height = H + 'px';
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
// サイズ依存値の再計算
playerW = W * 0.07;
playerH = H * 0.035;
playerSpeed = W * PLAYER_SPEED_RATIO;
playerBulletSpeed = H * PLAYER_BULLET_SPEED_RATIO;
enemyBulletSpeed = H * ENEMY_BULLET_SPEED_RATIO;
enemyStepX = W * ENEMY_BASE_STEP_RATIO;
enemyDropY = H * ENEMY_DROP_RATIO;
// プレイヤー位置を制限
if (playerX !== undefined) {
playerX = clamp(playerX, playerW / 2, W - playerW / 2);
}
initStars();
}
// ============================================
// ユーティリティ
// ============================================
function clamp(val, min, max) {
return val < min ? min : (val > max ? max : val);
}
function randInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function randFloat(min, max) {
return Math.random() * (max - min) + min;
}
function rectsOverlap(ax, ay, aw, ah, bx, by, bw, bh) {
return ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by;
}
// ============================================
// 背景の星
// ============================================
function initStars() {
stars = [];
var count = Math.floor(W * H / 700);
for (var i = 0; i < count; i++) {
stars.push({
x: Math.random() * W,
y: Math.random() * H,
size: Math.random() * 1.5 + 0.5,
speed: Math.random() * 0.3 + 0.1,
alpha: Math.random() * 0.5 + 0.2
});
}
}
function updateStars(dt) {
for (var i = 0; i < stars.length; i++) {
var s = stars[i];
s.y += s.speed * dt * 0.06;
if (s.y > H) {
s.y = 0;
s.x = Math.random() * W;
}
}
}
function drawStars() {
for (var i = 0; i < stars.length; i++) {
var s = stars[i];
ctx.fillStyle = 'rgba(255,255,255,' + s.alpha + ')';
ctx.fillRect(s.x, s.y, s.size, s.size);
}
}
// ============================================
// ライフ表示
// ============================================
function updateLivesDisplay() {
livesDisplay.innerHTML = '';
for (var i = 0; i < MAX_LIVES; i++) {
var dot = document.createElement('div');
dot.className = 'life-icon' + (i >= lives ? ' lost' : '');
livesDisplay.appendChild(dot);
}
}
// ============================================
// スコア更新
// ============================================
function updateScore(pts) {
score += pts;
scoreEl.textContent = score;
if (score > bestScore) {
bestScore = score;
bestScoreEl.textContent = bestScore;
localStorage.setItem('bestInvader', bestScore);
}
}
// ============================================
// インベーダー描画 (fillRect ピクセルアート)
// ============================================
// 小型インベーダー (row 0) - 触角つき小さいボディ
function drawSmallInvader(cx, cy, w, h, frame, color) {
var u = w / 8;
var v = h / 6;
ctx.fillStyle = color;
if (frame === 0) {
// 触角
ctx.fillRect(cx - 3 * u, cy - 3 * v, u, 2 * v);
ctx.fillRect(cx + 2 * u, cy - 3 * v, u, 2 * v);
// 頭
ctx.fillRect(cx - 2 * u, cy - 1 * v, 4 * u, 2 * v);
// 目
ctx.fillStyle = '#0a0a1a';
ctx.fillRect(cx - 1.5 * u, cy - 0.5 * v, u, v);
ctx.fillRect(cx + 0.5 * u, cy - 0.5 * v, u, v);
ctx.fillStyle = color;
// ボディ
ctx.fillRect(cx - 3 * u, cy + 1 * v, 6 * u, 2 * v);
// 足
ctx.fillRect(cx - 2 * u, cy + 3 * v, u, v);
ctx.fillRect(cx + 1 * u, cy + 3 * v, u, v);
} else {
// 触角(開き)
ctx.fillRect(cx - 4 * u, cy - 3 * v, u, v);
ctx.fillRect(cx - 3 * u, cy - 2 * v, u, v);
ctx.fillRect(cx + 2 * u, cy - 2 * v, u, v);
ctx.fillRect(cx + 3 * u, cy - 3 * v, u, v);
// 頭
ctx.fillRect(cx - 2 * u, cy - 1 * v, 4 * u, 2 * v);
// 目
ctx.fillStyle = '#0a0a1a';
ctx.fillRect(cx - 1.5 * u, cy - 0.5 * v, u, v);
ctx.fillRect(cx + 0.5 * u, cy - 0.5 * v, u, v);
ctx.fillStyle = color;
// ボディ
ctx.fillRect(cx - 3 * u, cy + 1 * v, 6 * u, 2 * v);
// 足(広がり)
ctx.fillRect(cx - 3 * u, cy + 3 * v, u, v);
ctx.fillRect(cx + 2 * u, cy + 3 * v, u, v);
}
}
// 中型インベーダー (rows 1-2) - 腕つき
function drawMediumInvader(cx, cy, w, h, frame, color) {
var u = w / 10;
var v = h / 6;
ctx.fillStyle = color;
// 頭部
ctx.fillRect(cx - 2 * u, cy - 3 * v, 4 * u, 2 * v);
// 目
ctx.fillStyle = '#0a0a1a';
ctx.fillRect(cx - 1.5 * u, cy - 2.5 * v, u, v);
ctx.fillRect(cx + 0.5 * u, cy - 2.5 * v, u, v);
ctx.fillStyle = color;
// ボディ
ctx.fillRect(cx - 4 * u, cy - 1 * v, 8 * u, 3 * v);
if (frame === 0) {
// 腕下がり
ctx.fillRect(cx - 5 * u, cy + 0 * v, u, 2 * v);
ctx.fillRect(cx + 4 * u, cy + 0 * v, u, 2 * v);
// 足
ctx.fillRect(cx - 3 * u, cy + 2 * v, 2 * u, v);
ctx.fillRect(cx + 1 * u, cy + 2 * v, 2 * u, v);
} else {
// 腕上がり
ctx.fillRect(cx - 5 * u, cy - 2 * v, u, 2 * v);
ctx.fillRect(cx + 4 * u, cy - 2 * v, u, 2 * v);
// 足(広がり)
ctx.fillRect(cx - 4 * u, cy + 2 * v, 2 * u, v);
ctx.fillRect(cx + 2 * u, cy + 2 * v, 2 * u, v);
}
}
// 大型インベーダー (rows 3-4) - 脚つき
function drawLargeInvader(cx, cy, w, h, frame, color) {
var u = w / 12;
var v = h / 7;
ctx.fillStyle = color;
// 頭
ctx.fillRect(cx - 3 * u, cy - 3 * v, 6 * u, 2 * v);
// 目
ctx.fillStyle = '#0a0a1a';
ctx.fillRect(cx - 2 * u, cy - 2.5 * v, 1.5 * u, v);
ctx.fillRect(cx + 0.5 * u, cy - 2.5 * v, 1.5 * u, v);
ctx.fillStyle = color;
// ボディ
ctx.fillRect(cx - 5 * u, cy - 1 * v, 10 * u, 3 * v);
// 下ボディ
ctx.fillRect(cx - 4 * u, cy + 2 * v, 8 * u, v);
if (frame === 0) {
// 足(内側)
ctx.fillRect(cx - 4 * u, cy + 3 * v, u, v);
ctx.fillRect(cx - 1 * u, cy + 3 * v, 2 * u, v);
ctx.fillRect(cx + 3 * u, cy + 3 * v, u, v);
} else {
// 足(外側)
ctx.fillRect(cx - 5 * u, cy + 3 * v, u, v);
ctx.fillRect(cx - 2 * u, cy + 3 * v, u, v);
ctx.fillRect(cx + 1 * u, cy + 3 * v, u, v);
ctx.fillRect(cx + 4 * u, cy + 3 * v, u, v);
}
}
function drawInvader(inv) {
var drawFn;
if (inv.row === 0) {
drawFn = drawSmallInvader;
} else if (inv.row <= 2) {
drawFn = drawMediumInvader;
} else {
drawFn = drawLargeInvader;
}
drawFn(inv.x, inv.y, inv.w, inv.h, enemyAnimFrame, inv.color);
}
// ============================================
// インベーダーグリッドの初期化
// ============================================
function initInvaders() {
invaders = [];
var invW = W * INVADER_WIDTH_RATIO;
var invH = H * INVADER_HEIGHT_RATIO;
var spacingX = W * INVADER_SPACING_X_RATIO;
var spacingY = H * INVADER_SPACING_Y_RATIO;
var gridW = GRID_COLS * spacingX;
var startX = (W - gridW) / 2 + spacingX / 2;
// ウェーブが進むと少しだけ低くスタート(最大で少し下)
var extraDrop = Math.min((wave - 1) * invH * 0.4, H * 0.1);
var startY = H * GRID_TOP_RATIO + extraDrop;
for (var r = 0; r < GRID_ROWS; r++) {
for (var c = 0; c < GRID_COLS; c++) {
invaders.push({
row: r,
col: c,
x: startX + c * spacingX,
y: startY + r * spacingY,
w: invW,
h: invH,
alive: true,
color: ROW_COLORS[r],
points: ROW_POINTS[r]
});
}
}
totalEnemies = GRID_ROWS * GRID_COLS;
enemyDir = 1;
enemyAnimFrame = 0;
enemyAnimStepCount = 0;
enemyMoveTimer = 0;
enemyMoveInterval = 45; // 正規化dt単位の移動間隔
}
function getAliveCount() {
var count = 0;
for (var i = 0; i < invaders.length; i++) {
if (invaders[i].alive) count++;
}
return count;
}
function getGridBounds() {
var minX = Infinity, maxX = -Infinity;
var maxY = -Infinity;
for (var i = 0; i < invaders.length; i++) {
var inv = invaders[i];
if (!inv.alive) continue;
var left = inv.x - inv.w / 2;
var right = inv.x + inv.w / 2;
var bottom = inv.y + inv.h / 2;
if (left < minX) minX = left;
if (right > maxX) maxX = right;
if (bottom > maxY) maxY = bottom;
}
return { minX: minX, maxX: maxX, maxY: maxY };
}
// ============================================
// 敵移動
// ============================================
function updateInvaders(dt) {
var alive = getAliveCount();
if (alive === 0) return;
// スピードアップ: 残り少ないほど速い
var speedMult = 1 + 0.5 * (totalEnemies - alive) / totalEnemies;
var waveSpeedMult = 1 + (wave - 1) * 0.12;
var interval = enemyMoveInterval / (speedMult * waveSpeedMult);
enemyMoveTimer += dt;
if (enemyMoveTimer < interval) return;
enemyMoveTimer = 0;
// アニメフレーム切り替え
enemyAnimStepCount++;
if (enemyAnimStepCount >= MOVE_STEPS_PER_ANIM) {
enemyAnimStepCount = 0;
enemyAnimFrame = 1 - enemyAnimFrame;
}
var bounds = getGridBounds();
var stepX = enemyStepX;
var margin = W * GRID_MARGIN_RATIO;
var needDrop = false;
if (enemyDir === 1 && bounds.maxX + stepX > W - margin) {
needDrop = true;
} else if (enemyDir === -1 && bounds.minX - stepX < margin) {
needDrop = true;
}
for (var i = 0; i < invaders.length; i++) {
if (!invaders[i].alive) continue;
if (needDrop) {
invaders[i].y += enemyDropY;
} else {
invaders[i].x += stepX * enemyDir;
}
}
if (needDrop) {
enemyDir = -enemyDir;
}
// ゲームオーバー判定: 敵がプレイヤーラインに到達
var playerLineY = H * PLAYER_Y_RATIO - playerH;
bounds = getGridBounds();
if (bounds.maxY >= playerLineY) {
triggerGameOver();
}
}
// ============================================
// プレイヤー
// ============================================
function initPlayer() {
playerX = W / 2;
lives = MAX_LIVES;
invincibleUntil = 0;
playerBullet = null;
updateLivesDisplay();
}
function drawPlayer() {
var now = Date.now();
// 無敵中は点滅
if (now < invincibleUntil) {
if (Math.floor(now / 100) % 2 === 0) return;
}
var px = playerX;
var py = H * PLAYER_Y_RATIO;
var hw = playerW / 2;
var hh = playerH / 2;
ctx.fillStyle = '#33ff33';
// 本体(長方形)
ctx.fillRect(px - hw, py - hh + hh * 0.4, playerW, playerH * 0.6);
// 砲台(上部)
var barrelW = playerW * 0.2;
var barrelH = playerH * 0.5;
ctx.fillRect(px - barrelW / 2, py - hh - barrelH * 0.3, barrelW, barrelH);
// 中央コア
ctx.fillStyle = '#22cc22';
ctx.fillRect(px - hw * 0.4, py - hh * 0.2, playerW * 0.4, playerH * 0.3);
}
function updatePlayer(dt) {
if (leftPressed || touchSide === 'left') {
playerX -= playerSpeed * dt;
}
if (rightPressed || touchSide === 'right') {
playerX += playerSpeed * dt;
}
playerX = clamp(playerX, playerW / 2, W - playerW / 2);
// モバイル自動発射
if (touchSide !== null) {
touchAutoFireTimer += dt;
if (touchAutoFireTimer > 5) {
firePlayerBullet();
}
}
// PC: スペースキー長押し
if (spacePressed) {
firePlayerBullet();
}
}
// ============================================
// プレイヤー弾
// ============================================
function firePlayerBullet() {
if (playerBullet !== null) return;
var now = Date.now();
if (now - lastFireTime < FIRE_COOLDOWN) return;
lastFireTime = now;
playerBullet = {
x: playerX,
y: H * PLAYER_Y_RATIO - playerH,
w: 3,
h: 10
};
}
function updatePlayerBullet(dt) {
if (playerBullet === null) return;
playerBullet.y -= playerBulletSpeed * dt;
if (playerBullet.y + playerBullet.h < 0) {
playerBullet = null;
return;
}
// 敵との衝突判定
var b = playerBullet;
for (var i = 0; i < invaders.length; i++) {
var inv = invaders[i];
if (!inv.alive) continue;
if (rectsOverlap(
b.x - b.w / 2, b.y - b.h / 2, b.w, b.h,
inv.x - inv.w / 2, inv.y - inv.h / 2, inv.w, inv.h
)) {
inv.alive = false;
playerBullet = null;
updateScore(inv.points);
spawnExplosion(inv.x, inv.y, inv.color);
addFloatingText('+' + inv.points, inv.x, inv.y, inv.color);
return;
}
}
// UFOとの衝突判定
if (ufo !== null) {
if (rectsOverlap(
b.x - b.w / 2, b.y - b.h / 2, b.w, b.h,
ufo.x - ufo.w / 2, ufo.y - ufo.h / 2, ufo.w, ufo.h
)) {
playerBullet = null;
var pts = ufo.points;
updateScore(pts);
spawnExplosion(ufo.x, ufo.y, '#ff3333');
addFloatingText('+' + pts, ufo.x, ufo.y, '#ff3333');
ufo = null;
return;
}
}
// シールドとの衝突判定
if (checkBulletShieldCollision(b)) {
playerBullet = null;
}
}
function drawPlayerBullet() {
if (playerBullet === null) return;
ctx.fillStyle = '#ffffff';
ctx.fillRect(
playerBullet.x - playerBullet.w / 2,
playerBullet.y - playerBullet.h / 2,
playerBullet.w,
playerBullet.h
);
}
// ============================================
// 敵弾
// ============================================
function updateEnemyFiring(dt) {
var now = Date.now();
// ゲーム開始直後は発射しない
if (now - gameStartTime < ENEMY_FIRE_INITIAL_DELAY) return;
var interval = ENEMY_FIRE_BASE_INTERVAL - (wave - 1) * 120;
if (interval < 500) interval = 500;
enemyFireInterval = interval;
if (now - lastEnemyFireTime < enemyFireInterval) return;
// 最下段の敵から発射
var bottomInvaders = getBottomInvaders();
if (bottomInvaders.length === 0) return;
var shooter = bottomInvaders[randInt(0, bottomInvaders.length - 1)];
enemyBullets.push({
x: shooter.x,
y: shooter.y + shooter.h / 2,
w: 3,
h: 8
});
lastEnemyFireTime = now;
}
function getBottomInvaders() {
var colBottom = {};
for (var i = 0; i < invaders.length; i++) {
var inv = invaders[i];
if (!inv.alive) continue;
var key = inv.col;
if (!colBottom[key] || inv.row > colBottom[key].row) {
colBottom[key] = inv;
}
}
var result = [];
for (var k in colBottom) {
if (colBottom.hasOwnProperty(k)) {
result.push(colBottom[k]);
}
}
return result;
}
function updateEnemyBullets(dt) {
for (var i = enemyBullets.length - 1; i >= 0; i--) {
var b = enemyBullets[i];
b.y += enemyBulletSpeed * dt;
// 画面外
if (b.y > H) {
enemyBullets.splice(i, 1);
continue;
}
// プレイヤーとの衝突
var now = Date.now();
if (now >= invincibleUntil) {
var px = playerX - playerW / 2;
var py = H * PLAYER_Y_RATIO - playerH / 2;
if (rectsOverlap(b.x - b.w / 2, b.y - b.h / 2, b.w, b.h, px, py, playerW, playerH)) {
enemyBullets.splice(i, 1);
hitPlayer();
continue;
}
}
// シールドとの衝突
if (checkBulletShieldCollision(b)) {
enemyBullets.splice(i, 1);
}
}
}
function drawEnemyBullets() {
ctx.fillStyle = '#ffff33';
for (var i = 0; i < enemyBullets.length; i++) {
var b = enemyBullets[i];
ctx.fillRect(b.x - b.w / 2, b.y - b.h / 2, b.w, b.h);
}
}
// ============================================
// シールド
// ============================================
function initShields() {
shields = [];
var shieldCount = 4;
var blockSize = W * SHIELD_BLOCK_RATIO;
if (blockSize < 3) blockSize = 3;
var bw = 5; // ブロック数(横)
var bh = 4; // ブロック数(縦)
var shieldW = bw * blockSize;
var totalShieldsW = shieldCount * shieldW;
var gap = (W - totalShieldsW) / (shieldCount + 1);
var shieldY = H * SHIELD_Y_RATIO;
for (var s = 0; s < shieldCount; s++) {
var sx = gap + s * (shieldW + gap);
var shieldBlocks = [];
for (var r = 0; r < bh; r++) {
for (var c = 0; c < bw; c++) {
// 下段中央をくりぬいてアーチ型にする
if (r >= bh - 2 && c >= 1 && c <= 3) continue;
shieldBlocks.push({
x: sx + c * blockSize,
y: shieldY + r * blockSize,
w: blockSize,
h: blockSize,
alive: true
});
}
}
shields.push(shieldBlocks);
}
}
function drawShields() {
ctx.fillStyle = '#33ff33';
for (var s = 0; s < shields.length; s++) {
var blocks = shields[s];
for (var i = 0; i < blocks.length; i++) {
if (!blocks[i].alive) continue;
ctx.fillRect(blocks[i].x, blocks[i].y, blocks[i].w, blocks[i].h);
}
}
}
function checkBulletShieldCollision(bullet) {
for (var s = 0; s < shields.length; s++) {
var blocks = shields[s];
for (var i = 0; i < blocks.length; i++) {
var block = blocks[i];
if (!block.alive) continue;
if (rectsOverlap(
bullet.x - bullet.w / 2, bullet.y - bullet.h / 2, bullet.w, bullet.h,
block.x, block.y, block.w, block.h
)) {
block.alive = false;
spawnSmallExplosion(block.x + block.w / 2, block.y + block.h / 2, '#33ff33');
return true;
}
}
}
return false;
}
// インベーダーがシールドに当たったらシールド破壊
function checkInvaderShieldCollision() {
for (var i = 0; i < invaders.length; i++) {
var inv = invaders[i];
if (!inv.alive) continue;
for (var s = 0; s < shields.length; s++) {
var blocks = shields[s];
for (var j = 0; j < blocks.length; j++) {
var block = blocks[j];
if (!block.alive) continue;
if (rectsOverlap(
inv.x - inv.w / 2, inv.y - inv.h / 2, inv.w, inv.h,
block.x, block.y, block.w, block.h
)) {
block.alive = false;
}
}
}
}
}
// ============================================
// UFO
// ============================================
function scheduleUfo() {
nextUfoTime = Date.now() + randInt(UFO_MIN_INTERVAL, UFO_MAX_INTERVAL);
}
function updateUfo(dt) {
var now = Date.now();
if (ufo === null) {
if (now >= nextUfoTime) {
ufo = {
x: -W * UFO_WIDTH_RATIO,
y: H * 0.04,
w: W * UFO_WIDTH_RATIO,
h: H * UFO_HEIGHT_RATIO,
speed: W * UFO_SPEED_RATIO,
points: UFO_POINTS[randInt(0, UFO_POINTS.length - 1)],
animTimer: 0
};
}
return;
}
ufo.x += ufo.speed * dt;
ufo.animTimer += dt;
if (ufo.x - ufo.w / 2 > W) {
ufo = null;
scheduleUfo();
}
}
function drawUfo() {
if (ufo === null) return;
var cx = ufo.x;
var cy = ufo.y;
var u = ufo.w / 12;
var v = ufo.h / 4;
ctx.fillStyle = '#ff3333';
// ドーム
ctx.fillRect(cx - 2 * u, cy - 2 * v, 4 * u, v);
// 本体
ctx.fillRect(cx - 5 * u, cy - v, 10 * u, 2 * v);
// 下部
ctx.fillRect(cx - 3 * u, cy + v, 6 * u, v);
// ライト点滅
var blink = Math.floor(ufo.animTimer / 8) % 3;
var lightPositions = [-3 * u, 0, 3 * u];
for (var i = 0; i < lightPositions.length; i++) {
ctx.fillStyle = (i === blink) ? '#ffff33' : '#ff6666';
ctx.fillRect(cx + lightPositions[i] - u * 0.4, cy - 0.5 * v, u * 0.8, v * 0.5);
}
}
// ============================================
// プレイヤー被弾
// ============================================
function hitPlayer() {
lives--;
updateLivesDisplay();
spawnExplosion(playerX, H * PLAYER_Y_RATIO, '#33ff33');
if (lives <= 0) {
triggerGameOver();
} else {
invincibleUntil = Date.now() + INVINCIBLE_DURATION;
}
}
// ============================================
// パーティクルエフェクト
// ============================================
function spawnExplosion(x, y, color) {
var count = 12;
for (var i = 0; i < count; i++) {
var angle = (Math.PI * 2 / count) * i + Math.random() * 0.3;
var speed = randFloat(1, 3);
particles.push({
x: x,
y: y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
life: 1.0,
decay: randFloat(0.015, 0.03),
size: randFloat(2, 5),
color: color
});
}
}
function spawnSmallExplosion(x, y, color) {
var count = 4;
for (var i = 0; i < count; i++) {
var angle = Math.random() * Math.PI * 2;
var speed = randFloat(0.5, 1.5);
particles.push({
x: x,
y: y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
life: 1.0,
decay: randFloat(0.03, 0.06),
size: randFloat(1, 3),
color: color
});
}
}
function updateParticles(dt) {
for (var i = particles.length - 1; i >= 0; i--) {
var p = particles[i];
p.x += p.vx * dt;
p.y += p.vy * dt;
p.life -= p.decay * dt;
if (p.life <= 0) {
particles.splice(i, 1);
}
}
}
function drawParticles() {
for (var i = 0; i < particles.length; i++) {
var p = particles[i];
ctx.globalAlpha = p.life;
ctx.fillStyle = p.color;
ctx.fillRect(p.x - p.size / 2, p.y - p.size / 2, p.size, p.size);
}
ctx.globalAlpha = 1;
}
// ============================================
// フローティングテキスト
// ============================================
function addFloatingText(text, x, y, color) {
floatingTexts.push({
text: text,
x: x,
y: y,
life: 1.0,
decay: 0.02,
color: color
});
}
function updateFloatingTexts(dt) {
for (var i = floatingTexts.length - 1; i >= 0; i--) {
var t = floatingTexts[i];
t.y -= 0.5 * dt;
t.life -= t.decay * dt;
if (t.life <= 0) {
floatingTexts.splice(i, 1);
}
}
}
function drawFloatingTexts() {
for (var i = 0; i < floatingTexts.length; i++) {
var t = floatingTexts[i];
ctx.globalAlpha = t.life;
ctx.fillStyle = t.color;
ctx.font = 'bold ' + Math.floor(W * 0.035) + 'px monospace';
ctx.textAlign = 'center';
ctx.fillText(t.text, t.x, t.y);
}
ctx.globalAlpha = 1;
}
// ============================================
// ウェーブクリア
// ============================================
function checkWaveClear() {
if (waveClearFlag) return;
var alive = getAliveCount();
if (alive === 0) {
waveClearFlag = true;
waveClearTime = Date.now();
enemyBullets = [];
}
}
function updateWaveClear() {
if (!waveClearFlag) return;
var now = Date.now();
if (now - waveClearTime >= WAVE_CLEAR_DURATION) {
wave++;
waveClearFlag = false;
initInvaders();
initShields();
scheduleUfo();
ufo = null;
playerBullet = null;
enemyBullets = [];
lastEnemyFireTime = now;
gameStartTime = now; // ウェーブ開始時も発射ディレイを入れる
}
}
function drawWaveClearText() {
if (!waveClearFlag) return;
var elapsed = Date.now() - waveClearTime;
var alpha = 1;
if (elapsed > WAVE_CLEAR_DURATION * 0.7) {
alpha = 1 - (elapsed - WAVE_CLEAR_DURATION * 0.7) / (WAVE_CLEAR_DURATION * 0.3);
}
ctx.globalAlpha = clamp(alpha, 0, 1);
ctx.fillStyle = '#ffffff';
ctx.font = 'bold ' + Math.floor(W * 0.08) + 'px monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('WAVE CLEAR!', W / 2, H * 0.4);
ctx.font = Math.floor(W * 0.05) + 'px monospace';
ctx.fillText('WAVE ' + (wave + 1), W / 2, H * 0.48);
ctx.globalAlpha = 1;
ctx.textBaseline = 'alphabetic';
}
// ============================================
// ウェーブ表示
// ============================================
function drawWaveNumber() {
ctx.fillStyle = 'rgba(255,255,255,0.4)';
ctx.font = Math.floor(W * 0.03) + 'px monospace';
ctx.textAlign = 'left';
ctx.fillText('WAVE ' + wave, 8, H * 0.03);
}
// ============================================
// ゲームオーバー
// ============================================
function triggerGameOver() {
gameRunning = false;
gameOverFlag = true;
finalScoreEl.textContent = score;
if (score > 0 && score >= bestScore) {
bestResultEl.textContent = 'NEW BEST!';
bestResultEl.style.color = '#33ff33';
} else {
bestResultEl.textContent = (TEXT.best || 'ベスト: ') + bestScore;
bestResultEl.style.color = '#776E65';
}
gameOverOverlay.classList.add('active');
if (window.Leaderboard) Leaderboard.show('invader', score);
}
// ============================================
// ゲーム初期化・開始
// ============================================
function startGame() {
score = 0;
wave = 1;
gameRunning = true;
gameOverFlag = false;
waveClearFlag = false;
particles = [];
floatingTexts = [];
enemyBullets = [];
playerBullet = null;
ufo = null;
lastFireTime = 0;
gameStartTime = Date.now();
lastEnemyFireTime = Date.now();
touchAutoFireTimer = 0;
scoreEl.textContent = '0';
bestScoreEl.textContent = bestScore;
gameOverOverlay.classList.remove('active');
gameStartOverlay.classList.remove('active');
resizeCanvas();
initPlayer();
initInvaders();
initShields();
scheduleUfo();
lastTime = 0;
if (animFrameId) cancelAnimationFrame(animFrameId);
animFrameId = requestAnimationFrame(gameLoop);
}
// ============================================
// ゲームループ
// ============================================
function gameLoop(timestamp) {
if (!lastTime) lastTime = timestamp;
var rawDt = timestamp - lastTime;
lastTime = timestamp;
// dtを制限 (タブ切り替え時の巨大dt防止)
var dt = Math.min(rawDt, 50) / (1000 / 60);
// 更新
updateStars(dt);
if (gameRunning && !waveClearFlag) {
updatePlayer(dt);
updateInvaders(dt);
updatePlayerBullet(dt);
updateEnemyFiring(dt);
updateEnemyBullets(dt);
updateUfo(dt);
checkInvaderShieldCollision();
checkWaveClear();
}
updateWaveClear();
updateParticles(dt);
updateFloatingTexts(dt);
// 描画
ctx.clearRect(0, 0, W, H);
ctx.fillStyle = '#0a0a1a';
ctx.fillRect(0, 0, W, H);
drawStars();
drawWaveNumber();
drawShields();
// 生きているインベーダーを描画
for (var i = 0; i < invaders.length; i++) {
if (invaders[i].alive) {
drawInvader(invaders[i]);
}
}
drawUfo();
if (gameRunning || waveClearFlag) {
drawPlayer();
}
drawPlayerBullet();
drawEnemyBullets();
drawParticles();
drawFloatingTexts();
drawWaveClearText();
animFrameId = requestAnimationFrame(gameLoop);
}
// ============================================
// 入力処理
// ============================================
function onKeyDown(e) {
if (!gameRunning) return;
switch (e.key) {
case 'ArrowLeft':
case 'a':
case 'A':
leftPressed = true;
e.preventDefault();
break;
case 'ArrowRight':
case 'd':
case 'D':
rightPressed = true;
e.preventDefault();
break;
case ' ':
spacePressed = true;
e.preventDefault();
break;
}
}
function onKeyUp(e) {
switch (e.key) {
case 'ArrowLeft':
case 'a':
case 'A':
leftPressed = false;
break;
case 'ArrowRight':
case 'd':
case 'D':
rightPressed = false;
break;
case ' ':
spacePressed = false;
break;
}
}
function getTouchSide(touch) {
var rect = canvas.getBoundingClientRect();
var x = touch.clientX - rect.left;
var mid = rect.width / 2;
return x < mid ? 'left' : 'right';
}
function onTouchStart(e) {
if (!gameRunning) return;
e.preventDefault();
if (e.touches.length > 0) {
touchSide = getTouchSide(e.touches[0]);
touchAutoFireTimer = 0;
firePlayerBullet();
}
}
function onTouchMove(e) {
if (!gameRunning) return;
e.preventDefault();
if (e.touches.length > 0) {
touchSide = getTouchSide(e.touches[0]);
}
}
function onTouchEnd(e) {
e.preventDefault();
if (e.touches.length === 0) {
touchSide = null;
touchAutoFireTimer = 0;
} else {
touchSide = getTouchSide(e.touches[0]);
}
}
// ============================================
// Visibility change (タブ非表示時にポーズ)
// ============================================
function onVisibilityChange() {
if (document.hidden) {
leftPressed = false;
rightPressed = false;
spacePressed = false;
touchSide = null;
}
}
// ============================================
// イベントリスナー登録
// ============================================
window.addEventListener('resize', resizeCanvas);
document.addEventListener('keydown', onKeyDown);
document.addEventListener('keyup', onKeyUp);
canvas.addEventListener('touchstart', onTouchStart, { passive: false });
canvas.addEventListener('touchmove', onTouchMove, { passive: false });
canvas.addEventListener('touchend', onTouchEnd, { passive: false });
document.addEventListener('visibilitychange', onVisibilityChange);
document.getElementById('btn-start').addEventListener('click', function () {
startGame();
});
document.getElementById('btn-retry').addEventListener('click', function () {
startGame();
});
document.getElementById('btn-new-game').addEventListener('click', function () {
startGame();
});
// ============================================
// 初期表示
// ============================================
bestScoreEl.textContent = bestScore;
resizeCanvas();
})();
fillRect squares and animate it with two alternating posesSave each code block above using its file name, put them all in the same folder, and open index.html in your browser — the game will run. The "Copy" button in each file-name bar makes this easy.
The live game also uses a few site-wide files (shared CSS and the header), so when you open just these files the header and other shared parts will be missing. The game itself works fine, which is all you need to study or tinker with it.
Once you feel comfortable, try changing the colors or tweaking the rules. After reading code, changing it is the best way to learn.
Yes — feel free to read, copy, run and modify this source code to learn programming. Using it for school projects is welcome too. Let it spark ideas about how you would build it.
Please don't republish or distribute the code as-is as your own game or service, though. It is meant for learning.
If reading the code makes you think "I want to build this myself!", check out the step-by-step guide for Retro Alien Shooter. While this page shows the finished code, the guide builds the game up from nothing, one step at a time.