シンプルなスマホ対応オンラインゲームの作り方
この記事では、HTML、JavaScript、Pythonを使ったシンプルなオンライン2D横スクロール風ゲームのサンプルを紹介します。
1. ゲーム概要
このゲームは、以下のような特徴を持っています。
- 2D横スクロール風ゲーム:画面上にキャラクターが描画され、左右の移動やジャンプが可能です。
- オンライン対応:Socket.IO を利用して複数のプレイヤーが同時に参加し、互いの動きをリアルタイムで共有します。
- シンプルなマップ生成:サーバ側でランダムな凸凹のある地面データ(マップ)を生成し、クライアント側と共有します。

2. クライアント側コードの解説
クライアント側は HTML と JavaScript を利用して、ゲームの描画やプレイヤー操作、サーバとの通信を行います。
HTML と CSS
まずは、HTML と CSS によってゲーム画面のレイアウトやスタイルを定義します。ログインオーバーレイ、ゲームキャンバス、チャットウィンドウ、ジョイスティックなどの UI 要素を配置しています。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>オンライン2D横スクロール風ゲーム</title>
<style>
body { margin: 0; overflow: hidden; font-family: sans-serif; }
canvas { display: block; background: #87ceeb; }
#chat {
position: absolute;
top: 10px;
left: 10px;
width: 300px;
height: 200px;
background: rgba(255,255,255,0.8);
overflow-y: auto;
padding: 5px;
font-size: 14px;
display: none;
}
#chatInput {
position: absolute;
top: 220px;
left: 10px;
width: 300px;
box-sizing: border-box;
display: none;
}
#joystick {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
width: 100px;
height: 100px;
background: rgba(0,0,0,0.2);
border-radius: 50%;
touch-action: none;
display: none;
}
#joystickInner {
position: absolute;
left: 50%;
top: 50%;
width: 40px;
height: 40px;
background: rgba(0,0,0,0.5);
border-radius: 50%;
transform: translate(-50%, -50%);
}
/* ログインオーバーレイ */
#loginOverlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
color: white;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
z-index: 10;
}
#loginOverlay input {
padding: 10px;
font-size: 16px;
margin-bottom: 10px;
}
#loginOverlay button {
padding: 10px 20px;
font-size: 16px;
}
</style>
</head>
<body>
<!-- ログインオーバーレイ -->
<div id="loginOverlay">
<h2>ニックネームを入力してください</h2>
<input type="text" id="nicknameInput" placeholder="例:Player1">
<button id="loginButton">スタート</button>
</div>
<canvas id="gameCanvas"></canvas>
<div id="chat"></div>
<input type="text" id="chatInput" placeholder="チャットメッセージを入力">
<div id="joystick">
<div id="joystickInner"></div>
</div>
<!-- Socket.IO クライアントライブラリ -->
<script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
<script>
// ここからJavaScriptでゲームロジックを実装します
// DOM要素の取得
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const chat = document.getElementById('chat');
const chatInput = document.getElementById('chatInput');
const joystick = document.getElementById('joystick');
const joystickInner = document.getElementById('joystickInner');
const loginOverlay = document.getElementById('loginOverlay');
const nicknameInput = document.getElementById('nicknameInput');
const loginButton = document.getElementById('loginButton');
// キャンバスサイズの設定
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
// キャラクター画像の読み込み
const characterImg = new Image();
characterImg.src = "/wp-content/uploads/2025/03/ふくろくじゅさん.png";
const charWidth = 50, charHeight = 50;
// 自キャラと他プレイヤーの状態を管理するための変数
let myPlayer = { id: null, nickname: "", x: 50, y: canvas.height - 150, vx: 0, vy: 0, direction: "left", onGround: false };
let players = {};
// サーバーから送られてくる共有マップデータ
let mapData = [];
// ジョイスティック用の設定変数
let joystickStart = null;
let joystickDelta = {x: 0, y: 0};
// Socket.IO を利用してサーバーに接続
const socket = io();
// ログイン処理
loginButton.addEventListener('click', function() {
const nickname = nicknameInput.value.trim();
if (nickname === "") return;
// サーバーにログインイベントを送信
socket.emit('login', { nickname: nickname });
myPlayer.nickname = nickname;
// ログイン画面を隠し、ゲームのUI(チャットやジョイスティック)を表示
loginOverlay.style.display = 'none';
chat.style.display = 'block';
chatInput.style.display = 'block';
joystick.style.display = 'block';
});
// Socket.IO の各種イベントハンドラ
socket.on('connect', () => {
console.log('Socket.IO 接続成功:', socket.id);
});
socket.on('init', (data) => {
myPlayer.id = data.id;
console.log('初期化情報:', data);
});
socket.on('state', (data) => {
// 全プレイヤーの状態を更新
players = data.players;
});
socket.on('map', (data) => {
// サーバーから受信した共有マップの設定
mapData = data.map;
});
socket.on('chat', (data) => {
// チャットメッセージを画面に表示
const msgDiv = document.createElement('div');
msgDiv.textContent = `${data.nickname}: ${data.message}`;
chat.appendChild(msgDiv);
chat.scrollTop = chat.scrollHeight;
});
// チャット入力の送信処理
chatInput.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
const message = chatInput.value.trim();
if (message !== "") {
socket.emit('chat', { message: message });
chatInput.value = '';
}
}
});
// ジョイスティックのタッチ操作
joystick.addEventListener('touchstart', function(e) {
e.preventDefault();
const touch = e.touches[0];
joystickStart = { x: touch.clientX, y: touch.clientY };
});
joystick.addEventListener('touchmove', function(e) {
e.preventDefault();
if (!joystickStart) return;
const touch = e.touches[0];
joystickDelta.x = touch.clientX - joystickStart.x;
joystickDelta.y = touch.clientY - joystickStart.y;
const maxDist = 30;
const dist = Math.sqrt(joystickDelta.x ** 2 + joystickDelta.y ** 2);
if (dist > maxDist) {
joystickDelta.x = (joystickDelta.x / dist) * maxDist;
joystickDelta.y = (joystickDelta.y / dist) * maxDist;
}
joystickInner.style.transform = `translate(${joystickDelta.x}px, ${joystickDelta.y}px)`;
});
joystick.addEventListener('touchend', function(e) {
e.preventDefault();
// 下方向のスワイプでジャンプ(地面にいる場合)
if (joystickDelta.y > 20 && myPlayer.onGround) {
myPlayer.vy = -15;
myPlayer.onGround = false;
}
// 左右移動:オフセットに応じて速度を設定
if (Math.abs(joystickDelta.x) > 5) {
myPlayer.vx = joystickDelta.x * 0.5;
myPlayer.direction = joystickDelta.x > 0 ? "right" : "left";
} else {
myPlayer.vx = 0;
}
joystickStart = null;
joystickDelta = {x: 0, y: 0};
joystickInner.style.transform = `translate(0px, 0px)`;
});
// ゲームループ処理:物理演算、状態の送信、描画処理を行う
function gameLoop() {
// 物理演算:重力や移動の計算
myPlayer.vy += 0.8;
myPlayer.x += myPlayer.vx;
myPlayer.y += myPlayer.vy;
// 地面との衝突判定(共有マップデータを利用)
let groundY = baseGroundY;
if (mapData.length > 0) {
for (let i = 0; i < mapData.length - 1; i++) {
const p1 = mapData[i];
const p2 = mapData[i+1];
if (myPlayer.x + charWidth/2 >= p1.x && myPlayer.x + charWidth/2 <= p2.x) {
const t = (myPlayer.x + charWidth/2 - p1.x) / (p2.x - p1.x);
groundY = p1.y * (1 - t) + p2.y * t;
break;
}
}
}
if (myPlayer.y + charHeight > groundY) {
myPlayer.y = groundY - charHeight;
myPlayer.vy = 0;
myPlayer.onGround = true;
}
// サーバーへ自分の状態を送信
if (socket.connected) {
socket.emit('update', {
id: myPlayer.id,
x: myPlayer.x,
y: myPlayer.y,
vx: myPlayer.vx,
vy: myPlayer.vy,
direction: myPlayer.direction
});
}
// 描画処理
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 背景(空)は canvas の背景色をそのまま使用
// 地面の描画(共有マップ)
if (mapData.length > 0) {
ctx.fillStyle = "#808080";
ctx.beginPath();
ctx.moveTo(0, canvas.height);
ctx.lineTo(mapData[0].x, mapData[0].y);
for (let i = 1; i < mapData.length; i++) {
ctx.lineTo(mapData[i].x, mapData[i].y);
}
ctx.lineTo(canvas.width, canvas.height);
ctx.closePath();
ctx.fill();
}
// 各プレイヤーの描画
for (let id in players) {
const p = players[id];
ctx.save();
if (p.direction === "right") {
ctx.translate(p.x + charWidth/2, 0);
ctx.scale(-1, 1);
ctx.drawImage(characterImg, -charWidth/2, p.y, charWidth, charHeight);
} else {
ctx.drawImage(characterImg, p.x, p.y, charWidth, charHeight);
}
ctx.restore();
}
requestAnimationFrame(gameLoop);
}
gameLoop();
// 画面リサイズに対応
window.addEventListener('resize', function() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
});
</script>
</body>
</html>
クライアント側コードのポイント
- ログイン処理:プレイヤーがゲームに参加する際、ニックネームを入力することでログイン画面が消え、チャットやジョイスティックが有効になります。
- Socket.IO による通信:サーバーとの接続確立後、
login
、update
、chat
などのイベントを通じてリアルタイム通信を行います。 - ゲームループ:
requestAnimationFrame
を利用して、物理演算(重力、移動、衝突判定)と描画処理を毎フレーム実行しています。 - キャラクター画像: 「DOTOWN」様の素材をお借りしています。

