コンテンツにスキップ

Webブラウザ対応

Promete.Web パッケージを使うと、Prometeで作ったゲームを、.NET の WebAssembly と WebGL2 でWebブラウザ上に動かせます。 シーンやノードのコードは、デスクトップ版とほとんど同じように書けます。

  • .NET 10 SDK
  • wasm-tools ワークロード(後述)
  • WebGL2 に対応したWebブラウザ

Promete.Web は、ビルド時にネイティブのコードを Emscripten でリンクします。そのため、wasm-tools ワークロードが必要です。

Terminal window
dotnet workload install wasm-tools

Web版のプロジェクトは、デスクトップ版とは別のプロジェクトとして作ります。 SDKに Microsoft.NET.Sdk.WebAssembly を指定し、Promete.Web パッケージを参照します。

MyGame.Web.csproj
<Project Sdk="Microsoft.NET.Sdk.WebAssembly">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<RuntimeIdentifier>browser-wasm</RuntimeIdentifier>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Promete.Web" Version="2.1.0-preview.1" />
</ItemGroup>
<ItemGroup>
<!-- ゲームで使うアセット。起動前にすべて読み込まれる -->
<PrometeAsset Include="assets/**" />
</ItemGroup>
</Project>

Promete 本体のパッケージは、Promete.Web が参照しているので、別に追加する必要はありません。 HTMLやJavaScriptのファイルも、パッケージが既定のものを用意するので、書く必要はありません。

Main は async にして、最初に PrometeWeb.InitializeAsync() を呼びます。 そのあと、BuildWithOpenGLDesktop() の代わりに BuildWithWeb() でアプリケーションを構築します。

Program.cs
using Promete;
using Promete.Input;
using Promete.Web;
using Promete.Windowing;
public static class Program
{
public static async Task Main()
{
await PrometeWeb.InitializeAsync();
var app = PrometeApp.Create()
.Use<Keyboard>()
.Use<Mouse>()
.BuildWithWeb(WindowOptions.Default with
{
Size = (320, 240),
Scale = 2,
Title = "My Game",
});
app.Run<MainScene>();
}
}

WindowOptions の Size と Scale はページ上の描画領域(canvas)の大きさに、Title はページのタイトルに反映されます。 Scale で拡大するときは、ドットがぼやけないように表示されます。

Terminal window
dotnet run

ビルドが終わると、App url: http://localhost:xxxx/ のようにURLが表示されます。このURLをブラウザで開くと、ゲームが起動します。 実行中のエラーは、ブラウザの開発者ツールのコンソールに表示されます。

ポートを固定したい場合は、Properties/launchSettings.json に指定します。

Properties/launchSettings.json
{
"profiles": {
"MyGame.Web": {
"commandName": "Project",
"launchBrowser": true,
"applicationUrl": "http://localhost:5000"
}
}
}

dotnet watch を使うと、コードの変更を、ゲームを再起動せずに反映できます(ホットリロード)。 ただし、OnStart のように一度しか呼ばれないメソッドの変更は、シーンを開き直すまで反映されません。

ブラウザのゲームは、ディスク上のファイルを直接読むことができません。 そこで Promete.Web は、<PrometeAsset> で指定したファイルを、ゲームの起動前にすべてダウンロードして、メモリ上のファイルシステムに置きます。

<ItemGroup>
<PrometeAsset Include="assets/**" />
</ItemGroup>

これにより、デスクトップ版と同じコードでアセットを読み込めます。

var texture = App.TextureFactory.Load("assets/player.png");
var font = Font.FromFile("assets/fonts/MyFont.ttf", 16);

ファイルのパスは、プロジェクトのフォルダーからの相対パスになります。 プロジェクトの外にあるファイル(デスクトップ版と共有しているアセットなど)を使う場合は、TargetPath でゲームから見えるパスを指定します。

<ItemGroup>
<PrometeAsset Include="../MyGame/assets/**">
<TargetPath>assets/%(RecursiveDir)%(Filename)%(Extension)</TargetPath>
</PrometeAsset>
</ItemGroup>
Terminal window
dotnet publish -c Release -o publish

publish/wwwroot フォルダーに、公開に必要なファイル一式(HTML、JavaScript、アセンブリ、アセット)が出力されます。 このフォルダーを、静的ファイルを配信できる任意のWebサーバーに置けば、ゲームを公開できます。

Webサーバーには、次の設定が必要です。

  • .wasm ファイルを application/wasm として配信する
  • 圧縮済みのファイル(.br、.gz)を配信できると、ダウンロードのサイズを大きく減らせる

