ブラウザで動く経営シミュレーションゲームの作り方 🎮
こんにちは、@YushiYamamotoです!今回は「ブラウザで動作する経営シミュレーションゲーム」の作り方について、初心者の方にも分かりやすく解説していきます。JavaScriptを使って、シンプルながらも奥深いゲーム開発の世界に飛び込んでみましょう!
See the Pen ラク子のWEB制作会社 by Yushi Yamamoto (@yamamotoyushi) on CodePen.
🌟 なぜ経営シミュレーションゲーム?
経営シミュレーションゲームは、プレイヤーが経営者となって会社やお店を運営するゲームです。資金管理、スタッフ雇用、設備投資など、現実の経営要素を取り入れつつ、ゲームとして楽しめる要素が満載です。
また、技術的な観点からも:
- オブジェクト指向プログラミングの練習に最適
- 状態管理の仕組みを学べる
- UIとロジックの分離を実践できる
📋 開発の流れ
ゲーム開発の基本的な流れは以下のようになります:
今回は、「WEB制作会社経営シミュレーション」というテーマで進めていきます。プレイヤーはWEB制作会社の社長となり、クライアントから依頼を受けてWebサイトを制作し、会社を成長させていくゲームです。
💻 開発環境の準備
ブラウザゲーム開発に必要なものは非常にシンプルです:
- テキストエディタ(VSCode推奨)
- Webブラウザ(Chrome推奨)
- 基本的なHTML/CSS/JavaScript知識
特別なゲームエンジンは使わず、純粋なJavaScriptで実装していきます。これにより、基本的なプログラミングの概念を深く理解できます。
🏗️ ゲームの基本設計
まずは、ゲームの基本構造を考えましょう。経営シミュレーションゲームの核となる要素は以下の通りです:
- 会社の状態管理:資金、評判、所有設備など
- 時間の経過:日付進行システム
- プロジェクト管理:依頼の受注、進行、完了
- スタッフ管理:雇用、スキル成長、給与支払い
- 設備投資:機材購入、オフィス拡張
これらの要素をクラス図で表すと次のようになります:
+------------------+ +------------------+
| Game | | Project |
+------------------+ +------------------+
| - company || - id |
| - gameTime | | - name |
| - advanceDay() | | - budget |
| - saveGame() | | - deadline |
+------------------+ | - assignedStaff |
| +------------------+
|
v
+------------------+ +------------------+
| Company || Staff |
+------------------+ +------------------+
| - name | | - id |
| - funds | | - name |
| - reputation | | - skills |
| - staff | | - salary |
| - projects | | - motivation |
| - equipment | +------------------+
+------------------+
🚀 基本システムの実装
それでは、実際にコードを書いていきましょう。まずは、ゲームの基本クラスを作成します。
JS
// ゲームのメインクラス
class WebDevTycoon {
constructor() {
// 会社の初期状態
this.company = {
name: "マイWEB制作会社",
funds: 500000, // 初期資金:50万円
reputation: 50, // 評判:0-100
staff: [], // スタッフリスト
projects: [], // プロジェクトリスト
equipment: [], // 所有機材リスト
office: {size: "small", rent: 50000} // オフィス情報
};
// ゲーム内時間
this.gameTime = {
day: 1,
month: 4,
year: 2025
};
// 初期スタッフ(プレイヤー自身)を追加
this.company.staff.push({
id: 1,
name: "あなた",
role: "社長",
salary: 0, // 社長なので給料なし
skills: {
design: 50,
coding: 50,
management: 50,
communication: 60
},
motivation: 100
});
}
// 日付を進める
advanceDay() {
this.gameTime.day++;
// 月末処理
if (this.gameTime.day > 30) {
this.gameTime.day = 1;
this.gameTime.month++;
this.payMonthlyExpenses(); // 月次経費支払い
}
// 年末処理
if (this.gameTime.month > 12) {
this.gameTime.month = 1;
this.gameTime.year++;
this.annualReport(); // 年次レポート
}
// プロジェクトの進行
this.updateProjects();
// ランダムイベント発生
this.generateRandomEvents();
}
// 月次経費の支払い
payMonthlyExpenses() {
// オフィス賃料
this.company.funds -= this.company.office.rent;
// スタッフの給料
for (const staff of this.company.staff) {
if (staff.salary > 0) { // プレイヤー以外のスタッフ
this.company.funds -= staff.salary;
}
}
// 設備維持費
const equipmentMaintenance = this.company.equipment.length * 5000;
this.company.funds -= equipmentMaintenance;
return {
rent: this.company.office.rent,
salaries: this.company.staff.reduce((sum, staff) => sum + staff.salary, 0),
maintenance: equipmentMaintenance
};
}
// プロジェクトの更新
updateProjects() {
for (const project of this.company.projects) {
if (!project.completed) {
project.daysLeft--;
// プロジェクトが期限切れになった場合
if (project.daysLeft p.id === projectId);
if (!project) return { success: false, message: "プロジェクトが見つかりません。" };
// 品質計算
const quality = this.calculateProjectQuality(project);
// 報酬と評判の更新
this.company.funds += project.remainingPayment;
// 品質に応じた評判変動
let reputationChange = 0;
if (quality > 80) {
reputationChange = 5;
} else if (quality > 50) {
reputationChange = 2;
} else {
reputationChange = -3;
}
this.company.reputation = Math.max(0, Math.min(100, this.company.reputation + reputationChange));
// プロジェクト完了フラグを立てる
project.completed = true;
// スタッフのスキルアップ
for (const staffId of project.assignedStaff) {
const staff = this.company.staff.find(s => s.id === staffId);
if (staff) {
// プロジェクトの種類に応じたスキルアップ
for (const skill of project.requiredSkills) {
if (staff.skills[skill]) {
staff.skills[skill] = Math.min(100, staff.skills[skill] + 1);
}
}
}
}
return {
success: true,
message: `${project.name}が完了しました!品質: ${quality}点、報酬: ${project.remainingPayment.toLocaleString()}円`,
quality: quality,
payment: project.remainingPayment,
reputationChange: reputationChange
};
}
// プロジェクトの品質を計算
calculateProjectQuality(project) {
// 基本品質
let quality = 50;
// スタッフのスキル影響
const assignedStaff = project.assignedStaff || [];
for (const staffId of assignedStaff) {
const staff = this.company.staff.find(s => s.id === staffId);
if (staff) {
// プロジェクトに必要なスキルの平均値を計算
let skillSum = 0;
let skillCount = 0;
for (const skill of project.requiredSkills) {
if (staff.skills[skill]) {
skillSum += staff.skills[skill];
skillCount++;
}
}
if (skillCount > 0) {
const avgSkill = skillSum / skillCount;
quality += avgSkill / assignedStaff.length;
}
// モチベーションの影響
quality += (staff.motivation - 50) / 10;
}
}
// 納期影響
if (project.daysLeft > 5) {
quality += 10; // 余裕があると品質アップ
} else if (project.daysLeft
HTML
HTML
<div class="game-container">
<div class="game-header">
<div class="company-info">
<h1 class="company-name" id="company-name">ラク子のWEB制作会社</h1>
<div class="company-stats">
<div class="stat-item">
<span class="stat-label">資金</span>
<span class="stat-value" id="funds">500,000円</span>
</div>
<div class="stat-item">
<span class="stat-label">評判</span>
<span class="stat-value" id="reputation">50</span>
</div>
</div>
</div>
<div class="game-date" id="date">2025年4月1日</div>
</div>
<div class="rakuko-area">
<img src="https://rakuraku-site.prodouga.com/img/RakuRaku.avif" alt="ラク子" class="rakuko-image normal" id="rakuko-image">
<div class="rakuko-message" id="rakuko-message">今日も頑張りましょう!</div>
<button class="next-day-button" id="next-day-button">次の日へ</button>
</div>
<div class="game-main">
<div class="action-buttons">
<button class="action-button" id="hire-staff-button">スタッフを雇用</button>
<button class="action-button" id="buy-equipment-button">機材を購入</button>
<button class="action-button" id="upgrade-office-button">オフィスをアップグレード</button>
</div>
<div class="game-content">
<div class="content-section projects-section">
<h2 class="section-title">プロジェクト</h2>
<ul class="projects-list" id="projects-list">
<li class="empty-list">現在進行中のプロジェクトはありません</li>
</ul>
</div>
<div class="content-section staff-section">
<h2 class="section-title">スタッフ</h2>
<ul class="staff-list" id="staff-list"></ul>
</div>
<div class="content-section equipment-section">
<h2 class="section-title">機材</h2>
<ul class="equipment-list" id="equipment-list"></ul>
</div>
</div>
<div class="message-log" id="message-log"></div>
</div>
</div>
CSS
CSS
/* ゲーム全体のスタイル */
body {
font-family: 'Hiragino Kaku Gothic Pro', 'メイリオ', sans-serif;
margin: 0;
padding: 0;
background-color: #f5f5f5;
color: #333;
}
.game-container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
display: grid;
grid-template-columns: 300px 1fr;
grid-template-rows: auto 1fr;
gap: 20px;
height: 100vh;
}
/* ヘッダー部分 */
.game-header {
grid-column: 1 / 3;
display: flex;
justify-content: space-between;
align-items: center;
background-color: #fff;
padding: 15px;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
.company-info {
display: flex;
flex-direction: column;
}
.company-name {
font-size: 24px;
font-weight: bold;
margin: 0;
}
.company-stats {
display: flex;
gap: 20px;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
}
.stat-label {
font-size: 12px;
color: #666;
}
.stat-value {
font-size: 18px;
font-weight: bold;
}
.game-date {
padding: 5px 10px;
background-color: #f0f0f0;
border-radius: 4px;
font-size: 14px;
}
/* ラク子エリア */
.rakuko-area {
grid-column: 1;
background-color: #fff;
padding: 15px;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
display: flex;
flex-direction: column;
align-items: center;
}
.rakuko-image {
width: 200px;
height: 200px;
object-fit: cover;
border-radius: 50%;
margin-bottom: 15px;
transition: all 0.3s ease;
}
.rakuko-image.happy {
border: 5px solid #4CAF50;
}
.rakuko-image.normal {
border: 5px solid #FFC107;
}
.rakuko-image.sad {
border: 5px solid #F44336;
}
.rakuko-message {
font-size: 16px;
text-align: center;
margin-bottom: 20px;
min-height: 48px;
}
.next-day-button {
padding: 10px 20px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.3s;
}
.next-day-button:hover {
background-color: #45a049;
}
/* ゲームメインエリア */
.game-main {
grid-column: 2;
display: grid;
grid-template-rows: auto 1fr;
gap: 20px;
}
.action-buttons {
display: flex;
gap: 10px;
}
.action-button {
padding: 10px 15px;
background-color: #2196F3;
color: white;
border: none;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: background-color 0.3s;
}
.action-button:hover {
background-color: #0b7dda;
}
.game-content {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
gap: 20px;
}
.content-section {
background-color: #fff;
padding: 15px;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
overflow-y: auto;
}
.section-title {
font-size: 18px;
font-weight: bold;
margin-top: 0;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
}
/* プロジェクトリスト */
.projects-section {
grid-column: 1 / 3;
grid-row: 1;
}
.projects-list {
list-style: none;
padding: 0;
margin: 0;
}
.project-item {
margin-bottom: 15px;
border: 1px solid #eee;
border-radius: 4px;
overflow: hidden;
}
.project-header {
padding: 10px;
display: flex;
justify-content: space-between;
align-items: center;
}
.project-header.normal {
background-color: #E3F2FD;
}
.project-header.urgent {
background-color: #FFF3E0;
}
.project-header.overdue {
background-color: #FFEBEE;
}
.project-header.completed {
background-color: #E8F5E9;
}
.project-header h3 {
margin: 0;
font-size: 16px;
}
.project-client {
font-size: 12px;
color: #666;
}
.project-details {
padding: 10px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.project-details p {
margin: 0;
font-size: 14px;
}
.project-actions {
padding: 10px;
display: flex;
justify-content: flex-end;
gap: 10px;
background-color: #f9f9f9;
}
.project-actions button {
padding: 5px 10px;
background-color: #2196F3;
color: white;
border: none;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
}
.project-actions button:hover {
background-color: #0b7dda;
}
.status-badge {
padding: 5px 10px;
border-radius: 4px;
font-size: 12px;
}
.status-badge.completed {
background-color: #4CAF50;
color: white;
}
.empty-list {
padding: 20px;
text-align: center;
color: #999;
}
/* スタッフリスト */
.staff-section {
grid-column: 1;
grid-row: 2;
}
.staff-list {
list-style: none;
padding: 0;
margin: 0;
}
.staff-item {
margin-bottom: 15px;
border: 1px solid #eee;
border-radius: 4px;
overflow: hidden;
}
.staff-header {
padding: 10px;
background-color: #E8EAF6;
display: flex;
justify-content: space-between;
align-items: center;
}
.staff-header h3 {
margin: 0;
font-size: 16px;
}
.staff-role {
font-size: 12px;
color: #666;
}
.staff-details {
padding: 10px;
}
.staff-details p {
margin: 5px 0;
font-size: 14px;
}
.staff-skills {
margin: 10px 0;
}
.skill-bar {
display: flex;
align-items: center;
margin-bottom: 5px;
}
.skill-name {
width: 100px;
font-size: 12px;
}
.skill-value-bar {
flex-grow: 1;
height: 8px;
background-color: #eee;
border-radius: 4px;
overflow: hidden;
margin: 0 10px;
}
.skill-value {
height: 100%;
background-color: #2196F3;
}
.skill-value-text {
font-size: 12px;
width: 30px;
text-align: right;
}
.motivation {
font-weight: bold;
}
.motivation-high {
color: #4CAF50;
}
.motivation-medium {
color: #FFC107;
}
.motivation-low {
color: #F44336;
}
.staff-actions {
padding: 10px;
display: flex;
justify-content: flex-end;
gap: 10px;
background-color: #f9f9f9;
}
.staff-actions button {
padding: 5px 10px;
background-color: #673AB7;
color: white;
border: none;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
}
.staff-actions button:hover {
background-color: #5e35b1;
}
/* 機材リスト */
.equipment-section {
grid-column: 2;
grid-row: 2;
}
.equipment-list {
list-style: none;
padding: 0;
margin: 0;
}
.equipment-item {
margin-bottom: 15px;
border: 1px solid #eee;
border-radius: 4px;
overflow: hidden;
}
.equipment-header {
padding: 10px;
background-color: #E0F7FA;
display: flex;
justify-content: space-between;
align-items: center;
}
.equipment-header h3 {
margin: 0;
font-size: 16px;
}
.equipment-details {
padding: 10px;
}
.equipment-details p {
margin: 5px 0;
font-size: 14px;
}
.condition {
font-weight: bold;
}
.condition-good {
color: #4CAF50;
}
.condition-fair {
color: #FFC107;
}
.condition-poor {
color: #F44336;
}
.equipment-actions {
padding: 10px;
display: flex;
justify-content: flex-end;
gap: 10px;
background-color: #f9f9f9;
}
.equipment-actions button {
padding: 5px 10px;
background-color: #00BCD4;
color: white;
border: none;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
}
.equipment-actions button:hover {
background-color: #00acc1;
}
/* メッセージログ */
.message-log {
grid-column: 1 / 3;
background-color: #fff;
padding: 15px;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
max-height: 200px;
overflow-y: auto;
}
.message {
padding: 5px 10px;
margin-bottom: 5px;
border-radius: 4px;
font-size: 14px;
}
.message.info {
background-color: #E3F2FD;
}
.message.warning {
background-color: #FFF3E0;
}
.message.error {
background-color: #FFEBEE;
}
/* ダイアログ */
.dialog {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.dialog-content {
background-color: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 5px 15px rgba(0,0,0,0.3);
max-width: 80%;
max-height: 80%;
overflow-y: auto;
}
.dialog-content h2 {
margin-top: 0;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
}
.dialog-staff-list,
.dialog-skill-list,
.dialog-hire-list,
.dialog-equipment-list,
.dialog-office-list {
list-style: none;
padding: 0;
margin: 0;
}
.dialog-staff-item,
.dialog-skill-item,
.dialog-hire-item,
.dialog-equipment-item,
.dialog-office-item {
margin-bottom: 15px;
padding: 10px;
border: 1px solid #eee;
border-radius: 4px;
display: flex;
justify-content: space-between;
align-items: center;
}
.dialog-staff-item.assigned {
background-color: #E8F5E9;
}
.dialog-office-item.current {
background-color: #E8F5E9;
}
.dialog-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 20px;
}
.dialog-actions button {
padding: 10px 20px;
background-color: #2196F3;
color: white;
border: none;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
}
.dialog-actions button:hover {
background-color: #0b7dda;
}
.close-dialog-button {
background-color: #9E9E9E !important;
}
.close-dialog-button:hover {
background-color: #757575 !important;
}
/* レスポンシブデザイン */
@media (max-width: 1024px) {
.game-container {
grid-template-columns: 1fr;
}
.game-header {
grid-column: 1;
}
.rakuko-area {
grid-column: 1;
}
.game-main {
grid-column: 1;
}
.game-content {
grid-template-columns: 1fr;
}
.projects-section {
grid-column: 1;
}
.staff-section,
.equipment-section {
grid-column: 1;
}
}
🔄 ゲームループの実装
ゲームの動作を制御するために、HTMLとJavaScriptを連携させます。以下は、UIの更新とゲームの進行を管理するコードです。
// HTMLとの連携用コード
document.addEventListener('DOMContentLoaded', function() {
// ゲームインスタンスの作成
const game = new WebDevTycoon();
// UI要素の取得
const companyNameElement = document.getElementById('company-name');
const fundsElement = document.getElementById('funds');
const reputationElement = document.getElementById('reputation');
const dateElement = document.getElementById('date');
const characterMessageElement = document.getElementById('character-message');
const projectsListElement = document.getElementById('projects-list');
const staffListElement = document.getElementById('staff-list');
const equipmentListElement = document.getElementById('equipment-list');
const nextDayButton = document.getElementById('next-day-button');
const messageLogElement = document.getElementById('message-log');
// ゲーム状態の更新
function updateUI() {
// 基本情報の更新
companyNameElement.textContent = game.company.name;
fundsElement.textContent = game.company.funds.toLocaleString() + '円';
reputationElement.textContent = game.company.reputation;
dateElement.textContent = `${game.gameTime.year}年${game.gameTime.month}月${game.gameTime.day}日`;
// プロジェクトリストの更新
updateProjectsList(game.company.projects);
// スタッフリストの更新
updateStaffList(game.company.staff);
// 機材リストの更新
updateEquipmentList(game.company.equipment);
}
// 次の日へボタンのイベントリスナー
nextDayButton.addEventListener('click', function() {
const result = game.advanceDay();
addMessageToLog(`${game.gameTime.year}年${game.gameTime.month}月${game.gameTime.day}日になりました。`);
// ランダムイベントの処理
const event = game.generateRandomEvents();
if (event) {
addMessageToLog(event.message);
if (event.type === 'inquiry') {
showNewProjectDialog(event.project);
}
}
updateUI();
});
// 初期UI更新
updateUI();
// 初期メッセージ
addMessageToLog('ゲームを開始しました。WEB制作会社経営へようこそ!');
});
🧩 主要機能の実装
ここからは、ゲームの主要機能を一つずつ実装していきます。
1. プロジェクト管理システム
プロジェクト管理は経営シミュレーションの核となる部分です。プロジェクトの受注から完了までの流れを実装します。
// プロジェクトリストの更新
function updateProjectsList(projects) {
projectsListElement.innerHTML = '';
if (projects.length === 0) {
projectsListElement.innerHTML = '現在進行中のプロジェクトはありません';
return;
}
for (const project of projects) {
const projectElement = document.createElement('li');
projectElement.className = 'project-item';
const statusClass = project.completed ? 'completed' :
(project.daysLeft
${project.name}
${project.client}
予算: ${project.budget.toLocaleString()}円
残り日数: ${project.daysLeft}日
難易度: ${getDifficultyText(project.difficulty)}
担当者: ${getAssignedStaffNames(project.assignedStaff)}
${project.completed ?
'完了' :
`スタッフ割当
納品する`
}
`;
projectsListElement.appendChild(projectElement);
}
// イベントリスナーの追加
document.querySelectorAll('.assign-staff-button').forEach(button => {
button.addEventListener('click', function() {
const projectId = parseInt(this.getAttribute('data-project-id'));
showAssignStaffDialog(projectId);
});
});
}
2. スタッフ管理システム
会社の成長には優秀なスタッフが欠かせません。スタッフの雇用、育成、管理機能を実装します。
// スタッフを雇用
function hireStaff(staffType) {
const staffTypes = {
"デザイナー": {
baseSalary: 250000,
skills: { design: 70, coding: 20, management: 30, communication: 50 },
hireCost: 100000
},
"エンジニア": {
baseSalary: 300000,
skills: { design: 30, coding: 80, management: 40, communication: 40 },
hireCost: 150000
},
"ディレクター": {
baseSalary: 350000,
skills: { design: 40, coding: 30, management: 70, communication: 60 },
hireCost: 200000
}
};
const template = staffTypes[staffType];
if (!template) return { success: false, message: "指定されたスタッフタイプが見つかりません。" };
if (game.company.funds s.id === staffId);
if (staff) {
// プロジェクトに必要なスキルの平均値を計算
let skillSum = 0;
let skillCount = 0;
for (const skill of project.requiredSkills) {
if (staff.skills[skill]) {
skillSum += staff.skills[skill];
skillCount++;
}
}
if (skillCount > 0) {
const avgSkill = skillSum / skillCount;
// スタッフのスキルの影響を大きくする
quality += (avgSkill / assignedStaff.length) * 1.5;
}
// モチベーションの影響も大きくする
quality += (staff.motivation - 50) / 5;
}
}
// 設備の影響(新たに追加)
const relevantEquipment = game.company.equipment.filter(eq =>
eq.name.includes("PC") ||
(project.requiredSkills.includes("design") && eq.name.includes("タブレット"))
);
if (relevantEquipment.length > 0) {
const avgEquipmentPerformance = relevantEquipment.reduce((sum, eq) => sum + eq.performance, 0) / relevantEquipment.length;
quality += (avgEquipmentPerformance - 50) / 4;
} else {
// 必要な機材がない場合はペナルティ
quality -= 10;
}
// 納期影響(調整)
if (project.daysLeft > project.duration * 0.3) {
quality += 15; // 余裕があると品質アップ(より大きく)
} else if (project.daysLeft < 0) {
quality -= 25; // 納期遅れは大幅減点(より厳しく)
}
// 最終調整
return Math.max(0, Math.min(100, Math.floor(quality)));
}
🎮 ゲームの完成と拡張
基本的なゲームシステムが完成したら、以下のような拡張機能を追加して、ゲームをより面白くすることができます:
- セーブ&ロード機能:LocalStorageを使ってゲーム状態を保存
- 実績システム:特定の条件を達成すると解除される実績
- ランダムイベント:予期せぬ出来事が発生するシステム
- 競合他社:AIが操作する競合会社との競争
- 特殊プロジェクト:稀に出現する高難度・高報酬のプロジェクト
例えば、セーブ&ロード機能は以下のように実装できます:
// ゲームの状態を保存
function saveGame() {
const saveData = {
company: game.company,
gameTime: game.gameTime,
version: "1.0.0" // バージョン管理用
};
localStorage.setItem('webDevTycoonSave', JSON.stringify(saveData));
return { success: true, message: "ゲームを保存しました。" };
}
// ゲームの状態を読み込み
function loadGame() {
const saveData = localStorage.getItem('webDevTycoonSave');
if (!saveData) return { success: false, message: "セーブデータが見つかりません。" };
try {
const parsedData = JSON.parse(saveData);
// バージョンチェック(将来的な互換性のため)
if (parsedData.version !== "1.0.0") {
return { success: false, message: "互換性のないセーブデータです。" };
}
// データの復元
game.company = parsedData.company;
game.gameTime = parsedData.gameTime;
updateUI();
return { success: true, message: "ゲームを読み込みました。" };
} catch (error) {
return { success: false, message: "セーブデータの読み込みに失敗しました。" };
}
}
📱 レスポンシブデザインの実装
現代のWebアプリケーションには、レスポンシブデザインが不可欠です。様々な画面サイズに対応するためのCSSを追加しましょう。
/* レスポンシブデザイン */
@media (max-width: 1024px) {
.game-container {
grid-template-columns: 1fr;
}
.game-header {
grid-column: 1;
}
.side-panel {
grid-column: 1;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.character-area {
display: flex;
align-items: center;
}
.character-image {
width: 80px;
height: 80px;
margin-right: 15px;
}
.game-main {
grid-column: 1;
}
.game-content {
grid-template-columns: 1fr;
}
.projects-section,
.staff-section,
.equipment-section {
grid-column: 1;
}
}
@media (max-width: 600px) {
.game-header {
flex-direction: column;
align-items: flex-start;
}
.company-stats {
margin-top: 10px;
}
.game-date {
margin-top: 10px;
align-self: flex-end;
}
.action-buttons {
flex-direction: column;
gap: 5px;
}
.project-details,
.staff-details {
grid-template-columns: 1fr;
}
}
🔍 デバッグとテスト
ゲーム開発では、デバッグとテストが非常に重要です。以下のようなテスト関数を作成して、ゲームの各機能をテストしましょう。
// テスト用関数
function runGameTests() {
console.log("=== ゲームテスト開始 ===");
// 1. 日付進行テスト
console.log("1. 日付進行テスト");
const initialDate = { ...game.gameTime };
game.advanceDay();
console.assert(
game.gameTime.day === initialDate.day + 1 ||
(initialDate.day === 30 && game.gameTime.day === 1),
"日付進行に問題があります"
);
// 2. プロジェクト生成テスト
console.log("2. プロジェクト生成テスト");
const newProject = game.generateNewProject();
console.assert(
newProject && newProject.name && newProject.budget > 0,
"プロジェクト生成に問題があります"
);
// 3. プロジェクト受注テスト
console.log("3. プロジェクト受注テスト");
const initialFunds = game.company.funds;
const initialProjectCount = game.company.projects.length;
const acceptResult = game.acceptProject(newProject);
console.assert(
acceptResult.success &&
game.company.funds === initialFunds + newProject.advancePayment &&
game.company.projects.length === initialProjectCount + 1,
"プロジェクト受注に問題があります"
);
// 4. スタッフ割り当てテスト
console.log("4. スタッフ割り当てテスト");
const projectId = game.company.projects.id;
const staffId = game.company.staff.id;
const assignResult = game.assignStaffToProject(projectId, staffId);
console.assert(
assignResult.success &&
game.company.projects.assignedStaff.includes(staffId),
"スタッフ割り当てに問題があります"
);
// 5. プロジェクト完了テスト
console.log("5. プロジェクト完了テスト");
const projectToComplete = game.company.projects;
projectToComplete.daysLeft = 0; // 強制的に納期を到来させる
const completeResult = game.completeProject(projectToComplete.id);
console.assert(
completeResult.success &&
projectToComplete.completed,
"プロジェクト完了に問題があります"
);
console.log("=== テスト完了 ===");
}
🚀 パフォーマンス最適化
ブラウザゲームでは、パフォーマンスも重要な要素です。特に、UIの更新処理は効率的に行う必要があります。
// パフォーマンス最適化版のUI更新
function optimizedUpdateUI() {
// 変更があった部分だけを更新する
// 1. 基本情報の更新(変更があった場合のみ)
if (companyNameElement.textContent !== game.company.name) {
companyNameElement.textContent = game.company.name;
}
const formattedFunds = game.company.funds.toLocaleString() + '円';
if (fundsElement.textContent !== formattedFunds) {
fundsElement.textContent = formattedFunds;
}
if (reputationElement.textContent != game.company.reputation) {
reputationElement.textContent = game.company.reputation;
}
const formattedDate = `${game.gameTime.year}年${game.gameTime.month}月${game.gameTime.day}日`;
if (dateElement.textContent !== formattedDate) {
dateElement.textContent = formattedDate;
}
// 2. リストの更新(差分だけ更新)
updateProjectsListOptimized(game.company.projects);
updateStaffListOptimized(game.company.staff);
updateEquipmentListOptimized(game.company.equipment);
}
// プロジェクトリストの最適化更新
function updateProjectsListOptimized(projects) {
// 既存のプロジェクト要素を取得
const existingProjects = Array.from(projectsListElement.children).filter(
el => el.classList.contains('project-item')
);
// 空リストの表示/非表示を切り替え
const emptyListElement = projectsListElement.querySelector('.empty-list');
if (projects.length === 0) {
if (!emptyListElement) {
const emptyEl = document.createElement('li');
emptyEl.className = 'empty-list';
emptyEl.textContent = '現在進行中のプロジェクトはありません';
projectsListElement.appendChild(emptyEl);
}
// 既存のプロジェクト要素をすべて削除
existingProjects.forEach(el => el.remove());
return;
} else if (emptyListElement) {
emptyListElement.remove();
}
// 既存のプロジェクトIDを取得
const existingProjectIds = existingProjects.map(
el => parseInt(el.querySelector('button')?.getAttribute('data-project-id') || '0')
);
// 新しいプロジェクトを追加
for (const project of projects) {
const existingIndex = existingProjectIds.indexOf(project.id);
if (existingIndex === -1) {
// 新規プロジェクトの場合、要素を作成して追加
const projectElement = createProjectElement(project);
projectsListElement.appendChild(projectElement);
} else {
// 既存プロジェクトの場合、内容を更新
updateExistingProjectElement(existingProjects[existingIndex], project);
}
}
// 削除されたプロジェクトの要素を削除
const currentProjectIds = projects.map(p => p.id);
existingProjects.forEach((el, index) => {
const projectId = existingProjectIds[index];
if (!currentProjectIds.includes(projectId)) {
el.remove();
}
});
}
🎯 実践的なゲーム開発のポイント
最後に、ブラウザゲーム開発で重要なポイントをいくつか紹介します:
1. 状態管理の設計
ゲームの状態管理は非常に重要です。今回は単純なオブジェクトで管理しましたが、規模が大きくなる場合は以下の方法も検討してください:
- Reduxパターン:状態の一元管理と予測可能な更新
- イミュータブルデータ構造:状態の変更を追跡しやすくする
- オブザーバーパターン:状態変更を監視して自動的にUIを更新
2. モジュール化と再利用性
コードの再利用性を高めるために、機能ごとにモジュール化することが重要です:
// モジュール化の例
const GameTimeModule = {
advanceDay(gameTime, callbacks) {
gameTime.day++;
if (gameTime.day > 30) {
gameTime.day = 1;
gameTime.month++;
if (callbacks.onMonthEnd) {
callbacks.onMonthEnd();
}
}
if (gameTime.month > 12) {
gameTime.month = 1;
gameTime.year++;
if (callbacks.onYearEnd) {
callbacks.onYearEnd();
}
}
return gameTime;
}
};
const ProjectModule = {
generateNewProject(companyReputation, templates) {
// プロジェクト生成ロジック
},
calculateQuality(project, assignedStaff, equipment) {
// 品質計算ロジック
}
};
3. ローカライゼーション対応
国際的なユーザーに対応するために、ローカライゼーション(多言語対応)を考慮することも重要です:
// 言語ファイルの例
const translations = {
ja: {
next_day: "次の日へ",
hire_staff: "スタッフを雇用",
buy_equipment: "機材を購入",
upgrade_office: "オフィスをアップグレード",
// ...
},
en: {
next_day: "Next Day",
hire_staff: "Hire Staff",
buy_equipment: "Buy Equipment",
upgrade_office: "Upgrade Office",
// ...
}
};
// 言語切り替え関数
function setLanguage(lang) {
const texts = translations[lang] || translations.en;
// UI要素のテキストを更新
document.getElementById('next-day-button').textContent = texts.next_day;
document.getElementById('hire-staff-button').textContent = texts.hire_staff;
document.getElementById('buy-equipment-button').textContent = texts.buy_equipment;
document.getElementById('upgrade-office-button').textContent = texts.upgrade_office;
// ...
}
📝 まとめ
今回は、JavaScriptを使ったブラウザゲーム開発の基本を解説しました。経営シミュレーションゲームは、オブジェクト指向プログラミングの練習に最適であり、状態管理やUI設計など、Webアプリケーション開発の多くの要素を学ぶことができます。
このゲームはシンプルな構造ですが、拡張性が高いため、様々な機能を追加してより複雑で面白いゲームに発展させることができます。例えば、マーケティング要素の追加、スタッフ間の相性システム、技術ツリーの実装など、アイデア次第で無限に拡張可能です。
ぜひ、このコードをベースに自分だけのオリジナルゲームを作ってみてください!プログラミングの楽しさを実感できるはずです。
次回は、このゲームにアニメーションやサウンドを追加して、よりリッチな体験を提供する方法について解説する予定です。お楽しみに!
🔗 参考リソース
この記事が気に入ったら、ぜひコメントやシェアをお願いします!また、質問や感想も大歓迎です。一緒にゲーム開発の世界を楽しみましょう!
最後に:業務委託のご相談を承ります
私は、業務委託エンジニアとしてWEB制作やシステム開発を請け負っています。最新技術を活用し、レスポンシブなWebサイトやインタラクティブなアプリケーション、API連携など、幅広いニーズに対応可能です。
「課題解決に向けた即戦力が欲しい」「高品質なWeb制作を依頼したい」という方は、ぜひお気軽にご相談ください。一緒にビジネスの成長を目指しましょう!
👉 ポートフォリオ
🌳 らくらくサイト