このページでは、ひなテックGamesで実際に動いている「タワーディフェンス」のソースコードを丸ごと公開しています。ここに載っているのは説明用に書き直した特別なコードではなく、いまあなたが遊んでいるゲームそのものを動かしている本物のファイルです。
タワーディフェンスは、道を進んでくる敵をタワーで撃ち落とすゲームです。このゲームには「<canvas>に絵をかく」「敵を道に沿って動かす」「タワーがねらいを定めて弾を撃つ」「敵の波(ウェーブ)を管理する」という、本格的なゲーム作りの要素がぎっしり詰まっています。
コードはコピーして自分のパソコンで自由に動かせます。「たくさんの敵や弾がどうやって動いているんだろう?」と思ったら、解説を読みながらコードを追いかけてみてください。
タワーディフェンスは、次のファイルが役割を分担して動いています。それぞれの中身は下で1つずつ解説します。
| ファイル | 役割 |
|---|---|
index.html | ゲーム画面の骨組み。盤面・スコア表示・ボタンなどの部品をHTMLで配置する |
style.css | 見た目のデザイン。色・大きさ・レイアウト・アニメーション |
game.js | ゲームのルールを動かす頭脳。操作の受け付け・判定・スコア計算など |
この「HTMLで構造、CSSで見た目、JavaScriptで動き」という分担は、ほとんどのWebページ・Webゲームに共通する考え方です。
index.htmlは、ブラウザが最初に読み込むファイルです。ここにはゲームの「部品」をHTMLで並べているだけで、動きそのものは書かれていません。
このゲームの絵は、たった1つの<canvas>(お絵かきボード)の上にすべてかかれます。マップも敵もタワーも弾も、JavaScriptがこの1枚のキャンバスに毎フレームかき直しているのです。HTMLには他に、ウェーブ・ライフ・ゴールドを出すgame-hud、6種類のタワーを選ぶtower-select、早送りボタンのspeed-controlが並んでいます。タワーのボタンにはdata-towerで種類の名前が付けてあります。
ゲーム本体は<body>の中から始まります。
※ 下のコードでは、ゲームの動きと関係のない広告・アクセス解析用のタグ(ページ先頭の数行)を省いています。
<!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/tower-defense/">
<!-- I18N:hreflang -->
<link rel="alternate" hreflang="ja" href="https://hinata-ya.tech/games/games/tower-defense/">
<link rel="alternate" hreflang="en" href="https://hinata-ya.tech/games/en/games/tower-defense/">
<link rel="alternate" hreflang="x-default" href="https://hinata-ya.tech/games/games/tower-defense/">
<!-- /I18N:hreflang -->
<!-- OGP -->
<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/tower-defense/">
<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/tower-defense.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/tower-defense.png">
<!-- /SEO:meta-extra -->
<link rel="stylesheet" href="../../css/style.css">
<link rel="stylesheet" href="style.css?v=20260607">
<link rel="stylesheet" href="../../css/leaderboard.css">
<!-- FAQ:ld+json(手動管理。enhance-seo-games.js の SEO:ld+json マーカー外なので再生成で上書きされない) -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "このタワーディフェンスは無料で遊べますか?",
"acceptedAnswer": { "@type": "Answer", "text": "はい、完全無料です。料金や課金は一切ありません。" }
},
{
"@type": "Question",
"name": "ダウンロードやインストールは必要ですか?",
"acceptedAnswer": { "@type": "Answer", "text": "不要です。ブラウザを開くだけですぐに遊べます。" }
},
{
"@type": "Question",
"name": "スマホでも遊べますか?",
"acceptedAnswer": { "@type": "Answer", "text": "遊べます。スマホは空きマスをタップしてタワーを設置、PCはクリックで設置します。" }
},
{
"@type": "Question",
"name": "タワーはどう使い分けますか?",
"acceptedAnswer": { "@type": "Answer", "text": "スロウで足止め、ばくはつで密集処理、スナイパーで遠距離狙撃など、ウェーブに合わせて組み合わせます。" }
},
{
"@type": "Question",
"name": "スコアは保存されますか?",
"acceptedAnswer": { "@type": "Answer", "text": "ベストスコアは自動で保存され、ランキングにスコアを登録することもできます。" }
}
]
}
</script>
<!-- SEO:ld+json -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "VideoGame",
"name": "タワーディフェンス",
"description": "タワーディフェンスを無料でプレイ!ダウンロード不要・登録不要でブラウザからすぐ遊べる。タワーを設置して敵の侵攻を防ぎ、ウェーブをクリアしてハイスコアを目指そう。攻略のコツも解説。PC・スマホ対応。",
"url": "https://hinata-ya.tech/games/games/tower-defense/",
"image": "https://hinata-ya.tech/games/images/og/tower-defense.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/tower-defense/"
}
]
}
</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>
<p class="game-lead">6種類のタワーを設置して、道を進む敵の侵攻を防ぐ戦略ゲーム。<strong>ダウンロード不要・登録不要</strong>で、PCでもスマホでもブラウザからすぐ無料で遊べます。</p>
<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="game-hud">
<div class="hud-item">
<span class="hud-label">WAVE</span>
<span class="hud-value" id="wave">1</span>
</div>
<div class="hud-item">
<span class="hud-label">LIFE</span>
<span class="hud-value" id="lives">10</span>
</div>
<div class="hud-item">
<span class="hud-label">GOLD</span>
<span class="hud-value" id="gold">100</span>
</div>
</div>
<div class="tower-select">
<button class="tower-btn selected" data-tower="normal">
<span class="tower-icon" style="background:#3498DB"></span>
<span class="tower-info">ノーマル<br><small>20G</small></span>
</button>
<button class="tower-btn" data-tower="fast">
<span class="tower-icon" style="background:#2ECC71"></span>
<span class="tower-info">はやい<br><small>35G</small></span>
</button>
<button class="tower-btn" data-tower="strong">
<span class="tower-icon" style="background:#E74C3C"></span>
<span class="tower-info">つよい<br><small>50G</small></span>
</button>
<button class="tower-btn" data-tower="slow">
<span class="tower-icon" style="background:#9B59B6"></span>
<span class="tower-info">スロウ<br><small>30G</small></span>
</button>
<button class="tower-btn" data-tower="splash">
<span class="tower-icon" style="background:#F39C12"></span>
<span class="tower-info">ばくはつ<br><small>60G</small></span>
</button>
<button class="tower-btn" data-tower="sniper">
<span class="tower-icon" style="background:#1ABC9C"></span>
<span class="tower-info">スナイパー<br><small>75G</small></span>
</button>
<button class="tower-btn tower-btn-sell" data-tower="sell">
<span class="tower-icon" style="background:#999"></span>
<span class="tower-info">撤去<br><small>半額戻る</small></span>
</button>
</div>
<div class="speed-control">
<button class="speed-btn selected" data-speed="1" id="btn-speed-1">x1</button>
<button class="speed-btn" data-speed="2" id="btn-speed-2">x2</button>
<button class="speed-btn" data-speed="3" id="btn-speed-3">x3</button>
</div>
<div class="board-wrapper">
<canvas id="game-canvas"></canvas>
<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>敵がゴールに到達するとライフが減る。0になるとゲームオーバー</li>
<li>6種類のタワーを使い分けてハイスコアを目指そう!</li>
<li>設置済みタワーは「撤去」モードで壊すと半額のゴールドが戻る</li>
<li>早送りボタン(x1/x2/x3)でスピードを変えられる</li>
</ul>
</div>
<div class="game-instructions">
<h3>タワーディフェンス攻略のコツ</h3>
<ul>
<li><strong>カーブや交差点に集中</strong>:敵が長く射程内に留まる曲がり角にタワーを固めると効率よく削れる。</li>
<li><strong>序盤はノーマルで資金を貯める</strong>:いきなり高額タワーを置かず、安いタワーで稼いでから強化する。</li>
<li><strong>スロウで足止め+火力で削る</strong>:スロウタワーで遅くした敵を、つよい・スナイパーでまとめて倒す。</li>
<li><strong>ばくはつは密集に強い</strong>:敵が固まって出るウェーブには範囲攻撃のばくはつタワーが効く。</li>
<li><strong>ゴールドを使い切らない</strong>:強いウェーブの前に少し残し、増援や強化に回せるようにする。</li>
</ul>
</div>
<div class="game-instructions">
<h3>よくある質問</h3>
<ul>
<li><strong>Q. 無料で遊べますか?</strong><br>A. はい、完全無料です。料金や課金は一切ありません。</li>
<li><strong>Q. ダウンロードやインストールは必要?</strong><br>A. 不要です。ブラウザを開くだけですぐに遊べます。</li>
<li><strong>Q. スマホでも遊べますか?</strong><br>A. 遊べます。スマホは空きマスをタップしてタワーを設置、PCはクリックで設置します。</li>
<li><strong>Q. タワーはどう使い分けますか?</strong><br>A. スロウで足止め、ばくはつで密集処理、スナイパーで遠距離狙撃など、ウェーブに合わせて組み合わせます。</li>
<li><strong>Q. スコアは保存されますか?</strong><br>A. ベストスコアは自動で保存され、ランキングにスコアを登録することもできます。</li>
</ul>
</div>
<div class="game-promo">
<h3>このゲーム、自分でも作れるよ!</h3>
<p>
経路探索、タワーのAI、ウェーブ管理など<br>
ゲーム開発の面白い要素が詰まっています。<br><br>
ひなテックでは、こうしたゲームの<br>
作り方を楽しく学べます!
</p>
<div class="promo-links">
<a href="tutorial/" class="btn-primary">このゲームの作り方を見る →</a>
<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は、ゲームのまわりの見た目を作っているファイルです。ゲームの絵そのものはJavaScriptがキャンバスにかくので、このCSSはおもにキャンバスの外側 — タワー選択ボタン・HUD・早送りボタンの配置や色を整えています。
いま選んでいるタワーや速度のボタンには.selectedクラスが付き、色が変わって「これを選んでいるよ」とわかるようになっています。
スタート画面やゲームオーバー画面のgame-overlayはキャンバスの上に重ねて表示します。ファイルの下のほうの@mediaは、スマホでボタンが小さくなりすぎないよう調整する指定です。
/* ============================================
タワーディフェンス - 固有スタイル
============================================ */
/* リード文(H1直下の説明) */
.game-lead {
font-size: 0.95rem;
line-height: 1.7;
color: #555;
margin-bottom: 1.5rem;
}
/* スコアエリア */
.score-area {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.5rem;
}
.score-box {
background: #3d2b1f;
border-radius: 8px;
padding: 0.5rem 1rem;
text-align: center;
min-width: 70px;
}
.score-label {
display: block;
font-size: 0.7rem;
color: #c4a45a;
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: #8B4513;
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: #6b3410;
}
/* HUD */
.game-hud {
display: flex;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.hud-item {
background: #2d5a27;
border-radius: 6px;
padding: 0.3rem 0.7rem;
text-align: center;
flex: 1;
}
.hud-label {
display: block;
font-size: 0.6rem;
color: #a0d48a;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.hud-value {
display: block;
font-size: 1rem;
font-weight: 700;
color: #fff;
}
/* タワー選択 */
.tower-select {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
margin-bottom: 0.5rem;
}
.tower-btn {
display: flex;
align-items: center;
gap: 0.3rem;
flex: 1 1 calc(25% - 0.35rem);
min-width: 0;
background: #444;
border: 2px solid transparent;
border-radius: 6px;
padding: 0.3rem 0.4rem;
cursor: pointer;
transition: border-color 0.2s, background 0.2s;
color: #ccc;
}
.tower-btn:hover {
background: #555;
}
.tower-btn.selected {
border-color: #F1C40F;
background: #555;
}
.tower-btn-sell.selected {
border-color: #E74C3C;
}
.tower-icon {
display: block;
width: 16px;
height: 16px;
border-radius: 50%;
flex-shrink: 0;
}
.tower-info {
font-size: 0.6rem;
font-weight: 600;
line-height: 1.2;
text-align: left;
white-space: nowrap;
overflow: hidden;
}
.tower-info small {
font-weight: 400;
color: #F1C40F;
}
.tower-btn-sell .tower-info small {
color: #E74C3C;
}
/* 速度コントロール */
.speed-control {
display: flex;
gap: 0.35rem;
margin-bottom: 0.75rem;
}
.speed-btn {
flex: 1;
background: #444;
color: #ccc;
border: 2px solid transparent;
border-radius: 6px;
padding: 0.3rem;
font-size: 0.75rem;
font-weight: 700;
cursor: pointer;
transition: border-color 0.2s, background 0.2s;
}
.speed-btn:hover {
background: #555;
}
.speed-btn.selected {
border-color: #F1C40F;
background: #555;
color: #F1C40F;
}
/* ゲームボード */
.board-wrapper {
position: relative;
width: 100%;
max-width: 600px;
margin: 0 auto;
aspect-ratio: 12 / 9;
touch-action: none;
}
#game-canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
background: #2d5a27;
border-radius: 8px;
display: block;
}
/* ゲームオーバーレイ */
.game-overlay {
position: absolute;
inset: 0;
background: rgba(30, 30, 20, 0.88);
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: #c4a45a;
margin-bottom: 0.5rem;
}
.overlay-content p {
font-size: 1.1rem;
color: #d4c9a0;
margin-bottom: 0.75rem;
}
.overlay-content .best-result {
font-size: 0.95rem;
color: #F1C40F;
font-weight: 600;
margin-bottom: 1rem;
}
.overlay-content .btn-primary {
margin-bottom: 0.5rem;
}
/* レスポンシブ */
@media (max-width: 500px) {
.board-wrapper {
border-radius: 6px;
}
#game-canvas {
border-radius: 6px;
}
.tower-btn {
flex: 1 1 calc(25% - 0.35rem);
}
}
@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;
}
.tower-btn {
padding: 0.25rem 0.3rem;
}
.tower-info {
font-size: 0.55rem;
}
.tower-icon {
width: 14px;
height: 14px;
}
}
game.jsがこのゲームの心臓部です。少し長いですが、やっていることを順番に追えば必ず理解できます。全体は(function () { ... })();という「即時実行関数」で囲まれています。
このゲームの土台はMAPという2次元配列です。数字の0が空きマス、1が道、2がスタート、3がゴールを表しています。そしてPATHという配列に、スタートからゴールまで道のマスを順番に並べてあります。敵はこのPATHをpathIndexという番号で1つずつたどって進みます。次のマスへの向き(dx・dy)を求めて少しずつ近づける、というのが敵の移動の仕組みです。
このゲームのいちばんの中心はgameLoop()です。requestAnimationFrameを使って自分をくり返し呼び出し、毎回update()(計算)とdraw()(お絵かき)の2つを行います。早送りのときはupdate()をgameSpeed回まとめて動かすので、絵はそのままで時間だけが速く進みます。
update()の中ではたくさんのことが起きています。タワーは射程内の敵をさがし、いちばんゴールに近い敵をねらってbullets配列に弾を1発追加します。Math.atan2()で砲身を敵の方へ向けているのも芸が細かいところです。弾が敵に当たるとhitEnemy()がHPを減らし、HPが0になると敵を消してゴールドとスコアを増やします。splash(ばくはつ)タワーは当たった敵のまわりにもダメージを与え、slow(スロウ)タワーは敵のspeedを一時的に遅くします。particlesはヒットや撃破のときに飛び散る小さな粒の演出です。
敵の波はstartWave()が管理します。ウェーブが進むほど敵のHPと数が増え、5の倍数のウェーブには大きくて固いボスが出てきます。TOWER_TYPESには6種類のタワーの値段・射程・攻撃力・連射速度がまとめて書いてあり、ここを変えるだけでゲームバランスを調整できます。ベストスコアはlocalStorageに保存されます。
// ============================================
// タワーディフェンス ゲームロジック
// ============================================
(function () {
'use strict';
// 言語別テキスト(英語版ページが window.GAME_TEXT を定義して上書きする。docs/i18n.md 参照)
var TEXT = window.GAME_TEXT || {};
// ============================================
// マップ定義 (0=空, 1=道, 2=スタート, 3=ゴール)
// ============================================
var MAP_COLS = 12;
var MAP_ROWS = 9;
var MAP = [
[0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0],
[0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0],
[0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0],
[0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0]
];
var PATH = [
{r:0,c:2},{r:1,c:2},{r:2,c:2},{r:3,c:2},{r:3,c:3},{r:3,c:4},{r:3,c:5},
{r:2,c:5},{r:1,c:5},{r:1,c:6},{r:1,c:7},{r:1,c:8},{r:1,c:9},
{r:2,c:9},{r:3,c:9},{r:4,c:9},{r:5,c:9},{r:6,c:9},{r:7,c:9},
{r:7,c:8},{r:7,c:7},{r:7,c:6},{r:6,c:6},{r:5,c:6},{r:5,c:5},{r:5,c:4},{r:5,c:3},{r:5,c:2},
{r:6,c:2},{r:7,c:2},{r:8,c:2}
];
// ============================================
// タワー定義(6種類)
// ============================================
var TOWER_TYPES = {
normal: { cost: 20, range: 2.2, damage: 1, fireRate: 50, color: '#3498DB', bulletColor: '#85C1E9', name: 'ノーマル', splash: 0, slow: 0 },
fast: { cost: 35, range: 1.8, damage: 0.6, fireRate: 18, color: '#2ECC71', bulletColor: '#ABEBC6', name: 'はやい', splash: 0, slow: 0 },
strong: { cost: 50, range: 2.5, damage: 3.5, fireRate: 90, color: '#E74C3C', bulletColor: '#F5B7B1', name: 'つよい', splash: 0, slow: 0 },
slow: { cost: 30, range: 2.0, damage: 0.3, fireRate: 40, color: '#9B59B6', bulletColor: '#D2B4DE', name: 'スロウ', splash: 0, slow: 0.5 },
splash: { cost: 60, range: 2.0, damage: 1.5, fireRate: 70, color: '#F39C12', bulletColor: '#F9E79F', name: 'ばくはつ', splash: 1.2, slow: 0 },
sniper: { cost: 75, range: 4.0, damage: 8, fireRate: 140, color: '#1ABC9C', bulletColor: '#A3E4D7', name: 'スナイパー', splash: 0, slow: 0 }
};
// ============================================
// ゲーム定数
// ============================================
var INITIAL_GOLD = 100;
var INITIAL_LIVES = 10;
var GOLD_PER_KILL_BASE = 5;
var ENEMY_BASE_HP = 3;
var ENEMY_BASE_SPEED = 0.015;
var ENEMIES_PER_WAVE_BASE = 6;
var SPAWN_DELAY = 30;
// ============================================
// 状態変数
// ============================================
var canvas, ctx;
var canvasW, canvasH, cellW, cellH;
var towers = [];
var enemies = [];
var bullets = [];
var particles = [];
var gold, lives, wave, score;
var bestScore = parseInt(localStorage.getItem('bestTowerDefense') || '0', 10);
var gameRunning = false;
var gameOverFlag = false;
var animFrameId = null;
var selectedTower = 'normal';
var waveInProgress = false;
var enemiesToSpawn = 0;
var spawnTimer = 0;
var waveDelay = 0;
var frameCount = 0;
var gameSpeed = 1;
// ============================================
// DOM要素
// ============================================
var scoreEl = document.getElementById('score');
var bestScoreEl = document.getElementById('best-score');
var waveEl = document.getElementById('wave');
var livesEl = document.getElementById('lives');
var goldEl = document.getElementById('gold');
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();
var dpr = window.devicePixelRatio || 1;
canvasW = rect.width;
canvasH = rect.height;
cellW = canvasW / MAP_COLS;
cellH = canvasH / MAP_ROWS;
canvas.width = canvasW * dpr;
canvas.height = canvasH * dpr;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
// ============================================
// ユーティリティ
// ============================================
function cellCenter(r, c) {
return { x: (c + 0.5) * cellW, y: (r + 0.5) * cellH };
}
function dist(x1, y1, x2, y2) {
var dx = x1 - x2;
var dy = y1 - y2;
return Math.sqrt(dx * dx + dy * dy);
}
function findTowerAt(r, c) {
for (var i = 0; i < towers.length; i++) {
if (towers[i].r === r && towers[i].c === c) return i;
}
return -1;
}
// ============================================
// 敵の生成
// ============================================
function createEnemy() {
var hp = ENEMY_BASE_HP + wave * 1.5;
var speed = ENEMY_BASE_SPEED + wave * 0.001;
var start = cellCenter(PATH[0].r, PATH[0].c);
var isBoss = (wave % 5 === 0) && enemiesToSpawn === 1;
if (isBoss) {
hp *= 5;
speed *= 0.6;
}
return {
x: start.x,
y: start.y,
pathIndex: 0,
hp: hp,
maxHp: hp,
speed: speed,
baseSpeed: speed,
radius: isBoss ? Math.min(cellW, cellH) * 0.4 : Math.min(cellW, cellH) * 0.25,
isBoss: isBoss,
color: isBoss ? '#8E44AD' : '#E67E22',
slowTimer: 0
};
}
// ============================================
// タワー設置・撤去
// ============================================
function placeTower(r, c) {
if (r < 0 || r >= MAP_ROWS || c < 0 || c >= MAP_COLS) return;
// 撤去モード
if (selectedTower === 'sell') {
var idx = findTowerAt(r, c);
if (idx !== -1) {
var refund = Math.floor(TOWER_TYPES[towers[idx].type].cost / 2);
gold += refund;
goldEl.textContent = gold;
towers.splice(idx, 1);
}
return;
}
if (MAP[r][c] !== 0) return;
if (findTowerAt(r, c) !== -1) return;
var type = TOWER_TYPES[selectedTower];
if (gold < type.cost) return;
gold -= type.cost;
goldEl.textContent = gold;
var pos = cellCenter(r, c);
towers.push({
r: r,
c: c,
x: pos.x,
y: pos.y,
type: selectedTower,
range: type.range * cellW,
damage: type.damage,
fireRate: type.fireRate,
color: type.color,
bulletColor: type.bulletColor,
splash: type.splash,
slow: type.slow,
cooldown: 0,
angle: 0
});
}
// ============================================
// 更新
// ============================================
function update() {
if (!gameRunning || gameOverFlag) return;
frameCount++;
// ウェーブ間の待ち
if (waveDelay > 0) {
waveDelay--;
if (waveDelay === 0) {
startWave();
}
updateParticles();
return;
}
// 敵のスポーン
if (enemiesToSpawn > 0) {
spawnTimer--;
if (spawnTimer <= 0) {
enemies.push(createEnemy());
enemiesToSpawn--;
spawnTimer = SPAWN_DELAY;
}
}
// 敵の移動
for (var i = enemies.length - 1; i >= 0; i--) {
var e = enemies[i];
// スロウタイマー
if (e.slowTimer > 0) {
e.slowTimer--;
if (e.slowTimer <= 0) {
e.speed = e.baseSpeed;
}
}
if (e.pathIndex >= PATH.length - 1) {
lives--;
livesEl.textContent = lives;
enemies.splice(i, 1);
if (lives <= 0) {
gameOver();
return;
}
continue;
}
var target = cellCenter(PATH[e.pathIndex + 1].r, PATH[e.pathIndex + 1].c);
var dx = target.x - e.x;
var dy = target.y - e.y;
var d = Math.sqrt(dx * dx + dy * dy);
if (d < 2) {
e.pathIndex++;
e.x = target.x;
e.y = target.y;
} else {
var moveSpeed = e.speed * cellW;
e.x += (dx / d) * moveSpeed;
e.y += (dy / d) * moveSpeed;
}
}
// タワーの攻撃
for (var t = 0; t < towers.length; t++) {
var tower = towers[t];
tower.cooldown--;
var bestTarget = null;
var bestProgress = -1;
for (var ei = 0; ei < enemies.length; ei++) {
var en = enemies[ei];
var d = dist(tower.x, tower.y, en.x, en.y);
if (d <= tower.range && en.pathIndex > bestProgress) {
bestProgress = en.pathIndex;
bestTarget = en;
}
}
if (bestTarget) {
tower.angle = Math.atan2(bestTarget.y - tower.y, bestTarget.x - tower.x);
if (tower.cooldown <= 0) {
tower.cooldown = tower.fireRate;
bullets.push({
x: tower.x,
y: tower.y,
tx: bestTarget.x,
ty: bestTarget.y,
target: bestTarget,
damage: tower.damage,
splash: tower.splash * cellW,
slow: tower.slow,
speed: 5,
color: tower.bulletColor
});
}
}
}
// 弾の移動
for (var bi = bullets.length - 1; bi >= 0; bi--) {
var b = bullets[bi];
var tx = b.target ? b.target.x : b.tx;
var ty = b.target ? b.target.y : b.ty;
var dx = tx - b.x;
var dy = ty - b.y;
var d = Math.sqrt(dx * dx + dy * dy);
if (d < 5) {
if (b.target && b.target.hp > 0) {
hitEnemy(b.target, b);
// スプラッシュダメージ
if (b.splash > 0) {
for (var si = enemies.length - 1; si >= 0; si--) {
if (enemies[si] === b.target) continue;
if (dist(b.target.x, b.target.y, enemies[si].x, enemies[si].y) < b.splash) {
hitEnemy(enemies[si], { damage: b.damage * 0.5, slow: b.slow, color: b.color, splash: 0 });
}
}
// スプラッシュエフェクト
for (var sp = 0; sp < 12; sp++) {
var angle = (sp / 12) * Math.PI * 2;
particles.push({
x: b.target.x, y: b.target.y,
vx: Math.cos(angle) * 3,
vy: Math.sin(angle) * 3,
life: 15,
color: '#F39C12'
});
}
}
}
bullets.splice(bi, 1);
} else {
b.x += (dx / d) * b.speed;
b.y += (dy / d) * b.speed;
}
}
updateParticles();
// ウェーブ完了チェック
if (waveInProgress && enemiesToSpawn === 0 && enemies.length === 0) {
waveInProgress = false;
wave++;
waveEl.textContent = wave;
waveDelay = 90;
}
}
function hitEnemy(enemy, bullet) {
enemy.hp -= bullet.damage;
// スロウ適用
if (bullet.slow > 0) {
enemy.speed = enemy.baseSpeed * (1 - bullet.slow);
enemy.slowTimer = 90;
}
// ヒットパーティクル
for (var pi = 0; pi < 3; pi++) {
particles.push({
x: enemy.x, y: enemy.y,
vx: (Math.random() - 0.5) * 3,
vy: (Math.random() - 0.5) * 3,
life: 15,
color: bullet.color
});
}
if (enemy.hp <= 0) {
var killGold = GOLD_PER_KILL_BASE + Math.floor(wave * 0.5);
if (enemy.isBoss) killGold *= 3;
gold += killGold;
goldEl.textContent = gold;
var killScore = enemy.isBoss ? 50 + wave * 5 : 10 + wave;
score += killScore;
updateScore();
for (var pi2 = 0; pi2 < 8; pi2++) {
particles.push({
x: enemy.x, y: enemy.y,
vx: (Math.random() - 0.5) * 5,
vy: (Math.random() - 0.5) * 5,
life: 25,
color: enemy.color
});
}
var idx = enemies.indexOf(enemy);
if (idx !== -1) enemies.splice(idx, 1);
}
}
function updateParticles() {
for (var i = particles.length - 1; i >= 0; i--) {
var p = particles[i];
p.x += p.vx;
p.y += p.vy;
p.life--;
if (p.life <= 0) particles.splice(i, 1);
}
}
function startWave() {
waveInProgress = true;
enemiesToSpawn = ENEMIES_PER_WAVE_BASE + Math.floor(wave * 1.5);
spawnTimer = 0;
}
// ============================================
// 描画
// ============================================
function draw() {
ctx.clearRect(0, 0, canvasW, canvasH);
drawMap();
drawTowers();
drawEnemies();
drawBullets();
drawParticles();
if (waveDelay > 0 && gameRunning) {
ctx.save();
ctx.fillStyle = '#fff';
ctx.font = 'bold ' + (cellH * 0.6) + 'px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.globalAlpha = 0.6;
ctx.fillText('WAVE ' + wave, canvasW / 2, canvasH / 2);
ctx.restore();
}
// 早送り表示
if (gameSpeed > 1 && gameRunning) {
ctx.save();
ctx.fillStyle = '#F1C40F';
ctx.font = 'bold ' + (cellH * 0.35) + 'px sans-serif';
ctx.textAlign = 'right';
ctx.textBaseline = 'top';
ctx.globalAlpha = 0.5;
ctx.fillText('x' + gameSpeed, canvasW - 6, 4);
ctx.restore();
}
}
function drawMap() {
for (var r = 0; r < MAP_ROWS; r++) {
for (var c = 0; c < MAP_COLS; c++) {
var x = c * cellW;
var y = r * cellH;
var tile = MAP[r][c];
if (tile === 0) {
ctx.fillStyle = '#2d5a27';
ctx.fillRect(x, y, cellW, cellH);
ctx.fillStyle = 'rgba(0,0,0,0.08)';
if ((r + c) % 2 === 0) ctx.fillRect(x, y, cellW, cellH);
} else if (tile === 1) {
ctx.fillStyle = '#c4a45a';
ctx.fillRect(x, y, cellW, cellH);
ctx.fillStyle = 'rgba(0,0,0,0.05)';
if ((r + c) % 2 === 0) ctx.fillRect(x, y, cellW, cellH);
} else if (tile === 2) {
ctx.fillStyle = '#3498DB';
ctx.fillRect(x, y, cellW, cellH);
ctx.fillStyle = '#fff';
ctx.font = 'bold ' + (cellH * 0.35) + 'px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('START', x + cellW / 2, y + cellH / 2);
} else if (tile === 3) {
ctx.fillStyle = '#E74C3C';
ctx.fillRect(x, y, cellW, cellH);
ctx.fillStyle = '#fff';
ctx.font = 'bold ' + (cellH * 0.35) + 'px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('GOAL', x + cellW / 2, y + cellH / 2);
}
ctx.strokeStyle = 'rgba(0,0,0,0.15)';
ctx.lineWidth = 0.5;
ctx.strokeRect(x, y, cellW, cellH);
}
}
// 撤去モード中はタワーに×マークを表示
if (selectedTower === 'sell') {
for (var i = 0; i < towers.length; i++) {
var t = towers[i];
ctx.save();
ctx.fillStyle = 'rgba(231, 76, 60, 0.3)';
ctx.fillRect(t.x - cellW / 2, t.y - cellH / 2, cellW, cellH);
ctx.strokeStyle = '#E74C3C';
ctx.lineWidth = 2;
ctx.globalAlpha = 0.7;
var s = Math.min(cellW, cellH) * 0.2;
ctx.beginPath();
ctx.moveTo(t.x - s, t.y - s);
ctx.lineTo(t.x + s, t.y + s);
ctx.moveTo(t.x + s, t.y - s);
ctx.lineTo(t.x - s, t.y + s);
ctx.stroke();
ctx.restore();
}
}
}
function drawTowers() {
for (var i = 0; i < towers.length; i++) {
var t = towers[i];
// 台座
ctx.fillStyle = '#555';
ctx.fillRect(t.x - cellW * 0.35, t.y - cellH * 0.35, cellW * 0.7, cellH * 0.7);
// タワー本体
ctx.fillStyle = t.color;
ctx.beginPath();
ctx.arc(t.x, t.y, Math.min(cellW, cellH) * 0.28, 0, Math.PI * 2);
ctx.fill();
// 砲身
ctx.save();
ctx.translate(t.x, t.y);
ctx.rotate(t.angle);
ctx.fillStyle = '#333';
var barrelLen = t.type === 'sniper' ? 0.45 : 0.35;
ctx.fillRect(0, -2, Math.min(cellW, cellH) * barrelLen, 4);
ctx.restore();
// レンジ
ctx.beginPath();
ctx.arc(t.x, t.y, t.range, 0, Math.PI * 2);
ctx.strokeStyle = t.color;
ctx.globalAlpha = 0.08;
ctx.lineWidth = 1;
ctx.stroke();
ctx.globalAlpha = 1;
}
}
function drawEnemies() {
for (var i = 0; i < enemies.length; i++) {
var e = enemies[i];
ctx.beginPath();
ctx.arc(e.x, e.y, e.radius, 0, Math.PI * 2);
ctx.fillStyle = e.color;
ctx.fill();
ctx.strokeStyle = 'rgba(0,0,0,0.3)';
ctx.lineWidth = 1;
ctx.stroke();
// スロウ中の表示
if (e.slowTimer > 0) {
ctx.beginPath();
ctx.arc(e.x, e.y, e.radius + 2, 0, Math.PI * 2);
ctx.strokeStyle = '#9B59B6';
ctx.lineWidth = 1.5;
ctx.globalAlpha = 0.6;
ctx.stroke();
ctx.globalAlpha = 1;
}
if (e.isBoss) {
ctx.fillStyle = '#fff';
ctx.font = 'bold ' + (e.radius * 0.8) + 'px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('B', e.x, e.y);
}
// HPバー
var barW = e.radius * 2;
var barH = 3;
var barX = e.x - barW / 2;
var barY = e.y - e.radius - 6;
ctx.fillStyle = 'rgba(0,0,0,0.4)';
ctx.fillRect(barX, barY, barW, barH);
var hpRatio = Math.max(0, e.hp / e.maxHp);
ctx.fillStyle = hpRatio > 0.5 ? '#2ECC71' : hpRatio > 0.25 ? '#F1C40F' : '#E74C3C';
ctx.fillRect(barX, barY, barW * hpRatio, barH);
}
}
function drawBullets() {
for (var i = 0; i < bullets.length; i++) {
var b = bullets[i];
ctx.beginPath();
ctx.arc(b.x, b.y, b.splash > 0 ? 4 : 3, 0, Math.PI * 2);
ctx.fillStyle = b.color;
ctx.fill();
}
}
function drawParticles() {
for (var i = 0; i < particles.length; i++) {
var p = particles[i];
ctx.beginPath();
ctx.arc(p.x, p.y, 2, 0, Math.PI * 2);
ctx.fillStyle = p.color;
ctx.globalAlpha = p.life / 25;
ctx.fill();
ctx.globalAlpha = 1;
}
}
// ============================================
// スコア
// ============================================
function updateScore() {
scoreEl.textContent = score;
if (score > bestScore) {
bestScore = score;
bestScoreEl.textContent = bestScore;
localStorage.setItem('bestTowerDefense', bestScore.toString());
}
}
// ============================================
// ゲーム開始
// ============================================
function startGame() {
resizeCanvas();
towers = [];
enemies = [];
bullets = [];
particles = [];
gold = INITIAL_GOLD;
lives = INITIAL_LIVES;
wave = 1;
score = 0;
gameOverFlag = false;
waveInProgress = false;
enemiesToSpawn = 0;
frameCount = 0;
gameSpeed = 1;
scoreEl.textContent = score;
goldEl.textContent = gold;
livesEl.textContent = lives;
waveEl.textContent = wave;
// 速度ボタンリセット
var speedBtns = document.querySelectorAll('.speed-btn');
for (var i = 0; i < speedBtns.length; i++) {
speedBtns[i].classList.remove('selected');
}
document.getElementById('btn-speed-1').classList.add('selected');
gameOverOverlay.classList.remove('active');
gameStartOverlay.classList.remove('active');
gameRunning = true;
waveDelay = 60;
if (animFrameId) cancelAnimationFrame(animFrameId);
gameLoop();
}
// ============================================
// ゲームオーバー
// ============================================
function gameOver() {
gameRunning = false;
gameOverFlag = true;
if (animFrameId) {
cancelAnimationFrame(animFrameId);
animFrameId = null;
}
finalScoreEl.textContent = score;
if (score >= bestScore && score > 0) {
bestResultEl.textContent = TEXT.newBest || 'ハイスコア更新!';
} else {
bestResultEl.textContent = (TEXT.best || 'ベスト: ') + bestScore;
}
setTimeout(function () {
gameOverOverlay.classList.add('active');
if (window.Leaderboard) Leaderboard.show('tower-defense', score);
}, 300);
}
// ============================================
// ゲームループ(早送り対応)
// ============================================
function gameLoop() {
for (var s = 0; s < gameSpeed; s++) {
update();
if (!gameRunning || gameOverFlag) break;
}
draw();
if (gameRunning && !gameOverFlag) {
animFrameId = requestAnimationFrame(gameLoop);
}
}
// ============================================
// 入力処理
// ============================================
function handleClick(px, py) {
if (!gameRunning) return;
var c = Math.floor(px / cellW);
var r = Math.floor(py / cellH);
placeTower(r, c);
}
canvas.addEventListener('click', function (e) {
var rect = canvas.getBoundingClientRect();
handleClick(e.clientX - rect.left, e.clientY - rect.top);
});
canvas.addEventListener('touchstart', function (e) {
e.preventDefault();
if (!gameRunning) return;
var rect = canvas.getBoundingClientRect();
handleClick(e.touches[0].clientX - rect.left, e.touches[0].clientY - rect.top);
}, { passive: false });
// タワー選択ボタン
var towerBtns = document.querySelectorAll('.tower-btn');
for (var i = 0; i < towerBtns.length; i++) {
towerBtns[i].addEventListener('click', function () {
for (var j = 0; j < towerBtns.length; j++) {
towerBtns[j].classList.remove('selected');
}
this.classList.add('selected');
selectedTower = this.getAttribute('data-tower');
});
}
// 速度ボタン
var speedBtns = document.querySelectorAll('.speed-btn');
for (var si = 0; si < speedBtns.length; si++) {
speedBtns[si].addEventListener('click', function () {
for (var j = 0; j < speedBtns.length; j++) {
speedBtns[j].classList.remove('selected');
}
this.classList.add('selected');
gameSpeed = parseInt(this.getAttribute('data-speed'), 10);
});
}
document.getElementById('btn-new-game').addEventListener('click', function () { startGame(); });
document.getElementById('btn-retry').addEventListener('click', function () { startGame(); });
document.getElementById('btn-start').addEventListener('click', function () { startGame(); });
var resizeTimer;
window.addEventListener('resize', function () {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(function () {
resizeCanvas();
for (var i = 0; i < towers.length; i++) {
var pos = cellCenter(towers[i].r, towers[i].c);
towers[i].x = pos.x;
towers[i].y = pos.y;
towers[i].range = TOWER_TYPES[towers[i].type].range * cellW;
}
if (!gameRunning) draw();
}, 100);
});
document.addEventListener('visibilitychange', function () {
if (document.hidden && gameRunning) {
if (animFrameId) {
cancelAnimationFrame(animFrameId);
animFrameId = null;
}
gameRunning = false;
}
});
// ============================================
// 初期化
// ============================================
bestScoreEl.textContent = bestScore;
resizeCanvas();
draw();
})();
PATHを番号でたどって敵を進めるTOWER_TYPESに性能をまとめ、数値を変えるだけで調整できる上のコードをそれぞれのファイル名で同じフォルダに保存し、index.htmlをブラウザで開けばゲームが動きます。各コードブロックのファイル名バーにある「コピー」ボタンを使うと便利です。
このゲームはサイト全体で共通して使うファイル(共通CSSやヘッダー)も少し利用しているため、ソースのファイルだけで開くとヘッダーなどサイト共通の部分は表示されません。でもゲーム本体はちゃんと動くので、仕組みを学んだり改造してためすには十分です。
慣れてきたら、色を変えたりルールを少しいじったり、自由に改造してみてください。コードを「読む」次は「いじってみる」のがいちばんの上達法です。
このソースコードは、プログラミングの勉強のために自由に読んだり、コピーして動かしたり、改造したりして大丈夫です。学校の授業や自由研究で参考にするのも歓迎します。ぜひ「自分だったらどう作るか」を考えるきっかけにしてください。
ただし、コードをそのまままるごとコピーして自分の作品やサービスとして公開・配布するのはご遠慮ください。あくまで学習用としてお使いください。
コードを読んで「自分でも作ってみたい!」と思ったら、タワーディフェンスの作り方ガイドがおすすめです。このページが「完成したコード」を見せるのに対して、作り方ガイドは何もない状態から1ステップずつ組み立てていく流れを解説しています。
ひなテックでは、こうしたゲームづくりを先生といっしょに楽しく学べる教室を開いています。「コードを読むだけじゃなく、ちゃんと作れるようになりたい」という人は、体験レッスンにぜひ来てみてください。