3. サーバー側コードの解説
サーバー側は Python、Flask、Flask-SocketIO を利用して実装しています。ここでは、プレイヤーの管理や共有マップの生成、クライアントからの状態更新・チャットの中継を行っています。
import random
from flask import Flask, send_from_directory, request
from flask_socketio import SocketIO, emit
app = Flask(__name__, static_url_path='', static_folder='.')
app.config['SECRET_KEY'] = 'secret!'
socketio = SocketIO(app, cors_allowed_origins="*") # CORS を許可
# グローバルなプレイヤー情報(各キーは接続時の request.sid)
players = {}
# サーバー側で生成するマップデータ(地面の凸凹ポイント群)
map_data = []
point_interval = 100
# ここでは一例としてキャンバス幅 1200px を想定
canvas_width = 1200
base_ground_y = 500 # 地面の基本の高さ
for x in range(0, canvas_width + point_interval, point_interval):
# -10〜+10 のランダムなオフセットを適用
offset = random.randint(-10, 10)
map_data.append({'x': x, 'y': base_ground_y + offset})
@app.route('/')
def index():
return send_from_directory('.', 'index.html')
@socketio.on('connect')
def handle_connect():
print(f'Client connected: {request.sid}')
# クライアントへ共有マップデータと現在のプレイヤー状態を送信
emit('map', {'map': map_data})
emit('state', {'players': players})
@socketio.on('login')
def handle_login(data):
"""
クライアントから送られてくるデータ例:
{ "nickname": "Player1" }
"""
nickname = data.get("nickname", "NoName")
# 新規ログイン時に初期位置とニックネームを設定
players[request.sid] = {
"nickname": nickname,
"x": 50,
"y": 300,
"vx": 0,
"vy": 0,
"direction": "left"
}
print(f'Client {request.sid} logged in as {nickname}')
# 接続したクライアントへ初期情報を送信
emit('init', {'id': request.sid, 'nickname': nickname})
# 最新のプレイヤー状態を全クライアントにブロードキャスト
socketio.emit('state', {'players': players})
@socketio.on('update')
def handle_update(data):
# クライアントからの状態更新(例: { "x": 100, "y": 300, "vx": 5, "vy": 0, "direction": "right" })
if request.sid not in players:
return
players[request.sid].update({
"x": data.get("x", players[request.sid]["x"]),
"y": data.get("y", players[request.sid]["y"]),
"vx": data.get("vx", 0),
"vy": data.get("vy", 0),
"direction": data.get("direction", "left")
})
# 更新された状態を全クライアントに送信
socketio.emit('state', {'players': players})
@socketio.on('chat')
def handle_chat(data):
"""
クライアントからのチャットメッセージ例:
{ "message": "Hello everyone!" }
"""
message = data.get("message", "")
nickname = players.get(request.sid, {}).get("nickname", request.sid)
chat_data = {"nickname": nickname, "message": message}
print(f"Chat from {nickname}: {message}")
socketio.emit('chat', chat_data)
@socketio.on('disconnect')
def handle_disconnect():
print(f'Client disconnected: {request.sid}')
if request.sid in players:
del players[request.sid]
socketio.emit('state', {'players': players})
if __name__ == '__main__':
# 外部からの接続を許可するためにホストを 0.0.0.0、ポート5000で起動
socketio.run(app, host='0.0.0.0', port=5000)
サーバー側コードのポイント
- Flask と Socket.IO の連携:Flask アプリケーションに Socket.IO を組み込み、リアルタイム通信を実現しています。
- 共有マップの生成:
map_data
にランダムなオフセットを持つ地面のポイント群を生成し、クライアントと共有します。 - プレイヤー管理:各クライアントごとに
request.sid
をキーとしてプレイヤー情報を保持し、login
、update
、disconnect
イベントで更新・削除を行います。 - チャット機能:クライアントからのメッセージを受け取り、全員にブロードキャストします。
4. ゲームの動作確認と導入方法
必要な環境
- Python 3.x
- Flask および Flask-SocketIO のインストール
例:
pip install flask flask-socketio
起動方法
- サーバー側のコード(Python ファイル)を保存します(例:
server.py
)。 - ターミナルで保存先ディレクトリに移動し、以下のコマンドを実行します。
python server.py
- ブラウザで クライアント側のHTMLを開くと、ログイン画面が表示されます。
- ニックネームを入力して「スタート」ボタンをクリックすると、ゲーム画面が表示され、ジョイスティック操作やチャットで他のプレイヤーと連携したオンラインゲームが開始されます。
5. まとめ
今回紹介したサンプルは、シンプルながらもオンラインゲームの基本的な要素(リアルタイム通信、物理演算、マップ生成、チャット機能など)を含んでいます。初心者の方でもこのコードを通して、HTML/CSS/JavaScript によるフロントエンドの構築と、Python/Flask を使ったバックエンドの実装方法を学ぶことができます。ぜひ、実際にコードを動かしてみて、必要に応じてカスタマイズしながら自分だけのゲームを作ってみましょう!
関連記事