Releaseビルドでは、C# のコードを事前に WebAssembly へコンパイルするAOTコンパイルが既定で有効になります。 AOTを使わない場合(インタプリタ)と比べて、描画性能が大きく向上します(スプライトを大量に表示する計測で、10倍以上)。

その代わり、公開にかかる時間が数分に延び、配信するファイルのサイズも増えます。 配信サイズのうち、.NET のランタイム(dotnet.native.wasm と System.Private.CoreLib)は、インタプリタの場合、brotli圧縮後で約1.5MBです。これに、ゲームと Promete のアセンブリ、アセットが加わります。 AOTでは、コンパイルしたコードが dotnet.native.wasm に入るため、ゲームの規模に応じてさらに大きくなります。

AOTを無効にしたい場合は、プロジェクトファイルに次のように指定します。

<PropertyGroup>
<RunAOTCompilation>false</RunAOTCompilation>
</PropertyGroup>

dotnet run(Debugビルド)は、デバッガーを使えるように、常にインタプリタで動きます。 そのため、dotnet run での動作は、公開したゲームより遅くなります。

Promete.Web は、既定のHTML(index.html)とJavaScript(main.js)を用意しています。既定のページは、読み込み中に、画面の中央にプログレスバーを表示します。 プロジェクトの wwwroot フォルダーに同じ名前のファイルを置くと、そちらが使われます。片方だけを置くこともできます。

自分の index.html を使う場合は、ゲームを描画する <canvas> と、main.js の読み込みを書きます。

wwwroot/index.html
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<title>My Game</title>
<script type="module" src="./main.js"></script>
</head>
<body>
<canvas id="canvas"></canvas>
</body>
</html>

canvas の大きさは、ゲームの WindowOptions から実行時に設定されるので、HTML に書く必要はありません。

canvas の id を変えたい場合は、BuildWithWeb() の WebOptions で、canvas を指す CSS セレクタを指定します。

var app = PrometeApp.Create()
.BuildWithWeb(web: new WebOptions { CanvasSelector = "#game" });

自分の main.js を使う場合は、startPromete() を呼んでゲームを起動します。 読み込みの進み具合や、ゲームの開始、エラーを受け取ることができます。自分で読み込み画面を作る場合に使います。

wwwroot/main.js
import { startPromete } from './_content/Promete.Web/promete.js';
await startPromete({
onProgress: ({ phase, loaded, total }) => {
// 起動の進み具合
console.log(`${phase}: ${loaded}/${total}`);
},
onReady: () => {
// 最初のフレームを描画し終えた。読み込み画面を消す
},
onError: error => {
// 起動時、または実行中に起きたエラー
console.error(error);
},
});

onProgress の phase は、次の順に進みます。

  • 'runtime': .NET のランタイムとアセンブリの読み込み。loaded / total はファイルの数です
  • 'assets': <PrometeAsset> で指定したアセットの読み込み。loaded / total はファイルの数です
  • 'starting': Main と、最初のシーンの OnStart の実行。loaded / total は 0 です

デスクトップ版とコードを共有する

Section titled “デスクトップ版とコードを共有する”

デスクトップ版とWeb版の両方でゲームを公開する場合は、シーンやアセットを共有のプロジェクト(クラスライブラリ)にまとめ、起動部分だけをそれぞれのプロジェクトに分けるのがおすすめです。

MyGame/ 共有のプロジェクト(シーン、ノード、アセット)
MyGame.Desktop/ デスクトップ版の起動部分
MyGame.Web/ Web版の起動部分

通常のクラスライブラリとして作り、Promete パッケージを参照します。 バージョンは、Promete.Web が必要とする Promete のバージョン以上にしてください。

MyGame/MyGame.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Promete" Version="2.1.0-preview.1" />
</ItemGroup>
</Project>

シーンは、これまでどおりこのプロジェクトに書きます。

共有のプロジェクトを参照し、アセットを出力先にコピーします。

MyGame.Desktop/MyGame.Desktop.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../MyGame/MyGame.csproj" />
</ItemGroup>
<ItemGroup>
<None Include="../MyGame/assets/**" LinkBase="assets" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>

シーンが起動するプロジェクトとは別のプロジェクトにあるので、UseScenesFrom で、シーンのあるプロジェクトを指定します。

また、相対パスは、起動したときのカレントディレクトリを基準に解決されます。カレントディレクトリは起動方法によって変わり、dotnet run ではコマンドを実行したフォルダーになるため、出力先にコピーしたアセットが見つからないことがあります。 起動時に、カレントディレクトリを実行ファイルの場所(AppContext.BaseDirectory)に変えておくと、どの方法で起動してもアセットを読めます。

