Ursina Engine 3次元ゲームエンジン基礎
【概要】
本教材は、Python 用 3 次元ゲームエンジン Ursina Engine を用いた 10 項目のオンライン演習で構成される。座標系、カメラ、メッシュ・テクスチャ、ライティング、エンティティの階層、入力処理、アニメーションと物理、簡易ゲーム、データ可視化を体験的に扱い、3 次元コンピュータグラフィックス(3DCG)とゲームエンジンの基本概念を習得することを目的とする。各演習は《手順》《ヒント》《考察ポイント》で構成する。
【目次】
1. はじめに
予備知識
Ursina Engine は Python で 3 次元コンピュータグラフィックスとゲームを扱うためのエンジンである。少ないコードで 3D 空間にオブジェクトを配置し、動かし、ユーザー入力に反応させることができる。
用語解説
- フレーム:画面を更新する 1 コマ。一般的なゲームは毎秒 60 コマ(60fps、fps は frames per second の略)程度で連続更新する。
- デルタ時間(time.dt):直前のフレームから現在のフレームまでの経過秒数。
- update():毎フレーム自動的に呼ばれる関数。動きや状態の更新をここに記述する。
- フレームレート非依存:移動量に time.dt を乗じる(掛け合わせる)ことで、PC の処理速度に依存せず一定速度の動作を実現する考え方。
Ursina プログラムの基本構造は次のとおりである。
┌─────────────────────────────┐
│ 1. モジュールのインポート │
│ from ursina import * │
└─────────────────────────────┘
↓
┌─────────────────────────────┐
│ 2. アプリケーションの初期化 │
│ app = Ursina() │
└─────────────────────────────┘
↓
┌─────────────────────────────┐
│ 3. エンティティの作成 │
│ Entity(...) │
└─────────────────────────────┘
↓
┌─────────────────────────────┐
│ 4. アプリケーションの実行 │
│ app.run() │
└─────────────────────────────┘
前準備
Python 3.12 のインストール
Pythonのインストールを行い、Pythonのプログラムを実行する環境を整える。扱う環境は、Windows搭載パソコンである。金子研究室では、Python 3.12.10を推奨する。
[Windows での Python 3.12 のインストール手順を見るには、ここをクリック]
Windows での Python 3.12 のインストール
以下のいずれかの方法でPython 3.12をインストールする。Pythonがインストール済みの場合、この手順は不要である。
方法 1:winget によるインストール
【インストールコマンドの実行方法】
管理者権限でコマンドプロンプトを起動する(手順:Windowsキーまたはスタートメニュー → cmd と入力 → 右クリック → 「管理者として実行」)。そして、コマンド全体をコマンドプロンプトにコピー&ペーストする。
--scope machine を指定することで、システム全体(全ユーザー向け)にインストールされる。このオプションの実行には管理者権限が必要である。インストール完了後、コマンドプロンプトを再起動するとPATHが反映される。
REM Python 3.12 をシステム領域にインストール
winget install --id Python.Python.3.12 -e --scope machine --silent --accept-source-agreements --accept-package-agreements --override "/quiet InstallAllUsers=1 PrependPath=1 Include_test=0 Include_pip=1 Include_launcher=1 InstallLauncherAllUsers=1 TargetDir=\"C:\Program Files\Python312\""
REM Python と Scripts を PATH 先頭に追加
powershell -NoProfile -Command "$p='C:\Program Files\Python312'; $s=\"$p\Scripts\"; $c=[Environment]::GetEnvironmentVariable('Path','Machine'); if((Test-Path $p) -and (';'+$c+';' -notlike \"*;$p;*\") -and (';'+$c+';' -notlike \"*;$s;*\")){[Environment]::SetEnvironmentVariable('Path',\"$p;$s;$c\",'Machine')}"
方法 2:インストーラーによるインストール
- Python公式サイト(https://www.python.org/downloads/)にアクセスし、「Download Python 3.x.x」ボタンからWindows用インストーラーをダウンロードする。
- ダウンロードしたインストーラーを実行する。
- 初期画面の下部に表示される「Add python.exe to PATH」にチェックを入れてから「Customize installation」を選択する。このチェックを入れ忘れると、コマンドプロンプトから
pythonコマンドを実行できない。 - 「Install Python 3.xx for all users」にチェックを入れ、「Install」をクリックする。
インストールの確認
コマンドプロンプトで以下を実行する。
python --version
バージョン番号(例:Python 3.12.x)が表示されればインストール成功である。「'python' は、内部コマンドまたは外部コマンドとして認識されていません。」と表示される場合は、インストールが正常に完了していない。
Python の開発環境 Visual Studio Code のインストールと Python 用の設定
Python の開発環境Visual Studio Code(プログラムを編集するソフトウェア。以下、VS Code)を整える。
[Windows での Visual Studio Code のインストールと Python 用の設定手順を見るには、ここをクリック]
Windows での Visual Studio Code のインストールと Python 用の設定手順
1. VS Code と拡張機能のインストール
以下のコマンドにより,既存の VS Code を削除し,全ユーザー共有の設定で再インストールしたうえで,拡張機能(VS Code に機能を追加するソフトウェア)をまとめて導入する.
【インストールコマンドの実行方法】
管理者権限でコマンドプロンプトを起動する(手順:Windows キーまたはスタートメニュー → cmd と入力 → 右クリック → 「管理者として実行」)。そして,コマンド全体をコマンドプロンプトにコピー&ペーストする。
インストールコマンド
REM ============================================================
REM Microsoft Visual Studio Code
REM ============================================================
winget uninstall -e --id Microsoft.VisualStudioCode --silent --disable-interactivity --accept-source-agreements
rmdir /s /q C:\ProgramData\vscode-extensions 2>nul
rmdir /s /q "%APPDATA%\Code" 2>nul
rmdir /s /q "%USERPROFILE%\.vscode" 2>nul
rmdir /s /q "%LOCALAPPDATA%\Microsoft\vscode-update" 2>nul
REM VS Code をシステム領域に新規インストール
winget install --scope machine --id Microsoft.VisualStudioCode -e --silent --accept-source-agreements --accept-package-agreements
REM 全ユーザー共有の拡張機能フォルダ
mkdir C:\ProgramData\vscode-extensions 2>nul
icacls "C:\ProgramData\vscode-extensions" /grant "Everyone:(OI)(CI)M" /T
REM スタートメニューのショートカットを --extensions-dir 付きで再作成
rmdir /s /q "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Visual Studio Code" 2>nul
del "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Visual Studio Code.lnk" 2>nul
powershell -NoProfile -Command "$s=New-Object -ComObject WScript.Shell; $lnk=$s.CreateShortcut('C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Visual Studio Code.lnk'); $lnk.TargetPath='C:\Program Files\Microsoft VS Code\Code.exe'; $lnk.Arguments='--extensions-dir \"C:\ProgramData\vscode-extensions\"'; $lnk.Save()"
REM ショートカットの検証
powershell -NoProfile -Command "$s=New-Object -ComObject WScript.Shell; $lnk=$s.CreateShortcut('C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Visual Studio Code.lnk'); Write-Host 'TargetPath:' $lnk.TargetPath; Write-Host 'Arguments:' $lnk.Arguments"
REM ファイル / フォルダ右クリックの「Code で開く」を登録
reg add "HKLM\SOFTWARE\Classes\*\shell\VSCode\command" /ve /d "\"C:\Program Files\Microsoft VS Code\Code.exe\" --extensions-dir \"C:\ProgramData\vscode-extensions\" \"%1\"" /f
reg add "HKLM\SOFTWARE\Classes\Directory\shell\VSCode\command" /ve /d "\"C:\Program Files\Microsoft VS Code\Code.exe\" --extensions-dir \"C:\ProgramData\vscode-extensions\" \"%1\"" /f
reg add "HKLM\SOFTWARE\Classes\Directory\Background\shell\VSCode\command" /ve /d "\"C:\Program Files\Microsoft VS Code\Code.exe\" --extensions-dir \"C:\ProgramData\vscode-extensions\" \"%V\"" /f
REM --extensions-dir 付きで起動する code.cmd ラッパを作成
REM (%* を echo で書くと対話的 cmd で失われるため、PowerShell で [char]37+'*' を書き出す)
powershell -NoProfile -Command "$pct=[char]37; $q=[char]34; $c='@echo off'+[char]13+[char]10+$q+'C:\Program Files\Microsoft VS Code\bin\code.cmd'+$q+' --extensions-dir '+$q+'C:\ProgramData\vscode-extensions'+$q+' '+$pct+'*'+[char]13+[char]10; [IO.File]::WriteAllText('C:\ProgramData\vscode-extensions\vscode.cmd',$c,[Text.Encoding]::ASCII)"
REM 拡張機能のインストール
set "CODE=C:\Program Files\Microsoft VS Code\bin\code.cmd"
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --uninstall-extension GitHub.copilot
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --uninstall-extension GitHub.copilot-chat
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension ms-python.python
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension ms-python.vscode-pylance
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension ms-python.debugpy
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension MS-CEINTL.vscode-language-pack-ja
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension saoudrizwan.claude-dev
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension rust-lang.rust-analyzer
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension tamasfe.even-better-toml
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension anthropic.claude-code
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension almenon.arepl
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --list-extensions --show-versions
echo === セットアップ完了 ===
2. Python インタプリタの選択
同一マシンに複数の Python がインストールされている場合,VS Code で使用する Python 本体(インタプリタ:Python プログラムを解釈・実行するソフトウェア)を選択する必要がある.
- コマンドパレット(コマンド名で機能を呼び出す VS Code の入力欄)を開く(
Ctrl+Shift+P) Python: Select Interpreterと入力する
- 表示される一覧から,使用する Python(例:
C:\Program Files\Python312\python.exe)を選択する.
Python プログラム実行手順
[Windows での Python プログラム実行手順を見るには、ここをクリック]
Windows での Python 実行手順(Visual Studio Codeを使用)
プログラムファイルの作成と保存
- 左サイドバーの「エクスプローラー」アイコン(
Ctrl+Shift+E)をクリックする
- 「NO FOLDER OPENED」(作業対象フォルダが未選択の状態)と表示される場合は,「Open Folder」をクリックし,プログラムを保存するフォルダを選択する
続いて「フォルダを信用するか」を確認する画面(フォルダ内のコードを実行してよいか確認する VS Code の仕組み)が表示されるので,チェックして Yes を選択する
- フォルダ名の右側に表示される「新しいファイル」アイコンをクリックする
- ファイル名(例:
aitask.py.ファイル名は何でも良い)を入力しEnterを押す.拡張子は.py(Python ファイルを示す拡張子)とする
- 実行したいコードを選択し,
Ctrl+Cでコピーする.VS Code のエディタ領域にCtrl+Vで貼り付ける Ctrl+Sで保存する
プログラムの実行
- エディタ右上の三角形「▷」アイコン(Run Python File:現在開いている Python ファイルを実行するボタン)をクリックする.または,エディタ上で右クリックし「ターミナルで Python ファイルを実行」を選択する
- VS Code 下部のターミナル(コマンドの入出力を表示する画面)に,実行結果(
print関数の出力等)が表示される
- tkinter(Python 標準の GUI ライブラリ)のファイル選択ダイアログを使うプログラムを実行した場合は,ダイアログが開くので対象画像を選択する
- VS Code 下部のターミナルで実行結果を確認する.OpenCV ウィンドウ(OpenCV が画像を表示するために開く専用ウィンドウ)が開いた場合はそちらも確認する.OpenCV ウィンドウは,マウスクリックでウィンドウをアクティブ(操作対象の状態)にしてからキーを押すと終了する
Ursina のインストール
10 個の演習はすべて Ursina Engine を使う。Ursina は Python 用の 3 次元ゲームエンジンである。インストールは 1 回だけ行えばよい。
管理者権限でコマンドプロンプトを起動する
(手順:Windowsキーまたはスタートメニュー → cmd と入力 → 右クリック → 「管理者として実行」)。起動したコマンドプロンプトに次を入力して実行する。
pip install --no-user ursina
演習1. Ursina プログラムの実行
各演習のコードを実行する方法は 2 通りある。どちらでも動作する。
方法A:Visual Studio Code で実行する。Visual Studio Code がインストール・設定済みであれば、本ページのコードを編集画面にコピー&ペーストし、そのまま実行する。
方法B:ファイルに保存して実行する。メモ帳などのテキストエディタを開き、本ページのコードを貼り付ける。a.py のようなファイル名で保存する。コマンドプロンプトでそのファイルがあるフォルダに移動し、次を入力して実行する。
python a.py
演習1-1:最小プログラムの実行
《手順》
- 下記のコードを上記のいずれかの方法で実行する。
- 立方体が回転していることを確認する。ウィンドウを閉じるには、Alt+F4 キーを押す。
from ursina import *
# アプリケーションの初期化
app = Ursina()
# 回転する立方体
cube = Entity(
model='cube',
color=color.orange,
position=(0, 0, 5)
)
# 毎フレーム実行される関数
def update():
cube.rotation_y += 50 * time.dt
cube.rotation_x += 30 * time.dt
# アプリケーションの実行
app.run()
《ヒント》
- 回転速度の数値(50, 30)を変えて、回転の速さがどう変化するかを確かめる。これらは 1 秒あたりの回転角度(度)である。
position=(0, 0, 5)の Z 値を変えるとカメラからの距離が変わる。
《考察ポイント》
- 回転速度の式に
time.dtを掛ける理由は何か。掛けない場合に何が起こるかを実際に試して観察する。 update()がフレーム単位で呼ばれる仕組みは、通常の Python スクリプトとどのように違うか。
演習1-2:シーンの構成要素を確認する
《手順》
- 下記のコードを実行し、地面・複数オブジェクト・テキストが同時に表示されることを確認する。
- カメラの位置や色相(色合い)の値を変えて再実行し、見え方の違いを観察する。
from ursina import *
app = Ursina()
# 地面
ground = Entity(
model='plane',
scale=(10, 1, 10),
color=color.green,
texture='grass'
)
# 複数のオブジェクト
for i in range(3):
Entity(
model='cube',
color=color.hsv(i * 120, 1, 1),
position=(i * 2 - 2, 0.5, 3)
)
# テキスト表示
Text(
text='Ursina Engine Test',
position=(-0.5, 0.4),
scale=2
)
camera.position = (0, 5, -8)
camera.look_at((0, 0, 3))
app.run()
《ヒント》
camera.positionの Y 値を大きくすると俯瞰(上から見下ろす視点)、小さくすると水平視点になる。color.hsv(i * 120, 1, 1)の第 1 引数120は色相(HSV 色空間の H 成分、0〜360 度)を指定する値で、別の値にすると色合いが変わる。第 2 引数は彩度(S、0〜1、鮮やかさ)、第 3 引数は明度(V、0〜1、明るさ)である。
《考察ポイント》
- 3D 空間に配置されるオブジェクト(地面・立方体)と、画面に直接重ねて描かれる Text の役割の違いを整理する。
- シーン全体を構成する最小要素は何か(カメラ、エンティティの関係)を考える。
3D座標系とトランスフォーム(位置、回転、スケール)
予備知識
3 次元空間における点の位置は、互いに直交する X 軸、Y 軸、Z 軸の 3 つの数値の組 (x, y, z) で一意に表される。これを直交座標系という。
用語解説
- 直交座標系:3 つの軸が互いに直角に交わる座標系。
- 位置(position):オブジェクトの空間内での座標 (x, y, z)。
- 回転(rotation):オブジェクトの向き。各軸まわりの角度(度)で指定する。
- スケール(scale):各軸方向の拡大率。1.0 が原寸、2.0 で 2 倍、0.5 で半分。
- 左手座標系:左手の親指・人差し指・中指を互いに直角に開いたとき、それぞれが X・Y・Z 軸の正の方向に対応する座標系。Unity や Ursina など、多くのゲームエンジンが採用する。
Ursina は Y 軸が上、Z 軸が前(画面奥)を向く左手座標系を採用する(Unity と同じ規約)。
Y (上)
|
|
+---- Z (前)
/
/
X (右)
| 軸 | 正の方向 | 用途 |
|---|---|---|
| X 軸 | 右 | 左右の位置 |
| Y 軸 | 上 | 高さ |
| Z 軸 | 前(画面奥) | 前後の位置 |
演習2:位置・回転・スケールを操作する
《手順》
- 下記のコードを実行し、立方体の位置・回転・スケールを確認する。
position、rotation_y、scaleの値をそれぞれ別個に変更し、変化を観察する。camera.positionも変えて、見え方の違いを比較する。
from ursina import *
app = Ursina()
# エンティティの作成と配置
box = Entity(
model='cube',
color=color.orange,
position=(0, 1, 5) # X, Y, Z座標
)
# 回転の設定
box.rotation_y = 45 # Y軸周りに45度回転
# スケールの設定
box.scale = (2, 1, 1) # X方向に2倍
# 相対移動
box.position += Vec3(2, 0, 0) # 右に2単位移動
# カメラ設定
camera.position = (5, 3, -10)
camera.look_at(box)
app.run()
《ヒント》
box.position += Vec3(2, 0, 0)は「現在位置からの相対移動(今いる場所を基準にした移動)」を表す。=による直接代入(座標を絶対値で上書きする書き方)との違いに注意する。scaleをタプル (2, 1, 1) で与えると軸ごとに倍率を変えられる。
《考察ポイント》
- 位置・回転・スケールの 3 要素はどの順番で適用されるかを観察する(特にスケールと回転の組み合わせ)。
- 絶対座標(世界の原点を基準にした座標)で動かす書き方と相対座標(現在位置を基準にした座標)で動かす書き方の使い分けを整理する。
カメラとビューポート
予備知識
用語解説
- 視点(camera position):3D 空間を見る位置。
- 注視点:カメラが向く先の点。
camera.look_at()で指定する。 - ビューポート:3D シーンが描画される画面上の領域(実際にゲーム画面が表示される矩形範囲)。
- FirstPersonController:一人称視点(プレイヤー自身の目で見ているような視点)を実現する Ursina の組み込みクラス。内部で重力と地面判定を行うため、地面エンティティには
collider(衝突判定形状)の指定が必要である。
演習3:一人称視点でシーンを歩く
《手順》
- 下記のコードを実行する。
- WASD キーで移動、マウスで視点回転を行い、空間を歩き回る。
- ウィンドウを閉じるには、Alt+F4 キーを押す。
from ursina import *
from ursina.prefabs.first_person_controller import FirstPersonController
app = Ursina()
# テスト用のオブジェクト配置
for i in range(5):
Entity(
model='cube',
color=color.random_color(),
position=(i * 3, 1, 0),
collider='box'
)
# 地面(プレイヤーの初期位置が縁に来ないよう十分な大きさを確保する)
ground = Entity(
model='plane',
scale=(40, 1, 40),
color=color.gray,
collider='box'
)
# 一人称視点コントローラー(初期位置はコンストラクタで指定し,生成時の接地補正を機能させる)
player = FirstPersonController(position=(0, 2, -10))
app.run()
《ヒント》
player.positionの初期値を変えるとスタート地点が変わる。- 立方体の数や配置を変更すると、空間スケール(広さ・大きさの感覚)が変わる。
- 地面や障害物に
colliderを設定しないと、プレイヤーは床を貫通して落下する。
《考察ポイント》
- 一人称視点と固定カメラ(演習 2)では、空間把握の体験にどのような違いがあるか。
- 同じシーンでもカメラの扱い方によって「ゲーム」と「3D ビューア」の印象がどう変わるかを比較する。
メッシュ、マテリアル、テクスチャ
予備知識
用語解説
- メッシュ:3D 形状を構成する頂点と面の集合。
modelで指定する。 - テクスチャ:表面に貼る 2D 画像。質感を表現する。
- マテリアル:表面の見え方を決める属性の集合。Ursina では color と texture の組で表現される。
Ursina に組み込まれている基本モデルは次のとおりである。
| モデル名 | 形状 | 用途 |
|---|---|---|
| 'cube' | 立方体 | 建物、箱、障害物 |
| 'sphere' | 球体 | ボール、惑星、収集アイテム |
| 'plane' | 平面 | 地面、壁、床 |
| 'cylinder' | 円柱 | 柱、木、タワー |
| 'quad' | 四角形 | 看板、パネル、UI |
演習4:テクスチャと色の組み合わせ
《手順》
- 下記のコードを実行する。
- テクスチャ付き立方体・色付き球体・テクスチャ付き地面の見え方を確認する。
- 球体に
texture='white_cube'を追加して、color と texture を併用したとき(テクスチャの上から色が乗算される様子)の見え方を確かめる。
from ursina import *
app = Ursina()
# テクスチャ付き立方体
textured_cube = Entity(
model='cube',
texture='white_cube',
position=(-3, 1, 0)
)
# 色付き球体
colored_sphere = Entity(
model='sphere',
color=color.orange,
position=(0, 1, 0)
)
# 回転アニメーション
def update():
textured_cube.rotation_y += 30 * time.dt
colored_sphere.rotation_x += 50 * time.dt
# 地面
Entity(
model='plane',
texture='grass',
scale=(10, 1, 10)
)
camera.position = (0, 5, -10)
camera.look_at((0, 1, 0))
app.run()
《ヒント》
textureとcolorを同時に指定するとテクスチャの色にcolorが乗算される(画素ごとに掛け合わされ、テクスチャがcolorで着色されたように見える)。- 地面のスケールを大きくして、テクスチャがタイル状に並ぶ様子を観察する。
《考察ポイント》
- 同じ立方体モデルでも、テクスチャの有無で立体感の知覚はどう変わるか。
- テクスチャと色の乗算は、どのような表現に応用できそうか具体例を挙げる。
ライティングとシェーディング
予備知識
用語解説
- 環境光(AmbientLight):全方向から均一に照らす光。影は作らない。
- 指向性光源(DirectionalLight):太陽光のように平行に降り注ぐ光。
- シェーディング:光源の影響を計算して表面に明暗をつける処理。Ursina のエンティティは既定では光の影響を受けないシェーダー(unlit_with_fog_shader)を使うため、光源の効果を反映させるには、光の影響を受けるシェーダー(
lit_with_shadows_shaderなど)をエンティティに適用する必要がある。
| 光源の種類 | 特徴 | 用途 |
|---|---|---|
| AmbientLight | 全方向を均一に照らす | 全体の明るさ確保 |
| DirectionalLight | 平行光線(太陽光) | 主光源 |
| PointLight | 点から全方向に放射 | 電球、松明 |
演習5:環境光と指向性光源の効果
《手順》
- 下記のコードを実行する。
AmbientLightのcolor値をcolor.rgba32(50, 50, 50, 255)やcolor.rgba32(200, 200, 200, 255)に変えて違いを確認する(4 つの数値は順に R・G・B・A、各 0〜255。値が大きいほど明るい)。sun.look_at(Vec3(...))の方向ベクトル(光が進む向き)を変え、影の付き方を比較する。
from ursina import *
from ursina.shaders import lit_with_shadows_shader
app = Ursina()
# 以降に作成するエンティティに、光源の影響を受けるシェーダーを既定で適用する
Entity.default_shader = lit_with_shadows_shader
# 環境光(0〜255の整数値はrgba32で指定する)
AmbientLight(color=color.rgba32(100, 100, 100, 255))
# 指向性光源
sun = DirectionalLight()
sun.look_at(Vec3(1, -1, 1))
# 地面
Entity(
model='plane',
scale=(20, 1, 20),
color=color.gray,
texture='white_cube'
)
# オブジェクトの配置
for i in range(5):
Entity(
model='sphere',
position=(i * 3 - 6, 1, 0),
color=color.random_color()
)
camera.position = (0, 10, -20)
camera.look_at((0, 0, 0))
app.run()
《ヒント》
- 環境光だけにすると陰影が消え、立体感が乏しくなる。
- 指向性光源の向きは
look_at()に与えるベクトル(光の進む方向)で決まる。
《考察ポイント》
- 同じ形状でもライティングが変わると印象がどう変化するか。
- 環境光のみ/指向性光源のみ/両方の 3 パターンを比較し、それぞれが担う役割を整理する。
エンティティ(Entity)の生成と制御
予備知識
用語解説
- エンティティ(Entity):Ursina における全 3D オブジェクトの基底クラス。Unity の GameObject、Unreal の Actor に相当する。
- 親子関係(parent):オブジェクト間の階層関係。子は親の変換(移動・回転・スケール)の影響を受ける。
- 変換の伝播:親への変換が全ての子に自動的に適用される性質(親を動かすと子も一緒に動く)。
階層構造の例:
vehicle (親)
|
+-- body (車体)
|
+-- wheel_fl (前左車輪)
|
+-- wheel_fr (前右車輪)
|
+-- wheel_rl (後左車輪)
|
+-- wheel_rr (後右車輪)
親を移動 → 全ての子も移動
子を移動 → 親は影響を受けない
演習6:親子関係を使った車両モデル
《手順》
- 下記のコードを実行し、車体と 4 輪が一体で前進することを確認する。
- 4 輪の 1 つだけを
parent=vehicleから外し、再実行する。挙動の違いを観察する。 vehicle.rotation_yをupdate()内で変化させ、車体ごとの回転を試す。
from ursina import *
app = Ursina()
# 親エンティティ(車体)
vehicle = Entity()
# 車体本体
body = Entity(
model='cube',
color=color.red,
scale=(2, 1, 3),
parent=vehicle
)
# 車輪の作成(4つ)
wheel_positions = [
(-1, -0.5, 1), # 前左
(1, -0.5, 1), # 前右
(-1, -0.5, -1), # 後左
(1, -0.5, -1) # 後右
]
for pos in wheel_positions:
Entity(
model='sphere',
color=color.black,
scale=0.5,
position=pos,
parent=vehicle
)
# 親を移動すると全ての子も移動する
def update():
vehicle.z += time.dt * 3
if vehicle.z > 10:
vehicle.z = -10
camera.position = (0, 5, -15)
camera.look_at(vehicle)
app.run()
《ヒント》
- 子の
positionは親に対する相対座標である(親の原点を基準に測った位置)。 - 親に回転を加えると、子の相対位置を保ったまま全体が回転する。
《考察ポイント》
- 階層構造を使うことで、コードはどのように簡潔になるか(4 輪を個別管理する場合と比較する)。
- 太陽系(太陽 → 惑星 → 衛星)を表現するには、どのような親子関係が適切か設計する。
入力処理(キーボード、マウス)
予備知識
Ursina では、押し続けの入力を held_keys、押した瞬間の入力を input() 関数で扱う。
| 入力方法 | 用途 | 例 |
|---|---|---|
| held_keys | 継続的な入力(押し続ける) | 移動、回転 |
| input() 関数 | 単発の入力(押した瞬間) | ジャンプ、攻撃 |
| キー | 記述 | 用途 |
|---|---|---|
| W/A/S/D | 'w', 'a', 's', 'd' | 移動 |
| スペース | 'space' | ジャンプ |
| マウス左 | 'left mouse down' | 選択、攻撃 |
| ESC | 'escape' | ポーズメニューなど、スクリプト側で処理を定義したときの終了・メニュー用キー |
演習7:移動とジャンプの実装
《手順》
- 下記のコードを実行する。
- WASD で移動、Space でジャンプの動作を確認する。
jump_force = 5の値(ジャンプ初速度)や重力-20(重力加速度)を変更し、跳躍と落下の挙動の違いを観察する。
from ursina import *
app = Ursina()
# プレイヤー
player = Entity(
model='cube',
color=color.azure,
position=(0, 1, 0)
)
# 地面
Entity(
model='plane',
scale=(20, 1, 20),
color=color.green
)
# 移動速度とジャンプ
speed = 5
jump_force = 0
is_jumping = False
def input(key):
global jump_force, is_jumping
if key == 'space' and not is_jumping:
jump_force = 5
is_jumping = True
def update():
global jump_force, is_jumping
# キーボードによる移動
if held_keys['w']:
player.z += speed * time.dt
if held_keys['s']:
player.z -= speed * time.dt
if held_keys['a']:
player.x -= speed * time.dt
if held_keys['d']:
player.x += speed * time.dt
# ジャンプと重力
jump_force += -20 * time.dt
player.y += jump_force * time.dt
if player.y <= 1:
player.y = 1
jump_force = 0
is_jumping = False
camera.position = (0, 5, -15)
camera.look_at(player)
app.run()
《ヒント》
is_jumpingフラグを使うことで、空中での連続ジャンプを抑止している(地面に着いていないときは再度ジャンプできない)。- 移動量に
time.dtを掛けないと、PC の速度に応じて移動量が変わってしまう。
《考察ポイント》
- 移動を
held_keys、ジャンプをinput()で処理する理由を整理する。 - 2 段ジャンプ(空中でもう一度ジャンプできる仕組み)を実装するには、どの変数をどのように変更すればよいか。
アニメーションと物理演算
予備知識
用語解説
- 速度(velocity):単位時間あたりの移動量(ベクトル)。
- 加速度:速度の変化率。重力などで生じる。
- 重力加速度:地表でおよそ -9.8 m/s²(下向き)。
- 反発係数:衝突時の跳ね返り具合。0 で跳ねず、1 でエネルギー損失なし。
- コライダー(collider):衝突判定用の単純化された形状(実際の見た目とは別に用意される、当たり判定専用の形)。
- 衝突判定(intersects):2 つのオブジェクトが接触しているかを判定する処理。
物理シミュレーションの基本的な流れは次のとおりである(dt はデルタ時間)。
1. 加速度を速度に加算
velocity += acceleration * dt
2. 速度を位置に加算
position += velocity * dt
3. 衝突判定
if 衝突:
位置補正
速度反転(反発係数を乗算)
演習8-1:時間関数を使った周期アニメーション
《手順》
- 下記のコードを実行する。
- 3 種類のアニメーション(回転・上下移動・拡大縮小)を観察する。
math.sinの引数の係数(3、2)と振幅(0.5、0.3)を変更し、動きの違いを確認する。
from ursina import *
import math
app = Ursina()
# 地面
Entity(
model='plane',
scale=(20, 1, 20),
color=color.gray
)
# 回転するキューブ
rotating_cube = Entity(
model='cube',
color=color.red,
position=(-4, 1, 0)
)
# 上下移動する球体
bouncing_sphere = Entity(
model='sphere',
color=color.blue,
position=(0, 1, 0)
)
# 拡大縮小する円柱
scaling_cylinder = Entity(
model='cylinder',
color=color.green,
position=(4, 1, 0)
)
def update():
# 回転アニメーション
rotating_cube.rotation_y += 100 * time.dt
# 上下移動アニメーション
bouncing_sphere.y = 1 + math.sin(time.time() * 3) * 0.5
# 拡大縮小アニメーション
scale_factor = 1 + math.sin(time.time() * 2) * 0.3
scaling_cylinder.scale_y = scale_factor
camera.position = (0, 5, -12)
camera.look_at((0, 1, 0))
app.run()
《ヒント》
math.sin(time.time() * k)のkは周波数(振動の速さ)、外側に掛ける係数(0.5 など)は振幅(揺れ幅)に対応する。- 回転は加算、周期運動(行ったり来たりする動き)は値の代入で実装する点に着目する。
《考察ポイント》
- 「時間の経過に対して値を計算する」方式と「毎フレーム値を加算する」方式の違いを整理する。
sinをcosに置き換えると、何が変わり何が変わらないか。
演習8-2:重力と反発による落下
《手順》
- 下記のコードを実行する。
- 箱が落下し、地面でバウンドして徐々に止まる様子を観察する。
- 反発係数 0.5(コード中の
* 0.5)を 0.9、0.1 に変えて挙動を比較する。
from ursina import *
app = Ursina()
# 背景とライト(箱を立体的に見せる)
Sky()
DirectionalLight(rotation=(45, -45, 0))
AmbientLight(color=color.rgba(120, 120, 120, 0.15))
# 地面(グリッド柄で奥行きを分かりやすく)
ground = Entity(
model='plane',
scale=(20, 1, 20),
color=color.green,
texture='white_cube',
texture_scale=(20, 20),
collider='box'
)
# 落下する箱
boxes = []
for i in range(5):
box = Entity(
model='cube',
color=color.random_color(),
position=(i * 2 - 4, 5 + i, 0), # y=5〜9(画角内に収まる高さ)
collider='box'
)
box.velocity = Vec3(0, 0, 0) # カスタム属性として速度を追加
boxes.append(box)
gravity = -9.8
def update():
for box in boxes:
# 重力を適用
box.velocity.y += gravity * time.dt
box.position += box.velocity * time.dt
# 地面との衝突判定(箱の底=地面上面 y=0、立方体の半分=0.5 で停止)
if box.y < 0.5:
box.y = 0.5
if box.velocity.y < 0:
box.velocity.y = -box.velocity.y * 0.5 # 反発(減衰)
# 十分遅くなったら停止
if abs(box.velocity.y) < 1:
box.velocity.y = 0
# カメラ(落下開始から着地まで全体が収まる位置・向き)
camera.position = (0, 7, -22)
camera.rotation_x = 13
# 自由に視点を動かしたい場合は上の2行を消して次を有効化:
# EditorCamera()
app.run()
《ヒント》
colliderを設定したオブジェクト同士でないと衝突判定は機能しない。velocityはエンティティに後から追加した属性であり、Ursina の標準機能ではない(Python の動的属性追加を利用している)。
《考察ポイント》
- 反発係数を 1 にすると永久にバウンドし続けるか、実際に試して結果を考察する。
- 水平方向の速度(
velocity.x)を初期値で与えるとどうなるか。
ゲーム制作(簡易的な3Dアクションゲーム)
予備知識
3D ゲームは、プレイヤー、環境(地面・障害物)、ゲームロジック(スコア・クリア条件)、入出力(操作・表示)を組み合わせて構成される。本演習ではアイテム収集型の最小構成を扱う。
演習9:アイテム収集ゲーム
《手順》
- 下記のコードを実行する。
- WASD でプレイヤーを動かし、金色の球体を集めてスコアを増やす。
- 全アイテムを取ると「Game Clear!」が表示されることを確認する。
from ursina import *
from random import uniform
app = Ursina()
# プレイヤー
player = Entity(
model='cube',
color=color.azure,
position=(0, 1, 0),
collider='box'
)
# 地面
Entity(
model='plane',
scale=(50, 1, 50),
color=color.green,
texture='grass',
collider='box'
)
# 収集アイテム
collectibles = []
for i in range(10):
item = Entity(
model='sphere',
color=color.gold,
position=(uniform(-20, 20), 0.5, uniform(-20, 20)),
scale=0.5,
collider='sphere'
)
collectibles.append(item)
# スコア表示
score = 0
score_text = Text(
text=f'Score: {score}',
position=(-0.85, 0.45),
scale=2
)
# ゲームクリア判定用フラグ
game_clear = False
speed = 5
# カメラの初期設定(プレイヤーを見下ろす三人称視点)
camera.position = player.position + Vec3(0, 10, -15)
camera.look_at(player)
def update():
global score, game_clear
# クリア後は処理を止める
if game_clear:
return
# プレイヤー移動
if held_keys['w']:
player.z += speed * time.dt
if held_keys['s']:
player.z -= speed * time.dt
if held_keys['a']:
player.x -= speed * time.dt
if held_keys['d']:
player.x += speed * time.dt
# アイテム収集判定
for item in collectibles[:]:
if player.intersects(item).hit:
destroy(item)
collectibles.remove(item)
score += 10
score_text.text = f'Score: {score}'
# ゲームクリア判定
if len(collectibles) == 0:
score_text.text = 'Game Clear!'
game_clear = True
# カメラがプレイヤーを追従(毎フレーム位置と向きを更新)
camera.position = player.position + Vec3(0, 10, -15)
camera.look_at(player)
app.run()
《ヒント》
collectibles[:]はリストのコピーを反復するための書き方である(ループ中にremove()で元のリストを変更しても安全に処理を続けられる)。- カメラを
update()内でplayer.position + Vec3(...)に追従させると、追跡カメラ(プレイヤーを追いかける視点)に改造できる。
《考察ポイント》
- 制限時間を設けるには、どこにどのような変数と判定を加えればよいか設計する。
- 動く敵を 1 体追加してゲーム性を高める方針を考える(衝突時にゲームオーバー等)。
ビジュアライゼーション(データの3D可視化)
予備知識
3D 空間で数値を表現すると、大小関係や分布を視覚的に把握できる。代表的な手法は次のとおりである。
| 可視化方法 | 適したデータ | 表現方法 |
|---|---|---|
| 棒グラフ | 離散的な数値 | 高さで値を表現 |
| 散布図 | 3 次元座標データ | 位置と色で表現 |
| ヒートマップ | 格子状のデータ | 色の濃淡で表現 |
| ネットワーク図 | 関係性データ | ノードと線で表現 |
演習10:3D 棒グラフによるデータ可視化
《手順》
- 下記のコードを実行する。
- 10 個の値が高さの異なる棒として表示されることを確認する。
- リスト
dataの値を自分のデータに置き換えて再実行する。
from ursina import *
app = Ursina()
# データの可視化(棒グラフ)
data = [3, 7, 5, 9, 4, 6, 8, 2, 10, 6]
bars = []
for i, value in enumerate(data):
bar = Entity(
model='cube',
color=color.hsv(i * 36, 1, 1),
position=(i * 1.5 - 6.75, value / 2, 0),
scale=(1, value, 1)
)
# ラベル(3D空間内に配置するため、既定のUI空間ではなくシーン空間を親にする)
Text(
text=str(value),
parent=scene,
position=(bar.x, value + 0.5, 0),
scale=3,
billboard=True
)
bars.append(bar)
# グリッド(基準面)
Entity(
model='plane',
scale=(20, 1, 10),
color=color.dark_gray,
texture='white_cube',
alpha=0.3
)
# アニメーション
def update():
for i, bar in enumerate(bars):
bar.rotation_y += (i + 1) * 10 * time.dt
camera.position = (0, 8, -15)
camera.look_at((0, 5, 0))
app.run()
《ヒント》
- 棒の中心 Y を
value / 2、高さ(scaleの Y 成分)をvalueにすることで、棒が地面から立ち上がる形になる。 billboard=True(ビルボード:常にカメラを向くように回転する設定)を指定したテキストは、カメラを動かしても常にこちらを向く。
《考察ポイント》
- 2D 棒グラフと比較し、3D 表現の利点と欠点を整理する。
- 2 次元配列のデータを高さの異なる棒の格子に変換すると、どのような可視化(地形・ヒートマップ)になるか考える。