C# 用 2D ゲームライブラリ MonoGame をインストールして Visual Studio 2022 で使えるようになったので、インストールとセットアップ手順を共有します。
動作環境
MonoGame 3.8.4
.Net 8.0/9.0
Visual Studio 2022
Windows 11 Home
- MonoGame に必要な Visual Studio 2022 のインストール方法
- MonoGame のインストール方法
- MonoGame の最小プロジェクトを作成する方法
- MonoGame でテクスチャアトラスを使ってスプライトアニメする方法
- 公式に Visual Studio Code の説明はあるのですが、Visual Studio 2022 の説明がなかったから。
- アトラスを使ってスププライトアニメするまでが少々面倒だったから。
Visual Studio 2022 のダウンロードページ
※企業に所属している人や、企業案件に関わっている人はライセンスを確認してください。
※個人の場合はコミュニティ(無料版)が使えます。
Visual Studio Installer で全てインストールできます。
※ .Net 9.0 を選んでますが、.Net 8.0 以上なら何でもいいです。
※ MonoGame のバージョンが新しくなったら変わるかも知れないので、公式を確認してください。
コマンドプロンプトを開いて、以下のコマンドを入力するだけです。
dotnet new install MonoGame.Templates.CSharp
コマンドプロンプトで C: を入力してエンターを押すだけです。
このフォルダにある MonoGame.Templates.CSharp.3.8.4.nupkg が MonoGame の本体です。
MonoGame をインストールすると、Visual Studio に MonoGame 用プロジェクトのテンプレートが追加されます。
プロジェクトを作成したら、ローカルで設定しているコードスタイルに合わせてコードを修正して実行。
例えば、メンバの参照は this. つけるとか、名前空間は {} で挟まない…とか。
コードスタイルを強制しない設定なら不要です。
MonoGame はセットアップがめちゃめちゃ簡単です。
Python Arcade Library みたいに、簡単に試してコードを確認できるようなサンプルプログラムはありません。
キャラクターの絵など、無料で使えるアセットもありません。
デモを試そうとすると GitHub のリポジトリに飛ばされます…。
ライブラリの使い方については、MonoGame のチュートリアルを通じて、使い方を学習する必要があります。
言いたいことは分かるんだけど、じっくり勉強したいわけじゃないんだ…。
という、筆者のような方のために、最速でアトラスを使ってスプライトアニメする方法を説明します。
結果だけ知りたい方は、この項はスキップしてください。
// Game1.cs using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; namespace MonoGameProject1; public class Game1 : Game { private GraphicsDeviceManager _graphics; private SpriteBatch _spriteBatch; Texture2D _textureAtlas; // 追加 public Game1() { _graphics = new GraphicsDeviceManager(this); this.Content.RootDirectory = "Content"; this.IsMouseVisible = true; } protected override void Initialize() { // TODO: Add your initialization logic here base.Initialize(); } protected override void LoadContent() { _textureAtlas = this.Content.Load<Texture2D>("テクスチャのファイル名(拡張子不要)"); // 追加 _spriteBatch = new SpriteBatch(this.GraphicsDevice); // TODO: use this.Content to load your game content here } protected override void Update(GameTime gameTime) { if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape)) { this.Exit(); } // TODO: Add your update logic here base.Update(gameTime); } protected override void Draw(GameTime gameTime) { this.GraphicsDevice.Clear(Color.CornflowerBlue); // TODO: Add your drawing code here // ここから追加 _spriteBatch.Begin(); _spriteBatch.Draw(_textureAtlas, new Vector2(10, 10), new Rectangle(0, 0, 320, 329), Color.White); _spriteBatch.End(); // ここまで base.Draw(gameTime); } }
テクスチャは以下の場所にコピーします。
※ MonoGameProject1 という名前でプロジェクトを作成した場合。
プロジェクトフォルダ
└ MonoGameProject1
└ bin
└ Debug
└ net8.0-windows
└ Content
└ ここ
bin フォルダ、bin/Debug フォルダはビルドに成功しないと存在しません。
Content フォルダは自分で作る必要があります。
// _spriteBatch.Draw(_textureAtlas, new Vector2(10, 10), new Rectangle(0, 0, 320, 329), Color.White); //
テクスチャを描画しているのは、この一行です。
_spriteBatch.Draw(Texture2D, ウィンドウ内での表示位置, テクスチャの表示範囲, 基本色)
テクスチャの表示方法が分かれば、あとはどうにでもなりますが、MonoGame の公式チュートリアルにスプライトアニメのサンプルコードがあるので、それを使ってみます。
画像はこちらから拝借したものです。
このアトラス画像を使ってスプライトアニメさせたのが↑の動画です。
公式のチュートリアル通りにやるなら、プロジェクトに3つのソースファイルを追加する必要があります。
- Sprite.cs
- TextureAtlas.cs
- Animation.cs
追加したら、以下のコードをそれぞれのファイルにコピペします。
// Sprite.cs using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; namespace MonoGameProject1; public class Sprite { public TextureRegion Region { get; set; } public Color Color { get; set; } = Color.White; public float Rotation { get; set; } = 0.0f; public Vector2 Scale { get; set; } = Vector2.One; public Vector2 Origin { get; set; } = Vector2.Zero; public SpriteEffects Effects { get; set; } = SpriteEffects.None; public float LayerDepth { get; set; } = 0.0f; public float Width { get { return this.Region.Width * this.Scale.X; } } public float Height { get { return this.Region.Height * this.Scale.Y; } } public Sprite() { } public Sprite(TextureRegion region) { this. Region = region; } public void CenterOrigin() { this.Origin = new Vector2(this.Region.Width, this.Region.Height) * 0.5f; } public void Draw(SpriteBatch spriteBatch, Vector2 position) { this.Region.Draw(spriteBatch, position, this.Color, this.Rotation, this.Origin, this.Scale, this.Effects, this.LayerDepth); } } public class AnimatedSprite : Sprite { int currentFrame; TimeSpan elapsed; Animation animation; public Animation Animation { get { return this.animation; } set { this.animation = value; this.Region = this.animation.Frames[0]; } } public AnimatedSprite() { } public AnimatedSprite(Animation animation) { this.Animation = animation; } public void Update(GameTime gameTime) { this.elapsed += gameTime.ElapsedGameTime; if (this.elapsed >= this.animation.Delay) { this.elapsed -= this.animation.Delay; this.currentFrame++; if (this.currentFrame >= this.animation.Frames.Count) { this.currentFrame = 0; } this.Region = this.animation.Frames[this.currentFrame]; } } }
// TextureAtlas.cs using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using System; using System.IO; using System.Collections.Generic; using System.Xml; using System.Xml.Linq; namespace MonoGameProject1; public sealed class TextureRegion { public Texture2D Texture { get; private set; } public Rectangle SourceRectangle { get; private set; } public int Width { get { return this.SourceRectangle.Width; } } public int Height { get { return this.SourceRectangle.Height; } } public TextureRegion(Texture2D texture, int x, int y, int width, int height) { this.Texture = texture; this.SourceRectangle = new Rectangle(x, y, width, height); } public void Draw(SpriteBatch spriteBatch, Vector2 position) { this.Draw(spriteBatch, position, Color.White, 0.0f, Vector2.Zero, Vector2.One, SpriteEffects.None, 0.0f); } public void Draw(SpriteBatch spriteBatch, Vector2 position, Color color) { this.Draw(spriteBatch, position, color, 0.0f, Vector2.Zero, Vector2.One, SpriteEffects.None, 0.0f); } void Draw(SpriteBatch spriteBatch, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects effects, float layerDepth) { this.Draw(spriteBatch, position, color, rotation, origin, new Vector2(scale, scale), effects, layerDepth); } public void Draw(SpriteBatch spriteBatch, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth) { spriteBatch.Draw(this.Texture, position, this.SourceRectangle, color, rotation, origin, scale, effects, layerDepth); } } public sealed class TextureAtlas { SpriteBatch spriteBatch; Dictionary<string, TextureRegion> regions = new (); Dictionary<string, Animation> animations = new (); public Texture2D Texture{ get; set; } public TextureAtlas() { } public TextureAtlas(SpriteBatch spriteBatch, Texture2D texture) { this.Texture = texture; this.spriteBatch = spriteBatch; } public void AddRegion(string name, int x, int y, int width, int height) { if (this.regions.ContainsKey(name)) { throw new System.ArgumentException($"Texture region '{name}' already exists in atlas."); } var region = new TextureRegion(this.Texture, x, y, width, height); this.regions[name] = region; } public TextureRegion GetRegion(string name) { if (this.regions.TryGetValue(name, out TextureRegion region)) { return region; } throw new KeyNotFoundException($"Texture region '{name}' not found in atlas."); } public Sprite CreateSprite(string regionName) { TextureRegion region = this.GetRegion(regionName); return new Sprite(region); } public AnimatedSprite CreateAnimatedSprite(string animationName) { Animation animation = this.GetAnimation(animationName); return new AnimatedSprite(animation); } public void AddAnimation(string animationName, Animation animation) { this.animations.Add(animationName, animation); } public Animation GetAnimation(string animationName) { return this.animations[animationName]; } public bool RemoveAnimation(string animationName) { return this.animations.Remove(animationName); } public static TextureAtlas FromFile(ContentManager content, string fileName) { var atlas = new TextureAtlas(); string filePath = Path.Combine(content.RootDirectory, fileName); using (Stream stream = TitleContainer.OpenStream(filePath)) { using (var reader = XmlReader.Create(stream)) { var doc = XDocument.Load(reader); XElement root = doc.Root; string texturePath = root.Element("Texture").Value; atlas.Texture = content.Load(texturePath); IEnumerable<XElement> regions = root.Element("Regions")?.Elements("Region"); if (regions != null) { foreach (XElement region in regions) { string name = region.Attribute("name")?.Value; int x = int.Parse(region.Attribute("x")?.Value ?? "0"); int y = int.Parse(region.Attribute("y")?.Value ?? "0"); int width = int.Parse(region.Attribute("width")?.Value ?? "0"); int height = int.Parse(region.Attribute("height")?.Value ?? "0"); if (!string.IsNullOrEmpty(name)) { atlas.AddRegion(name, x, y, width, height); } } } IEnumerable<XElement> animationElements = root.Element("Animations").Elements("Animation"); if (animationElements != null) { foreach (XElement animationElement in animationElements) { string name = animationElement.Attribute("name")?.Value; float delayInMilliseconds = float.Parse(animationElement.Attribute("delay")?.Value ?? "0"); var delay = TimeSpan.FromMilliseconds(delayInMilliseconds); var frames = new List<TextureRegion>(); IEnumerable<XElement> frameElements = animationElement.Elements("Frame"); if (frameElements != null) { foreach (XElement frameElement in frameElements) { string regionName = frameElement.Attribute("region").Value; TextureRegion region = atlas.GetRegion(regionName); frames.Add(region); } } var animation = new Animation(frames, delay); atlas.AddAnimation(name, animation); } } return atlas; } } } }
// Animation.cs using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace MonoGameProject1; public class Animation { public List<TextureRegion> Frames { get; set; } public TimeSpan Delay { get; set; } public Animation() { this.Frames = new List<TextureRegion>(); this.Delay = TimeSpan.FromMilliseconds(100); } public Animation(List<TextureRegion> frames, TimeSpan delay) { this.Frames = frames; this.Delay = delay; } }
名前空間は Game1.cs で使用しているものに合わせてください。
Game1.cs を以下のように書き換えます(コピペすれば良いです)。
// Game1.cs using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; namespace MonoGameProject1; public class Game1 : Game { GraphicsDeviceManager _graphics; SpriteBatch _spriteBatch; TextureAtlas _atlas; Sprite _idle; AnimatedSprite _walk; AnimatedSprite _attack; AnimatedSprite _dead; AnimatedSprite _whipBack1; AnimatedSprite _whipBack2; AnimatedSprite _whipFront; public Game1() { _graphics = new GraphicsDeviceManager(this); this.Content.RootDirectory = "Content"; this.IsMouseVisible = true; } protected override void Initialize() { // TODO: Add your initialization logic here base.Initialize(); } protected override void LoadContent() { _atlas = TextureAtlas.FromFile(this.Content, "atlas-definition.xml"); _spriteBatch = new SpriteBatch(this.GraphicsDevice); // TODO: use this.Content to load your game content here _idle = _atlas.CreateSprite("idle"); _walk = _atlas.CreateAnimatedSprite("walk-animation"); _attack = _atlas.CreateAnimatedSprite("attack-animation"); _dead = _atlas.CreateAnimatedSprite("dead-animation"); _whipBack1 = _atlas.CreateAnimatedSprite("whipb1-animation"); _whipBack2 = _atlas.CreateAnimatedSprite("whipb2-animation"); _whipFront = _atlas.CreateAnimatedSprite("whipf-animation"); _idle.Scale = new Vector2(3.0f, 3.0f); _walk.Scale = new Vector2(3.0f, 3.0f); _attack.Scale = new Vector2(3.0f, 3.0f); _dead.Scale = new Vector2(3.0f, 3.0f); _whipBack1.Scale = new Vector2(3.0f, 3.0f); _whipBack2.Scale = new Vector2(3.0f, 3.0f); _whipFront.Scale = new Vector2(3.0f, 3.0f); } protected override void Update(GameTime gameTime) { if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape)) { this.Exit(); } // TODO: Add your update logic here _walk.Update(gameTime); _attack.Update(gameTime); _dead.Update(gameTime); _whipBack1.Update(gameTime); _whipBack2.Update(gameTime); _whipFront.Update(gameTime); base.Update(gameTime); } protected override void Draw(GameTime gameTime) { this.GraphicsDevice.Clear(Color.CornflowerBlue); // TODO: Add your drawing code here _spriteBatch.Begin(); _idle.Draw(_spriteBatch, new Vector2(100, 100)); _walk.Draw(_spriteBatch, new Vector2(200, 100)); _attack.Draw(_spriteBatch, new Vector2(300, 100)); _dead.Draw(_spriteBatch, new Vector2(500, 100)); _whipBack1.Draw(_spriteBatch, new Vector2(312 - _whipBack1.Width, 142)); _whipBack2.Draw(_spriteBatch, new Vector2(336 - _whipBack2.Width, 136)); _whipFront.Draw(_spriteBatch, new Vector2(401, 148)); _spriteBatch.End(); base.Draw(gameTime); } }
実用性は一切考慮していません。
鞭の表示の仕方は「表示位置が合っていればいいや。」ぐらいのレベルのものです。
アトラスのどこを表示するか?を指定するための XML ファイルを作ります。
公式のチュートリアルが XML を使っていたので、そのまま利用していますが、構造化したデータを扱えるなら、JSON でも CSV でも何でもいいと思います。
<?xml version="1.0" encoding="utf-8"?> <!-- atlas-definition.xml --> <TextureAtlas> <Texture>simon-belmont-custom</Texture> <Regions> <Region name="idle" x="2" y="8" width="18" height="38" /> <Region name="walk-1" x="23" y="8" width="17" height="38" /> <Region name="walk-2" x="44" y="8" width="18" height="38" /> <Region name="walk-3" x="66" y="8" width="17" height="38" /> <Region name="walk-4" x="44" y="8" width="18" height="38" /> <Region name="attack-1" x="86" y="8" width="39" height="39" /> <Region name="attack-2" x="128" y="8" width="39" height="39" /> <Region name="attack-3" x="170" y="8" width="39" height="39" /> <Region name="whipb1-1" x="23" y="113" width="17" height="24" /> <Region name="whipb1-2" x="2" y="92" width="17" height="18" /> <Region name="whipb1-3" x="2" y="92" width="17" height="18" /> <Region name="whipb2-1" x="2" y="92" width="17" height="18" /> <Region name="whipb2-2" x="23" y="92" width="17" height="18" /> <Region name="whipb2-3" x="2" y="92" width="17" height="18" /> <Region name="whipf-1" x="2" y="92" width="17" height="18" /> <Region name="whipf-2" x="2" y="92" width="17" height="18" /> <Region name="whipf-3" x="90" y="99" width="24" height="6" /> <Region name="dead-1" x="212" y="8" width="18" height="39" /> <Region name="dead-2" x="233" y="8" width="18" height="39" /> <Region name="dead-3" x="212" y="50" width="39" height="39" /> </Regions> <Animations> <Animation name="walk-animation" delay="200"> <Frame region="walk-1" /> <Frame region="walk-2" /> <Frame region="walk-3" /> <Frame region="walk-4" /> </Animation> <Animation name="attack-animation" delay="200"> <Frame region="attack-1" /> <Frame region="attack-2" /> <Frame region="attack-3" /> </Animation> <Animation name="whipb1-animation" delay="200"> <Frame region="whipb1-1" /> <Frame region="whipb1-2" /> <Frame region="whipb1-3" /> </Animation> <Animation name="whipb2-animation" delay="200"> <Frame region="whipb2-1" /> <Frame region="whipb2-2" /> <Frame region="whipb2-3" /> </Animation> <Animation name="whipf-animation" delay="200"> <Frame region="whipf-1" /> <Frame region="whipf-2" /> <Frame region="whipf-3" /> </Animation> <Animation name="dead-animation" delay="500"> <Frame region="dead-1" /> <Frame region="dead-2" /> <Frame region="dead-3" /> </Animation> </Animations> </TextureAtlas>
この XML は、テクスチャアトラスと同じフォルダに配置します。
2Dゲームのモックを素早く作る環境を探していて、MonoGame にたどり着きました。
Visual Studio だけでパッと作れることが条件です。
Python Arcade も使ってみたのですが、テクスチャアトラスを使ってスプライトアニメーションさせるのが面倒だったのでやめました。
テクスチャアトラスを使わずに、アニメーションをひとコマずつ画像ファイルに保存して、それらを全てロードしてスプライトアニメーションさせるような場合は Arcade の方が楽そうです。
Arcade は動的にアトラスを作ってくれる…みたいな説明があった気がする…(うろ覚え)
スプライト画像の作り方が合わなかったので、MonoGame の使い方も調べることになりましたが、ゲームエンジンを使わずに素早く2Dゲームのモックを作る…という条件なら、どっちを使ってもいいと思います。