MyGame.Desktop/Program.cs
using MyGame.Scenes;
using Promete;
using Promete.GLDesktop;
using Promete.Input;
// アセットを実行ファイルの場所から読めるようにする
Directory.SetCurrentDirectory(AppContext.BaseDirectory);
var app = PrometeApp.Create()
.Use<Keyboard>()
.UseScenesFrom<MainScene>()
.BuildWithOpenGLDesktop();
return app.Run<MainScene>();

共有のプロジェクトを参照し、アセットを <PrometeAsset> で指定します。 TargetPath を指定して、デスクトップ版と同じ assets/... というパスで読めるようにします。

MyGame.Web/MyGame.Web.csproj
<Project Sdk="Microsoft.NET.Sdk.WebAssembly">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<RuntimeIdentifier>browser-wasm</RuntimeIdentifier>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Promete.Web" Version="2.1.0-preview.1" />
<ProjectReference Include="../MyGame/MyGame.csproj" />
</ItemGroup>
<ItemGroup>
<PrometeAsset Include="../MyGame/assets/**">
<TargetPath>assets/%(RecursiveDir)%(Filename)%(Extension)</TargetPath>
</PrometeAsset>
</ItemGroup>
</Project>
MyGame.Web/Program.cs
using MyGame.Scenes;
using Promete;
using Promete.Input;
using Promete.Web;
public static class Program
{
public static async Task Main()
{
await PrometeWeb.InitializeAsync();
var app = PrometeApp.Create()
.Use<Keyboard>()
.UseScenesFrom<MainScene>()
.BuildWithWeb();
app.Run<MainScene>();
}
}

プラグインの登録(Use<Keyboard>() など)は、両方の起動部分で同じにしておきます。 シーンが必要とするプラグインが片方で登録されていないと、そのシーンを開いたときにエラーになります。

ブラウザで動いているかを調べる

Section titled “ブラウザで動いているかを調べる”

ブラウザで動いているかどうかは、OperatingSystem.IsBrowser() で調べられます。 共有のプロジェクトで、一部の処理だけを切り替えたい場合に使えます。

if (!OperatingSystem.IsBrowser())
{
// デスクトップ版だけで行う処理
App.View.IsFullScreen = true;
}

Webバックエンドでは、次の機能に対応していません。 対応していないプロパティを設定しても、エラーにはならず、無視されます。

  • ウィンドウの位置(Location、X、Y)、TopMost、IsFullScreen、Mode、IsVisible は、設定しても画面に反映されません
  • IsFocused は常に true を返します
  • ファイルのドロップ(FileDropped)は発生しません
  • スクリーンショット(TakeScreenshot、SaveScreenshotAsync)は、NotSupportedException をスローします
  • ブラウザのタブが非表示になっている間は、ゲームの進行が止まるか、大きく遅くなります
  • キーボードとマウスに対応しています。ゲームパッドには対応していません
  • タッチ操作は、最初の1本の指を、左ボタンを押しているマウスとして扱います (v2.2~)。2本目以降の指は無視します。ピンチやスワイプなどのジェスチャーには対応していません
  • マウスカーソルの見た目の変更や、カーソルの非表示・固定には対応していません
  • クリップボードには対応していません
  • Font.FromFile で読み込むフォントファイルは、<PrometeAsset> で指定しておく必要があります
  • Font.FromStream には対応していません(NotSupportedException をスローします)
  • 文字はブラウザの機能で描画するため、アンチエイリアスの無効化やカーニングの設定は効きません。デスクトップ版と見た目が少し異なる場合があります
  • Font.GetDefault は、ブラウザの既定のゴシック体(sans-serif)を使います。実際のフォントは、閲覧する環境によって異なります
  • ブラウザの制限により、ユーザーがページをクリックするかキーを押すまで、音は鳴りません
  • Ogg Vorbis ファイルの読み込み中は、ゲームが一時的に止まります(長い曲では数秒)
  • ImGui連携プラグインには対応していません
  • カスタムシェーダー(GLSL 3.30)は、WebGL2 向け(GLSL ES 3.00)に自動で変換されます。ただし、GLSL ES 3.00 にない機能は使えません

dotnet run で起動したゲームは、ブラウザの開発者ツールでエラーやログを確認できます。

Visual Studio Code でブレークポイントやステップ実行を使う場合は、Prometeのリポジトリの .vscode/launch.json と .vscode/tasks.json、Promete.Web.Example/Properties/launchSettings.json を参考にしてください。

  • Webバックエンドは実験的な機能です。不具合を見つけた場合は、デスクトップ版で同じ問題が起きるかどうかを確認してみてください
  • Prometeのリポジトリにある Promete.Web.Example は、Web版のサンプルです。dotnet run --project Promete.Web.Example で動かせます