🎮 ブラウザで遊べる！JavaScript×Canvas で作る「ラク子のサイト防衛戦」タワーディフェンスゲーム開発入門 💻

# ブラウザで遊べる！タワーディフェンスゲーム「ラク子のサイト防衛戦」の作り方 🎮

こんにちは、@YushiYamamotoです！今回は、JavaScriptとHTML5を使って、ブラウザ上で動作するタワーディフェンスゲーム「ラク子のサイト防衛戦」の作り方について解説します。このゲームはWEBサイトをサイバー攻撃から守るという面白いテーマで、プログラミング初心者の方でも理解しやすいように丁寧に説明していきます。

<p class="codepen" data-height="729" data-default-tab="result" data-slug-hash="mydwZBO" data-pen-title="ラク子のサイト防衛戦" data-editable="true" data-user="yamamotoyushi" style="height: 729px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/yamamotoyushi/pen/mydwZBO">
  ラク子のサイト防衛戦</a> by Yushi Yamamoto (<a href="https://codepen.io/yamamotoyushi">@yamamotoyushi</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://public.codepenassets.com/embed/index.js"></script>

## タワーディフェンスゲームとは？ 🤔

タワーディフェンスゲームは、自分の陣地を守るために工夫を凝らすことを目的とするゲームです。敵の大群から陣地を守るため、砲台や防御設備を配置して敵の進撃を阻むというのが基本的な流れです。

一般的なタワーディフェンスゲームの流れは以下の通りです：

1. **ユニット設置フェーズ** - 防御設備を配置する
2. **戦闘ウェーブ開始** - 敵が波状攻撃してくる
3. **ウェーブ間の強化** - 次の波に備えて設備を補充・強化する

今回作る「ラク子のサイト防衛戦」では、ラク子さんが作成したWEBサイトをサイバー攻撃から守るというテーマで、ファイアウォールやセキュリティプログラムなどを配置して、ウイルスやハッカーの攻撃から大切なサイトを守ります。

## 開発環境の準備 🛠️

まずは開発環境を準備しましょう。今回はシンプルに以下のファイル構成で進めます：

```
project/
├── index.html
├── style.css
└── game.js
```

特別なライブラリやフレームワークは使わず、純粋なJavaScript、HTML5、CSSのみで実装します。

## HTML構造の作成 📝

まずは基本的なHTML構造を作成しましょう。`index.html`ファイルに以下のコードを記述します：

```html
    <div id="gameContainer">
        <canvas id="gameCanvas" width="800" height="600"></canvas>
        <div id="ui">
            <div>ライフ: <span id="lives">10</span></div>
            <div>リソース: <span id="resources">100</span></div>
            <div>ウェーブ: <span id="wave">1</span>/10</div>
        </div>
        <div id="towerButtons">
            <button id="firewall">ファイアウォール (25)</button>
            <button id="security">セキュリティプログラム (50)</button>
            <button id="backup">バックアップシステム (75)</button>
            <button id="startWave">ウェーブ開始</button>
        </div>
    </div>
```

このHTMLでは、ゲームのキャンバス要素と、UIコントロール（ライフ、リソース、ウェーブ表示）、タワー配置ボタンを定義しています。

## CSSでゲームの見た目を整える 🎨

次に、`style.css`ファイルでゲームの見た目を整えます：

```css
body {
    margin: 0;
    padding: 0;
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
    background-color: #f0f0f0;
    font-family: 'Arial', sans-serif;
}

#gameContainer {
    position: relative;
    width: 800px;
    height: 600px;
}

canvas {
    border: 2px solid #333;
    background-color: #fff;
}

#ui {
    position: absolute;
    top: 10px;
    left: 10px;
    color: #fff;
    background-color: rgba(0, 0, 0, 0.5);
    padding: 10px;
    border-radius: 5px;
}

#towerButtons {
    position: absolute;
    bottom: 10px;
    left: 50%;
    transform: translateX(-50%);
    display: flex;
    gap: 10px;
    background-color: rgba(0, 0, 0, 0.5);
    padding: 10px;
    border-radius: 5px;
}

button {
    padding: 8px 12px;
    cursor: pointer;
    border: none;
    border-radius: 4px;
    background-color: #4CAF50;
    color: white;
    font-weight: bold;
}

button:hover {
    background-color: #45a049;
}
```

## ゲームロジックの実装 💻

最後に、`game.js`ファイルでゲームのロジックを実装します。以下は基本的な構造です：

```javascript
// ゲームの状態を管理するクラス
class GameState {
    constructor() {
        this.lives = 10;
        this.resources = 100;
        this.wave = 1;
        this.isWaveActive = false;
        this.towers = [];
        this.enemies = [];
        this.projectiles = [];
        this.path = this.generatePath();
        this.selectedTower = null;
        this.rakukoImage = new Image();
        this.rakukoImage.src = 'https://rakuraku-site.prodouga.com/img/RakuRaku.avif';
        this.enemyTypes = {
            virus: { hp: 50, speed: 1, reward: 10, color: 'green', damage: 1 },
            hacker: { hp: 30, speed: 2, reward: 15, color: 'red', damage: 2 },
            ddos: { hp: 100, speed: 0.5, reward: 20, color: 'purple', damage: 3 }
        };
        this.towerTypes = {
            firewall: { cost: 25, range: 100, damage: 10, fireRate: 1, color: 'blue' },
            security: { cost: 50, range: 150, damage: 20, fireRate: 0.8, color: 'orange' },
            backup: { cost: 75, range: 200, damage: 5, fireRate: 2, color: 'cyan' }
        };
    }

    // 敵の進行パスを生成
    generatePath() {
        return [
            { x: 0, y: 300 },
            { x: 200, y: 300 },
            { x: 200, y: 150 },
            { x: 400, y: 150 },
            { x: 400, y: 450 },
            { x: 600, y: 450 },
            { x: 600, y: 300 },
            { x: 800, y: 300 }
        ];
    }

    // ウェーブを開始する
    startWave() {
        if (this.isWaveActive) return;
        
        this.isWaveActive = true;
        const enemiesCount = 5 + this.wave * 2;
        
        // 敵を生成
        let delay = 0;
        for (let i = 0; i  {
                const types = ['virus', 'hacker', 'ddos'];
                const randomType = types[Math.floor(Math.random() * types.length)];
                this.spawnEnemy(randomType);
            }, delay);
            delay += 1000; // 1秒ごとに敵を生成
        }
        
        // ウェーブ終了チェック
        const checkWaveEnd = setInterval(() => {
            if (this.enemies.length === 0 && this.isWaveActive) {
                this.isWaveActive = false;
                this.wave++;
                this.resources += 50; // ウェーブクリアボーナス
                updateUI();
                clearInterval(checkWaveEnd);
                
                if (this.wave > 10) {
                    alert('ゲームクリア！おめでとうございます！');
                    this.reset();
                }
            }
        }, 1000);
    }

    // 敵を生成する
    spawnEnemy(type) {
        const enemyType = this.enemyTypes[type];
        const enemy = {
            x: this.path[0].x,
            y: this.path[0].y,
            hp: enemyType.hp * (1 + (this.wave - 1) * 0.2), // ウェーブごとに強化
            maxHp: enemyType.hp * (1 + (this.wave - 1) * 0.2),
            speed: enemyType.speed,
            reward: enemyType.reward,
            color: enemyType.color,
            damage: enemyType.damage,
            pathIndex: 0
        };
        this.enemies.push(enemy);
    }

    // タワーを配置する
    placeTower(type, x, y) {
        const towerType = this.towerTypes[type];
        if (this.resources  1) {
            xx = x2;
            yy = y2;
        } else {
            xx = x1 + param * C;
            yy = y1 + param * D;
        }
        
        const dx = x - xx;
        const dy = y - yy;
        
        return Math.sqrt(dx * dx + dy * dy);
    }

    // ゲーム状態の更新
    update(deltaTime) {
        // 敵の移動
        for (let i = this.enemies.length - 1; i >= 0; i--) {
            const enemy = this.enemies[i];
            const targetPoint = this.path[enemy.pathIndex];
            
            // 目標地点への方向ベクトル
            const dx = targetPoint.x - enemy.x;
            const dy = targetPoint.y - enemy.y;
            const distance = Math.sqrt(dx * dx + dy * dy);
            
            if (distance = this.path.length) {
                    this.lives -= enemy.damage;
                    this.enemies.splice(i, 1);
                    updateUI();
                    
                    if (this.lives  maxPathIndex) {
                    target = enemy;
                    maxPathIndex = enemy.pathIndex;
                }
            }
            
            if (target) {
                tower.lastFired = now;
                
                // 弾を発射
                this.projectiles.push({
                    x: tower.x,
                    y: tower.y,
                    targetX: target.x,
                    targetY: target.y,
                    target: target,
                    damage: tower.damage,
                    speed: 5,
                    color: tower.color
                });
            }
        }
        
        // 弾の移動と衝突判定
        for (let i = this.projectiles.length - 1; i >= 0; i--) {
            const proj = this.projectiles[i];
            
            // 目標地点への方向ベクトル
            const dx = proj.targetX - proj.x;
            const dy = proj.targetY - proj.y;
            const distance = Math.sqrt(dx * dx + dy * dy);
            
            if (distance < proj.speed || !this.enemies.includes(proj.target)) {
                // 弾が敵に当たった、または敵が既に消滅している
                if (this.enemies.includes(proj.target)) {
                    proj.target.hp -= proj.damage;
                    
                    // 敵のHPが0以下なら消滅
                    if (proj.target.hp <= 0) {
                        this.resources += proj.target.reward;
                        this.enemies.splice(this.enemies.indexOf(proj.target), 1);
                        updateUI();
                    }
                }
                
                this.projectiles.splice(i, 1);
            } else {
                // 弾の移動
                proj.x += (dx / distance) * proj.speed;
                proj.y += (dy / distance) * proj.speed;
            }
        }
    }

    // ゲームをリセット
    reset() {
        this.lives = 10;
        this.resources = 100;
        this.wave = 1;
        this.isWaveActive = false;
        this.towers = [];
        this.enemies = [];
        this.projectiles = [];
        updateUI();
    }
}

// ゲームの描画を担当するクラス
class GameRenderer {
    constructor(canvas, gameState) {
        this.canvas = canvas;
        this.ctx = canvas.getContext('2d');
        this.gameState = gameState;
    }
    
    render() {
        this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
        
        // 背景
        this.ctx.fillStyle = '#e6f7ff';
        this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
        
        // パスの描画
        this.drawPath();
        
        // タワーの描画
        this.drawTowers();
        
        // 敵の描画
        this.drawEnemies();
        
        // 弾の描画
        this.drawProjectiles();
        
        // ラク子さんの描画
        if (this.gameState.rakukoImage.complete) {
            this.ctx.drawImage(
                this.gameState.rakukoImage, 
                this.canvas.width - 100, 
                10, 
                80, 
                80
            );
        }
        
        // 選択中のタワー範囲表示
        if (this.gameState.selectedTower) {
            const tower = this.gameState.towerTypes[this.gameState.selectedTower];
            this.ctx.beginPath();
            this.ctx.arc(mouseX, mouseY, tower.range, 0, Math.PI * 2);
            this.ctx.fillStyle = 'rgba(255, 255, 255, 0.2)';
            this.ctx.fill();
            this.ctx.strokeStyle = 'rgba(0, 0, 0, 0.3)';
            this.ctx.stroke();
        }
    }
    
    drawPath() {
        this.ctx.beginPath();
        this.ctx.moveTo(this.gameState.path[0].x, this.gameState.path[0].y);
        
        for (let i = 1; i < this.gameState.path.length; i++) {
            this.ctx.lineTo(this.gameState.path[i].x, this.gameState.path[i].y);
        }
        
this.ctx.strokeStyle = '#ccc';
        this.ctx.lineWidth = 26;
        this.ctx.stroke();
    }
    
    drawTowers() {
        for (const tower of this.gameState.towers) {
            // タワー本体
            this.ctx.beginPath();
            this.ctx.arc(tower.x, tower.y, 20, 0, Math.PI * 2);
            this.ctx.fillStyle = tower.color;
            this.ctx.fill();
            this.ctx.strokeStyle = '#000';
            this.ctx.lineWidth = 2;
            this.ctx.stroke();
            
            // タワーアイコン（タイプによって異なる形状）
            this.ctx.fillStyle = '#fff';
            if (tower.type === 'firewall') {
                this.ctx.fillRect(tower.x - 8, tower.y - 8, 16, 16);
            } else if (tower.type === 'security') {
                this.ctx.beginPath();
                this.ctx.moveTo(tower.x, tower.y - 10);
                this.ctx.lineTo(tower.x + 10, tower.y + 5);
                this.ctx.lineTo(tower.x - 10, tower.y + 5);
                this.ctx.closePath();
                this.ctx.fill();
            } else if (tower.type === 'backup') {
                this.ctx.beginPath();
                this.ctx.arc(tower.x, tower.y, 8, 0, Math.PI * 2);
                this.ctx.fill();
            }
        }
    }
    
    drawEnemies() {
        for (const enemy of this.gameState.enemies) {
            // 敵本体
            this.ctx.beginPath();
            this.ctx.arc(enemy.x, enemy.y, 15, 0, Math.PI * 2);
            this.ctx.fillStyle = enemy.color;
            this.ctx.fill();
            this.ctx.strokeStyle = '#000';
            this.ctx.lineWidth = 2;
            this.ctx.stroke();
            
            // HPバー
            const hpRatio = enemy.hp / enemy.maxHp;
            this.ctx.fillStyle = '#000';
            this.ctx.fillRect(enemy.x - 15, enemy.y - 25, 30, 5);
            this.ctx.fillStyle = hpRatio > 0.5 ? 'green' : hpRatio > 0.2 ? 'yellow' : 'red';
            this.ctx.fillRect(enemy.x - 15, enemy.y - 25, 30 * hpRatio, 5);
        }
    }
    
    drawProjectiles() {
        for (const proj of this.gameState.projectiles) {
            this.ctx.beginPath();
            this.ctx.arc(proj.x, proj.y, 5, 0, Math.PI * 2);
            this.ctx.fillStyle = proj.color;
            this.ctx.fill();
        }
    }
}

// UI更新関数
function updateUI() {
    document.getElementById('lives').textContent = gameState.lives;
    document.getElementById('resources').textContent = gameState.resources;
    document.getElementById('wave').textContent = gameState.wave;
}

// ゲームの初期化
const canvas = document.getElementById('gameCanvas');
const gameState = new GameState();
const renderer = new GameRenderer(canvas, gameState);
let lastTime = 0;
let mouseX = 0;
let mouseY = 0;

// マウス位置の追跡
canvas.addEventListener('mousemove', (e) => {
    const rect = canvas.getBoundingClientRect();
    mouseX = e.clientX - rect.left;
    mouseY = e.clientY - rect.top;
});

// タワー選択ボタンのイベントリスナー
document.getElementById('firewall').addEventListener('click', () => {
    gameState.selectedTower = 'firewall';
});

document.getElementById('security').addEventListener('click', () => {
    gameState.selectedTower = 'security';
});

document.getElementById('backup').addEventListener('click', () => {
    gameState.selectedTower = 'backup';
});

// ウェーブ開始ボタンのイベントリスナー
document.getElementById('startWave').addEventListener('click', () => {
    gameState.startWave();
});

// キャンバスクリックでタワー設置
canvas.addEventListener('click', () => {
    if (gameState.selectedTower) {
        if (gameState.placeTower(gameState.selectedTower, mouseX, mouseY)) {
            updateUI();
        } else {
            // 設置できない場合は効果音やメッセージを表示
            console.log('ここにはタワーを設置できません');
        }
    }
});

// ESCキーでタワー選択解除
document.addEventListener('keydown', (e) => {
    if (e.key === 'Escape') {
        gameState.selectedTower = null;
    }
});

// ゲームループ
function gameLoop(currentTime) {
    if (!lastTime) lastTime = currentTime;
    const deltaTime = (currentTime - lastTime) / 1000;
    lastTime = currentTime;
    
    // ゲーム状態の更新
    gameState.update(deltaTime);
    
    // 描画
    renderer.render();
    
    // 次のフレームをリクエスト
    requestAnimationFrame(gameLoop);
}

// ゲーム開始
requestAnimationFrame(gameLoop);
updateUI();
```

## ゲームの仕組みを解説 🧩

ここからは、このゲームがどのように動作しているのか、詳しく解説していきます。

### ゲームの基本構造

このゲームは主に以下の2つのクラスで構成されています：

1. **GameState** - ゲームの状態（敵、タワー、弾など）を管理
2. **GameRenderer** - ゲームの描画を担当

そして、これらを制御するためのイベントリスナーとゲームループが実装されています。

### ゲームの流れ図 📊

```
┌───────────────┐
│  ゲーム初期化  │
└───────┬───────┘
        │
        ▼
┌───────────────┐     ┌───────────────┐
│ タワー選択・配置 │◄────┤  リソース管理  │
└───────┬───────┘     └───────────────┘
        │
        ▼
┌───────────────┐
│  ウェーブ開始  │
└───────┬───────┘
        │
        ▼
┌───────────────┐
│   敵の出現    │
└───────┬───────┘
        │
        ▼
┌───────────────┐     ┌───────────────┐
│  タワーの攻撃  │────►│   敵の撃退    │
└───────┬───────┘     └───────┬───────┘
        │                     │
        │                     ▼
        │             ┌───────────────┐
        │             │ リソース獲得   │
        │             └───────┬───────┘
        │                     │
        ▼                     │
┌───────────────┐             │
│  敵が終点到達  │             │
└───────┬───────┘             │
        │                     │
        ▼                     │
┌───────────────┐             │
│ ライフ減少     │             │
└───────┬───────┘             │
        │                     │
        ▼                     │
┌───────────────┐     ┌───────────────┐
│ゲームオーバー判定│◄────┤ウェーブクリア判定│◄──┘
└───────┬───────┘     └───────┬───────┘
        │                     │
        ▼                     ▼
┌───────────────┐     ┌───────────────┐
│ゲームオーバー   │     │  次ウェーブへ  │
└───────────────┘     └───────────────┘
```

## 実装のポイント解説 💡

### 1. Canvas APIの活用

HTML5のCanvas APIを使って2Dグラフィックスを描画しています。Canvas APIは、JavaScriptでブラウザ上に描画するための強力なAPIです。

```javascript
// キャンバスの取得と描画コンテキストの設定
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');

// 円を描画する例
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fillStyle = 'blue';
ctx.fill();
```

### 2. ゲームループの実装

ゲームループは、ゲームの状態を更新し、画面を再描画する無限ループです。`requestAnimationFrame`を使用することで、ブラウザの描画タイミングに合わせて効率的に実行できます。

```javascript
function gameLoop(currentTime) {
    // 前回のフレームからの経過時間を計算
    const deltaTime = (currentTime - lastTime) / 1000;
    lastTime = currentTime;
    
    // ゲーム状態の更新
    gameState.update(deltaTime);
    
    // 描画
    renderer.render();
    
    // 次のフレームをリクエスト
    requestAnimationFrame(gameLoop);
}

// ゲームループの開始
requestAnimationFrame(gameLoop);
```

### 3. 敵の移動パスの実装

敵はあらかじめ定義されたパスに沿って移動します。パスは座標の配列として定義し、敵はそれぞれの座標を順番に目指して移動します。

```javascript
generatePath() {
    return [
        { x: 0, y: 300 },
        { x: 200, y: 300 },
        { x: 200, y: 150 },
        // 他の座標...
    ];
}
```

敵の移動は、現在の位置から次の目標地点への方向ベクトルを計算し、その方向に速度を掛けて移動させています。

```javascript
// 目標地点への方向ベクトル
const dx = targetPoint.x - enemy.x;
const dy = targetPoint.y - enemy.y;
const distance = Math.sqrt(dx * dx + dy * dy);

// 移動
enemy.x += (dx / distance) * enemy.speed;
enemy.y += (dy / distance) * enemy.speed;
```

### 4. タワーの攻撃ロジック

タワーは一定の範囲内にいる敵を検出し、攻撃します。攻撃頻度は`fireRate`によって制御されます。

```javascript
// 範囲内の敵を探す
for (const enemy of this.enemies) {
    const distance = Math.sqrt((tower.x - enemy.x) ** 2 + (tower.y - enemy.y) ** 2);
    if (distance  maxPathIndex) {
        target = enemy;
        maxPathIndex = enemy.pathIndex;
    }
}
```

### 5. 衝突判定

弾と敵の衝突判定は、弾が敵の位置に十分近づいたかどうかで判定しています。

```javascript
if (distance < proj.speed || !this.enemies.includes(proj.target)) {
    // 弾が敵に当たった、または敵が既に消滅している
    if (this.enemies.includes(proj.target)) {
        proj.target.hp -= proj.damage;
        
        // 敵のHPが0以下なら消滅
        if (proj.target.hp <= 0) {
            this.resources += proj.target.reward;
            this.enemies.splice(this.enemies.indexOf(proj.target), 1);
            updateUI();
        }
    }
    
    this.projectiles.splice(i, 1);
}
```

## ゲームバランスの調整 ⚖️

ゲームバランスは非常に重要です。以下のパラメータを調整することで、ゲームの難易度や面白さを調整できます：

1. **敵のHP** - 敵の耐久力
2. **敵の速度** - 敵の移動速度
3. **敵の報酬** - 敵を倒した時に得られるリソース
4. **タワーのコスト** - タワーの設置に必要なリソース
5. **タワーの攻撃力** - タワーの与えるダメージ
6. **タワーの射程** - タワーの攻撃範囲
7. **タワーの発射速度** - タワーの攻撃頻度

今回のゲームでは、以下のように設定しています：

```javascript
this.enemyTypes = {
    virus: { hp: 50, speed: 1, reward: 10, color: 'green', damage: 1 },
    hacker: { hp: 30, speed: 2, reward: 15, color: 'red', damage: 2 },
    ddos: { hp: 100, speed: 0.5, reward: 20, color: 'purple', damage: 3 }
};

this.towerTypes = {
    firewall: { cost: 25, range: 100, damage: 10, fireRate: 1, color: 'blue' },
    security: { cost: 50, range: 150, damage: 20, fireRate: 0.8, color: 'orange' },
    backup: { cost: 75, range: 200, damage: 5, fireRate: 2, color: 'cyan' }
};
```

## 拡張アイデア 🚀

このゲームは基本的な機能を実装していますが、さらに以下のような拡張が可能です：

1. **タワーアップグレード** - 既存のタワーを強化する機能
2. **特殊能力** - 一時的に全敵を遅くするなどの特殊スキル
3. **ボスエネミー** - 各ウェーブの最後に出現する強力な敵
4. **サウンドエフェクト** - ゲーム体験を向上させる効果音
5. **ビジュアルエフェクト** - 攻撃時のアニメーションなど
6. **ローカルストレージ** - ハイスコアの保存
7. **複数マップ** - 異なるレイアウトのマップ

## まとめ 📝

今回は、HTML5とJavaScriptを使って、シンプルながらも楽しいタワーディフェンスゲーム「ラク子のサイト防衛戦」を作成しました。このゲームを通じて、以下のような技術やコンセプトを学ぶことができました：

- Canvas APIを使った2D描画
- ゲームループの実装
- オブジェクト指向プログラミング
- 衝突判定
- イベント処理
- ゲームバランスの調整

このコードをベースに、さらに機能を追加したり、グラフィックを改善したりして、オリジナルのゲームを作ってみてください！

最後に、このゲームを実際に動かすには、上記のHTMLファイル、CSSファイル、JavaScriptファイルを同じディレクトリに保存し、HTMLファイルをブラウザで開くだけです。ぜひ試してみてください！

---

いかがでしたか？今回は基本的なタワーディフェンスゲームの作り方を解説しましたが、これをベースにさらに発展させることで、より複雑で面白いゲームを作ることができます。次回は、このゲームにさらに機能を追加する方法について解説する予定です。お楽しみに！

## 参考リンク

- [MDN Canvas API リファレンス](https://developer.mozilla.org/ja/docs/Web/API/Canvas_API)
- [requestAnimationFrame の使い方](https://developer.mozilla.org/ja/docs/Web/API/window/requestAnimationFrame)
- [ゲーム開発のためのJavaScriptチュートリアル](https://developer.mozilla.org/ja/docs/Games/Tutorials)


---

### 最後に：業務委託のご相談を承ります

私は、業務委託エンジニアとしてWEB制作やシステム開発を請け負っています。最新技術を活用し、レスポンシブなWebサイトやインタラクティブなアプリケーション、API連携など、幅広いニーズに対応可能です。

「課題解決に向けた即戦力が欲しい」「高品質なWeb制作を依頼したい」という方は、ぜひお気軽にご相談ください。一緒にビジネスの成長を目指しましょう！

👉 [ポートフォリオ](https://yushiyamamoto.vercel.app/)

🌳 [らくらくサイト](https://rakuraku-site.prodouga.com/)