diff --git a/.gitignore b/.gitignore
index 73c47a9d..0bff24c2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -108,4 +108,6 @@ Backup*/
UpgradeLog*.XML
.vs/
.user
-.vscode/
\ No newline at end of file
+.vscode/
+.idea
+.DS_Store
diff --git a/Tutorials/MobileDeployment/01-UpdatedLibs/.config/dotnet-tools.json b/Tutorials/MobileDeployment/01-UpdatedLibs/.config/dotnet-tools.json
new file mode 100644
index 00000000..7f4505d0
--- /dev/null
+++ b/Tutorials/MobileDeployment/01-UpdatedLibs/.config/dotnet-tools.json
@@ -0,0 +1,36 @@
+{
+ "version": 1,
+ "isRoot": true,
+ "tools": {
+ "dotnet-mgcb": {
+ "version": "3.8.4.1",
+ "commands": [
+ "mgcb"
+ ]
+ },
+ "dotnet-mgcb-editor": {
+ "version": "3.8.4.1",
+ "commands": [
+ "mgcb-editor"
+ ]
+ },
+ "dotnet-mgcb-editor-linux": {
+ "version": "3.8.4.1",
+ "commands": [
+ "mgcb-editor-linux"
+ ]
+ },
+ "dotnet-mgcb-editor-windows": {
+ "version": "3.8.4.1",
+ "commands": [
+ "mgcb-editor-windows"
+ ]
+ },
+ "dotnet-mgcb-editor-mac": {
+ "version": "3.8.4.1",
+ "commands": [
+ "mgcb-editor-mac"
+ ]
+ }
+ }
+}
diff --git a/Tutorials/MobileDeployment/01-UpdatedLibs/Directory.Packages.props b/Tutorials/MobileDeployment/01-UpdatedLibs/Directory.Packages.props
new file mode 100644
index 00000000..affaddb8
--- /dev/null
+++ b/Tutorials/MobileDeployment/01-UpdatedLibs/Directory.Packages.props
@@ -0,0 +1,10 @@
+
+
+ true
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime.slnx b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime.slnx
new file mode 100644
index 00000000..e573540c
--- /dev/null
+++ b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime.slnx
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/.config/dotnet-tools.json b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/.config/dotnet-tools.json
new file mode 100644
index 00000000..afd4e2c4
--- /dev/null
+++ b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/.config/dotnet-tools.json
@@ -0,0 +1,36 @@
+{
+ "version": 1,
+ "isRoot": true,
+ "tools": {
+ "dotnet-mgcb": {
+ "version": "3.8.3",
+ "commands": [
+ "mgcb"
+ ]
+ },
+ "dotnet-mgcb-editor": {
+ "version": "3.8.3",
+ "commands": [
+ "mgcb-editor"
+ ]
+ },
+ "dotnet-mgcb-editor-linux": {
+ "version": "3.8.3",
+ "commands": [
+ "mgcb-editor-linux"
+ ]
+ },
+ "dotnet-mgcb-editor-windows": {
+ "version": "3.8.3",
+ "commands": [
+ "mgcb-editor-windows"
+ ]
+ },
+ "dotnet-mgcb-editor-mac": {
+ "version": "3.8.3",
+ "commands": [
+ "mgcb-editor-mac"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Content/Content.mgcb b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Content/Content.mgcb
new file mode 100644
index 00000000..d26ea4f1
--- /dev/null
+++ b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Content/Content.mgcb
@@ -0,0 +1,104 @@
+
+#----------------------------- Global Properties ----------------------------#
+
+/outputDir:bin/$(Platform)
+/intermediateDir:obj/$(Platform)
+/platform:DesktopGL
+/config:
+/profile:Reach
+/compress:False
+
+#-------------------------------- References --------------------------------#
+
+
+#---------------------------------- Content ---------------------------------#
+
+#begin audio/bounce.wav
+/importer:WavImporter
+/processor:SoundEffectProcessor
+/processorParam:Quality=Best
+/build:audio/bounce.wav
+
+#begin audio/collect.wav
+/importer:WavImporter
+/processor:SoundEffectProcessor
+/processorParam:Quality=Best
+/build:audio/collect.wav
+
+#begin audio/theme.ogg
+/importer:OggImporter
+/processor:SongProcessor
+/processorParam:Quality=Best
+/build:audio/theme.ogg
+
+#begin audio/ui.wav
+/importer:WavImporter
+/processor:SoundEffectProcessor
+/processorParam:Quality=Best
+/build:audio/ui.wav
+
+#begin effects/grayscaleEffect.fx
+/importer:EffectImporter
+/processor:EffectProcessor
+/processorParam:DebugMode=Auto
+/build:effects/grayscaleEffect.fx
+
+#begin fonts/04B_30_5x.spritefont
+/importer:FontDescriptionImporter
+/processor:FontDescriptionProcessor
+/processorParam:PremultiplyAlpha=True
+/processorParam:TextureFormat=Compressed
+/build:fonts/04B_30_5x.spritefont
+
+#begin fonts/04b_30.fnt
+/copy:fonts/04b_30.fnt
+
+#begin fonts/04B_30.spritefont
+/importer:FontDescriptionImporter
+/processor:FontDescriptionProcessor
+/processorParam:PremultiplyAlpha=True
+/processorParam:TextureFormat=Compressed
+/build:fonts/04B_30.spritefont
+
+#begin images/atlas-definition.xml
+/copy:images/atlas-definition.xml
+
+#begin images/atlas.png
+/importer:TextureImporter
+/processor:TextureProcessor
+/processorParam:ColorKeyColor=255,0,255,255
+/processorParam:ColorKeyEnabled=True
+/processorParam:GenerateMipmaps=False
+/processorParam:PremultiplyAlpha=True
+/processorParam:ResizeToPowerOfTwo=False
+/processorParam:MakeSquare=False
+/processorParam:TextureFormat=Color
+/build:images/atlas.png
+
+#begin images/background-pattern.png
+/importer:TextureImporter
+/processor:TextureProcessor
+/processorParam:ColorKeyColor=255,0,255,255
+/processorParam:ColorKeyEnabled=True
+/processorParam:GenerateMipmaps=False
+/processorParam:PremultiplyAlpha=True
+/processorParam:ResizeToPowerOfTwo=False
+/processorParam:MakeSquare=False
+/processorParam:TextureFormat=Color
+/build:images/background-pattern.png
+
+#begin images/logo.png
+/importer:TextureImporter
+/processor:TextureProcessor
+/processorParam:ColorKeyColor=255,0,255,255
+/processorParam:ColorKeyEnabled=True
+/processorParam:GenerateMipmaps=False
+/processorParam:PremultiplyAlpha=True
+/processorParam:ResizeToPowerOfTwo=False
+/processorParam:MakeSquare=False
+/processorParam:TextureFormat=Color
+/build:images/logo.png
+
+#begin images/tilemap-definition.xml
+/copy:images/tilemap-definition.xml
+
diff --git a/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Content/audio/bounce.wav b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Content/audio/bounce.wav
new file mode 100644
index 00000000..baa7a47b
Binary files /dev/null and b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Content/audio/bounce.wav differ
diff --git a/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Content/audio/collect.wav b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Content/audio/collect.wav
new file mode 100644
index 00000000..506220de
Binary files /dev/null and b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Content/audio/collect.wav differ
diff --git a/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Content/audio/theme.ogg b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Content/audio/theme.ogg
new file mode 100644
index 00000000..72e1fd3b
Binary files /dev/null and b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Content/audio/theme.ogg differ
diff --git a/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Content/audio/ui.wav b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Content/audio/ui.wav
new file mode 100644
index 00000000..63e8941e
Binary files /dev/null and b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Content/audio/ui.wav differ
diff --git a/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Content/effects/grayscaleEffect.fx b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Content/effects/grayscaleEffect.fx
new file mode 100644
index 00000000..5dd0d8b6
--- /dev/null
+++ b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Content/effects/grayscaleEffect.fx
@@ -0,0 +1,53 @@
+#if OPENGL
+ #define SV_POSITION POSITION
+ #define VS_SHADERMODEL vs_3_0
+ #define PS_SHADERMODEL ps_3_0
+#else
+ #define VS_SHADERMODEL vs_4_0_level_9_1
+ #define PS_SHADERMODEL ps_4_0_level_9_1
+#endif
+
+Texture2D SpriteTexture;
+
+// A value between 0 and 1 that controls the intensity of the grayscale effect.
+// 0 = full color, 1 = full grayscale.
+float Saturation = 1.0;
+
+sampler2D SpriteTextureSampler = sampler_state
+{
+ Texture = ;
+};
+
+struct VertexShaderOutput
+{
+ float4 Position : SV_POSITION;
+ float4 Color : COLOR0;
+ float2 TextureCoordinates : TEXCOORD0;
+};
+
+float4 MainPS(VertexShaderOutput input) : COLOR
+{
+ // Sample the texture
+ float4 color = tex2D(SpriteTextureSampler, input.TextureCoordinates) * input.Color;
+
+ // Calculate the grayscale value based on human perception of colors
+ float grayscale = dot(color.rgb, float3(0.3, 0.59, 0.11));
+
+ // create a grayscale color vector (same value for R, G, and B)
+ float3 grayscaleColor = float3(grayscale, grayscale, grayscale);
+
+ // Linear interpolation between he grayscale color and the original color's
+ // rgb values based on the saturation parameter.
+ float3 finalColor = lerp(grayscale, color.rgb, Saturation);
+
+ // Return the final color with the original alpha value
+ return float4(finalColor, color.a);
+}
+
+technique SpriteDrawing
+{
+ pass P0
+ {
+ PixelShader = compile PS_SHADERMODEL MainPS();
+ }
+};
diff --git a/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Content/fonts/04B_30.spritefont b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Content/fonts/04B_30.spritefont
new file mode 100644
index 00000000..63d4728c
--- /dev/null
+++ b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Content/fonts/04B_30.spritefont
@@ -0,0 +1,16 @@
+
+
+
+ 04B_30.ttf
+ 17.5
+ 0
+ true
+
+
+
+
+ ~
+
+
+
+
diff --git a/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Content/fonts/04B_30.ttf b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Content/fonts/04B_30.ttf
new file mode 100644
index 00000000..4b93740c
Binary files /dev/null and b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Content/fonts/04B_30.ttf differ
diff --git a/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Content/fonts/04B_30_5x.spritefont b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Content/fonts/04B_30_5x.spritefont
new file mode 100644
index 00000000..dd239a53
--- /dev/null
+++ b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Content/fonts/04B_30_5x.spritefont
@@ -0,0 +1,16 @@
+
+
+
+ 04B_30.ttf
+ 87.5
+ 0
+ true
+
+
+
+
+ ~
+
+
+
+
diff --git a/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Content/fonts/04b_30.fnt b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Content/fonts/04b_30.fnt
new file mode 100644
index 00000000..772f8c54
--- /dev/null
+++ b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Content/fonts/04b_30.fnt
@@ -0,0 +1,99 @@
+info face="04b30" size=35 bold=0 italic=0 charset="" unicode=1 stretchH=100 smooth=0 aa=1 padding=0,0,0,0 spacing=1,1 outline=0
+common lineHeight=35 base=31 scaleW=256 scaleH=512 pages=1 packed=0 alphaChnl=0 redChnl=4 greenChnl=4 blueChnl=4
+page id=0 file="../images/atlas.png"
+chars count=95
+char id=32 x=30 y=152 width=3 height=1 xoffset=-1 yoffset=34 xadvance=29 page=0 chnl=15
+char id=33 x=240 y=30 width=14 height=29 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=34 x=102 y=232 width=25 height=15 xoffset=1 yoffset=4 xadvance=29 page=0 chnl=15
+char id=35 x=184 y=0 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=36 x=250 y=150 width=3 height=1 xoffset=-1 yoffset=34 xadvance=29 page=0 chnl=15
+char id=37 x=0 y=34 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=38 x=30 y=150 width=3 height=1 xoffset=-1 yoffset=34 xadvance=29 page=0 chnl=15
+char id=39 x=245 y=202 width=10 height=15 xoffset=1 yoffset=4 xadvance=14 page=0 chnl=15
+char id=40 x=106 y=150 width=16 height=29 xoffset=1 yoffset=2 xadvance=21 page=0 chnl=15
+char id=41 x=123 y=150 width=16 height=29 xoffset=1 yoffset=2 xadvance=21 page=0 chnl=15
+char id=42 x=128 y=232 width=14 height=15 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=43 x=94 y=0 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=44 x=143 y=232 width=10 height=14 xoffset=1 yoffset=19 xadvance=14 page=0 chnl=15
+char id=45 x=154 y=232 width=25 height=11 xoffset=1 yoffset=12 xadvance=29 page=0 chnl=15
+char id=46 x=231 y=228 width=10 height=10 xoffset=1 yoffset=19 xadvance=14 page=0 chnl=15
+char id=47 x=60 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=48 x=90 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=49 x=46 y=150 width=19 height=29 xoffset=1 yoffset=2 xadvance=23 page=0 chnl=15
+char id=50 x=150 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=51 x=180 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=52 x=210 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=53 x=0 y=94 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=54 x=180 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=55 x=60 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=56 x=90 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=57 x=120 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=58 x=234 y=202 width=10 height=25 xoffset=1 yoffset=4 xadvance=14 page=0 chnl=15
+char id=59 x=244 y=0 width=10 height=29 xoffset=1 yoffset=4 xadvance=14 page=0 chnl=15
+char id=60 x=86 y=150 width=19 height=29 xoffset=1 yoffset=2 xadvance=23 page=0 chnl=15
+char id=61 x=182 y=176 width=25 height=25 xoffset=1 yoffset=4 xadvance=29 page=0 chnl=15
+char id=62 x=237 y=120 width=18 height=29 xoffset=1 yoffset=2 xadvance=23 page=0 chnl=15
+char id=63 x=180 y=120 width=28 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=64 x=34 y=150 width=3 height=1 xoffset=-1 yoffset=34 xadvance=29 page=0 chnl=15
+char id=65 x=120 y=120 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=66 x=150 y=120 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=67 x=124 y=0 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=68 x=154 y=0 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=69 x=214 y=0 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=70 x=30 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=71 x=60 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=72 x=90 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=73 x=240 y=90 width=14 height=29 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=74 x=120 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=75 x=150 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=76 x=209 y=120 width=27 height=29 xoffset=1 yoffset=2 xadvance=31 page=0 chnl=15
+char id=77 x=30 y=0 width=31 height=29 xoffset=1 yoffset=2 xadvance=35 page=0 chnl=15
+char id=78 x=210 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=79 x=0 y=64 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=80 x=30 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=81 x=0 y=0 width=29 height=33 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=82 x=120 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=83 x=30 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=84 x=150 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=85 x=180 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=86 x=210 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=87 x=62 y=0 width=31 height=29 xoffset=1 yoffset=2 xadvance=35 page=0 chnl=15
+char id=88 x=0 y=124 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=89 x=30 y=120 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=90 x=60 y=120 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=91 x=240 y=60 width=14 height=29 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=92 x=90 y=120 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=93 x=140 y=150 width=14 height=29 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=94 x=180 y=232 width=16 height=11 xoffset=1 yoffset=4 xadvance=21 page=0 chnl=15
+char id=95 x=0 y=262 width=29 height=10 xoffset=1 yoffset=21 xadvance=33 page=0 chnl=15
+char id=96 x=197 y=228 width=16 height=11 xoffset=1 yoffset=4 xadvance=21 page=0 chnl=15
+char id=97 x=208 y=176 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=98 x=0 y=210 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=99 x=26 y=210 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=100 x=52 y=206 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=101 x=78 y=206 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=102 x=104 y=206 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=103 x=130 y=206 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=104 x=156 y=206 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=105 x=234 y=176 width=12 height=25 xoffset=1 yoffset=6 xadvance=16 page=0 chnl=15
+char id=106 x=182 y=202 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=107 x=208 y=202 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=108 x=78 y=232 width=23 height=25 xoffset=1 yoffset=6 xadvance=27 page=0 chnl=15
+char id=109 x=197 y=150 width=26 height=25 xoffset=1 yoffset=6 xadvance=31 page=0 chnl=15
+char id=110 x=0 y=236 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=111 x=26 y=236 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=112 x=78 y=180 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=113 x=0 y=154 width=25 height=29 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=114 x=52 y=232 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=115 x=224 y=150 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=116 x=0 y=184 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=117 x=26 y=184 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=118 x=52 y=180 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=119 x=170 y=150 width=26 height=25 xoffset=1 yoffset=6 xadvance=31 page=0 chnl=15
+char id=120 x=104 y=180 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=121 x=130 y=180 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=122 x=156 y=180 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=123 x=26 y=154 width=19 height=29 xoffset=1 yoffset=2 xadvance=23 page=0 chnl=15
+char id=124 x=155 y=150 width=14 height=29 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=125 x=66 y=150 width=19 height=29 xoffset=1 yoffset=2 xadvance=23 page=0 chnl=15
+char id=126 x=214 y=228 width=16 height=11 xoffset=1 yoffset=4 xadvance=21 page=0 chnl=15
diff --git a/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Content/images/atlas-definition.xml b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Content/images/atlas-definition.xml
new file mode 100644
index 00000000..21772022
--- /dev/null
+++ b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Content/images/atlas-definition.xml
@@ -0,0 +1,34 @@
+
+
+ images/atlas
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Content/images/atlas.png b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Content/images/atlas.png
new file mode 100644
index 00000000..f7def20f
Binary files /dev/null and b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Content/images/atlas.png differ
diff --git a/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Content/images/background-pattern.png b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Content/images/background-pattern.png
new file mode 100644
index 00000000..2d8d878e
Binary files /dev/null and b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Content/images/background-pattern.png differ
diff --git a/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Content/images/logo.png b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Content/images/logo.png
new file mode 100644
index 00000000..1509036c
Binary files /dev/null and b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Content/images/logo.png differ
diff --git a/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Content/images/tilemap-definition.xml b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Content/images/tilemap-definition.xml
new file mode 100644
index 00000000..85658c60
--- /dev/null
+++ b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Content/images/tilemap-definition.xml
@@ -0,0 +1,15 @@
+
+
+ images/atlas
+
+ 00 01 02 01 02 01 02 01 02 01 02 01 02 01 02 03
+ 04 05 05 06 05 05 06 05 05 06 05 05 06 05 05 07
+ 08 09 09 09 09 09 09 09 09 09 09 09 09 09 09 11
+ 04 09 09 09 09 09 09 09 10 09 09 09 09 10 09 07
+ 08 09 10 09 09 09 09 09 09 09 09 09 09 09 09 11
+ 04 09 09 09 09 09 09 09 09 09 09 09 09 09 09 07
+ 08 10 09 09 09 09 09 09 09 09 10 09 09 09 09 11
+ 04 09 09 09 09 09 10 09 09 09 09 09 09 09 09 07
+ 12 13 14 13 14 13 14 13 14 13 14 13 14 13 14 15
+
+
diff --git a/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/DungeonSlime.csproj b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/DungeonSlime.csproj
new file mode 100644
index 00000000..3dbcf3ce
--- /dev/null
+++ b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/DungeonSlime.csproj
@@ -0,0 +1,33 @@
+
+
+ WinExe
+ net9.0
+ Major
+ false
+ false
+
+
+ app.manifest
+ Icon.ico
+
+
+
+
+
+
+
+ Icon.ico
+
+
+ Icon.bmp
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Game1.cs b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Game1.cs
new file mode 100644
index 00000000..bf64e6b8
--- /dev/null
+++ b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Game1.cs
@@ -0,0 +1,79 @@
+using System;
+using DungeonSlime.Scenes;
+using Microsoft.Xna.Framework.Media;
+using MonoGameLibrary;
+using MonoGameGum;
+using Gum.Forms.Controls;
+
+namespace DungeonSlime;
+
+public class Game1 : Core
+{
+ // The background theme song
+ private Song _themeSong;
+
+ public Game1() : base("Dungeon Slime", 1280, 720, false)
+ {
+ }
+
+ protected override void Initialize()
+ {
+ try
+ {
+ base.Initialize();
+
+ // Start playing the background music
+ Audio.PlaySong(_themeSong);
+
+ // Initialize the Gum UI service
+ InitializeGum();
+
+ // Start the game with the title scene.
+ ChangeScene(new TitleScene());
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine(e);
+ throw;
+ }
+ }
+
+ private void InitializeGum()
+ {
+ // Initialize the Gum service
+ GumService.Default.Initialize(this);
+
+ // Tell the Gum service which content manager to use. We will tell it to
+ // use the global content manager from our Core.
+ GumService.Default.ContentLoader.XnaContentManager = Core.Content;
+
+ // Register keyboard input for UI control.
+ FrameworkElement.KeyboardsForUiControl.Add(GumService.Default.Keyboard);
+
+ // Register gamepad input for Ui control.
+ FrameworkElement.GamePadsForUiControl.AddRange(GumService.Default.Gamepads);
+
+ // Customize the tab reverse UI navigation to also trigger when the keyboard
+ // Up arrow key is pushed.
+ FrameworkElement.TabReverseKeyCombos.Add(
+ new KeyCombo() { PushedKey = Microsoft.Xna.Framework.Input.Keys.Up });
+
+ // Customize the tab UI navigation to also trigger when the keyboard
+ // Down arrow key is pushed.
+ FrameworkElement.TabKeyCombos.Add(
+ new KeyCombo() { PushedKey = Microsoft.Xna.Framework.Input.Keys.Down });
+
+ // The assets created for the UI were done so at 1/4th the size to keep the size of the
+ // texture atlas small. So we will set the default canvas size to be 1/4th the size of
+ // the game's resolution then tell gum to zoom in by a factor of 4.
+ GumService.Default.CanvasWidth = GraphicsDevice.PresentationParameters.BackBufferWidth / 4.0f;
+ GumService.Default.CanvasHeight = GraphicsDevice.PresentationParameters.BackBufferHeight / 4.0f;
+ GumService.Default.Renderer.Camera.Zoom = 4.0f;
+ }
+
+ protected override void LoadContent()
+ {
+ // Load the background theme music
+ _themeSong = Content.Load("audio/theme");
+ }
+}
diff --git a/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/GameController.cs b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/GameController.cs
new file mode 100644
index 00000000..92165382
--- /dev/null
+++ b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/GameController.cs
@@ -0,0 +1,79 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Input;
+using MonoGameLibrary;
+using MonoGameLibrary.Input;
+
+namespace DungeonSlime;
+
+///
+/// Provides a game-specific input abstraction that maps physical inputs
+/// to game actions, bridging our input system with game-specific functionality.
+///
+public static class GameController
+{
+ private static KeyboardInfo s_keyboard => Core.Input.Keyboard;
+ private static GamePadInfo s_gamePad => Core.Input.GamePads[(int)PlayerIndex.One];
+
+ ///
+ /// Returns true if the player has triggered the "move up" action.
+ ///
+ public static bool MoveUp()
+ {
+ return s_keyboard.WasKeyJustPressed(Keys.Up) ||
+ s_keyboard.WasKeyJustPressed(Keys.W) ||
+ s_gamePad.WasButtonJustPressed(Buttons.DPadUp) ||
+ s_gamePad.WasButtonJustPressed(Buttons.LeftThumbstickUp);
+ }
+
+ ///
+ /// Returns true if the player has triggered the "move down" action.
+ ///
+ public static bool MoveDown()
+ {
+ return s_keyboard.WasKeyJustPressed(Keys.Down) ||
+ s_keyboard.WasKeyJustPressed(Keys.S) ||
+ s_gamePad.WasButtonJustPressed(Buttons.DPadDown) ||
+ s_gamePad.WasButtonJustPressed(Buttons.LeftThumbstickDown);
+ }
+
+ ///
+ /// Returns true if the player has triggered the "move left" action.
+ ///
+ public static bool MoveLeft()
+ {
+ return s_keyboard.WasKeyJustPressed(Keys.Left) ||
+ s_keyboard.WasKeyJustPressed(Keys.A) ||
+ s_gamePad.WasButtonJustPressed(Buttons.DPadLeft) ||
+ s_gamePad.WasButtonJustPressed(Buttons.LeftThumbstickLeft);
+ }
+
+ ///
+ /// Returns true if the player has triggered the "move right" action.
+ ///
+ public static bool MoveRight()
+ {
+ return s_keyboard.WasKeyJustPressed(Keys.Right) ||
+ s_keyboard.WasKeyJustPressed(Keys.D) ||
+ s_gamePad.WasButtonJustPressed(Buttons.DPadRight) ||
+ s_gamePad.WasButtonJustPressed(Buttons.LeftThumbstickRight);
+ }
+
+ ///
+ /// Returns true if the player has triggered the "pause" action.
+ ///
+ public static bool Pause()
+ {
+ return s_keyboard.WasKeyJustPressed(Keys.Escape) ||
+ s_gamePad.WasButtonJustPressed(Buttons.Start);
+ }
+
+ ///
+ /// Returns true if the player has triggered the "action" button,
+ /// typically used for menu confirmation.
+ ///
+ public static bool Action()
+ {
+ return s_keyboard.WasKeyJustPressed(Keys.Enter) ||
+ s_gamePad.WasButtonJustPressed(Buttons.A);
+ }
+}
diff --git a/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/GameObjects/Bat.cs b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/GameObjects/Bat.cs
new file mode 100644
index 00000000..ddc855ed
--- /dev/null
+++ b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/GameObjects/Bat.cs
@@ -0,0 +1,123 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Audio;
+using MonoGameLibrary;
+using MonoGameLibrary.Graphics;
+
+namespace DungeonSlime.GameObjects;
+
+public class Bat
+{
+ private const float MOVEMENT_SPEED = 5.0f;
+
+ // The velocity of the bat that defines the direction and how much in that
+ // direction to update the bats position each update cycle.
+ private Vector2 _velocity;
+
+ // The AnimatedSprite used when drawing the bat.
+ private AnimatedSprite _sprite;
+
+ // The sound effect to play when the bat bounces off the edge of the room.
+ private SoundEffect _bounceSoundEffect;
+
+ ///
+ /// Gets or Sets the position of the bat.
+ ///
+ public Vector2 Position { get; set; }
+
+ ///
+ /// Creates a new Bat using the specified animated sprite and sound effect.
+ ///
+ /// The AnimatedSprite ot use when drawing the bat.
+ /// The sound effect to play when the bat bounces off a wall.
+ public Bat(AnimatedSprite sprite, SoundEffect bounceSoundEffect)
+ {
+ _sprite = sprite;
+ _bounceSoundEffect = bounceSoundEffect;
+ }
+
+ ///
+ /// Randomizes the velocity of the bat.
+ ///
+ public void RandomizeVelocity()
+ {
+ // Generate a random angle
+ float angle = (float)(Random.Shared.NextDouble() * MathHelper.TwoPi);
+
+ // Convert the angle to a direction vector
+ float x = (float)Math.Cos(angle);
+ float y = (float)Math.Sin(angle);
+ Vector2 direction = new Vector2(x, y);
+
+ // Multiply the direction vector by the movement speed to get the
+ // final velocity
+ _velocity = direction * MOVEMENT_SPEED;
+ }
+
+ ///
+ /// Handles a bounce event when the bat collides with a wall or boundary.
+ ///
+ /// The normal vector of the surface the bat is bouncing against.
+ public void Bounce(Vector2 normal)
+ {
+ Vector2 newPosition = Position;
+
+ // Adjust the position based on the normal to prevent sticking to walls.
+ if (normal.X != 0)
+ {
+ // We are bouncing off a vertical wall (left/right).
+ // Move slightly away from the wall in the direction of the normal.
+ newPosition.X += normal.X * (_sprite.Width * 0.1f);
+ }
+
+ if (normal.Y != 0)
+ {
+ // We are bouncing off a horizontal wall (top/bottom).
+ // Move slightly way from the wall in the direction of the normal.
+ newPosition.Y += normal.Y * (_sprite.Height * 0.1f);
+ }
+
+ // Apply the new position
+ Position = newPosition;
+
+ // Apply reflection based on the normal.
+ _velocity = Vector2.Reflect(_velocity, normal);
+
+ // Play the bounce sound effect.
+ Core.Audio.PlaySoundEffect(_bounceSoundEffect);
+ }
+
+ ///
+ /// Returns a Circle value that represents collision bounds of the bat.
+ ///
+ /// A Circle value.
+ public Circle GetBounds()
+ {
+ int x = (int)(Position.X + _sprite.Width * 0.5f);
+ int y = (int)(Position.Y + _sprite.Height * 0.5f);
+ int radius = (int)(_sprite.Width * 0.25f);
+
+ return new Circle(x, y, radius);
+ }
+
+ ///
+ /// Updates the bat.
+ ///
+ /// A snapshot of the timing values for the current update cycle.
+ public void Update(GameTime gameTime)
+ {
+ // Update the animated sprite
+ _sprite.Update(gameTime);
+
+ // Update the position of the bat based on the velocity.
+ Position += _velocity;
+ }
+
+ ///
+ /// Draws the bat.
+ ///
+ public void Draw()
+ {
+ _sprite.Draw(Core.SpriteBatch, Position);
+ }
+}
diff --git a/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/GameObjects/Slime.cs b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/GameObjects/Slime.cs
new file mode 100644
index 00000000..08b5a63d
--- /dev/null
+++ b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/GameObjects/Slime.cs
@@ -0,0 +1,265 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Xna.Framework;
+using MonoGameLibrary;
+using MonoGameLibrary.Graphics;
+
+namespace DungeonSlime.GameObjects;
+
+public class Slime
+{
+ // A constant value that represents the amount of time to wait between
+ // movement updates.
+ private static readonly TimeSpan s_movementTime = TimeSpan.FromMilliseconds(200);
+
+ // The amount of time that has elapsed since the last movement update.
+ private TimeSpan _movementTimer;
+
+ // Normalized value (0-1) representing progress between movement ticks for visual interpolation
+ private float _movementProgress;
+
+ // The next direction to apply to the head of the slime chain during the
+ // next movement update.
+ private Vector2 _nextDirection;
+
+ // The number of pixels to move the head segment during the movement cycle.
+ private float _stride;
+
+ // Tracks the segments of the slime chain.
+ private List _segments;
+
+ // The AnimatedSprite used when drawing each slime segment
+ private AnimatedSprite _sprite;
+
+ // Buffer to queue inputs input by player during input polling.
+ private Queue _inputBuffer;
+
+ // The maximum size of the buffer queue.
+ private const int MAX_BUFFER_SIZE = 2;
+
+ ///
+ /// Event that is raised if it is detected that the head segment of the slime
+ /// has collided with a body segment.
+ ///
+ public event EventHandler BodyCollision;
+
+ ///
+ /// Creates a new Slime using the specified animated sprite.
+ ///
+ /// The AnimatedSprite to use when drawing the slime.
+ public Slime(AnimatedSprite sprite)
+ {
+ _sprite = sprite;
+ }
+
+ ///
+ /// Initializes the slime, can be used to reset it back to an initial state.
+ ///
+ /// The position the slime should start at.
+ /// The total number of pixels to move the head segment during each movement cycle.
+ public void Initialize(Vector2 startingPosition, float stride)
+ {
+ // Initialize the segment collection.
+ _segments = new List();
+
+ // Set the stride
+ _stride = stride;
+
+ // Create the initial head of the slime chain.
+ SlimeSegment head = new SlimeSegment();
+ head.At = startingPosition;
+ head.To = startingPosition + new Vector2(_stride, 0);
+ head.Direction = Vector2.UnitX;
+
+ // Add it to the segment collection.
+ _segments.Add(head);
+
+ // Set the initial next direction as the same direction the head is
+ // moving.
+ _nextDirection = head.Direction;
+
+ // Zero out the movement timer.
+ _movementTimer = TimeSpan.Zero;
+
+ // initialize the input buffer.
+ _inputBuffer = new Queue(MAX_BUFFER_SIZE);
+ }
+
+ private void HandleInput()
+ {
+ Vector2 potentialNextDirection = Vector2.Zero;
+
+ if (GameController.MoveUp())
+ {
+ potentialNextDirection = -Vector2.UnitY;
+ }
+ else if (GameController.MoveDown())
+ {
+ potentialNextDirection = Vector2.UnitY;
+ }
+ else if (GameController.MoveLeft())
+ {
+ potentialNextDirection = -Vector2.UnitX;
+ }
+ else if (GameController.MoveRight())
+ {
+ potentialNextDirection = Vector2.UnitX;
+ }
+
+ // If a new direction was input, consider adding it to the buffer
+ if (potentialNextDirection != Vector2.Zero && _inputBuffer.Count < MAX_BUFFER_SIZE)
+ {
+ // If the buffer is empty, validate against the current direction;
+ // otherwise, validate against the last buffered direction
+ Vector2 validateAgainst = _inputBuffer.Count > 0 ?
+ _inputBuffer.Last() :
+ _segments[0].Direction;
+
+ // Only allow direction change if it is not reversing the current
+ // direction. This prevents th slime from backing into itself
+ float dot = Vector2.Dot(potentialNextDirection, validateAgainst);
+ if (dot >= 0)
+ {
+ _inputBuffer.Enqueue(potentialNextDirection);
+ }
+ }
+ }
+
+ private void Move()
+ {
+ // Get the next direction from the input buffer if one is available
+ if (_inputBuffer.Count > 0)
+ {
+ _nextDirection = _inputBuffer.Dequeue();
+ }
+
+ // Capture the value of the head segment
+ SlimeSegment head = _segments[0];
+
+ // Update the direction the head is supposed to move in to the
+ // next direction cached.
+ head.Direction = _nextDirection;
+
+ // Update the head's "at" position to be where it was moving "to"
+ head.At = head.To;
+
+ // Update the head's "to" position to the next tile in the direction
+ // it is moving.
+ head.To = head.At + head.Direction * _stride;
+
+ // Insert the new adjusted value for the head at the front of the
+ // segments and remove the tail segment. This effectively moves
+ // the entire chain forward without needing to loop through every
+ // segment and update its "at" and "to" positions.
+ _segments.Insert(0, head);
+ _segments.RemoveAt(_segments.Count - 1);
+
+ // Iterate through all of the segments except the head and check
+ // if they are at the same position as the head. If they are, then
+ // the head is colliding with a body segment and a body collision
+ // has occurred.
+ for (int i = 1; i < _segments.Count; i++)
+ {
+ SlimeSegment segment = _segments[i];
+
+ if (head.At == segment.At)
+ {
+ if (BodyCollision != null)
+ {
+ BodyCollision.Invoke(this, EventArgs.Empty);
+ }
+
+ return;
+ }
+ }
+ }
+
+ ///
+ /// Informs the slime to grow by one segment.
+ ///
+ public void Grow()
+ {
+ // Capture the value of the tail segment
+ SlimeSegment tail = _segments[_segments.Count - 1];
+
+ // Create a new tail segment that is positioned a grid cell in the
+ // reverse direction from the tail moving to the tail.
+ SlimeSegment newTail = new SlimeSegment();
+ newTail.At = tail.To + tail.ReverseDirection * _stride;
+ newTail.To = tail.At;
+ newTail.Direction = Vector2.Normalize(tail.At - newTail.At);
+
+ // Add the new tail segment
+ _segments.Add(newTail);
+ }
+
+ ///
+ /// Updates the slime.
+ ///
+ /// A snapshot of the timing values for the current update cycle.
+ public void Update(GameTime gameTime)
+ {
+ // Update the animated sprite.
+ _sprite.Update(gameTime);
+
+ // Handle any player input
+ HandleInput();
+
+ // Increment the movement timer by the frame elapsed time.
+ _movementTimer += gameTime.ElapsedGameTime;
+
+ // If the movement timer has accumulated enough time to be greater than
+ // the movement time threshold, then perform a full movement.
+ if (_movementTimer >= s_movementTime)
+ {
+ _movementTimer -= s_movementTime;
+ Move();
+ }
+
+ // Update the movement lerp offset amount
+ _movementProgress = (float)(_movementTimer.TotalSeconds / s_movementTime.TotalSeconds);
+ }
+
+ ///
+ /// Draws the slime.
+ ///
+ public void Draw()
+ {
+ // Iterate through each segment and draw it
+ foreach (SlimeSegment segment in _segments)
+ {
+ // Calculate the visual position of the segment at the moment by
+ // lerping between its "at" and "to" position by the movement
+ // offset lerp amount
+ Vector2 pos = Vector2.Lerp(segment.At, segment.To, _movementProgress);
+
+ // Draw the slime sprite at the calculated visual position of this
+ // segment
+ _sprite.Draw(Core.SpriteBatch, pos);
+ }
+ }
+
+ ///
+ /// Returns a Circle value that represents collision bounds of the slime.
+ ///
+ /// A Circle value.
+ public Circle GetBounds()
+ {
+ SlimeSegment head = _segments[0];
+
+ // Calculate the visual position of the head at the moment of this
+ // method call by lerping between the "at" and "to" position by the
+ // movement offset lerp amount
+ Vector2 pos = Vector2.Lerp(head.At, head.To, _movementProgress);
+
+ // Create the bounds using the calculated visual position of the head.
+ Circle bounds = new Circle(
+ (int)(pos.X + (_sprite.Width * 0.5f)),
+ (int)(pos.Y + (_sprite.Height * 0.5f)),
+ (int)(_sprite.Width * 0.5f)
+ );
+
+ return bounds;
+ }
+}
diff --git a/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/GameObjects/SlimeSegment.cs b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/GameObjects/SlimeSegment.cs
new file mode 100644
index 00000000..b00189eb
--- /dev/null
+++ b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/GameObjects/SlimeSegment.cs
@@ -0,0 +1,26 @@
+using Microsoft.Xna.Framework;
+
+namespace DungeonSlime.GameObjects;
+
+public struct SlimeSegment
+{
+ ///
+ /// The position this slime segment is at before the movement cycle occurs.
+ ///
+ public Vector2 At;
+
+ ///
+ /// The position this slime segment should move to during the next movement cycle.
+ ///
+ public Vector2 To;
+
+ ///
+ /// The direction this slime segment is moving.
+ ///
+ public Vector2 Direction;
+
+ ///
+ /// The opposite direction this slime segment is moving.
+ ///
+ public Vector2 ReverseDirection => new Vector2(-Direction.X, -Direction.Y);
+}
diff --git a/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Icon.bmp b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Icon.bmp
new file mode 100644
index 00000000..2b481653
Binary files /dev/null and b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Icon.bmp differ
diff --git a/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Icon.ico b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Icon.ico
new file mode 100644
index 00000000..7d9dec18
Binary files /dev/null and b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Icon.ico differ
diff --git a/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Program.cs b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Program.cs
new file mode 100644
index 00000000..d491c406
--- /dev/null
+++ b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Program.cs
@@ -0,0 +1,2 @@
+using var game = new DungeonSlime.Game1();
+game.Run();
diff --git a/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Scenes/GameScene.cs b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Scenes/GameScene.cs
new file mode 100644
index 00000000..d01a3b03
--- /dev/null
+++ b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Scenes/GameScene.cs
@@ -0,0 +1,428 @@
+using System;
+using DungeonSlime.GameObjects;
+using DungeonSlime.UI;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Audio;
+using Microsoft.Xna.Framework.Graphics;
+using MonoGameGum;
+using MonoGameLibrary;
+using MonoGameLibrary.Graphics;
+using MonoGameLibrary.Scenes;
+
+namespace DungeonSlime.Scenes;
+
+public class GameScene : Scene
+{
+ private enum GameState
+ {
+ Playing,
+ Paused,
+ GameOver
+ }
+
+ // Reference to the slime.
+ private Slime _slime;
+
+ // Reference to the bat.
+ private Bat _bat;
+
+ // Defines the tilemap to draw.
+ private Tilemap _tilemap;
+
+ // Defines the bounds of the room that the slime and bat are contained within.
+ private Rectangle _roomBounds;
+
+ // The sound effect to play when the slime eats a bat.
+ private SoundEffect _collectSoundEffect;
+
+ // Tracks the players score.
+ private int _score;
+
+ private GameSceneUI _ui;
+
+ private GameState _state;
+
+ // The grayscale shader effect.
+ private Effect _grayscaleEffect;
+
+ // The amount of saturation to provide the grayscale shader effect
+ private float _saturation = 1.0f;
+
+ // The speed of the fade to grayscale effect.
+ private const float FADE_SPEED = 0.02f;
+
+ public override void Initialize()
+ {
+ // LoadContent is called during base.Initialize().
+ base.Initialize();
+
+ // During the game scene, we want to disable exit on escape. Instead,
+ // the escape key will be used to return back to the title screen
+ Core.ExitOnEscape = false;
+
+ // Create the room bounds by getting the bounds of the screen then
+ // using the Inflate method to "Deflate" the bounds by the width and
+ // height of a tile so that the bounds only covers the inside room of
+ // the dungeon tilemap.
+ _roomBounds = Core.GraphicsDevice.PresentationParameters.Bounds;
+ _roomBounds.Inflate(-_tilemap.TileWidth, -_tilemap.TileHeight);
+
+ // Subscribe to the slime's BodyCollision event so that a game over
+ // can be triggered when this event is raised.
+ _slime.BodyCollision += OnSlimeBodyCollision;
+
+ // Create any UI elements from the root element created in previous
+ // scenes
+ GumService.Default.Root.Children.Clear();
+
+ // Initialize the user interface for the game scene.
+ InitializeUI();
+
+ // Initialize a new game to be played.
+ InitializeNewGame();
+ }
+
+ private void InitializeUI()
+ {
+ // Clear out any previous UI element incase we came here
+ // from a different scene.
+ GumService.Default.Root.Children.Clear();
+
+ // Create the game scene ui instance.
+ _ui = new GameSceneUI();
+
+ // Subscribe to the events from the game scene ui.
+ _ui.ResumeButtonClick += OnResumeButtonClicked;
+ _ui.RetryButtonClick += OnRetryButtonClicked;
+ _ui.QuitButtonClick += OnQuitButtonClicked;
+ }
+
+ private void OnResumeButtonClicked(object sender, EventArgs args)
+ {
+ // Change the game state back to playing
+ _state = GameState.Playing;
+ }
+
+ private void OnRetryButtonClicked(object sender, EventArgs args)
+ {
+ // Player has chosen to retry, so initialize a new game
+ InitializeNewGame();
+ }
+
+ private void OnQuitButtonClicked(object sender, EventArgs args)
+ {
+ // Player has chosen to quit, so return back to the title scene
+ Core.ChangeScene(new TitleScene());
+ }
+
+ private void InitializeNewGame()
+ {
+ // Calculate the position for the slime, which will be at the center
+ // tile of the tile map.
+ Vector2 slimePos = new Vector2();
+ slimePos.X = (_tilemap.Columns / 2) * _tilemap.TileWidth;
+ slimePos.Y = (_tilemap.Rows / 2) * _tilemap.TileHeight;
+
+ // Initialize the slime
+ _slime.Initialize(slimePos, _tilemap.TileWidth);
+
+ // Initialize the bat
+ _bat.RandomizeVelocity();
+ PositionBatAwayFromSlime();
+
+ // Reset the score
+ _score = 0;
+
+ // Set the game state to playing
+ _state = GameState.Playing;
+ }
+
+ public override void LoadContent()
+ {
+ // Create the texture atlas from the XML configuration file
+ TextureAtlas atlas = TextureAtlas.FromFile(Core.Content, "images/atlas-definition.xml");
+
+ // Create the tilemap from the XML configuration file.
+ _tilemap = Tilemap.FromFile(Content, "images/tilemap-definition.xml");
+ _tilemap.Scale = new Vector2(4.0f, 4.0f);
+
+ // Create the animated sprite for the slime from the atlas.
+ AnimatedSprite slimeAnimation = atlas.CreateAnimatedSprite("slime-animation");
+ slimeAnimation.Scale = new Vector2(4.0f, 4.0f);
+
+ // Create the slime
+ _slime = new Slime(slimeAnimation);
+
+ // Create the animated sprite for the bat from the atlas.
+ AnimatedSprite batAnimation = atlas.CreateAnimatedSprite("bat-animation");
+ batAnimation.Scale = new Vector2(4.0f, 4.0f);
+
+ // Load the bounce sound effect for the bat
+ SoundEffect bounceSoundEffect = Content.Load("audio/bounce");
+
+ // Create the bat
+ _bat = new Bat(batAnimation, bounceSoundEffect);
+
+ // Load the collect sound effect
+ _collectSoundEffect = Content.Load("audio/collect");
+
+ // Load the grayscale effect
+ _grayscaleEffect = Content.Load("effects/grayscaleEffect");
+ }
+
+ public override void Update(GameTime gameTime)
+ {
+ // Ensure the UI is always updated
+ _ui.Update(gameTime);
+
+ if (_state != GameState.Playing)
+ {
+ // The game is in either a paused or game over state, so
+ // gradually decrease the saturation to create the fading grayscale.
+ _saturation = Math.Max(0.0f, _saturation - FADE_SPEED);
+
+ // If its just a game over state, return back
+ if (_state == GameState.GameOver)
+ {
+ return;
+ }
+ }
+
+ // If the pause button is pressed, toggle the pause state
+ if (GameController.Pause())
+ {
+ TogglePause();
+ }
+
+ // At this point, if the game is paused, just return back early
+ if (_state == GameState.Paused)
+ {
+ return;
+ }
+
+ // Update the slime;
+ _slime.Update(gameTime);
+
+ // Update the bat;
+ _bat.Update(gameTime);
+
+ // Perform collision checks
+ CollisionChecks();
+ }
+
+ private void CollisionChecks()
+ {
+ // Capture the current bounds of the slime and bat
+ Circle slimeBounds = _slime.GetBounds();
+ Circle batBounds = _bat.GetBounds();
+
+ // FIrst perform a collision check to see if the slime is colliding with
+ // the bat, which means the slime eats the bat.
+ if (slimeBounds.Intersects(batBounds))
+ {
+ // Move the bat to a new position away from the slime.
+ PositionBatAwayFromSlime();
+
+ // Randomize the velocity of the bat.
+ _bat.RandomizeVelocity();
+
+ // Tell the slime to grow.
+ _slime.Grow();
+
+ // Increment the score.
+ _score += 100;
+
+ // Update the score display on the UI.
+ _ui.UpdateScoreText(_score);
+
+ // Play the collect sound effect
+ Core.Audio.PlaySoundEffect(_collectSoundEffect);
+ }
+
+ // Next check if the slime is colliding with the wall by validating if
+ // it is within the bounds of the room. If it is outside the room
+ // bounds, then it collided with a wall which triggers a game over.
+ if (slimeBounds.Top < _roomBounds.Top ||
+ slimeBounds.Bottom > _roomBounds.Bottom ||
+ slimeBounds.Left < _roomBounds.Left ||
+ slimeBounds.Right > _roomBounds.Right)
+ {
+ GameOver();
+ return;
+ }
+
+ // Finally, check if the bat is colliding with a wall by validating if
+ // it is within the bounds of the room. If it is outside the room
+ // bounds, then it collided with a wall, and the bat should bounce
+ // off of that wall.
+ if (batBounds.Top < _roomBounds.Top)
+ {
+ _bat.Bounce(Vector2.UnitY);
+ }
+ else if (batBounds.Bottom > _roomBounds.Bottom)
+ {
+ _bat.Bounce(-Vector2.UnitY);
+ }
+
+ if (batBounds.Left < _roomBounds.Left)
+ {
+ _bat.Bounce(Vector2.UnitX);
+ }
+ else if (batBounds.Right > _roomBounds.Right)
+ {
+ _bat.Bounce(-Vector2.UnitX);
+ }
+ }
+
+ private void PositionBatAwayFromSlime()
+ {
+ // Calculate the position that is in the center of the bounds
+ // of the room.
+ float roomCenterX = _roomBounds.X + _roomBounds.Width * 0.5f;
+ float roomCenterY = _roomBounds.Y + _roomBounds.Height * 0.5f;
+ Vector2 roomCenter = new Vector2(roomCenterX, roomCenterY);
+
+ // Get the bounds of the slime and calculate the center position
+ Circle slimeBounds = _slime.GetBounds();
+ Vector2 slimeCenter = new Vector2(slimeBounds.X, slimeBounds.Y);
+
+ // Calculate the distance vector from the center of the room to the
+ // center of the slime.
+ Vector2 centerToSlime = slimeCenter - roomCenter;
+
+ // Get the bounds of the bat
+ Circle batBounds = _bat.GetBounds();
+
+ // Calculate the amount of padding we will add to the new position of
+ // the bat to ensure it is not sticking to walls
+ int padding = batBounds.Radius * 2;
+
+ // Calculate the new position of the bat by finding which component of
+ // the center to slime vector (X or Y) is larger and in which direction.
+ Vector2 newBatPosition = Vector2.Zero;
+ if (Math.Abs(centerToSlime.X) > Math.Abs(centerToSlime.Y))
+ {
+ // The slime is closer to either the left or right wall, so the Y
+ // position will be a random position between the top and bottom
+ // walls.
+ newBatPosition.Y = Random.Shared.Next(
+ _roomBounds.Top + padding,
+ _roomBounds.Bottom - padding
+ );
+
+ if (centerToSlime.X > 0)
+ {
+ // The slime is closer to the right side wall, so place the
+ // bat on the left side wall
+ newBatPosition.X = _roomBounds.Left + padding;
+ }
+ else
+ {
+ // The slime is closer ot the left side wall, so place the
+ // bat on the right side wall.
+ newBatPosition.X = _roomBounds.Right - padding * 2;
+ }
+ }
+ else
+ {
+ // The slime is closer to either the top or bottom wall, so the X
+ // position will be a random position between the left and right
+ // walls.
+ newBatPosition.X = Random.Shared.Next(
+ _roomBounds.Left + padding,
+ _roomBounds.Right - padding
+ );
+
+ if (centerToSlime.Y > 0)
+ {
+ // The slime is closer to the top wall, so place the bat on the
+ // bottom wall
+ newBatPosition.Y = _roomBounds.Top + padding;
+ }
+ else
+ {
+ // The slime is closer to the bottom wall, so place the bat on
+ // the top wall.
+ newBatPosition.Y = _roomBounds.Bottom - padding * 2;
+ }
+ }
+
+ // Assign the new bat position
+ _bat.Position = newBatPosition;
+ }
+
+ private void OnSlimeBodyCollision(object sender, EventArgs args)
+ {
+ GameOver();
+ }
+
+ private void TogglePause()
+ {
+ if (_state == GameState.Paused)
+ {
+ // We're now unpausing the game, so hide the pause panel
+ _ui.HidePausePanel();
+
+ // And set the state back to playing
+ _state = GameState.Playing;
+ }
+ else
+ {
+ // We're now pausing the game, so show the pause panel
+ _ui.ShowPausePanel();
+
+ // And set the state to paused
+ _state = GameState.Paused;
+
+ // Set the grayscale effect saturation to 1.0f;
+ _saturation = 1.0f;
+ }
+ }
+
+ private void GameOver()
+ {
+ // Show the game over panel
+ _ui.ShowGameOverPanel();
+
+ // Set the game state to game over
+ _state = GameState.GameOver;
+
+ // Set the grayscale effect saturation to 1.0f;
+ _saturation = 1.0f;
+ }
+
+ public override void Draw(GameTime gameTime)
+ {
+ // Clear the back buffer.
+ Core.GraphicsDevice.Clear(Color.CornflowerBlue);
+
+ if (_state != GameState.Playing)
+ {
+ // We are in a game over state, so apply the saturation parameter.
+ _grayscaleEffect.Parameters["Saturation"].SetValue(_saturation);
+
+ // And begin the sprite batch using the grayscale effect.
+ Core.SpriteBatch.Begin(samplerState: SamplerState.PointClamp, effect: _grayscaleEffect);
+ }
+ else
+ {
+ // Otherwise, just begin the sprite batch as normal.
+ Core.SpriteBatch.Begin(samplerState: SamplerState.PointClamp);
+ }
+
+ // Draw the tilemap
+ _tilemap.Draw(Core.SpriteBatch);
+
+ // Draw the slime.
+ _slime.Draw();
+
+ // Draw the bat.
+ _bat.Draw();
+
+ // Always end the sprite batch when finished.
+ Core.SpriteBatch.End();
+
+ // Draw the UI
+ _ui.Draw();
+ }
+}
diff --git a/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Scenes/TitleScene.cs b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Scenes/TitleScene.cs
new file mode 100644
index 00000000..248c48dd
--- /dev/null
+++ b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/Scenes/TitleScene.cs
@@ -0,0 +1,345 @@
+using System;
+using DungeonSlime.UI;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Audio;
+using Microsoft.Xna.Framework.Graphics;
+using Microsoft.Xna.Framework.Input;
+using MonoGameGum;
+using Gum.Forms.Controls;
+using MonoGameGum.GueDeriving;
+using MonoGameLibrary;
+using MonoGameLibrary.Graphics;
+using MonoGameLibrary.Scenes;
+
+namespace DungeonSlime.Scenes;
+
+public class TitleScene : Scene
+{
+ private const string DUNGEON_TEXT = "Dungeon";
+ private const string SLIME_TEXT = "Slime";
+ private const string PRESS_ENTER_TEXT = "Press Enter To Start";
+
+ // The font to use to render normal text.
+ private SpriteFont _font;
+
+ // The font used to render the title text.
+ private SpriteFont _font5x;
+
+ // The position to draw the dungeon text at.
+ private Vector2 _dungeonTextPos;
+
+ // The origin to set for the dungeon text.
+ private Vector2 _dungeonTextOrigin;
+
+ // The position to draw the slime text at.
+ private Vector2 _slimeTextPos;
+
+ // The origin to set for the slime text.
+ private Vector2 _slimeTextOrigin;
+
+ // The position to draw the press enter text at.
+ private Vector2 _pressEnterPos;
+
+ // The origin to set for the press enter text when drawing it.
+ private Vector2 _pressEnterOrigin;
+
+ // The texture used for the background pattern.
+ private Texture2D _backgroundPattern;
+
+ // The destination rectangle for the background pattern to fill.
+ private Rectangle _backgroundDestination;
+
+ // The offset to apply when drawing the background pattern so it appears to
+ // be scrolling.
+ private Vector2 _backgroundOffset;
+
+ // The speed that the background pattern scrolls.
+ private float _scrollSpeed = 50.0f;
+
+ private SoundEffect _uiSoundEffect;
+ private Panel _titleScreenButtonsPanel;
+ private Panel _optionsPanel;
+
+ // The options button used to open the options menu.
+ private AnimatedButton _optionsButton;
+
+ // The back button used to exit the options menu back to the title menu.
+ private AnimatedButton _optionsBackButton;
+
+ // Reference to the texture atlas that we can pass to UI elements when they
+ // are created.
+ private TextureAtlas _atlas;
+
+ public override void Initialize()
+ {
+ // LoadContent is called during base.Initialize().
+ base.Initialize();
+
+ // While on the title screen, we can enable exit on escape so the player
+ // can close the game by pressing the escape key.
+ Core.ExitOnEscape = true;
+
+ // Set the position and origin for the Dungeon text.
+ Vector2 size = _font5x.MeasureString(DUNGEON_TEXT);
+ _dungeonTextPos = new Vector2(640, 100);
+ _dungeonTextOrigin = size * 0.5f;
+
+ // Set the position and origin for the Slime text.
+ size = _font5x.MeasureString(SLIME_TEXT);
+ _slimeTextPos = new Vector2(757, 207);
+ _slimeTextOrigin = size * 0.5f;
+
+ // Set the position and origin for the press enter text.
+ size = _font.MeasureString(PRESS_ENTER_TEXT);
+ _pressEnterPos = new Vector2(640, 620);
+ _pressEnterOrigin = size * 0.5f;
+
+ // Initialize the offset of the background pattern at zero
+ _backgroundOffset = Vector2.Zero;
+
+ // Set the background pattern destination rectangle to fill the entire
+ // screen background
+ _backgroundDestination = Core.GraphicsDevice.PresentationParameters.Bounds;
+
+ InitializeUI();
+ }
+
+ public override void LoadContent()
+ {
+ // Load the font for the standard text.
+ _font = Core.Content.Load("fonts/04B_30");
+
+ // Load the font for the title text
+ _font5x = Content.Load("fonts/04B_30_5x");
+
+ // Load the background pattern texture.
+ _backgroundPattern = Content.Load("images/background-pattern");
+
+ // Load the sound effect to play when ui actions occur.
+ _uiSoundEffect = Core.Content.Load("audio/ui");
+
+ // Load the texture atlas from the xml configuration file.
+ _atlas = TextureAtlas.FromFile(Core.Content, "images/atlas-definition.xml");
+ }
+
+ private void CreateTitlePanel()
+ {
+ // Create a container to hold all of our buttons
+ _titleScreenButtonsPanel = new Panel();
+ _titleScreenButtonsPanel.Dock(Gum.Wireframe.Dock.Fill);
+ _titleScreenButtonsPanel.AddToRoot();
+
+ AnimatedButton startButton = new AnimatedButton(_atlas);
+ startButton.Anchor(Gum.Wireframe.Anchor.BottomLeft);
+ startButton.Visual.X = 50;
+ startButton.Visual.Y = -12;
+ startButton.Text = "Start";
+ startButton.Click += HandleStartClicked;
+ _titleScreenButtonsPanel.AddChild(startButton);
+
+ _optionsButton = new AnimatedButton(_atlas);
+ _optionsButton.Anchor(Gum.Wireframe.Anchor.BottomRight);
+ _optionsButton.Visual.X = -50;
+ _optionsButton.Visual.Y = -12;
+ _optionsButton.Text = "Options";
+ _optionsButton.Click += HandleOptionsClicked;
+ _titleScreenButtonsPanel.AddChild(_optionsButton);
+
+ startButton.IsFocused = true;
+ }
+
+ private void HandleStartClicked(object sender, EventArgs e)
+ {
+ // A UI interaction occurred, play the sound effect
+ Core.Audio.PlaySoundEffect(_uiSoundEffect);
+
+ // Change to the game scene to start the game.
+ Core.ChangeScene(new GameScene());
+ }
+
+ private void HandleOptionsClicked(object sender, EventArgs e)
+ {
+ // A UI interaction occurred, play the sound effect
+ Core.Audio.PlaySoundEffect(_uiSoundEffect);
+
+ // Set the title panel to be invisible.
+ _titleScreenButtonsPanel.IsVisible = false;
+
+ // Set the options panel to be visible.
+ _optionsPanel.IsVisible = true;
+
+ // Give the back button on the options panel focus.
+ _optionsBackButton.IsFocused = true;
+ }
+
+ private void CreateOptionsPanel()
+ {
+ _optionsPanel = new Panel();
+ _optionsPanel.Dock(Gum.Wireframe.Dock.Fill);
+ _optionsPanel.IsVisible = false;
+ _optionsPanel.AddToRoot();
+
+ TextRuntime optionsText = new TextRuntime();
+ optionsText.X = 10;
+ optionsText.Y = 10;
+ optionsText.Text = "OPTIONS";
+ optionsText.UseCustomFont = true;
+ optionsText.FontScale = 0.5f;
+ optionsText.CustomFontFile = @"fonts/04b_30.fnt";
+ _optionsPanel.AddChild(optionsText);
+
+ OptionsSlider musicSlider = new OptionsSlider(_atlas);
+ musicSlider.Name = "MusicSlider";
+ musicSlider.Text = "MUSIC";
+ musicSlider.Anchor(Gum.Wireframe.Anchor.Top);
+ musicSlider.Visual.Y = 30f;
+ musicSlider.Minimum = 0;
+ musicSlider.Maximum = 1;
+ musicSlider.Value = Core.Audio.SongVolume;
+ musicSlider.SmallChange = .1;
+ musicSlider.LargeChange = .2;
+ musicSlider.ValueChanged += HandleMusicSliderValueChanged;
+ musicSlider.ValueChangeCompleted += HandleMusicSliderValueChangeCompleted;
+ _optionsPanel.AddChild(musicSlider);
+
+ OptionsSlider sfxSlider = new OptionsSlider(_atlas);
+ sfxSlider.Name = "SfxSlider";
+ sfxSlider.Text = "SFX";
+ sfxSlider.Anchor(Gum.Wireframe.Anchor.Top);
+ sfxSlider.Visual.Y = 93;
+ sfxSlider.Minimum = 0;
+ sfxSlider.Maximum = 1;
+ sfxSlider.Value = Core.Audio.SoundEffectVolume;
+ sfxSlider.SmallChange = .1;
+ sfxSlider.LargeChange = .2;
+ sfxSlider.ValueChanged += HandleSfxSliderChanged;
+ sfxSlider.ValueChangeCompleted += HandleSfxSliderChangeCompleted;
+ _optionsPanel.AddChild(sfxSlider);
+
+ _optionsBackButton = new AnimatedButton(_atlas);
+ _optionsBackButton.Text = "BACK";
+ _optionsBackButton.Anchor(Gum.Wireframe.Anchor.BottomRight);
+ _optionsBackButton.X = -28f;
+ _optionsBackButton.Y = -10f;
+ _optionsBackButton.Click += HandleOptionsButtonBack;
+ _optionsPanel.AddChild(_optionsBackButton);
+ }
+
+ private void HandleSfxSliderChanged(object sender, EventArgs args)
+ {
+ // Intentionally not playing the UI sound effect here so that it is not
+ // constantly triggered as the user adjusts the slider's thumb on the
+ // track.
+
+ // Get a reference to the sender as a Slider.
+ var slider = (Slider)sender;
+
+ // Set the global sound effect volume to the value of the slider.;
+ Core.Audio.SoundEffectVolume = (float)slider.Value;
+ }
+
+ private void HandleSfxSliderChangeCompleted(object sender, EventArgs e)
+ {
+ // Play the UI Sound effect so the player can hear the difference in audio.
+ Core.Audio.PlaySoundEffect(_uiSoundEffect);
+ }
+
+ private void HandleMusicSliderValueChanged(object sender, EventArgs args)
+ {
+ // Intentionally not playing the UI sound effect here so that it is not
+ // constantly triggered as the user adjusts the slider's thumb on the
+ // track.
+
+ // Get a reference to the sender as a Slider.
+ var slider = (Slider)sender;
+
+ // Set the global song volume to the value of the slider.
+ Core.Audio.SongVolume = (float)slider.Value;
+ }
+
+ private void HandleMusicSliderValueChangeCompleted(object sender, EventArgs args)
+ {
+ // A UI interaction occurred, play the sound effect
+ Core.Audio.PlaySoundEffect(_uiSoundEffect);
+ }
+
+ private void HandleOptionsButtonBack(object sender, EventArgs e)
+ {
+ // A UI interaction occurred, play the sound effect
+ Core.Audio.PlaySoundEffect(_uiSoundEffect);
+
+ // Set the title panel to be visible.
+ _titleScreenButtonsPanel.IsVisible = true;
+
+ // Set the options panel to be invisible.
+ _optionsPanel.IsVisible = false;
+
+ // Give the options button on the title panel focus since we are coming
+ // back from the options screen.
+ _optionsButton.IsFocused = true;
+ }
+
+ private void InitializeUI()
+ {
+ // Clear out any previous UI in case we came here from
+ // a different screen:
+ GumService.Default.Root.Children.Clear();
+
+ CreateTitlePanel();
+ CreateOptionsPanel();
+ }
+
+ public override void Update(GameTime gameTime)
+ {
+ // Update the offsets for the background pattern wrapping so that it
+ // scrolls down and to the right.
+ float offset = _scrollSpeed * (float)gameTime.ElapsedGameTime.TotalSeconds;
+ _backgroundOffset.X -= offset;
+ _backgroundOffset.Y -= offset;
+
+ // Ensure that the offsets do not go beyond the texture bounds so it is
+ // a seamless wrap
+ _backgroundOffset.X %= _backgroundPattern.Width;
+ _backgroundOffset.Y %= _backgroundPattern.Height;
+
+ GumService.Default.Update(gameTime);
+ }
+
+ public override void Draw(GameTime gameTime)
+ {
+ Core.GraphicsDevice.Clear(new Color(32, 40, 78, 255));
+
+ // Draw the background pattern first using the PointWrap sampler state.
+ Core.SpriteBatch.Begin(samplerState: SamplerState.PointWrap);
+ Core.SpriteBatch.Draw(_backgroundPattern, _backgroundDestination, new Rectangle(_backgroundOffset.ToPoint(), _backgroundDestination.Size), Color.White * 0.5f);
+ Core.SpriteBatch.End();
+
+ if (_titleScreenButtonsPanel.IsVisible)
+ {
+ // Begin the sprite batch to prepare for rendering.
+ Core.SpriteBatch.Begin(samplerState: SamplerState.PointClamp);
+
+ // The color to use for the drop shadow text.
+ Color dropShadowColor = Color.Black * 0.5f;
+
+ // Draw the Dungeon text slightly offset from it is original position and
+ // with a transparent color to give it a drop shadow
+ Core.SpriteBatch.DrawString(_font5x, DUNGEON_TEXT, _dungeonTextPos + new Vector2(10, 10), dropShadowColor, 0.0f, _dungeonTextOrigin, 1.0f, SpriteEffects.None, 1.0f);
+
+ // Draw the Dungeon text on top of that at its original position
+ Core.SpriteBatch.DrawString(_font5x, DUNGEON_TEXT, _dungeonTextPos, Color.White, 0.0f, _dungeonTextOrigin, 1.0f, SpriteEffects.None, 1.0f);
+
+ // Draw the Slime text slightly offset from it is original position and
+ // with a transparent color to give it a drop shadow
+ Core.SpriteBatch.DrawString(_font5x, SLIME_TEXT, _slimeTextPos + new Vector2(10, 10), dropShadowColor, 0.0f, _slimeTextOrigin, 1.0f, SpriteEffects.None, 1.0f);
+
+ // Draw the Slime text on top of that at its original position
+ Core.SpriteBatch.DrawString(_font5x, SLIME_TEXT, _slimeTextPos, Color.White, 0.0f, _slimeTextOrigin, 1.0f, SpriteEffects.None, 1.0f);
+
+ // Always end the sprite batch when finished.
+ Core.SpriteBatch.End();
+ }
+
+ GumService.Default.Draw();
+ }
+}
diff --git a/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/UI/AnimatedButton.cs b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/UI/AnimatedButton.cs
new file mode 100644
index 00000000..56ba8cfc
--- /dev/null
+++ b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/UI/AnimatedButton.cs
@@ -0,0 +1,163 @@
+using System;
+using Gum.DataTypes;
+using Gum.DataTypes.Variables;
+using Gum.Forms.Controls;
+using Gum.Forms.DefaultVisuals;
+using Gum.Graphics.Animation;
+using Gum.Managers;
+using Microsoft.Xna.Framework.Input;
+using MonoGameGum.GueDeriving;
+using MonoGameLibrary.Graphics;
+
+namespace DungeonSlime.UI;
+
+///
+/// A custom button implementation that inherits from Gum's Button class to provide
+/// animated visual feedback when focused.
+///
+internal class AnimatedButton : Button
+{
+ ///
+ /// Creates a new AnimatedButton instance using graphics from the specified texture atlas.
+ ///
+ /// The texture atlas containing button graphics and animations
+ public AnimatedButton(TextureAtlas atlas)
+ {
+ // Each Forms conrol has a general Visual property that
+ // has properties shared by all control types. This Visual
+ // type matches the Forms type. It can be casted to access
+ // controls-specific properties.
+ ButtonVisual buttonVisual = (ButtonVisual)Visual;
+ // Width is relative to children with extra padding, height is fixed
+ buttonVisual.Height = 14f;
+ buttonVisual.HeightUnits = DimensionUnitType.Absolute;
+ buttonVisual.Width = 21f;
+ buttonVisual.WidthUnits = DimensionUnitType.RelativeToChildren;
+
+ // Get a reference to the nine-slice background to display the button graphics
+ // A nine-slice allows the button to stretch while preserving corner appearance
+ NineSliceRuntime background = buttonVisual.Background;
+ background.Texture = atlas.Texture;
+ background.TextureAddress = TextureAddress.Custom;
+ background.Color = Microsoft.Xna.Framework.Color.White;
+ // texture coordinates for the background are set down below
+
+ TextRuntime textInstance = buttonVisual.TextInstance;
+ textInstance.Text = "START";
+ textInstance.Blue = 130;
+ textInstance.Green = 86;
+ textInstance.Red = 70;
+ textInstance.UseCustomFont = true;
+ textInstance.CustomFontFile = "fonts/04b_30.fnt";
+ textInstance.FontScale = 0.25f;
+ textInstance.Anchor(Gum.Wireframe.Anchor.Center);
+ textInstance.Width = 0;
+ textInstance.WidthUnits = DimensionUnitType.RelativeToChildren;
+
+ // Get the texture region for the unfocused button state from the atlas
+ TextureRegion unfocusedTextureRegion = atlas.GetRegion("unfocused-button");
+
+ // Create an animation chain for the unfocused state with a single frame
+ AnimationChain unfocusedAnimation = new AnimationChain();
+ unfocusedAnimation.Name = nameof(unfocusedAnimation);
+ AnimationFrame unfocusedFrame = new AnimationFrame
+ {
+ TopCoordinate = unfocusedTextureRegion.TopTextureCoordinate,
+ BottomCoordinate = unfocusedTextureRegion.BottomTextureCoordinate,
+ LeftCoordinate = unfocusedTextureRegion.LeftTextureCoordinate,
+ RightCoordinate = unfocusedTextureRegion.RightTextureCoordinate,
+ FrameLength = 0.3f,
+ Texture = unfocusedTextureRegion.Texture
+ };
+ unfocusedAnimation.Add(unfocusedFrame);
+
+ // Get the multi-frame animation for the focused button state from the atlas
+ Animation focusedAtlasAnimation = atlas.GetAnimation("focused-button-animation");
+
+ // Create an animation chain for the focused state using all frames from the atlas animation
+ AnimationChain focusedAnimation = new AnimationChain();
+ focusedAnimation.Name = nameof(focusedAnimation);
+ foreach (TextureRegion region in focusedAtlasAnimation.Frames)
+ {
+ AnimationFrame frame = new AnimationFrame
+ {
+ TopCoordinate = region.TopTextureCoordinate,
+ BottomCoordinate = region.BottomTextureCoordinate,
+ LeftCoordinate = region.LeftTextureCoordinate,
+ RightCoordinate = region.RightTextureCoordinate,
+ FrameLength = (float)focusedAtlasAnimation.Delay.TotalSeconds,
+ Texture = region.Texture
+ };
+
+ focusedAnimation.Add(frame);
+ }
+
+ // Assign both animation chains to the nine-slice background
+ background.AnimationChains = new AnimationChainList
+ {
+ unfocusedAnimation,
+ focusedAnimation
+ };
+
+
+ // Reset all state to default so we don't have unexpected variable assignments:
+ buttonVisual.ButtonCategory.ResetAllStates();
+
+ // Get the enabled (default/unfocused) state
+ StateSave enabledState = buttonVisual.States.Enabled;
+ enabledState.Apply = () =>
+ {
+ // When enabled but not focused, use the unfocused animation
+ background.CurrentChainName = unfocusedAnimation.Name;
+ };
+
+ // Create the focused state
+ StateSave focusedState = buttonVisual.States.Focused;
+ focusedState.Apply = () =>
+ {
+ // When focused, use the focused animation and enable animation playback
+ background.CurrentChainName = focusedAnimation.Name;
+ background.Animate = true;
+ };
+
+ // Create the highlighted+focused state (for mouse hover while focused)
+ StateSave highlightedFocused = buttonVisual.States.HighlightedFocused;
+ highlightedFocused.Apply = focusedState.Apply;
+
+ // Create the highlighted state (for mouse hover)
+ // by cloning the enabled state since they appear the same
+ StateSave highlighted = buttonVisual.States.Highlighted;
+ highlighted.Apply = enabledState.Apply;
+
+ // Add event handlers for keyboard input.
+ KeyDown += HandleKeyDown;
+
+ // Add event handler for mouse hover focus.
+ buttonVisual.RollOn += HandleRollOn;
+ }
+
+ ///
+ /// Handles keyboard input for navigation between buttons using left/right keys.
+ ///
+ private void HandleKeyDown(object sender, KeyEventArgs e)
+ {
+ if (e.Key == Keys.Left)
+ {
+ // Left arrow navigates to previous control
+ HandleTab(TabDirection.Up, loop: true);
+ }
+ if (e.Key == Keys.Right)
+ {
+ // Right arrow navigates to next control
+ HandleTab(TabDirection.Down, loop: true);
+ }
+ }
+
+ ///
+ /// Automatically focuses the button when the mouse hovers over it.
+ ///
+ private void HandleRollOn(object sender, EventArgs e)
+ {
+ IsFocused = true;
+ }
+}
diff --git a/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/UI/GameSceneUI.cs b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/UI/GameSceneUI.cs
new file mode 100644
index 00000000..84be9293
--- /dev/null
+++ b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/UI/GameSceneUI.cs
@@ -0,0 +1,340 @@
+using System;
+using Gum.DataTypes;
+using Gum.Managers;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Audio;
+using Microsoft.Xna.Framework.Content;
+using MonoGameGum;
+using Gum.Forms.Controls;
+using MonoGameGum.GueDeriving;
+using MonoGameLibrary;
+using MonoGameLibrary.Graphics;
+
+namespace DungeonSlime.UI;
+
+public class GameSceneUI : ContainerRuntime
+{
+ // The string format to use when updating the text for the score display.
+ private static readonly string s_scoreFormat = "SCORE: {0:D6}";
+
+ // The sound effect to play for auditory feedback of the user interface.
+ private SoundEffect _uiSoundEffect;
+
+ // The pause panel
+ private Panel _pausePanel;
+
+ // The resume button on the pause panel. Field is used to track reference so
+ // focus can be set when the pause panel is shown.
+ private AnimatedButton _resumeButton;
+
+ // The game over panel.
+ private Panel _gameOverPanel;
+
+ // The retry button on the game over panel. Field is used to track reference
+ // so focus can be set when the game over panel is shown.
+ private AnimatedButton _retryButton;
+
+ // The text runtime used to display the players score on the game screen.
+ private TextRuntime _scoreText;
+
+ ///
+ /// Event invoked when the Resume button on the Pause panel is clicked.
+ ///
+ public event EventHandler ResumeButtonClick;
+
+ ///
+ /// Event invoked when the Quit button on either the Pause panel or the
+ /// Game Over panel is clicked.
+ ///
+ public event EventHandler QuitButtonClick;
+
+ ///
+ /// Event invoked when the Retry button on the Game Over panel is clicked.
+ ///
+ public event EventHandler RetryButtonClick;
+
+ public GameSceneUI()
+ {
+ // The game scene UI inherits from ContainerRuntime, so we set its
+ // doc to fill so it fills the entire screen.
+ Dock(Gum.Wireframe.Dock.Fill);
+
+ // Add it to the root element.
+ this.AddToRoot();
+
+ // Get a reference to the content manager that was registered with the
+ // GumService when it was original initialized.
+ ContentManager content = GumService.Default.ContentLoader.XnaContentManager;
+
+ // Use that content manager to load the sound effect and atlas for the
+ // user interface elements
+ _uiSoundEffect = content.Load("audio/ui");
+ TextureAtlas atlas = TextureAtlas.FromFile(content, "images/atlas-definition.xml");
+
+ // Create the text that will display the players score and add it as
+ // a child to this container.
+ _scoreText = CreateScoreText();
+ AddChild(_scoreText);
+
+ // Create the Pause panel that is displayed when the game is paused and
+ // add it as a child to this container
+ _pausePanel = CreatePausePanel(atlas);
+ AddChild(_pausePanel.Visual);
+
+ // Create the Game Over panel that is displayed when a game over occurs
+ // and add it as a child to this container
+ _gameOverPanel = CreateGameOverPanel(atlas);
+ AddChild(_gameOverPanel.Visual);
+ }
+
+ private TextRuntime CreateScoreText()
+ {
+ TextRuntime text = new TextRuntime();
+ text.Anchor(Gum.Wireframe.Anchor.TopLeft);
+ text.WidthUnits = DimensionUnitType.RelativeToChildren;
+ text.X = 20.0f;
+ text.Y = 5.0f;
+ text.UseCustomFont = true;
+ text.CustomFontFile = @"fonts/04b_30.fnt";
+ text.FontScale = 0.25f;
+ text.Text = string.Format(s_scoreFormat, 0);
+
+ return text;
+ }
+
+ private Panel CreatePausePanel(TextureAtlas atlas)
+ {
+ Panel panel = new Panel();
+ panel.Anchor(Gum.Wireframe.Anchor.Center);
+ panel.Visual.WidthUnits = DimensionUnitType.Absolute;
+ panel.Visual.HeightUnits = DimensionUnitType.Absolute;
+ panel.Visual.Width = 264.0f;
+ panel.Visual.Height = 70.0f;
+ panel.IsVisible = false;
+
+ TextureRegion backgroundRegion = atlas.GetRegion("panel-background");
+
+ NineSliceRuntime background = new NineSliceRuntime();
+ background.Dock(Gum.Wireframe.Dock.Fill);
+ background.Texture = backgroundRegion.Texture;
+ background.TextureAddress = TextureAddress.Custom;
+ background.TextureHeight = backgroundRegion.Height;
+ background.TextureWidth = backgroundRegion.Width;
+ background.TextureTop = backgroundRegion.SourceRectangle.Top;
+ background.TextureLeft = backgroundRegion.SourceRectangle.Left;
+ panel.AddChild(background);
+
+ TextRuntime text = new TextRuntime();
+ text.Text = "PAUSED";
+ text.UseCustomFont = true;
+ text.CustomFontFile = "fonts/04b_30.fnt";
+ text.FontScale = 0.5f;
+ text.X = 10.0f;
+ text.Y = 10.0f;
+ panel.AddChild(text);
+
+ _resumeButton = new AnimatedButton(atlas);
+ _resumeButton.Text = "RESUME";
+ _resumeButton.Anchor(Gum.Wireframe.Anchor.BottomLeft);
+ _resumeButton.Visual.X = 9.0f;
+ _resumeButton.Visual.Y = -9.0f;
+
+ _resumeButton.Click += OnResumeButtonClicked;
+ _resumeButton.GotFocus += OnElementGotFocus;
+
+ panel.AddChild(_resumeButton);
+
+ AnimatedButton quitButton = new AnimatedButton(atlas);
+ quitButton.Text = "QUIT";
+ quitButton.Anchor(Gum.Wireframe.Anchor.BottomRight);
+ quitButton.Visual.X = -9.0f;
+ quitButton.Visual.Y = -9.0f;
+
+ quitButton.Click += OnQuitButtonClicked;
+ quitButton.GotFocus += OnElementGotFocus;
+
+ panel.AddChild(quitButton);
+
+ return panel;
+ }
+
+ private Panel CreateGameOverPanel(TextureAtlas atlas)
+ {
+ Panel panel = new Panel();
+ panel.Anchor(Gum.Wireframe.Anchor.Center);
+ panel.Visual.WidthUnits = DimensionUnitType.Absolute;
+ panel.Visual.HeightUnits = DimensionUnitType.Absolute;
+ panel.Visual.Width = 264.0f;
+ panel.Visual.Height = 70.0f;
+ panel.IsVisible = false;
+
+ TextureRegion backgroundRegion = atlas.GetRegion("panel-background");
+
+ NineSliceRuntime background = new NineSliceRuntime();
+ background.Dock(Gum.Wireframe.Dock.Fill);
+ background.Texture = backgroundRegion.Texture;
+ background.TextureAddress = TextureAddress.Custom;
+ background.TextureHeight = backgroundRegion.Height;
+ background.TextureWidth = backgroundRegion.Width;
+ background.TextureTop = backgroundRegion.SourceRectangle.Top;
+ background.TextureLeft = backgroundRegion.SourceRectangle.Left;
+ panel.AddChild(background);
+
+ TextRuntime text = new TextRuntime();
+ text.Text = "GAME OVER";
+ text.WidthUnits = DimensionUnitType.RelativeToChildren;
+ text.UseCustomFont = true;
+ text.CustomFontFile = "fonts/04b_30.fnt";
+ text.FontScale = 0.5f;
+ text.X = 10.0f;
+ text.Y = 10.0f;
+ panel.AddChild(text);
+
+ _retryButton = new AnimatedButton(atlas);
+ _retryButton.Text = "RETRY";
+ _retryButton.Anchor(Gum.Wireframe.Anchor.BottomLeft);
+ _retryButton.Visual.X = 9.0f;
+ _retryButton.Visual.Y = -9.0f;
+
+ _retryButton.Click += OnRetryButtonClicked;
+ _retryButton.GotFocus += OnElementGotFocus;
+
+ panel.AddChild(_retryButton);
+
+ AnimatedButton quitButton = new AnimatedButton(atlas);
+ quitButton.Text = "QUIT";
+ quitButton.Anchor(Gum.Wireframe.Anchor.BottomRight);
+ quitButton.Visual.X = -9.0f;
+ quitButton.Visual.Y = -9.0f;
+
+ quitButton.Click += OnQuitButtonClicked;
+ quitButton.GotFocus += OnElementGotFocus;
+
+ panel.AddChild(quitButton);
+
+ return panel;
+ }
+
+ private void OnResumeButtonClicked(object sender, EventArgs args)
+ {
+ // Button was clicked, play the ui sound effect for auditory feedback.
+ Core.Audio.PlaySoundEffect(_uiSoundEffect);
+
+ // Since the resume button was clicked, we need to hide the pause panel.
+ HidePausePanel();
+
+ // Invoke the ResumeButtonClick event
+ if (ResumeButtonClick != null)
+ {
+ ResumeButtonClick(sender, args);
+ }
+ }
+
+ private void OnRetryButtonClicked(object sender, EventArgs args)
+ {
+ // Button was clicked, play the ui sound effect for auditory feedback.
+ Core.Audio.PlaySoundEffect(_uiSoundEffect);
+
+ // Since the retry button was clicked, we need to hide the game over panel.
+ HideGameOverPanel();
+
+ // Invoke the RetryButtonClick event.
+ if (RetryButtonClick != null)
+ {
+ RetryButtonClick(sender, args);
+ }
+ }
+
+ private void OnQuitButtonClicked(object sender, EventArgs args)
+ {
+ // Button was clicked, play the ui sound effect for auditory feedback.
+ Core.Audio.PlaySoundEffect(_uiSoundEffect);
+
+ // Both panels have a quit button, so hide both panels
+ HidePausePanel();
+ HideGameOverPanel();
+
+ // Invoke the QuitButtonClick event.
+ if (QuitButtonClick != null)
+ {
+ QuitButtonClick(sender, args);
+ }
+ }
+
+ private void OnElementGotFocus(object sender, EventArgs args)
+ {
+ // A ui element that can receive focus has received focus, play the
+ // ui sound effect for auditory feedback.
+ Core.Audio.PlaySoundEffect(_uiSoundEffect);
+ }
+
+ ///
+ /// Updates the text on the score display.
+ ///
+ /// The score to display.
+ public void UpdateScoreText(int score)
+ {
+ _scoreText.Text = string.Format(s_scoreFormat, score);
+ }
+
+ ///
+ /// Tells the game scene ui to show the pause panel.
+ ///
+ public void ShowPausePanel()
+ {
+ _pausePanel.IsVisible = true;
+
+ // Give the resume button focus for keyboard/gamepad input.
+ _resumeButton.IsFocused = true;
+
+ // Ensure the game over panel isn't visible.
+ _gameOverPanel.IsVisible = false;
+ }
+
+ ///
+ /// Tells the game scene ui to hide the pause panel.
+ ///
+ public void HidePausePanel()
+ {
+ _pausePanel.IsVisible = false;
+ }
+
+ ///
+ /// Tells the game scene ui to show the game over panel.
+ ///
+ public void ShowGameOverPanel()
+ {
+ _gameOverPanel.IsVisible = true;
+
+ // Give the retry button focus for keyboard/gamepad input.
+ _retryButton.IsFocused = true;
+
+ // Ensure the pause panel isn't visible.
+ _pausePanel.IsVisible = false;
+ }
+
+ ///
+ /// Tells the game scene ui to hide the game over panel.
+ ///
+ public void HideGameOverPanel()
+ {
+ _gameOverPanel.IsVisible = false;
+ }
+
+ ///
+ /// Updates the game scene ui.
+ ///
+ /// A snapshot of the timing values for the current update cycle.
+ public void Update(GameTime gameTime)
+ {
+ GumService.Default.Update(gameTime);
+ }
+
+ ///
+ /// Draws the game scene ui.
+ ///
+ public void Draw()
+ {
+ GumService.Default.Draw();
+ }
+}
diff --git a/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/UI/OptionsSlider.cs b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/UI/OptionsSlider.cs
new file mode 100644
index 00000000..86d03281
--- /dev/null
+++ b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/UI/OptionsSlider.cs
@@ -0,0 +1,253 @@
+using System;
+using Gum.DataTypes;
+using Gum.DataTypes.Variables;
+using Gum.Managers;
+using Microsoft.Xna.Framework;
+using Gum.Forms.Controls;
+using MonoGameGum.GueDeriving;
+using MonoGameLibrary.Graphics;
+
+namespace DungeonSlime.UI;
+
+///
+/// A custom slider control that inherits from Gum's Slider class.
+///
+public class OptionsSlider : Slider
+{
+ // Reference to the text label that displays the slider's title
+ private TextRuntime _textInstance;
+
+ // Reference to the rectangle that visually represents the current value
+ private ColoredRectangleRuntime _fillRectangle;
+
+ ///
+ /// Gets or sets the text label for this slider.
+ ///
+ public string Text
+ {
+ get => _textInstance.Text;
+ set => _textInstance.Text = value;
+ }
+
+ ///
+ /// Creates a new OptionsSlider instance using graphics from the specified texture atlas.
+ ///
+ /// The texture atlas containing slider graphics.
+ public OptionsSlider(TextureAtlas atlas)
+ {
+ // Create the top-level container for all visual elements
+ ContainerRuntime topLevelContainer = new ContainerRuntime();
+ topLevelContainer.Height = 55f;
+ topLevelContainer.Width = 264f;
+
+ TextureRegion backgroundRegion = atlas.GetRegion("panel-background");
+
+ // Create the background panel that contains everything
+ NineSliceRuntime background = new NineSliceRuntime();
+ background.Texture = atlas.Texture;
+ background.TextureAddress = TextureAddress.Custom;
+ background.TextureHeight = backgroundRegion.Height;
+ background.TextureLeft = backgroundRegion.SourceRectangle.Left;
+ background.TextureTop = backgroundRegion.SourceRectangle.Top;
+ background.TextureWidth = backgroundRegion.Width;
+ background.Dock(Gum.Wireframe.Dock.Fill);
+ topLevelContainer.AddChild(background);
+
+ // Create the title text element
+ _textInstance = new TextRuntime();
+ _textInstance.CustomFontFile = @"fonts/04b_30.fnt";
+ _textInstance.UseCustomFont = true;
+ _textInstance.FontScale = 0.5f;
+ _textInstance.Text = "Replace Me";
+ _textInstance.X = 10f;
+ _textInstance.Y = 10f;
+ _textInstance.WidthUnits = DimensionUnitType.RelativeToChildren;
+ topLevelContainer.AddChild(_textInstance);
+
+ // Create the container for the slider track and decorative elements
+ ContainerRuntime innerContainer = new ContainerRuntime();
+ innerContainer.Height = 13f;
+ innerContainer.Width = 241f;
+ innerContainer.X = 10f;
+ innerContainer.Y = 33f;
+ topLevelContainer.AddChild(innerContainer);
+
+ TextureRegion offBackgroundRegion = atlas.GetRegion("slider-off-background");
+
+ // Create the "OFF" side of the slider (left end)
+ NineSliceRuntime offBackground = new NineSliceRuntime();
+ offBackground.Dock(Gum.Wireframe.Dock.Left);
+ offBackground.Texture = atlas.Texture;
+ offBackground.TextureAddress = TextureAddress.Custom;
+ offBackground.TextureHeight = offBackgroundRegion.Height;
+ offBackground.TextureLeft = offBackgroundRegion.SourceRectangle.Left;
+ offBackground.TextureTop = offBackgroundRegion.SourceRectangle.Top;
+ offBackground.TextureWidth = offBackgroundRegion.Width;
+ offBackground.Width = 28f;
+ offBackground.WidthUnits = DimensionUnitType.Absolute;
+ offBackground.Dock(Gum.Wireframe.Dock.Left);
+ innerContainer.AddChild(offBackground);
+
+ TextureRegion middleBackgroundRegion = atlas.GetRegion("slider-middle-background");
+
+ // Create the middle track portion of the slider
+ NineSliceRuntime middleBackground = new NineSliceRuntime();
+ middleBackground.Dock(Gum.Wireframe.Dock.FillVertically);
+ middleBackground.Texture = middleBackgroundRegion.Texture;
+ middleBackground.TextureAddress = TextureAddress.Custom;
+ middleBackground.TextureHeight = middleBackgroundRegion.Height;
+ middleBackground.TextureLeft = middleBackgroundRegion.SourceRectangle.Left;
+ middleBackground.TextureTop = middleBackgroundRegion.SourceRectangle.Top;
+ middleBackground.TextureWidth = middleBackgroundRegion.Width;
+ middleBackground.Width = 179f;
+ middleBackground.WidthUnits = DimensionUnitType.Absolute;
+ middleBackground.Dock(Gum.Wireframe.Dock.Left);
+ middleBackground.X = 27f;
+ innerContainer.AddChild(middleBackground);
+
+ TextureRegion maxBackgroundRegion = atlas.GetRegion("slider-max-background");
+
+ // Create the "MAX" side of the slider (right end)
+ NineSliceRuntime maxBackground = new NineSliceRuntime();
+ maxBackground.Texture = maxBackgroundRegion.Texture;
+ maxBackground.TextureAddress = TextureAddress.Custom;
+ maxBackground.TextureHeight = maxBackgroundRegion.Height;
+ maxBackground.TextureLeft = maxBackgroundRegion.SourceRectangle.Left;
+ maxBackground.TextureTop = maxBackgroundRegion.SourceRectangle.Top;
+ maxBackground.TextureWidth = maxBackgroundRegion.Width;
+ maxBackground.Width = 36f;
+ maxBackground.WidthUnits = DimensionUnitType.Absolute;
+ maxBackground.Dock(Gum.Wireframe.Dock.Right);
+ innerContainer.AddChild(maxBackground);
+
+ // Create the interactive track that responds to clicks
+ // The special name "TrackInstance" is required for Slider functionality
+ ContainerRuntime trackInstance = new ContainerRuntime();
+ trackInstance.Name = "TrackInstance";
+ trackInstance.Dock(Gum.Wireframe.Dock.Fill);
+ trackInstance.Height = -2f;
+ trackInstance.Width = -2f;
+ middleBackground.AddChild(trackInstance);
+
+ // Create the fill rectangle that visually displays the current value
+ _fillRectangle = new ColoredRectangleRuntime();
+ _fillRectangle.Dock(Gum.Wireframe.Dock.Left);
+ _fillRectangle.Width = 90f; // Default to 90% - will be updated by value changes
+ _fillRectangle.WidthUnits = DimensionUnitType.PercentageOfParent;
+ trackInstance.AddChild(_fillRectangle);
+
+ // Add "OFF" text to the left end
+ TextRuntime offText = new TextRuntime();
+ offText.Red = 70;
+ offText.Green = 86;
+ offText.Blue = 130;
+ offText.CustomFontFile = @"fonts/04b_30.fnt";
+ offText.FontScale = 0.25f;
+ offText.UseCustomFont = true;
+ offText.Text = "OFF";
+ offText.Anchor(Gum.Wireframe.Anchor.Center);
+ offBackground.AddChild(offText);
+
+ // Add "MAX" text to the right end
+ TextRuntime maxText = new TextRuntime();
+ maxText.Red = 70;
+ maxText.Green = 86;
+ maxText.Blue = 130;
+ maxText.CustomFontFile = @"fonts/04b_30.fnt";
+ maxText.FontScale = 0.25f;
+ maxText.UseCustomFont = true;
+ maxText.Text = "MAX";
+ maxText.Anchor(Gum.Wireframe.Anchor.Center);
+ maxBackground.AddChild(maxText);
+
+ // Define colors for focused and unfocused states
+ Color focusedColor = Color.White;
+ Color unfocusedColor = Color.Gray;
+
+ // Create slider state category - Slider.SliderCategoryName is the required name
+ StateSaveCategory sliderCategory = new StateSaveCategory();
+ sliderCategory.Name = Slider.SliderCategoryName;
+ topLevelContainer.AddCategory(sliderCategory);
+
+ // Create the enabled (default/unfocused) state
+ StateSave enabled = new StateSave();
+ enabled.Name = FrameworkElement.EnabledStateName;
+ enabled.Apply = () =>
+ {
+ // When enabled but not focused, use gray coloring for all elements
+ background.Color = unfocusedColor;
+ _textInstance.Color = unfocusedColor;
+ offBackground.Color = unfocusedColor;
+ middleBackground.Color = unfocusedColor;
+ maxBackground.Color = unfocusedColor;
+ _fillRectangle.Color = unfocusedColor;
+ };
+ sliderCategory.States.Add(enabled);
+
+ // Create the focused state
+ StateSave focused = new StateSave();
+ focused.Name = FrameworkElement.FocusedStateName;
+ focused.Apply = () =>
+ {
+ // When focused, use white coloring for all elements
+ background.Color = focusedColor;
+ _textInstance.Color = focusedColor;
+ offBackground.Color = focusedColor;
+ middleBackground.Color = focusedColor;
+ maxBackground.Color = focusedColor;
+ _fillRectangle.Color = focusedColor;
+ };
+ sliderCategory.States.Add(focused);
+
+ // Create the highlighted+focused state by cloning the focused state
+ StateSave highlightedFocused = focused.Clone();
+ highlightedFocused.Name = FrameworkElement.HighlightedFocusedStateName;
+ sliderCategory.States.Add(highlightedFocused);
+
+ // Create the highlighted state by cloning the enabled state
+ StateSave highlighted = enabled.Clone();
+ highlighted.Name = FrameworkElement.HighlightedStateName;
+ sliderCategory.States.Add(highlighted);
+
+ // Assign the configured container as this slider's visual
+ Visual = topLevelContainer;
+
+ // Enable click-to-point functionality for the slider
+ // This allows users to click anywhere on the track to jump to that value
+ IsMoveToPointEnabled = true;
+
+ // Add event handlers
+ Visual.RollOn += HandleRollOn;
+ ValueChanged += HandleValueChanged;
+ ValueChangedByUi += HandleValueChangedByUi;
+ }
+
+ ///
+ /// Automatically focuses the slider when the user interacts with it
+ ///
+ private void HandleValueChangedByUi(object sender, EventArgs e)
+ {
+ IsFocused = true;
+ }
+
+ ///
+ /// Automatically focuses the slider when the mouse hovers over it
+ ///
+ private void HandleRollOn(object sender, EventArgs e)
+ {
+ IsFocused = true;
+ }
+
+ ///
+ /// Updates the fill rectangle width to visually represent the current value
+ ///
+ private void HandleValueChanged(object sender, EventArgs e)
+ {
+ // Calculate the ratio of the current value within its range
+ double ratio = (Value - Minimum) / (Maximum - Minimum);
+
+ // Update the fill rectangle width as a percentage
+ // _fillRectangle uses percentage width units, so we multiply by 100
+ _fillRectangle.Width = 100 * (float)ratio;
+ }
+}
diff --git a/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/app.manifest b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/app.manifest
new file mode 100644
index 00000000..caf45166
--- /dev/null
+++ b/Tutorials/MobileDeployment/01-UpdatedLibs/DungeonSlime/app.manifest
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true/pm
+ permonitorv2,permonitor
+
+
+
+
diff --git a/Tutorials/MobileDeployment/01-UpdatedLibs/MonoGameLibrary/Audio/AudioController.cs b/Tutorials/MobileDeployment/01-UpdatedLibs/MonoGameLibrary/Audio/AudioController.cs
new file mode 100644
index 00000000..1bffd636
--- /dev/null
+++ b/Tutorials/MobileDeployment/01-UpdatedLibs/MonoGameLibrary/Audio/AudioController.cs
@@ -0,0 +1,280 @@
+using System;
+using System.Collections.Generic;
+using Microsoft.Xna.Framework.Audio;
+using Microsoft.Xna.Framework.Media;
+
+namespace MonoGameLibrary.Audio;
+
+public class AudioController : IDisposable
+{
+ // Tracks sound effect instances created so they can be paused, unpaused, and/or disposed.
+ private readonly List _activeSoundEffectInstances;
+
+ // Tracks the volume for song playback when muting and unmuting.
+ private float _previousSongVolume;
+
+ // Tracks the volume for sound effect playback when muting and unmuting.
+ private float _previousSoundEffectVolume;
+
+ ///
+ /// Gets a value that indicates if audio is muted.
+ ///
+ public bool IsMuted { get; private set; }
+
+ ///
+ /// Gets or Sets the global volume of songs.
+ ///
+ ///
+ /// If IsMuted is true, the getter will always return back 0.0f and the
+ /// setter will ignore setting the volume.
+ ///
+ public float SongVolume
+ {
+ get
+ {
+ if (IsMuted)
+ {
+ return 0.0f;
+ }
+
+ return MediaPlayer.Volume;
+ }
+ set
+ {
+ if (IsMuted)
+ {
+ return;
+ }
+
+ MediaPlayer.Volume = Math.Clamp(value, 0.0f, 1.0f);
+ }
+ }
+
+ ///
+ /// Gets or Sets the global volume of sound effects.
+ ///
+ ///
+ /// If IsMuted is true, the getter will always return back 0.0f and the
+ /// setter will ignore setting the volume.
+ ///
+ public float SoundEffectVolume
+ {
+ get
+ {
+ if (IsMuted)
+ {
+ return 0.0f;
+ }
+
+ return SoundEffect.MasterVolume;
+ }
+ set
+ {
+ if (IsMuted)
+ {
+ return;
+ }
+
+ SoundEffect.MasterVolume = Math.Clamp(value, 0.0f, 1.0f);
+ }
+ }
+
+ ///
+ /// Gets a value that indicates if this audio controller has been disposed.
+ ///
+ public bool IsDisposed { get; private set; }
+
+ ///
+ /// Creates a new audio controller instance.
+ ///
+ public AudioController()
+ {
+ _activeSoundEffectInstances = new List();
+ }
+
+ // Finalizer called when object is collected by the garbage collector
+ ~AudioController() => Dispose(false);
+
+ ///
+ /// Updates this audio controller
+ ///
+ public void Update()
+ {
+ int index = 0;
+
+ while (index < _activeSoundEffectInstances.Count)
+ {
+ SoundEffectInstance instance = _activeSoundEffectInstances[index];
+
+ if (instance.State == SoundState.Stopped && !instance.IsDisposed)
+ {
+ instance.Dispose();
+ }
+
+ _activeSoundEffectInstances.RemoveAt(index);
+ }
+ }
+
+ ///
+ /// Plays the given sound effect.
+ ///
+ /// The sound effect to play.
+ /// The sound effect instance created by this method.
+ public SoundEffectInstance PlaySoundEffect(SoundEffect soundEffect)
+ {
+ return PlaySoundEffect(soundEffect, 1.0f, 1.0f, 0.0f, false);
+ }
+
+ ///
+ /// Plays the given sound effect with the specified properties.
+ ///
+ /// The sound effect to play.
+ /// The volume, ranging from 0.0 (silence) to 1.0 (full volume).
+ /// The pitch adjustment, ranging from -1.0 (down an octave) to 0.0 (no change) to 1.0 (up an octave).
+ /// The panning, ranging from -1.0 (left speaker) to 0.0 (centered), 1.0 (right speaker).
+ /// Whether the the sound effect should loop after playback.
+ /// The sound effect instance created by playing the sound effect.
+ /// The sound effect instance created by this method.
+ public SoundEffectInstance PlaySoundEffect(SoundEffect soundEffect, float volume, float pitch, float pan, bool isLooped)
+ {
+ // Create an instance from the sound effect given.
+ SoundEffectInstance soundEffectInstance = soundEffect.CreateInstance();
+
+ // Apply the volume, pitch, pan, and loop values specified.
+ soundEffectInstance.Volume = volume;
+ soundEffectInstance.Pitch = pitch;
+ soundEffectInstance.Pan = pan;
+ soundEffectInstance.IsLooped = isLooped;
+
+ // Tell the instance to play
+ soundEffectInstance.Play();
+
+ // Add it to the active instances for tracking
+ _activeSoundEffectInstances.Add(soundEffectInstance);
+
+ return soundEffectInstance;
+ }
+
+ ///
+ /// Plays the given song.
+ ///
+ /// The song to play.
+ /// Optionally specify if the song should repeat. Default is true.
+ public void PlaySong(Song song, bool isRepeating = true)
+ {
+ // Check if the media player is already playing, if so, stop it.
+ // If we do not stop it, this could cause issues on some platforms
+ if (MediaPlayer.State == MediaState.Playing)
+ {
+ MediaPlayer.Stop();
+ }
+
+ MediaPlayer.Play(song);
+ MediaPlayer.IsRepeating = isRepeating;
+ }
+
+ ///
+ /// Pauses all audio.
+ ///
+ public void PauseAudio()
+ {
+ // Pause any active songs playing
+ MediaPlayer.Pause();
+
+ // Pause any active sound effects
+ foreach (SoundEffectInstance soundEffectInstance in _activeSoundEffectInstances)
+ {
+ soundEffectInstance.Pause();
+ }
+ }
+
+ ///
+ /// Resumes play of all previous paused audio.
+ ///
+ public void ResumeAudio()
+ {
+ // Resume paused music
+ MediaPlayer.Resume();
+
+ // Resume any active sound effects
+ foreach (SoundEffectInstance soundEffectInstance in _activeSoundEffectInstances)
+ {
+ soundEffectInstance.Resume();
+ }
+ }
+
+ ///
+ /// Mutes all audio.
+ ///
+ public void MuteAudio()
+ {
+ // Store the volume so they can be restored during ResumeAudio
+ _previousSongVolume = MediaPlayer.Volume;
+ _previousSoundEffectVolume = SoundEffect.MasterVolume;
+
+ // Set all volumes to 0
+ MediaPlayer.Volume = 0.0f;
+ SoundEffect.MasterVolume = 0.0f;
+
+ IsMuted = true;
+ }
+
+ ///
+ /// Unmutes all audio to the volume level prior to muting.
+ ///
+ public void UnmuteAudio()
+ {
+ // Restore the previous volume values
+ MediaPlayer.Volume = _previousSongVolume;
+ SoundEffect.MasterVolume = _previousSoundEffectVolume;
+
+ IsMuted = false;
+ }
+
+ ///
+ /// Toggles the current audio mute state.
+ ///
+ public void ToggleMute()
+ {
+ if (IsMuted)
+ {
+ UnmuteAudio();
+ }
+ else
+ {
+ MuteAudio();
+ }
+ }
+
+ ///
+ /// Disposes of this audio controller and cleans up resources.
+ ///
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ /// Disposes this audio controller and cleans up resources.
+ ///
+ /// Indicates whether managed resources should be disposed.
+ protected void Dispose(bool disposing)
+ {
+ if (IsDisposed)
+ {
+ return;
+ }
+
+ if (disposing)
+ {
+ foreach (SoundEffectInstance soundEffectInstance in _activeSoundEffectInstances)
+ {
+ soundEffectInstance.Dispose();
+ }
+ _activeSoundEffectInstances.Clear();
+ }
+
+ IsDisposed = true;
+ }
+}
diff --git a/Tutorials/MobileDeployment/01-UpdatedLibs/MonoGameLibrary/Circle.cs b/Tutorials/MobileDeployment/01-UpdatedLibs/MonoGameLibrary/Circle.cs
new file mode 100644
index 00000000..0bb691bc
--- /dev/null
+++ b/Tutorials/MobileDeployment/01-UpdatedLibs/MonoGameLibrary/Circle.cs
@@ -0,0 +1,136 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGameLibrary;
+
+public readonly struct Circle : IEquatable
+{
+ private static readonly Circle s_empty = new Circle();
+
+ ///
+ /// The x-coordinate of the center of this circle.
+ ///
+ public readonly int X;
+
+ ///
+ /// The y-coordinate of the center of this circle.
+ ///
+ public readonly int Y;
+
+ ///
+ /// The length, in pixels, from the center of this circle to the edge.
+ ///
+ public readonly int Radius;
+
+ ///
+ /// Gets the location of the center of this circle.
+ ///
+ public readonly Point Location => new Point(X, Y);
+
+ ///
+ /// Gets a circle with X=0, Y=0, and Radius=0.
+ ///
+ public static Circle Empty => s_empty;
+
+ ///
+ /// Gets a value that indicates whether this circle has a radius of 0 and a location of (0, 0).
+ ///
+ public readonly bool IsEmpty => X == 0 && Y == 0 && Radius == 0;
+
+ ///
+ /// Gets the y-coordinate of the highest point on this circle.
+ ///
+ public readonly int Top => Y - Radius;
+
+ ///
+ /// Gets the y-coordinate of the lowest point on this circle.
+ ///
+ public readonly int Bottom => Y + Radius;
+
+ ///
+ /// Gets the x-coordinate of the leftmost point on this circle.
+ ///
+ public readonly int Left => X - Radius;
+
+ ///
+ /// Gets the x-coordinate of the rightmost point on this circle.
+ ///
+ public readonly int Right => X + Radius;
+
+ ///
+ /// Creates a new circle with the specified position and radius.
+ ///
+ /// The x-coordinate of the center of the circle.
+ /// The y-coordinate of the center of the circle..
+ /// The length from the center of the circle to an edge.
+ public Circle(int x, int y, int radius)
+ {
+ X = x;
+ Y = y;
+ Radius = radius;
+ }
+
+ ///
+ /// Creates a new circle with the specified position and radius.
+ ///
+ /// The center of the circle.
+ /// The length from the center of the circle to an edge.
+ public Circle(Point location, int radius)
+ {
+ X = location.X;
+ Y = location.Y;
+ Radius = radius;
+ }
+
+ ///
+ /// Returns a value that indicates whether the specified circle intersects with this circle.
+ ///
+ /// The other circle to check.
+ /// true if the other circle intersects with this circle; otherwise, false.
+ public bool Intersects(Circle other)
+ {
+ int radiiSquared = (this.Radius + other.Radius) * (this.Radius + other.Radius);
+ float distanceSquared = Vector2.DistanceSquared(this.Location.ToVector2(), other.Location.ToVector2());
+ return distanceSquared < radiiSquared;
+ }
+
+ ///
+ /// Returns a value that indicates whether this circle and the specified object are equal
+ ///
+ /// The object to compare with this circle.
+ /// true if this circle and the specified object are equal; otherwise, false.
+ public override readonly bool Equals(object obj) => obj is Circle other && Equals(other);
+
+ ///
+ /// Returns a value that indicates whether this circle and the specified circle are equal.
+ ///
+ /// The circle to compare with this circle.
+ /// true if this circle and the specified circle are equal; otherwise, false.
+ public readonly bool Equals(Circle other) => this.X == other.X &&
+ this.Y == other.Y &&
+ this.Radius == other.Radius;
+
+ ///
+ /// Returns the hash code for this circle.
+ ///
+ /// The hash code for this circle as a 32-bit signed integer.
+ public override readonly int GetHashCode() => HashCode.Combine(X, Y, Radius);
+
+ ///
+ /// Returns a value that indicates if the circle on the left hand side of the equality operator is equal to the
+ /// circle on the right hand side of the equality operator.
+ ///
+ /// The circle on the left hand side of the equality operator.
+ /// The circle on the right hand side of the equality operator.
+ /// true if the two circles are equal; otherwise, false.
+ public static bool operator ==(Circle lhs, Circle rhs) => lhs.Equals(rhs);
+
+ ///
+ /// Returns a value that indicates if the circle on the left hand side of the inequality operator is not equal to the
+ /// circle on the right hand side of the inequality operator.
+ ///
+ /// The circle on the left hand side of the inequality operator.
+ /// The circle on the right hand side fo the inequality operator.
+ /// true if the two circle are not equal; otherwise, false.
+ public static bool operator !=(Circle lhs, Circle rhs) => !lhs.Equals(rhs);
+}
diff --git a/Tutorials/MobileDeployment/01-UpdatedLibs/MonoGameLibrary/Core.cs b/Tutorials/MobileDeployment/01-UpdatedLibs/MonoGameLibrary/Core.cs
new file mode 100644
index 00000000..d83c54fe
--- /dev/null
+++ b/Tutorials/MobileDeployment/01-UpdatedLibs/MonoGameLibrary/Core.cs
@@ -0,0 +1,206 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content;
+using Microsoft.Xna.Framework.Graphics;
+using Microsoft.Xna.Framework.Input;
+using MonoGameLibrary.Audio;
+using MonoGameLibrary.Input;
+using MonoGameLibrary.Scenes;
+
+namespace MonoGameLibrary;
+
+public class Core : Game
+{
+ internal static Core s_instance;
+
+ ///
+ /// Gets a reference to the Core instance.
+ ///
+ public static Core Instance => s_instance;
+
+ // The scene that is currently active.
+ private static Scene s_activeScene;
+
+ // The next scene to switch to, if there is one.
+ private static Scene s_nextScene;
+
+ ///
+ /// Gets the graphics device manager to control the presentation of graphics.
+ ///
+ public static GraphicsDeviceManager Graphics { get; private set; }
+
+ ///
+ /// Gets the graphics device used to create graphical resources and perform primitive rendering.
+ ///
+ public static new GraphicsDevice GraphicsDevice { get; private set; }
+
+ ///
+ /// Gets the sprite batch used for all 2D rendering.
+ ///
+ public static SpriteBatch SpriteBatch { get; private set; }
+
+ ///
+ /// Gets the content manager used to load global assets.
+ ///
+ public static new ContentManager Content { get; private set; }
+
+ ///
+ /// Gets a reference to to the input management system.
+ ///
+ public static InputManager Input { get; private set; }
+
+ ///
+ /// Gets or Sets a value that indicates if the game should exit when the esc key on the keyboard is pressed.
+ ///
+ public static bool ExitOnEscape { get; set; }
+
+ ///
+ /// Gets a reference to the audio control system.
+ ///
+ public static AudioController Audio { get; private set; }
+
+ ///
+ /// Creates a new Core instance.
+ ///
+ /// The title to display in the title bar of the game window.
+ /// The initial width, in pixels, of the game window.
+ /// The initial height, in pixels, of the game window.
+ /// Indicates if the game should start in fullscreen mode.
+ public Core(string title, int width, int height, bool fullScreen)
+ {
+ // Ensure that multiple cores are not created.
+ if (s_instance != null)
+ {
+ throw new InvalidOperationException($"Only a single Core instance can be created");
+ }
+
+ // Store reference to engine for global member access.
+ s_instance = this;
+
+ // Create a new graphics device manager.
+ Graphics = new GraphicsDeviceManager(this);
+
+ // Set the graphics defaults
+ Graphics.PreferredBackBufferWidth = width;
+ Graphics.PreferredBackBufferHeight = height;
+ Graphics.IsFullScreen = fullScreen;
+
+ // Apply the graphic presentation changes
+ Graphics.ApplyChanges();
+
+ // Set the window title
+ Window.Title = title;
+
+ // Set the core's content manager to a reference of hte base Game's
+ // content manager.
+ Content = base.Content;
+
+ // Set the root directory for content
+ Content.RootDirectory = "Content";
+
+ // Mouse is visible by default
+ IsMouseVisible = true;
+ }
+
+ protected override void Initialize()
+ {
+ base.Initialize();
+
+ // Set the core's graphics device to a reference of the base Game's
+ // graphics device.
+ GraphicsDevice = base.GraphicsDevice;
+
+ // Create the sprite batch instance.
+ SpriteBatch = new SpriteBatch(GraphicsDevice);
+
+ // Create a new input manager
+ Input = new InputManager();
+
+ // Create a new audio controller.
+ Audio = new AudioController();
+ }
+
+ protected override void UnloadContent()
+ {
+ // Dispose of the audio controller.
+ Audio.Dispose();
+
+ base.UnloadContent();
+ }
+
+ protected override void Update(GameTime gameTime)
+ {
+ // Update the input manager.
+ Input.Update(gameTime);
+
+ // Update the audio controller.
+ Audio.Update();
+
+ if (ExitOnEscape && Input.Keyboard.WasKeyJustPressed(Keys.Escape))
+ {
+ Exit();
+ }
+
+ // if there is a next scene waiting to be switch to, then transition
+ // to that scene
+ if (s_nextScene != null)
+ {
+ TransitionScene();
+ }
+
+ // If there is an active scene, update it.
+ if (s_activeScene != null)
+ {
+ s_activeScene.Update(gameTime);
+ }
+
+ base.Update(gameTime);
+ }
+
+ protected override void Draw(GameTime gameTime)
+ {
+ // If there is an active scene, draw it.
+ if (s_activeScene != null)
+ {
+ s_activeScene.Draw(gameTime);
+ }
+
+ base.Draw(gameTime);
+ }
+
+ public static void ChangeScene(Scene next)
+ {
+ // Only set the next scene value if it is not the same
+ // instance as the currently active scene.
+ if (s_activeScene != next)
+ {
+ s_nextScene = next;
+ }
+ }
+
+ private static void TransitionScene()
+ {
+ // If there is an active scene, dispose of it
+ if (s_activeScene != null)
+ {
+ s_activeScene.Dispose();
+ }
+
+ // Force the garbage collector to collect to ensure memory is cleared
+ GC.Collect();
+
+ // Change the currently active scene to the new scene
+ s_activeScene = s_nextScene;
+
+ // Null out the next scene value so it does not trigger a change over and over.
+ s_nextScene = null;
+
+ // If the active scene now is not null, initialize it.
+ // Remember, just like with Game, the Initialize call also calls the
+ // Scene.LoadContent
+ if (s_activeScene != null)
+ {
+ s_activeScene.Initialize();
+ }
+ }
+}
diff --git a/Tutorials/MobileDeployment/01-UpdatedLibs/MonoGameLibrary/Graphics/AnimatedSprite.cs b/Tutorials/MobileDeployment/01-UpdatedLibs/MonoGameLibrary/Graphics/AnimatedSprite.cs
new file mode 100644
index 00000000..a1a3594e
--- /dev/null
+++ b/Tutorials/MobileDeployment/01-UpdatedLibs/MonoGameLibrary/Graphics/AnimatedSprite.cs
@@ -0,0 +1,60 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGameLibrary.Graphics;
+
+public class AnimatedSprite : Sprite
+{
+ private int _currentFrame;
+ private TimeSpan _elapsed;
+ private Animation _animation;
+
+ ///
+ /// Gets or Sets the animation for this animated sprite.
+ ///
+ public Animation Animation
+ {
+ get => _animation;
+ set
+ {
+ _animation = value;
+ Region = _animation.Frames[0];
+ }
+ }
+
+ ///
+ /// Creates a new animated sprite.
+ ///
+ public AnimatedSprite() { }
+
+ ///
+ /// Creates a new animated sprite with the specified frames and delay.
+ ///
+ /// The animation for this animated sprite.
+ public AnimatedSprite(Animation animation)
+ {
+ Animation = animation;
+ }
+
+ ///
+ /// Updates this animated sprite.
+ ///
+ /// A snapshot of the game timing values provided by the framework.
+ public void Update(GameTime gameTime)
+ {
+ _elapsed += gameTime.ElapsedGameTime;
+
+ if (_elapsed >= _animation.Delay)
+ {
+ _elapsed -= _animation.Delay;
+ _currentFrame++;
+
+ if (_currentFrame >= _animation.Frames.Count)
+ {
+ _currentFrame = 0;
+ }
+
+ Region = _animation.Frames[_currentFrame];
+ }
+ }
+}
diff --git a/Tutorials/MobileDeployment/01-UpdatedLibs/MonoGameLibrary/Graphics/Animation.cs b/Tutorials/MobileDeployment/01-UpdatedLibs/MonoGameLibrary/Graphics/Animation.cs
new file mode 100644
index 00000000..44d61b65
--- /dev/null
+++ b/Tutorials/MobileDeployment/01-UpdatedLibs/MonoGameLibrary/Graphics/Animation.cs
@@ -0,0 +1,38 @@
+using System;
+using System.Collections.Generic;
+
+namespace MonoGameLibrary.Graphics;
+
+public class Animation
+{
+ ///
+ /// The texture regions that make up the frames of this animation. The order of the regions within the collection
+ /// are the order that the frames should be displayed in.
+ ///
+ public List Frames { get; set; }
+
+ ///
+ /// The amount of time to delay between each frame before moving to the next frame for this animation.
+ ///
+ public TimeSpan Delay { get; set; }
+
+ ///
+ /// Creates a new animation.
+ ///
+ public Animation()
+ {
+ Frames = new List();
+ Delay = TimeSpan.FromMilliseconds(100);
+ }
+
+ ///
+ /// Creates a new animation with the specified frames and delay.
+ ///
+ /// An ordered collection of the frames for this animation.
+ /// The amount of time to delay between each frame of this animation.
+ public Animation(List frames, TimeSpan delay)
+ {
+ Frames = frames;
+ Delay = delay;
+ }
+}
diff --git a/Tutorials/MobileDeployment/01-UpdatedLibs/MonoGameLibrary/Graphics/Sprite.cs b/Tutorials/MobileDeployment/01-UpdatedLibs/MonoGameLibrary/Graphics/Sprite.cs
new file mode 100644
index 00000000..20c44f0b
--- /dev/null
+++ b/Tutorials/MobileDeployment/01-UpdatedLibs/MonoGameLibrary/Graphics/Sprite.cs
@@ -0,0 +1,108 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGameLibrary.Graphics;
+
+public class Sprite
+{
+ ///
+ /// Gets or Sets the source texture region represented by this sprite.
+ ///
+ public TextureRegion Region { get; set; }
+
+ ///
+ /// Gets or Sets the color mask to apply when rendering this sprite.
+ ///
+ ///
+ /// Default value is Color.White
+ ///
+ public Color Color { get; set; } = Color.White;
+
+ ///
+ /// Gets or Sets the amount of rotation, in radians, to apply when rendering this sprite.
+ ///
+ ///
+ /// Default value is 0.0f
+ ///
+ public float Rotation { get; set; } = 0.0f;
+
+ ///
+ /// Gets or Sets the scale factor to apply to the x- and y-axes when rendering this sprite.
+ ///
+ ///
+ /// Default value is Vector2.One
+ ///
+ public Vector2 Scale { get; set; } = Vector2.One;
+
+ ///
+ /// Gets or Sets the xy-coordinate origin point, relative to the top-left corner, of this sprite.
+ ///
+ ///
+ /// Default value is Vector2.Zero
+ ///
+ public Vector2 Origin { get; set; } = Vector2.Zero;
+
+ ///
+ /// Gets or Sets the sprite effects to apply when rendering this sprite.
+ ///
+ ///
+ /// Default value is SpriteEffects.None
+ ///
+ public SpriteEffects Effects { get; set; } = SpriteEffects.None;
+
+ ///
+ /// Gets or Sets the layer depth to apply when rendering this sprite.
+ ///
+ ///
+ /// Default value is 0.0f
+ ///
+ public float LayerDepth { get; set; } = 0.0f;
+
+ ///
+ /// Gets the width, in pixels, of this sprite.
+ ///
+ ///
+ /// Width is calculated by multiplying the width of the source texture region by the x-axis scale factor.
+ ///
+ public float Width => Region.Width * Scale.X;
+
+ ///
+ /// Gets the height, in pixels, of this sprite.
+ ///
+ ///
+ /// Height is calculated by multiplying the height of the source texture region by the y-axis scale factor.
+ ///
+ public float Height => Region.Height * Scale.Y;
+
+ ///
+ /// Creates a new sprite.
+ ///
+ public Sprite() { }
+
+ ///
+ /// Creates a new sprite using the specified source texture region.
+ ///
+ /// The texture region to use as the source texture region for this sprite.
+ public Sprite(TextureRegion region)
+ {
+ Region = region;
+ }
+
+ ///
+ /// Sets the origin of this sprite to the center
+ ///
+ public void CenterOrigin()
+ {
+ Origin = new Vector2(Region.Width, Region.Height) * 0.5f;
+ }
+
+ ///
+ /// Submit this sprite for drawing to the current batch.
+ ///
+ /// The SpriteBatch instance used for batching draw calls.
+ /// The xy-coordinate position to render this sprite at.
+ public void Draw(SpriteBatch spriteBatch, Vector2 position)
+ {
+ Region.Draw(spriteBatch, position, Color, Rotation, Origin, Scale, Effects, LayerDepth);
+ }
+}
diff --git a/Tutorials/MobileDeployment/01-UpdatedLibs/MonoGameLibrary/Graphics/TextureAtlas.cs b/Tutorials/MobileDeployment/01-UpdatedLibs/MonoGameLibrary/Graphics/TextureAtlas.cs
new file mode 100644
index 00000000..e48c9abd
--- /dev/null
+++ b/Tutorials/MobileDeployment/01-UpdatedLibs/MonoGameLibrary/Graphics/TextureAtlas.cs
@@ -0,0 +1,239 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Xml;
+using System.Xml.Linq;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content;
+using Microsoft.Xna.Framework.Graphics;
+
+
+namespace MonoGameLibrary.Graphics;
+
+public class TextureAtlas
+{
+ private Dictionary _regions;
+
+ // Stores animations added to this atlas.
+ private Dictionary _animations;
+
+ ///
+ /// Gets or Sets the source texture represented by this texture atlas.
+ ///
+ public Texture2D Texture { get; set; }
+
+ ///
+ /// Creates a new texture atlas.
+ ///
+ public TextureAtlas()
+ {
+ _regions = new Dictionary();
+ _animations = new Dictionary();
+ }
+
+ ///
+ /// Creates a new texture atlas instance using the given texture.
+ ///
+ /// The source texture represented by the texture atlas.
+ public TextureAtlas(Texture2D texture)
+ {
+ Texture = texture;
+ _regions = new Dictionary();
+ _animations = new Dictionary();
+ }
+
+ ///
+ /// Creates a new region and adds it to this texture atlas.
+ ///
+ /// The name to give the texture region.
+ /// The top-left x-coordinate position of the region boundary relative to the top-left corner of the source texture boundary.
+ /// The top-left y-coordinate position of the region boundary relative to the top-left corner of the source texture boundary.
+ /// The width, in pixels, of the region.
+ /// The height, in pixels, of the region.
+ public void AddRegion(string name, int x, int y, int width, int height)
+ {
+ TextureRegion region = new TextureRegion(Texture, x, y, width, height);
+ _regions.Add(name, region);
+ }
+
+ ///
+ /// Gets the region from this texture atlas with the specified name.
+ ///
+ /// The name of the region to retrieve.
+ /// The TextureRegion with the specified name.
+ public TextureRegion GetRegion(string name)
+ {
+ return _regions[name];
+ }
+
+ ///
+ /// Removes the region from this texture atlas with the specified name.
+ ///
+ /// The name of the region to remove.
+ ///
+ public bool RemoveRegion(string name)
+ {
+ return _regions.Remove(name);
+ }
+
+ ///
+ /// Removes all regions from this texture atlas.
+ ///
+ public void Clear()
+ {
+ _regions.Clear();
+ }
+
+ ///
+ /// Creates a new sprite using the region from this texture atlas with the specified name.
+ ///
+ /// The name of the region to create the sprite with.
+ /// A new Sprite using the texture region with the specified name.
+ public Sprite CreateSprite(string regionName)
+ {
+ TextureRegion region = GetRegion(regionName);
+ return new Sprite(region);
+ }
+
+ ///
+ /// Adds the given animation to this texture atlas with the specified name.
+ ///
+ /// The name of the animation to add.
+ /// The animation to add.
+ public void AddAnimation(string animationName, Animation animation)
+ {
+ _animations.Add(animationName, animation);
+ }
+
+ ///
+ /// Gets the animation from this texture atlas with the specified name.
+ ///
+ /// The name of the animation to retrieve.
+ /// The animation with the specified name.
+ public Animation GetAnimation(string animationName)
+ {
+ return _animations[animationName];
+ }
+
+ ///
+ /// Removes the animation with the specified name from this texture atlas.
+ ///
+ /// The name of the animation to remove.
+ /// true if the animation is removed successfully; otherwise, false.
+ public bool RemoveAnimation(string animationName)
+ {
+ return _animations.Remove(animationName);
+ }
+
+ ///
+ /// Creates a new animated sprite using the animation from this texture atlas with the specified name.
+ ///
+ /// The name of the animation to use.
+ /// A new AnimatedSprite using the animation with the specified name.
+ public AnimatedSprite CreateAnimatedSprite(string animationName)
+ {
+ Animation animation = GetAnimation(animationName);
+ return new AnimatedSprite(animation);
+ }
+
+ ///
+ /// Creates a new texture atlas based a texture atlas xml configuration file.
+ ///
+ /// The content manager used to load the texture for the atlas.
+ /// The path to the xml file, relative to the content root directory..
+ /// The texture atlas created by this method.
+ public static TextureAtlas FromFile(ContentManager content, string fileName)
+ {
+ TextureAtlas atlas = new TextureAtlas();
+
+ string filePath = Path.Combine(content.RootDirectory, fileName);
+
+ using (Stream stream = TitleContainer.OpenStream(filePath))
+ {
+ using (XmlReader reader = XmlReader.Create(stream))
+ {
+ XDocument doc = XDocument.Load(reader);
+ XElement root = doc.Root;
+
+ // The element contains the content path for the Texture2D to load.
+ // So we will retrieve that value then use the content manager to load the texture.
+ string texturePath = root.Element("Texture").Value;
+ atlas.Texture = content.Load(texturePath);
+
+ // The element contains individual elements, each one describing
+ // a different texture region within the atlas.
+ //
+ // Example:
+ //
+ //
+ //
+ //
+ //
+ // So we retrieve all of the elements then loop through each one
+ // and generate a new TextureRegion instance from it and add it to this atlas.
+ var regions = root.Element("Regions")?.Elements("Region");
+
+ if (regions != null)
+ {
+ foreach (var 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);
+ }
+ }
+ }
+
+ // The element contains individual elements, each one describing
+ // a different animation within the atlas.
+ //
+ // Example:
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ // So we retrieve all of the elements then loop through each one
+ // and generate a new Animation instance from it and add it to this atlas.
+ var animationElements = root.Element("Animations").Elements("Animation");
+
+ if (animationElements != null)
+ {
+ foreach (var animationElement in animationElements)
+ {
+ string name = animationElement.Attribute("name")?.Value;
+ float delayInMilliseconds = float.Parse(animationElement.Attribute("delay")?.Value ?? "0");
+ TimeSpan delay = TimeSpan.FromMilliseconds(delayInMilliseconds);
+
+ List frames = new List();
+
+ var frameElements = animationElement.Elements("Frame");
+
+ if (frameElements != null)
+ {
+ foreach (var frameElement in frameElements)
+ {
+ string regionName = frameElement.Attribute("region").Value;
+ TextureRegion region = atlas.GetRegion(regionName);
+ frames.Add(region);
+ }
+ }
+
+ Animation animation = new Animation(frames, delay);
+ atlas.AddAnimation(name, animation);
+ }
+ }
+
+ return atlas;
+ }
+ }
+ }
+}
diff --git a/Tutorials/MobileDeployment/01-UpdatedLibs/MonoGameLibrary/Graphics/TextureRegion.cs b/Tutorials/MobileDeployment/01-UpdatedLibs/MonoGameLibrary/Graphics/TextureRegion.cs
new file mode 100644
index 00000000..ecd69030
--- /dev/null
+++ b/Tutorials/MobileDeployment/01-UpdatedLibs/MonoGameLibrary/Graphics/TextureRegion.cs
@@ -0,0 +1,131 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGameLibrary.Graphics;
+
+///
+/// Represents a rectangular region within a texture.
+///
+public class TextureRegion
+{
+ ///
+ /// Gets or Sets the source texture this texture region is part of.
+ ///
+ public Texture2D Texture { get; set; }
+
+ ///
+ /// Gets or Sets the source rectangle boundary of this texture region within the source texture.
+ ///
+ public Rectangle SourceRectangle { get; set; }
+
+ ///
+ /// Gets the width, in pixels, of this texture region.
+ ///
+ public int Width => SourceRectangle.Width;
+
+ ///
+ /// Gets the height, in pixels, of this texture region.
+ ///
+ public int Height => SourceRectangle.Height;
+
+ ///
+ /// Gets the top normalized texture coordinate of this region.
+ ///
+ public float TopTextureCoordinate => SourceRectangle.Top / (float)Texture.Height;
+
+ ///
+ /// Gets the bottom normalized texture coordinate of this region.
+ ///
+ public float BottomTextureCoordinate => SourceRectangle.Bottom / (float)Texture.Height;
+
+ ///
+ /// Gets the left normalized texture coordinate of this region.
+ ///
+ public float LeftTextureCoordinate => SourceRectangle.Left / (float)Texture.Width;
+
+ ///
+ /// Gets the right normalized texture coordinate of this region.
+ ///
+ public float RightTextureCoordinate => SourceRectangle.Right / (float)Texture.Width;
+
+ ///
+ /// Creates a new texture region.
+ ///
+ public TextureRegion() { }
+
+ ///
+ /// Creates a new texture region using the specified source texture.
+ ///
+ /// The texture to use as the source texture for this texture region.
+ /// The x-coordinate position of the upper-left corner of this texture region relative to the upper-left corner of the source texture.
+ ///
+ /// The width, in pixels, of this texture region.
+ /// The height, in pixels, of this texture region.
+ public TextureRegion(Texture2D texture, int x, int y, int width, int height)
+ {
+ Texture = texture;
+ SourceRectangle = new Rectangle(x, y, width, height);
+ }
+
+ ///
+ /// Submit this texture region for drawing in the current batch.
+ ///
+ /// The spritebatch instance used for batching draw calls.
+ /// The xy-coordinate location to draw this texture region on the screen.
+ /// The color mask to apply when drawing this texture region on screen.
+ public void Draw(SpriteBatch spriteBatch, Vector2 position, Color color)
+ {
+ Draw(spriteBatch, position, color, 0.0f, Vector2.Zero, Vector2.One, SpriteEffects.None, 0.0f);
+ }
+
+ ///
+ /// Submit this texture region for drawing in the current batch.
+ ///
+ /// The spritebatch instance used for batching draw calls.
+ /// The xy-coordinate location to draw this texture region on the screen.
+ /// The color mask to apply when drawing this texture region on screen.
+ /// The amount of rotation, in radians, to apply when drawing this texture region on screen.
+ /// The center of rotation, scaling, and position when drawing this texture region on screen.
+ /// The scale factor to apply when drawing this texture region on screen.
+ /// Specifies if this texture region should be flipped horizontally, vertically, or both when drawing on screen.
+ /// The depth of the layer to use when drawing this texture region on screen.
+ public void Draw(SpriteBatch spriteBatch, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects effects, float layerDepth)
+ {
+ Draw(
+ spriteBatch,
+ position,
+ color,
+ rotation,
+ origin,
+ new Vector2(scale, scale),
+ effects,
+ layerDepth
+ );
+ }
+
+ ///
+ /// Submit this texture region for drawing in the current batch.
+ ///
+ /// The spritebatch instance used for batching draw calls.
+ /// The xy-coordinate location to draw this texture region on the screen.
+ /// The color mask to apply when drawing this texture region on screen.
+ /// The amount of rotation, in radians, to apply when drawing this texture region on screen.
+ /// The center of rotation, scaling, and position when drawing this texture region on screen.
+ /// The amount of scaling to apply to the x- and y-axes when drawing this texture region on screen.
+ /// Specifies if this texture region should be flipped horizontally, vertically, or both when drawing on screen.
+ /// The depth of the layer to use when drawing this texture region on screen.
+ public void Draw(SpriteBatch spriteBatch, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth)
+ {
+ spriteBatch.Draw(
+ Texture,
+ position,
+ SourceRectangle,
+ color,
+ rotation,
+ origin,
+ scale,
+ effects,
+ layerDepth
+ );
+ }
+}
diff --git a/Tutorials/MobileDeployment/01-UpdatedLibs/MonoGameLibrary/Graphics/Tilemap.cs b/Tutorials/MobileDeployment/01-UpdatedLibs/MonoGameLibrary/Graphics/Tilemap.cs
new file mode 100644
index 00000000..96e1ee5e
--- /dev/null
+++ b/Tutorials/MobileDeployment/01-UpdatedLibs/MonoGameLibrary/Graphics/Tilemap.cs
@@ -0,0 +1,231 @@
+using System;
+using System.IO;
+using System.Xml;
+using System.Xml.Linq;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGameLibrary.Graphics;
+
+public class Tilemap
+{
+ private readonly Tileset _tileset;
+ private readonly int[] _tiles;
+
+ ///
+ /// Gets the total number of rows in this tilemap.
+ ///
+ public int Rows { get; }
+
+ ///
+ /// Gets the total number of columns in this tilemap.
+ ///
+ public int Columns { get; }
+
+ ///
+ /// Gets the total number of tiles in this tilemap.
+ ///
+ public int Count { get; }
+
+ ///
+ /// Gets or Sets the scale factor to draw each tile at.
+ ///
+ public Vector2 Scale { get; set; }
+
+ ///
+ /// Gets the width, in pixels, each tile is drawn at.
+ ///
+ public float TileWidth => _tileset.TileWidth * Scale.X;
+
+ ///
+ /// Gets the height, in pixels, each tile is drawn at.
+ ///
+ public float TileHeight => _tileset.TileHeight * Scale.Y;
+
+ ///
+ /// Creates a new tilemap.
+ ///
+ /// The tileset used by this tilemap.
+ /// The total number of columns in this tilemap.
+ /// The total number of rows in this tilemap.
+ public Tilemap(Tileset tileset, int columns, int rows)
+ {
+ _tileset = tileset;
+ Rows = rows;
+ Columns = columns;
+ Count = Columns * Rows;
+ Scale = Vector2.One;
+ _tiles = new int[Count];
+ }
+
+ ///
+ /// Sets the tile at the given index in this tilemap to use the tile from
+ /// the tileset at the specified tileset id.
+ ///
+ /// The index of the tile in this tilemap.
+ /// The tileset id of the tile from the tileset to use.
+ public void SetTile(int index, int tilesetID)
+ {
+ _tiles[index] = tilesetID;
+ }
+
+ ///
+ /// Sets the tile at the given column and row in this tilemap to use the tile
+ /// from the tileset at the specified tileset id.
+ ///
+ /// The column of the tile in this tilemap.
+ /// The row of the tile in this tilemap.
+ /// The tileset id of the tile from the tileset to use.
+ public void SetTile(int column, int row, int tilesetID)
+ {
+ int index = row * Columns + column;
+ SetTile(index, tilesetID);
+ }
+
+ ///
+ /// Gets the texture region of the tile from this tilemap at the specified index.
+ ///
+ /// The index of the tile in this tilemap.
+ /// The texture region of the tile from this tilemap at the specified index.
+ public TextureRegion GetTile(int index)
+ {
+ return _tileset.GetTile(_tiles[index]);
+ }
+
+ ///
+ /// Gets the texture region of the tile frm this tilemap at the specified
+ /// column and row.
+ ///
+ /// The column of the tile in this tilemap.
+ /// The row of hte tile in this tilemap.
+ /// The texture region of the tile from this tilemap at the specified column and row.
+ public TextureRegion GetTile(int column, int row)
+ {
+ int index = row * Columns + column;
+ return GetTile(index);
+ }
+
+ ///
+ /// Draws this tilemap using the given sprite batch.
+ ///
+ /// The sprite batch used to draw this tilemap.
+ public void Draw(SpriteBatch spriteBatch)
+ {
+ for (int i = 0; i < Count; i++)
+ {
+ int tileSetIndex = _tiles[i];
+ TextureRegion tile = _tileset.GetTile(tileSetIndex);
+
+ int x = i % Columns;
+ int y = i / Columns;
+
+ Vector2 position = new Vector2(x * TileWidth, y * TileHeight);
+ tile.Draw(spriteBatch, position, Color.White, 0.0f, Vector2.Zero, Scale, SpriteEffects.None, 1.0f);
+ }
+ }
+
+ ///
+ /// Creates a new tilemap based on a tilemap xml configuration file.
+ ///
+ /// The content manager used to load the texture for the tileset.
+ /// The path to the xml file, relative to the content root directory.
+ /// The tilemap created by this method.
+ public static Tilemap FromFile(ContentManager content, string filename)
+ {
+ string filePath = Path.Combine(content.RootDirectory, filename);
+
+ using (Stream stream = TitleContainer.OpenStream(filePath))
+ {
+ using (XmlReader reader = XmlReader.Create(stream))
+ {
+ XDocument doc = XDocument.Load(reader);
+ XElement root = doc.Root;
+
+ // The element contains the information about the tileset
+ // used by the tilemap.
+ //
+ // Example
+ // contentPath
+ //
+ // The region attribute represents the x, y, width, and height
+ // components of the boundary for the texture region within the
+ // texture at the contentPath specified.
+ //
+ // the tileWidth and tileHeight attributes specify the width and
+ // height of each tile in the tileset.
+ //
+ // the contentPath value is the contentPath to the texture to
+ // load that contains the tileset
+ XElement tilesetElement = root.Element("Tileset");
+
+ string regionAttribute = tilesetElement.Attribute("region").Value;
+ string[] split = regionAttribute.Split(" ", StringSplitOptions.RemoveEmptyEntries);
+ int x = int.Parse(split[0]);
+ int y = int.Parse(split[1]);
+ int width = int.Parse(split[2]);
+ int height = int.Parse(split[3]);
+
+ int tileWidth = int.Parse(tilesetElement.Attribute("tileWidth").Value);
+ int tileHeight = int.Parse(tilesetElement.Attribute("tileHeight").Value);
+ string contentPath = tilesetElement.Value;
+
+ // Load the texture 2d at the content path
+ Texture2D texture = content.Load(contentPath);
+
+ // Create the texture region from the texture
+ TextureRegion textureRegion = new TextureRegion(texture, x, y, width, height);
+
+ // Create the tileset using the texture region
+ Tileset tileset = new Tileset(textureRegion, tileWidth, tileHeight);
+
+ // The element contains lines of strings where each line
+ // represents a row in the tilemap. Each line is a space
+ // separated string where each element represents a column in that
+ // row. The value of the column is the id of the tile in the
+ // tileset to draw for that location.
+ //
+ // Example:
+ //
+ // 00 01 01 02
+ // 03 04 04 05
+ // 03 04 04 05
+ // 06 07 07 08
+ //
+ XElement tilesElement = root.Element("Tiles");
+
+ // Split the value of the tiles data into rows by splitting on
+ // the new line character
+ string[] rows = tilesElement.Value.Trim().Split('\n', StringSplitOptions.RemoveEmptyEntries);
+
+ // Split the value of the first row to determine the total number of columns
+ int columnCount = rows[0].Split(" ", StringSplitOptions.RemoveEmptyEntries).Length;
+
+ // Create the tilemap
+ Tilemap tilemap = new Tilemap(tileset, columnCount, rows.Length);
+
+ // Process each row
+ for (int row = 0; row < rows.Length; row++)
+ {
+ // Split the row into individual columns
+ string[] columns = rows[row].Trim().Split(" ", StringSplitOptions.RemoveEmptyEntries);
+
+ // Process each column of the current row
+ for (int column = 0; column < columnCount; column++)
+ {
+ // Get the tileset index for this location
+ int tilesetIndex = int.Parse(columns[column]);
+
+ // Get the texture region of that tile from the tileset
+ TextureRegion region = tileset.GetTile(tilesetIndex);
+
+ // Add that region to the tilemap at the row and column location
+ tilemap.SetTile(column, row, tilesetIndex);
+ }
+ }
+
+ return tilemap;
+ }
+ }
+ }
+}
diff --git a/Tutorials/MobileDeployment/01-UpdatedLibs/MonoGameLibrary/Graphics/Tileset.cs b/Tutorials/MobileDeployment/01-UpdatedLibs/MonoGameLibrary/Graphics/Tileset.cs
new file mode 100644
index 00000000..80c2e65a
--- /dev/null
+++ b/Tutorials/MobileDeployment/01-UpdatedLibs/MonoGameLibrary/Graphics/Tileset.cs
@@ -0,0 +1,76 @@
+namespace MonoGameLibrary.Graphics;
+
+public class Tileset
+{
+ private readonly TextureRegion[] _tiles;
+
+ ///
+ /// Gets the width, in pixels, of each tile in this tileset.
+ ///
+ public int TileWidth { get; }
+
+ ///
+ /// Gets the height, in pixels, of each tile in this tileset.
+ ///
+ public int TileHeight { get; }
+
+ ///
+ /// Gets the total number of columns in this tileset.
+ ///
+ public int Columns { get; }
+
+ ///
+ /// Gets the total number of rows in this tileset.
+ ///
+ public int Rows { get; }
+
+ ///
+ /// Gets the total number of tiles in this tileset.
+ ///
+ public int Count { get; }
+
+ ///
+ /// Creates a new tileset based on the given texture region with the specified
+ /// tile width and height.
+ ///
+ /// The texture region that contains the tiles for the tileset.
+ /// The width of each tile in the tileset.
+ /// The height of each tile in the tileset.
+ public Tileset(TextureRegion textureRegion, int tileWidth, int tileHeight)
+ {
+ TileWidth = tileWidth;
+ TileHeight = tileHeight;
+ Columns = textureRegion.Width / tileWidth;
+ Rows = textureRegion.Height / tileHeight;
+ Count = Columns * Rows;
+
+ // Create the texture regions that make up each individual tile
+ _tiles = new TextureRegion[Count];
+
+ for (int i = 0; i < Count; i++)
+ {
+ int x = i % Columns * tileWidth;
+ int y = i / Columns * tileHeight;
+ _tiles[i] = new TextureRegion(textureRegion.Texture, textureRegion.SourceRectangle.X + x, textureRegion.SourceRectangle.Y + y, tileWidth, tileHeight);
+ }
+ }
+
+ ///
+ /// Gets the texture region for the tile from this tileset at the given index.
+ ///
+ /// The index of the texture region in this tile set.
+ /// The texture region for the tile form this tileset at the given index.
+ public TextureRegion GetTile(int index) => _tiles[index];
+
+ ///
+ /// Gets the texture region for the tile from this tileset at the given location.
+ ///
+ /// The column in this tileset of the texture region.
+ /// The row in this tileset of the texture region.
+ /// The texture region for the tile from this tileset at given location.
+ public TextureRegion GetTile(int column, int row)
+ {
+ int index = row * Columns + column;
+ return GetTile(index);
+ }
+}
diff --git a/Tutorials/MobileDeployment/01-UpdatedLibs/MonoGameLibrary/Input/GamePadInfo.cs b/Tutorials/MobileDeployment/01-UpdatedLibs/MonoGameLibrary/Input/GamePadInfo.cs
new file mode 100644
index 00000000..7fd16126
--- /dev/null
+++ b/Tutorials/MobileDeployment/01-UpdatedLibs/MonoGameLibrary/Input/GamePadInfo.cs
@@ -0,0 +1,140 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Input;
+
+namespace MonoGameLibrary.Input;
+
+public class GamePadInfo
+{
+ private TimeSpan _vibrationTimeRemaining = TimeSpan.Zero;
+
+ ///
+ /// Gets the index of the player this gamepad is for.
+ ///
+ public PlayerIndex PlayerIndex { get; }
+
+ ///
+ /// Gets the state of input for this gamepad during the previous update cycle.
+ ///
+ public GamePadState PreviousState { get; private set; }
+
+ ///
+ /// Gets the state of input for this gamepad during the current update cycle.
+ ///
+ public GamePadState CurrentState { get; private set; }
+
+ ///
+ /// Gets a value that indicates if this gamepad is currently connected.
+ ///
+ public bool IsConnected => CurrentState.IsConnected;
+
+ ///
+ /// Gets the value of the left thumbstick of this gamepad.
+ ///
+ public Vector2 LeftThumbStick => CurrentState.ThumbSticks.Left;
+
+ ///
+ /// Gets the value of the right thumbstick of this gamepad.
+ ///
+ public Vector2 RightThumbStick => CurrentState.ThumbSticks.Right;
+
+ ///
+ /// Gets the value of the left trigger of this gamepad.
+ ///
+ public float LeftTrigger => CurrentState.Triggers.Left;
+
+ ///
+ /// Gets the value of the right trigger of this gamepad.
+ ///
+ public float RightTrigger => CurrentState.Triggers.Right;
+
+ ///
+ /// Creates a new GamePadInfo for the gamepad connected at the specified player index.
+ ///
+ /// The index of the player for this gamepad.
+ public GamePadInfo(PlayerIndex playerIndex)
+ {
+ PlayerIndex = playerIndex;
+ PreviousState = new GamePadState();
+ CurrentState = GamePad.GetState(playerIndex);
+ }
+
+ ///
+ /// Updates the state information for this gamepad input.
+ ///
+ ///
+ public void Update(GameTime gameTime)
+ {
+ PreviousState = CurrentState;
+ CurrentState = GamePad.GetState(PlayerIndex);
+
+ if (_vibrationTimeRemaining > TimeSpan.Zero)
+ {
+ _vibrationTimeRemaining -= gameTime.ElapsedGameTime;
+
+ if (_vibrationTimeRemaining <= TimeSpan.Zero)
+ {
+ StopVibration();
+ }
+ }
+ }
+
+ ///
+ /// Returns a value that indicates whether the specified gamepad button is current down.
+ ///
+ /// The gamepad button to check.
+ /// true if the specified gamepad button is currently down; otherwise, false.
+ public bool IsButtonDown(Buttons button)
+ {
+ return CurrentState.IsButtonDown(button);
+ }
+
+ ///
+ /// Returns a value that indicates whether the specified gamepad button is currently up.
+ ///
+ /// The gamepad button to check.
+ /// true if the specified gamepad button is currently up; otherwise, false.
+ public bool IsButtonUp(Buttons button)
+ {
+ return CurrentState.IsButtonUp(button);
+ }
+
+ ///
+ /// Returns a value that indicates whether the specified gamepad button was just pressed on the current frame.
+ ///
+ ///
+ /// true if the specified gamepad button was just pressed on the current frame; otherwise, false.
+ public bool WasButtonJustPressed(Buttons button)
+ {
+ return CurrentState.IsButtonDown(button) && PreviousState.IsButtonUp(button);
+ }
+
+ ///
+ /// Returns a value that indicates whether the specified gamepad button was just released on the current frame.
+ ///
+ ///
+ /// true if the specified gamepad button was just released on the current frame; otherwise, false.
+ public bool WasButtonJustReleased(Buttons button)
+ {
+ return CurrentState.IsButtonUp(button) && PreviousState.IsButtonDown(button);
+ }
+
+ ///
+ /// Sets the vibration for all motors of this gamepad.
+ ///
+ /// The strength of the vibration from 0.0f (none) to 1.0f (full).
+ /// The amount of time the vibration should occur.
+ public void SetVibration(float strength, TimeSpan time)
+ {
+ _vibrationTimeRemaining = time;
+ GamePad.SetVibration(PlayerIndex, strength, strength);
+ }
+
+ ///
+ /// Stops the vibration of all motors for this gamepad.
+ ///
+ public void StopVibration()
+ {
+ GamePad.SetVibration(PlayerIndex, 0.0f, 0.0f);
+ }
+}
diff --git a/Tutorials/MobileDeployment/01-UpdatedLibs/MonoGameLibrary/Input/InputManager.cs b/Tutorials/MobileDeployment/01-UpdatedLibs/MonoGameLibrary/Input/InputManager.cs
new file mode 100644
index 00000000..1790eb70
--- /dev/null
+++ b/Tutorials/MobileDeployment/01-UpdatedLibs/MonoGameLibrary/Input/InputManager.cs
@@ -0,0 +1,52 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGameLibrary.Input;
+
+public class InputManager
+{
+ ///
+ /// Gets the state information of keyboard input.
+ ///
+ public KeyboardInfo Keyboard { get; private set; }
+
+ ///
+ /// Gets the state information of mouse input.
+ ///
+ public MouseInfo Mouse { get; private set; }
+
+ ///
+ /// Gets the state information of a gamepad.
+ ///
+ public GamePadInfo[] GamePads { get; private set; }
+
+ ///
+ /// Creates a new InputManager.
+ ///
+ /// The game this input manager belongs to.
+ public InputManager()
+ {
+ Keyboard = new KeyboardInfo();
+ Mouse = new MouseInfo();
+
+ GamePads = new GamePadInfo[4];
+ for (int i = 0; i < 4; i++)
+ {
+ GamePads[i] = new GamePadInfo((PlayerIndex)i);
+ }
+ }
+
+ ///
+ /// Updates the state information for the keyboard, mouse, and gamepad inputs.
+ ///
+ /// A snapshot of the timing values for the current frame.
+ public void Update(GameTime gameTime)
+ {
+ Keyboard.Update();
+ Mouse.Update();
+
+ for (int i = 0; i < 4; i++)
+ {
+ GamePads[i].Update(gameTime);
+ }
+ }
+}
diff --git a/Tutorials/MobileDeployment/01-UpdatedLibs/MonoGameLibrary/Input/KeyboardInfo.cs b/Tutorials/MobileDeployment/01-UpdatedLibs/MonoGameLibrary/Input/KeyboardInfo.cs
new file mode 100644
index 00000000..c6770cb0
--- /dev/null
+++ b/Tutorials/MobileDeployment/01-UpdatedLibs/MonoGameLibrary/Input/KeyboardInfo.cs
@@ -0,0 +1,74 @@
+using Microsoft.Xna.Framework.Input;
+
+namespace MonoGameLibrary.Input;
+
+public class KeyboardInfo
+{
+ ///
+ /// Gets the state of keyboard input during the previous update cycle.
+ ///
+ public KeyboardState PreviousState { get; private set; }
+
+ ///
+ /// Gets the state of keyboard input during the current input cycle.
+ ///
+ public KeyboardState CurrentState { get; private set; }
+
+ ///
+ /// Creates a new KeyboardInfo
+ ///
+ public KeyboardInfo()
+ {
+ PreviousState = new KeyboardState();
+ CurrentState = Keyboard.GetState();
+ }
+
+ ///
+ /// Updates the state information about keyboard input.
+ ///
+ public void Update()
+ {
+ PreviousState = CurrentState;
+ CurrentState = Keyboard.GetState();
+ }
+
+ ///
+ /// Returns a value that indicates if the specified key is currently down.
+ ///
+ /// The key to check.
+ /// true if the specified key is currently down; otherwise, false.
+ public bool IsKeyDown(Keys key)
+ {
+ return CurrentState.IsKeyDown(key);
+ }
+
+ ///
+ /// Returns a value that indicates whether the specified key is currently up.
+ ///
+ /// The key to check.
+ /// true if the specified key is currently up; otherwise, false.
+ public bool IsKeyUp(Keys key)
+ {
+ return CurrentState.IsKeyUp(key);
+ }
+
+ ///
+ /// Returns a value that indicates if the specified key was just pressed on the current frame.
+ ///
+ /// The key to check.
+ /// true if the specified key was just pressed on the current frame; otherwise, false.
+ public bool WasKeyJustPressed(Keys key)
+ {
+ return CurrentState.IsKeyDown(key) && PreviousState.IsKeyUp(key);
+ }
+
+ ///
+ /// Returns a value that indicates if the specified key was just released on the current frame.
+ ///
+ /// The key to check.
+ /// true if the specified key was just released on the current frame; otherwise, false.
+ public bool WasKeyJustReleased(Keys key)
+ {
+ return CurrentState.IsKeyUp(key) && PreviousState.IsKeyDown(key);
+ }
+}
diff --git a/Tutorials/MobileDeployment/01-UpdatedLibs/MonoGameLibrary/Input/MouseButton.cs b/Tutorials/MobileDeployment/01-UpdatedLibs/MonoGameLibrary/Input/MouseButton.cs
new file mode 100644
index 00000000..5b041f80
--- /dev/null
+++ b/Tutorials/MobileDeployment/01-UpdatedLibs/MonoGameLibrary/Input/MouseButton.cs
@@ -0,0 +1,10 @@
+namespace MonoGameLibrary.Input;
+
+public enum MouseButton
+{
+ Left,
+ Middle,
+ Right,
+ XButton1,
+ XButton2
+}
diff --git a/Tutorials/MobileDeployment/01-UpdatedLibs/MonoGameLibrary/Input/MouseInfo.cs b/Tutorials/MobileDeployment/01-UpdatedLibs/MonoGameLibrary/Input/MouseInfo.cs
new file mode 100644
index 00000000..09d6207c
--- /dev/null
+++ b/Tutorials/MobileDeployment/01-UpdatedLibs/MonoGameLibrary/Input/MouseInfo.cs
@@ -0,0 +1,208 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Input;
+
+namespace MonoGameLibrary.Input;
+
+public class MouseInfo
+{
+ ///
+ /// The state of mouse input during the previous update cycle.
+ ///
+ public MouseState PreviousState { get; private set; }
+
+ ///
+ /// The state of mouse input during the current update cycle.
+ ///
+ public MouseState CurrentState { get; private set; }
+
+ ///
+ /// Gets or Sets the current position of the mouse cursor in screen space.
+ ///
+ public Point Position
+ {
+ get => CurrentState.Position;
+ set => SetPosition(value.X, value.Y);
+ }
+
+ ///
+ /// Gets or Sets the current x-coordinate position of the mouse cursor in screen space.
+ ///
+ public int X
+ {
+ get => CurrentState.X;
+ set => SetPosition(value, CurrentState.Y);
+ }
+
+ ///
+ /// Gets or Sets the current y-coordinate position of the mouse cursor in screen space.
+ ///
+ public int Y
+ {
+ get => CurrentState.Y;
+ set => SetPosition(CurrentState.X, value);
+ }
+
+ ///
+ /// Gets the difference in the mouse cursor position between the previous and current frame.
+ ///
+ public Point PositionDelta => CurrentState.Position - PreviousState.Position;
+
+ ///
+ /// Gets the difference in the mouse cursor x-position between the previous and current frame.
+ ///
+ public int XDelta => CurrentState.X - PreviousState.X;
+
+ ///
+ /// Gets the difference in the mouse cursor y-position between the previous and current frame.
+ ///
+ public int YDelta => CurrentState.Y - PreviousState.Y;
+
+ ///
+ /// Gets a value that indicates if the mouse cursor moved between the previous and current frames.
+ ///
+ public bool WasMoved => PositionDelta != Point.Zero;
+
+ ///
+ /// Gets the cumulative value of the mouse scroll wheel since the start of the game.
+ ///
+ public int ScrollWheel => CurrentState.ScrollWheelValue;
+
+ ///
+ /// Gets the value of the scroll wheel between the previous and current frame.
+ ///
+ public int ScrollWheelDelta => CurrentState.ScrollWheelValue - PreviousState.ScrollWheelValue;
+
+ ///
+ /// Creates a new MouseInfo.
+ ///
+ public MouseInfo()
+ {
+ PreviousState = new MouseState();
+ CurrentState = Mouse.GetState();
+ }
+
+ ///
+ /// Updates the state information about mouse input.
+ ///
+ public void Update()
+ {
+ PreviousState = CurrentState;
+ CurrentState = Mouse.GetState();
+ }
+
+ ///
+ /// Returns a value that indicates whether the specified mouse button is currently down.
+ ///
+ /// The mouse button to check.
+ /// true if the specified mouse button is currently down; otherwise, false.
+ public bool IsButtonDown(MouseButton button)
+ {
+ switch (button)
+ {
+ case MouseButton.Left:
+ return CurrentState.LeftButton == ButtonState.Pressed;
+ case MouseButton.Middle:
+ return CurrentState.MiddleButton == ButtonState.Pressed;
+ case MouseButton.Right:
+ return CurrentState.RightButton == ButtonState.Pressed;
+ case MouseButton.XButton1:
+ return CurrentState.XButton1 == ButtonState.Pressed;
+ case MouseButton.XButton2:
+ return CurrentState.XButton2 == ButtonState.Pressed;
+ default:
+ return false;
+ }
+ }
+
+ ///
+ /// Returns a value that indicates whether the specified mouse button is current up.
+ ///
+ /// The mouse button to check.
+ /// true if the specified mouse button is currently up; otherwise, false.
+ public bool IsButtonUp(MouseButton button)
+ {
+ switch (button)
+ {
+ case MouseButton.Left:
+ return CurrentState.LeftButton == ButtonState.Released;
+ case MouseButton.Middle:
+ return CurrentState.MiddleButton == ButtonState.Released;
+ case MouseButton.Right:
+ return CurrentState.RightButton == ButtonState.Released;
+ case MouseButton.XButton1:
+ return CurrentState.XButton1 == ButtonState.Released;
+ case MouseButton.XButton2:
+ return CurrentState.XButton2 == ButtonState.Released;
+ default:
+ return false;
+ }
+ }
+
+ ///
+ /// Returns a value that indicates whether the specified mouse button was just pressed on the current frame.
+ ///
+ /// The mouse button to check.
+ /// true if the specified mouse button was just pressed on the current frame; otherwise, false.
+ public bool WasButtonJustPressed(MouseButton button)
+ {
+ switch (button)
+ {
+ case MouseButton.Left:
+ return CurrentState.LeftButton == ButtonState.Pressed && PreviousState.LeftButton == ButtonState.Released;
+ case MouseButton.Middle:
+ return CurrentState.MiddleButton == ButtonState.Pressed && PreviousState.MiddleButton == ButtonState.Released;
+ case MouseButton.Right:
+ return CurrentState.RightButton == ButtonState.Pressed && PreviousState.RightButton == ButtonState.Released;
+ case MouseButton.XButton1:
+ return CurrentState.XButton1 == ButtonState.Pressed && PreviousState.XButton1 == ButtonState.Released;
+ case MouseButton.XButton2:
+ return CurrentState.XButton2 == ButtonState.Pressed && PreviousState.XButton2 == ButtonState.Released;
+ default:
+ return false;
+ }
+ }
+
+ ///
+ /// Returns a value that indicates whether the specified mouse button was just released on the current frame.
+ ///
+ /// The mouse button to check.
+ /// true if the specified mouse button was just released on the current frame; otherwise, false.F
+ public bool WasButtonJustReleased(MouseButton button)
+ {
+ switch (button)
+ {
+ case MouseButton.Left:
+ return CurrentState.LeftButton == ButtonState.Released && PreviousState.LeftButton == ButtonState.Pressed;
+ case MouseButton.Middle:
+ return CurrentState.MiddleButton == ButtonState.Released && PreviousState.MiddleButton == ButtonState.Pressed;
+ case MouseButton.Right:
+ return CurrentState.RightButton == ButtonState.Released && PreviousState.RightButton == ButtonState.Pressed;
+ case MouseButton.XButton1:
+ return CurrentState.XButton1 == ButtonState.Released && PreviousState.XButton1 == ButtonState.Pressed;
+ case MouseButton.XButton2:
+ return CurrentState.XButton2 == ButtonState.Released && PreviousState.XButton2 == ButtonState.Pressed;
+ default:
+ return false;
+ }
+ }
+
+ ///
+ /// Sets the current position of the mouse cursor in screen space and updates the CurrentState with the new position.
+ ///
+ /// The x-coordinate location of the mouse cursor in screen space.
+ /// The y-coordinate location of the mouse cursor in screen space.
+ public void SetPosition(int x, int y)
+ {
+ Mouse.SetPosition(x, y);
+ CurrentState = new MouseState(
+ x,
+ y,
+ CurrentState.ScrollWheelValue,
+ CurrentState.LeftButton,
+ CurrentState.MiddleButton,
+ CurrentState.RightButton,
+ CurrentState.XButton1,
+ CurrentState.XButton2
+ );
+ }
+}
diff --git a/Tutorials/MobileDeployment/01-UpdatedLibs/MonoGameLibrary/MonoGameLibrary.csproj b/Tutorials/MobileDeployment/01-UpdatedLibs/MonoGameLibrary/MonoGameLibrary.csproj
new file mode 100644
index 00000000..edf352e7
--- /dev/null
+++ b/Tutorials/MobileDeployment/01-UpdatedLibs/MonoGameLibrary/MonoGameLibrary.csproj
@@ -0,0 +1,10 @@
+
+
+ net9.0
+
+
+
+ All
+
+
+
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/01-UpdatedLibs/MonoGameLibrary/Scenes/Scene.cs b/Tutorials/MobileDeployment/01-UpdatedLibs/MonoGameLibrary/Scenes/Scene.cs
new file mode 100644
index 00000000..627d220f
--- /dev/null
+++ b/Tutorials/MobileDeployment/01-UpdatedLibs/MonoGameLibrary/Scenes/Scene.cs
@@ -0,0 +1,104 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content;
+
+namespace MonoGameLibrary.Scenes;
+
+public abstract class Scene : IDisposable
+{
+ ///
+ /// Gets the ContentManager used for loading scene-specific assets.
+ ///
+ ///
+ /// Assets loaded through this ContentManager will be automatically unloaded when this scene ends.
+ ///
+ protected ContentManager Content { get; }
+
+ ///
+ /// Gets a value that indicates if the scene has been disposed of.
+ ///
+ public bool IsDisposed { get; private set; }
+
+ ///
+ /// Creates a new scene instance.
+ ///
+ public Scene()
+ {
+ // Create a content manager for the scene
+ Content = new ContentManager(Core.Content.ServiceProvider);
+
+ // Set the root directory for content to the same as the root directory
+ // for the game's content.
+ Content.RootDirectory = Core.Content.RootDirectory;
+ }
+
+ // Finalizer, called when object is cleaned up by garbage collector.
+ ~Scene() => Dispose(false);
+
+ ///
+ /// Initializes the scene.
+ ///
+ ///
+ /// When overriding this in a derived class, ensure that base.Initialize()
+ /// still called as this is when LoadContent is called.
+ ///
+ public virtual void Initialize()
+ {
+ LoadContent();
+ }
+
+ ///
+ /// Override to provide logic to load content for the scene.
+ ///
+ public virtual void LoadContent() { }
+
+ ///
+ /// Unloads scene-specific content.
+ ///
+ public virtual void UnloadContent()
+ {
+ Content.Unload();
+ }
+
+ ///
+ /// Updates this scene.
+ ///
+ /// A snapshot of the timing values for the current frame.
+ public virtual void Update(GameTime gameTime) { }
+
+ ///
+ /// Draws this scene.
+ ///
+ /// A snapshot of the timing values for the current frame.
+ public virtual void Draw(GameTime gameTime) { }
+
+ ///
+ /// Disposes of this scene.
+ ///
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ /// Disposes of this scene.
+ ///
+ /// '
+ /// Indicates whether managed resources should be disposed. This value is only true when called from the main
+ /// Dispose method. When called from the finalizer, this will be false.
+ ///
+ protected virtual void Dispose(bool disposing)
+ {
+ if (IsDisposed)
+ {
+ return;
+ }
+
+ if (disposing)
+ {
+ UnloadContent();
+ Content.Dispose();
+ }
+ }
+}
diff --git a/Tutorials/MobileDeployment/01-UpdatedLibs/README.md b/Tutorials/MobileDeployment/01-UpdatedLibs/README.md
new file mode 100644
index 00000000..7117d4a3
--- /dev/null
+++ b/Tutorials/MobileDeployment/01-UpdatedLibs/README.md
@@ -0,0 +1,22 @@
+# Chapter 1: Setting up for Cross-Platform Projects
+
+This chapter demonstrates how to convert a Windows-only MonoGame project to the latest libraries to support iOS and Android platforms.
+
+The chapter covers:
+
+* Updating third-party libraries for cross-platform compatibility.
+
+## Project Structure
+
+This sample includes:
+
+* **DungeonSlime** - Windows desktop project shell
+
+## Prerequisites
+
+* Completed the MonoGame 2D tutorial
+* Development environment set up
+
+## Key Features Demonstrated
+
+* Modern .NET project management with Central Package Management
diff --git a/Tutorials/MobileDeployment/02-Touch/.config/dotnet-tools.json b/Tutorials/MobileDeployment/02-Touch/.config/dotnet-tools.json
new file mode 100644
index 00000000..7f4505d0
--- /dev/null
+++ b/Tutorials/MobileDeployment/02-Touch/.config/dotnet-tools.json
@@ -0,0 +1,36 @@
+{
+ "version": 1,
+ "isRoot": true,
+ "tools": {
+ "dotnet-mgcb": {
+ "version": "3.8.4.1",
+ "commands": [
+ "mgcb"
+ ]
+ },
+ "dotnet-mgcb-editor": {
+ "version": "3.8.4.1",
+ "commands": [
+ "mgcb-editor"
+ ]
+ },
+ "dotnet-mgcb-editor-linux": {
+ "version": "3.8.4.1",
+ "commands": [
+ "mgcb-editor-linux"
+ ]
+ },
+ "dotnet-mgcb-editor-windows": {
+ "version": "3.8.4.1",
+ "commands": [
+ "mgcb-editor-windows"
+ ]
+ },
+ "dotnet-mgcb-editor-mac": {
+ "version": "3.8.4.1",
+ "commands": [
+ "mgcb-editor-mac"
+ ]
+ }
+ }
+}
diff --git a/Tutorials/MobileDeployment/02-Touch/Directory.Packages.props b/Tutorials/MobileDeployment/02-Touch/Directory.Packages.props
new file mode 100644
index 00000000..79688245
--- /dev/null
+++ b/Tutorials/MobileDeployment/02-Touch/Directory.Packages.props
@@ -0,0 +1,10 @@
+
+
+ true
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/02-Touch/README.md b/Tutorials/MobileDeployment/02-Touch/README.md
new file mode 100644
index 00000000..b9724ae2
--- /dev/null
+++ b/Tutorials/MobileDeployment/02-Touch/README.md
@@ -0,0 +1,47 @@
+# Chapter 2: Touch Gesture Demo
+
+This chapter demonstrates how to implement touch input handling in MonoGame with visual feedback for gesture recognition across mobile platforms.
+
+The chapter covers:
+
+* Registering for touch gesture types in MonoGame.
+* Processing touch gestures using the polling approach with `TouchPanel.IsGestureAvailable`.
+* Reading and interpreting different gesture types and their properties.
+* Implementing visual feedback with animated text that appears at touch points.
+* Understanding gesture data including position, delta, and multi-touch coordinates.
+* Creating engaging touch demonstrations with moving and fading visual elements.
+
+## Project Features
+
+This sample includes:
+
+* **Touch Gesture Registration** - Shows how to enable specific gesture types
+* **Visual Feedback System** - Text appears at touch points showing gesture type
+* **Animated Effects** - Text moves upward and fades out over time
+* **Cross-Platform Support** - Works on both iOS and Android devices
+* **Complete Gesture Coverage** - Demonstrates all major MonoGame gesture types
+
+## Prerequisites
+
+* Mobile device or simulator for testing touch input
+
+## Supported Gestures
+
+The demo recognizes and displays feedback for:
+
+* **Tap** - Single quick touch
+* **Double Tap** - Two quick successive taps
+* **Hold** - Press and hold gesture
+* **Flick** - Quick swipe motion
+* **Free Drag** - Continuous dragging
+* **Horizontal Drag** - Horizontal-only dragging
+* **Vertical Drag** - Vertical-only dragging
+* **Pinch** - Two-finger pinch/spread motion
+
+## Key Learning Concepts
+
+* Touch gesture lifecycle and event handling
+* Coordinate system management for touch input
+* Visual feedback techniques for mobile interfaces
+* Performance considerations for gesture recognition
+* Cross-platform touch input consistency
diff --git a/Tutorials/MobileDeployment/02-Touch/TouchExample.Android/.config/dotnet-tools.json b/Tutorials/MobileDeployment/02-Touch/TouchExample.Android/.config/dotnet-tools.json
new file mode 100644
index 00000000..b345335f
--- /dev/null
+++ b/Tutorials/MobileDeployment/02-Touch/TouchExample.Android/.config/dotnet-tools.json
@@ -0,0 +1,30 @@
+{
+ "version": 1,
+ "isRoot": true,
+ "tools": {
+ "dotnet-mgcb-editor": {
+ "version": "3.8.4",
+ "commands": [
+ "mgcb-editor"
+ ]
+ },
+ "dotnet-mgcb-editor-linux": {
+ "version": "3.8.4",
+ "commands": [
+ "mgcb-editor-linux"
+ ]
+ },
+ "dotnet-mgcb-editor-windows": {
+ "version": "3.8.4",
+ "commands": [
+ "mgcb-editor-windows"
+ ]
+ },
+ "dotnet-mgcb-editor-mac": {
+ "version": "3.8.4",
+ "commands": [
+ "mgcb-editor-mac"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/02-Touch/TouchExample.Android/Activity1.cs b/Tutorials/MobileDeployment/02-Touch/TouchExample.Android/Activity1.cs
new file mode 100644
index 00000000..e262a26a
--- /dev/null
+++ b/Tutorials/MobileDeployment/02-Touch/TouchExample.Android/Activity1.cs
@@ -0,0 +1,36 @@
+using Android.App;
+using Android.Content.PM;
+using Android.OS;
+using Android.Views;
+using Microsoft.Xna.Framework;
+using TouchExample;
+
+namespace DungeonSlime.Android
+{
+ [Activity(
+ Label = "@string/app_name",
+ MainLauncher = true,
+ Icon = "@drawable/icon",
+ AlwaysRetainTaskState = true,
+ LaunchMode = LaunchMode.SingleInstance,
+ ScreenOrientation = ScreenOrientation.Landscape,
+ ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.Keyboard | ConfigChanges.KeyboardHidden |
+ ConfigChanges.ScreenSize
+ )]
+ public class Activity1 : AndroidGameActivity
+ {
+ private Game1 _game;
+ private View _view;
+
+ protected override void OnCreate(Bundle bundle)
+ {
+ base.OnCreate(bundle);
+
+ _game = new Game1();
+ _view = _game.Services.GetService(typeof(View)) as View;
+
+ SetContentView(_view);
+ _game.Run();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/02-Touch/TouchExample.Android/AndroidManifest.xml b/Tutorials/MobileDeployment/02-Touch/TouchExample.Android/AndroidManifest.xml
new file mode 100644
index 00000000..9461d961
--- /dev/null
+++ b/Tutorials/MobileDeployment/02-Touch/TouchExample.Android/AndroidManifest.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/02-Touch/TouchExample.Android/Content/Content.mgcb b/Tutorials/MobileDeployment/02-Touch/TouchExample.Android/Content/Content.mgcb
new file mode 100644
index 00000000..ddc4c367
--- /dev/null
+++ b/Tutorials/MobileDeployment/02-Touch/TouchExample.Android/Content/Content.mgcb
@@ -0,0 +1,15 @@
+
+#----------------------------- Global Properties ----------------------------#
+
+/outputDir:bin/$(Platform)
+/intermediateDir:obj/$(Platform)
+/platform:DesktopGL
+/config:
+/profile:Reach
+/compress:False
+
+#-------------------------------- References --------------------------------#
+
+
+#---------------------------------- Content ---------------------------------#
+
diff --git a/Tutorials/MobileDeployment/02-Touch/TouchExample.Android/Default.png b/Tutorials/MobileDeployment/02-Touch/TouchExample.Android/Default.png
new file mode 100644
index 00000000..1f9b909f
Binary files /dev/null and b/Tutorials/MobileDeployment/02-Touch/TouchExample.Android/Default.png differ
diff --git a/Tutorials/MobileDeployment/02-Touch/TouchExample.Android/GameThumbnail.png b/Tutorials/MobileDeployment/02-Touch/TouchExample.Android/GameThumbnail.png
new file mode 100644
index 00000000..99814c32
Binary files /dev/null and b/Tutorials/MobileDeployment/02-Touch/TouchExample.Android/GameThumbnail.png differ
diff --git a/Tutorials/MobileDeployment/02-Touch/TouchExample.Android/Resources/Drawable/Icon.png b/Tutorials/MobileDeployment/02-Touch/TouchExample.Android/Resources/Drawable/Icon.png
new file mode 100644
index 00000000..25fe0444
Binary files /dev/null and b/Tutorials/MobileDeployment/02-Touch/TouchExample.Android/Resources/Drawable/Icon.png differ
diff --git a/Tutorials/MobileDeployment/02-Touch/TouchExample.Android/Resources/Values/Strings.xml b/Tutorials/MobileDeployment/02-Touch/TouchExample.Android/Resources/Values/Strings.xml
new file mode 100644
index 00000000..7a59ecad
--- /dev/null
+++ b/Tutorials/MobileDeployment/02-Touch/TouchExample.Android/Resources/Values/Strings.xml
@@ -0,0 +1,4 @@
+
+
+ DungeonSlime.Android
+
diff --git a/Tutorials/MobileDeployment/02-Touch/TouchExample.Android/TouchExample.Android.csproj b/Tutorials/MobileDeployment/02-Touch/TouchExample.Android/TouchExample.Android.csproj
new file mode 100644
index 00000000..ee52b6c9
--- /dev/null
+++ b/Tutorials/MobileDeployment/02-Touch/TouchExample.Android/TouchExample.Android.csproj
@@ -0,0 +1,22 @@
+
+
+ net9.0-android
+ TouchExample
+ Exe
+ 23
+ com.monogame.touchexample
+ 1
+ 1.0
+
+
+
+
+
+
+
+
+ Game1.cs
+
+
+
+
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/.config/dotnet-tools.json b/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/.config/dotnet-tools.json
new file mode 100644
index 00000000..b345335f
--- /dev/null
+++ b/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/.config/dotnet-tools.json
@@ -0,0 +1,30 @@
+{
+ "version": 1,
+ "isRoot": true,
+ "tools": {
+ "dotnet-mgcb-editor": {
+ "version": "3.8.4",
+ "commands": [
+ "mgcb-editor"
+ ]
+ },
+ "dotnet-mgcb-editor-linux": {
+ "version": "3.8.4",
+ "commands": [
+ "mgcb-editor-linux"
+ ]
+ },
+ "dotnet-mgcb-editor-windows": {
+ "version": "3.8.4",
+ "commands": [
+ "mgcb-editor-windows"
+ ]
+ },
+ "dotnet-mgcb-editor-mac": {
+ "version": "3.8.4",
+ "commands": [
+ "mgcb-editor-mac"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Assets.xcassets/AppIcon.appiconset/Contents.json b/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 00000000..0763ce12
--- /dev/null
+++ b/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,93 @@
+{
+ "images": [
+ {
+ "idiom": "iphone",
+ "size": "20x20",
+ "scale": "2x",
+ "filename": "appicon20x20@2x.png"
+ },
+ {
+ "idiom": "ipad",
+ "size": "20x20",
+ "scale": "2x",
+ "filename": "appicon20x20@2x.png"
+ },
+ {
+ "idiom": "iphone",
+ "size": "20x20",
+ "scale": "3x",
+ "filename": "appicon20x20@3x.png"
+ },
+ {
+ "idiom": "iphone",
+ "size": "29x29",
+ "scale": "2x",
+ "filename": "appicon29x29@2x.png"
+ },
+ {
+ "idiom": "ipad",
+ "size": "29x29",
+ "scale": "2x",
+ "filename": "appicon29x29@2x.png"
+ },
+ {
+ "idiom": "iphone",
+ "size": "29x29",
+ "scale": "3x",
+ "filename": "appicon29x29@3x.png"
+ },
+ {
+ "idiom": "iphone",
+ "size": "40x40",
+ "scale": "2x",
+ "filename": "appicon40x40@2x.png"
+ },
+ {
+ "idiom": "ipad",
+ "size": "40x40",
+ "scale": "2x",
+ "filename": "appicon40x40@2x.png"
+ },
+ {
+ "idiom": "iphone",
+ "size": "40x40",
+ "scale": "3x",
+ "filename": "appicon40x40@3x.png"
+ },
+ {
+ "idiom": "iphone",
+ "size": "60x60",
+ "scale": "2x",
+ "filename": "appicon60x60@2x.png"
+ },
+ {
+ "idiom": "iphone",
+ "size": "60x60",
+ "scale": "3x",
+ "filename": "appicon60x60@3x.png"
+ },
+ {
+ "idiom": "ipad",
+ "size": "76x76",
+ "scale": "2x",
+ "filename": "appicon76x76@2x.png"
+ },
+ {
+ "idiom": "ipad",
+ "size": "83.5x83.5",
+ "scale": "2x",
+ "filename": "appicon83.5x83.5@2x.png"
+ },
+ {
+ "idiom": "ios-marketing",
+ "size": "1024x1024",
+ "scale": "1x",
+ "filename": "appiconItunesArtwork.png"
+ }
+ ],
+ "properties": {},
+ "info": {
+ "version": 1,
+ "author": "xcode"
+ }
+}
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Assets.xcassets/AppIcon.appiconset/appicon20x20@2x.png b/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Assets.xcassets/AppIcon.appiconset/appicon20x20@2x.png
new file mode 100644
index 00000000..0d36c4be
Binary files /dev/null and b/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Assets.xcassets/AppIcon.appiconset/appicon20x20@2x.png differ
diff --git a/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Assets.xcassets/AppIcon.appiconset/appicon20x20@3x.png b/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Assets.xcassets/AppIcon.appiconset/appicon20x20@3x.png
new file mode 100644
index 00000000..13e81e5b
Binary files /dev/null and b/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Assets.xcassets/AppIcon.appiconset/appicon20x20@3x.png differ
diff --git a/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Assets.xcassets/AppIcon.appiconset/appicon29x29@2x.png b/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Assets.xcassets/AppIcon.appiconset/appicon29x29@2x.png
new file mode 100644
index 00000000..826942c9
Binary files /dev/null and b/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Assets.xcassets/AppIcon.appiconset/appicon29x29@2x.png differ
diff --git a/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Assets.xcassets/AppIcon.appiconset/appicon29x29@3x.png b/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Assets.xcassets/AppIcon.appiconset/appicon29x29@3x.png
new file mode 100644
index 00000000..eef69b7e
Binary files /dev/null and b/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Assets.xcassets/AppIcon.appiconset/appicon29x29@3x.png differ
diff --git a/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Assets.xcassets/AppIcon.appiconset/appicon40x40@2x.png b/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Assets.xcassets/AppIcon.appiconset/appicon40x40@2x.png
new file mode 100644
index 00000000..76bf8b6c
Binary files /dev/null and b/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Assets.xcassets/AppIcon.appiconset/appicon40x40@2x.png differ
diff --git a/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Assets.xcassets/AppIcon.appiconset/appicon40x40@3x.png b/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Assets.xcassets/AppIcon.appiconset/appicon40x40@3x.png
new file mode 100644
index 00000000..09da383e
Binary files /dev/null and b/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Assets.xcassets/AppIcon.appiconset/appicon40x40@3x.png differ
diff --git a/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Assets.xcassets/AppIcon.appiconset/appicon60x60@2x.png b/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Assets.xcassets/AppIcon.appiconset/appicon60x60@2x.png
new file mode 100644
index 00000000..2a6eba37
Binary files /dev/null and b/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Assets.xcassets/AppIcon.appiconset/appicon60x60@2x.png differ
diff --git a/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Assets.xcassets/AppIcon.appiconset/appicon60x60@3x.png b/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Assets.xcassets/AppIcon.appiconset/appicon60x60@3x.png
new file mode 100644
index 00000000..fd8c4a04
Binary files /dev/null and b/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Assets.xcassets/AppIcon.appiconset/appicon60x60@3x.png differ
diff --git a/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Assets.xcassets/AppIcon.appiconset/appicon76x76@2x.png b/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Assets.xcassets/AppIcon.appiconset/appicon76x76@2x.png
new file mode 100644
index 00000000..b3ef476c
Binary files /dev/null and b/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Assets.xcassets/AppIcon.appiconset/appicon76x76@2x.png differ
diff --git a/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Assets.xcassets/AppIcon.appiconset/appicon83.5x83.5@2x.png b/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Assets.xcassets/AppIcon.appiconset/appicon83.5x83.5@2x.png
new file mode 100644
index 00000000..cb03f365
Binary files /dev/null and b/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Assets.xcassets/AppIcon.appiconset/appicon83.5x83.5@2x.png differ
diff --git a/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Assets.xcassets/AppIcon.appiconset/appiconItunesArtwork.png b/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Assets.xcassets/AppIcon.appiconset/appiconItunesArtwork.png
new file mode 100644
index 00000000..3f789e7e
Binary files /dev/null and b/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Assets.xcassets/AppIcon.appiconset/appiconItunesArtwork.png differ
diff --git a/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Assets.xcassets/Contents.json b/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Assets.xcassets/Contents.json
new file mode 100644
index 00000000..121dee67
--- /dev/null
+++ b/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info": {
+ "version": 1,
+ "author": "xcode"
+ }
+}
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Content/Arial.spritefont b/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Content/Arial.spritefont
new file mode 100644
index 00000000..c9dbac58
--- /dev/null
+++ b/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Content/Arial.spritefont
@@ -0,0 +1,16 @@
+
+
+
+ Arial
+ 36
+ 0
+ true
+
+
+
+
+ ~
+
+
+
+
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Content/Content.mgcb b/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Content/Content.mgcb
new file mode 100644
index 00000000..7e01f88b
--- /dev/null
+++ b/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Content/Content.mgcb
@@ -0,0 +1,21 @@
+
+#----------------------------- Global Properties ----------------------------#
+
+/outputDir:bin/$(Platform)
+/intermediateDir:obj/$(Platform)
+/platform:DesktopGL
+/config:
+/profile:Reach
+/compress:False
+
+#-------------------------------- References --------------------------------#
+
+
+#---------------------------------- Content ---------------------------------#
+
+#begin Arial.spritefont
+/importer:FontDescriptionImporter
+/processor:FontDescriptionProcessor
+/processorParam:PremultiplyAlpha=True
+/processorParam:TextureFormat=Compressed
+/build:Arial.spritefont
diff --git a/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Default.png b/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Default.png
new file mode 100644
index 00000000..1f9b909f
Binary files /dev/null and b/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Default.png differ
diff --git a/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Entitlements.plist b/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Entitlements.plist
new file mode 100644
index 00000000..4eac5f62
--- /dev/null
+++ b/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Entitlements.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ get-task-allow
+
+
+
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/EntitlementsProduction.plist b/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/EntitlementsProduction.plist
new file mode 100644
index 00000000..efc29758
--- /dev/null
+++ b/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/EntitlementsProduction.plist
@@ -0,0 +1,9 @@
+
+
+
+
+
+ get-task-allow
+
+
+
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Game1.cs b/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Game1.cs
new file mode 100644
index 00000000..a89b608e
--- /dev/null
+++ b/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Game1.cs
@@ -0,0 +1,137 @@
+using System;
+using System.Collections.Generic;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+using Microsoft.Xna.Framework.Input.Touch;
+
+namespace TouchExample;
+
+public class Game1 : Game
+{
+ private GraphicsDeviceManager _graphics;
+ private SpriteBatch _spriteBatch;
+ private SpriteFont _font;
+
+ public Game1()
+ {
+ _graphics = new GraphicsDeviceManager(this);
+ Content.RootDirectory = "Content";
+ IsMouseVisible = true;
+ }
+
+ protected override void Initialize()
+ {
+ // this is the list of supported gestures.
+ TouchPanel.EnabledGestures = GestureType.DoubleTap |
+ GestureType.DragComplete |
+ GestureType.Flick |
+ GestureType.FreeDrag |
+ GestureType.Hold |
+ GestureType.HorizontalDrag |
+ GestureType.Pinch |
+ GestureType.PinchComplete |
+ GestureType.Tap |
+ GestureType.VerticalDrag;
+
+ base.Initialize();
+ }
+
+ protected override void LoadContent()
+ {
+ _spriteBatch = new SpriteBatch(GraphicsDevice);
+ _font = Content.Load("Arial");
+ }
+
+ protected override void Update(GameTime gameTime)
+ {
+ while (TouchPanel.IsGestureAvailable)
+ {
+ GestureSample gesture = TouchPanel.ReadGesture();
+
+ switch (gesture.GestureType)
+ {
+ case GestureType.DoubleTap:
+ _gestures.Add(new GestureText{ Text="Double Tap", X=gesture.Position.X, Y=gesture.Position.Y });
+ break;
+
+ case GestureType.Flick:
+ _gestures.Add(new GestureText{ Text="Flick", X=gesture.Position.X, Y=gesture.Position.Y });
+ break;
+
+ case GestureType.FreeDrag:
+ _gestures.Add(new GestureText{ Text="Free Drag", X=gesture.Position.X, Y=gesture.Position.Y });
+ break;
+
+ case GestureType.Hold:
+ _gestures.Add(new GestureText{ Text="Hold", X=gesture.Position.X, Y=gesture.Position.Y });
+ break;
+
+ case GestureType.HorizontalDrag:
+ _gestures.Add(new GestureText{ Text="Horizontal Drag", X=gesture.Position.X, Y=gesture.Position.Y });
+ break;
+
+ case GestureType.Pinch:
+ _gestures.Add(new GestureText{ Text="Pinch", X=gesture.Position.X, Y=gesture.Position.Y });
+ break;
+
+ case GestureType.Tap:
+ _gestures.Add(new GestureText{ Text="Tap", X=gesture.Position.X, Y=gesture.Position.Y });
+ break;
+
+ case GestureType.VerticalDrag:
+ _gestures.Add(new GestureText{ Text="Vertical Drag", X=gesture.Position.X, Y=gesture.Position.Y });
+ break;
+ }
+ }
+
+ List itemsToRemove = new();
+
+ foreach (GestureText gesture in _gestures)
+ {
+ gesture.Lifetime -= gameTime.ElapsedGameTime;
+ gesture.Y -= (float)(gameTime.ElapsedGameTime.TotalMilliseconds * 0.5f);
+
+ if (gesture.Lifetime < TimeSpan.Zero)
+ {
+ itemsToRemove.Add(gesture);
+ }
+ }
+
+ foreach (GestureText item in itemsToRemove)
+ {
+ _gestures.Remove(item);
+ }
+
+ base.Update(gameTime);
+ }
+
+ readonly List _gestures = new();
+
+ class GestureText
+ {
+ public string Text { get; set; }
+
+ public float X { get; set; }
+
+ public float Y { get; set; }
+
+ public TimeSpan Lifetime { get; set; } = TimeSpan.FromSeconds(1);
+
+ public float Alpha => (float)(Lifetime.TotalSeconds / 1.0);
+ }
+
+ protected override void Draw(GameTime gameTime)
+ {
+ GraphicsDevice.Clear(Color.CornflowerBlue);
+
+ foreach (GestureText text in _gestures)
+ {
+ _spriteBatch.Begin();
+ Color textColor = Color.White * text.Alpha;
+ _spriteBatch.DrawString(_font, text.Text, new Vector2(text.X, text.Y), textColor);
+ _spriteBatch.End();
+ }
+
+ base.Draw(gameTime);
+ }
+}
diff --git a/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/GameThumbnail.png b/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/GameThumbnail.png
new file mode 100644
index 00000000..99814c32
Binary files /dev/null and b/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/GameThumbnail.png differ
diff --git a/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Info.plist b/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Info.plist
new file mode 100644
index 00000000..28d2f2b6
--- /dev/null
+++ b/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Info.plist
@@ -0,0 +1,50 @@
+
+
+
+
+ CFBundleIconName
+ AppIcon
+ CFBundleIcons
+
+ CFBundlePrimaryIcon
+
+ CFBundleIconName
+ AppIcon
+
+
+ CFBundleDisplayName
+ TouchExample
+ CFBundleIdentifier
+ com.ogs.touchexample
+ MinimumOSVersion
+ 12.2
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationPortrait
+
+ CFBundleName
+ DungeonSlime
+ CFBundleVersion
+ 11
+ CFBundleShortVersionString
+ 1.10
+ UIRequiresFullScreen
+
+ UIStatusBarHidden
+
+ UIRequiredDeviceCapabilities
+
+ opengles-2
+
+ UILaunchStoryboardName
+ LaunchScreen
+ UIDeviceFamily
+
+ 1
+ 2
+
+ CFBundleExecutable
+
+
+
diff --git a/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/LaunchScreen.storyboard b/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/LaunchScreen.storyboard
new file mode 100644
index 00000000..5d2e905a
--- /dev/null
+++ b/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/LaunchScreen.storyboard
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Program.cs b/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Program.cs
new file mode 100644
index 00000000..62cb58ef
--- /dev/null
+++ b/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/Program.cs
@@ -0,0 +1,39 @@
+using System;
+using Foundation;
+using UIKit;
+
+namespace TouchExample
+{
+ [Register("AppDelegate")]
+ class Program : UIApplicationDelegate
+ {
+ private static Game1 game;
+
+ internal static void RunGame()
+ {
+ try
+ {
+ game = new Game1();
+ game.Run();
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Game failed to start: {ex}");
+ throw;
+ }
+ }
+
+ ///
+ /// The main entry point for the application.
+ ///
+ static void Main(string[] args)
+ {
+ UIApplication.Main(args, null, typeof(Program));
+ }
+
+ public override void FinishedLaunching(UIApplication app)
+ {
+ RunGame();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/TouchExample.iOS.csproj b/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/TouchExample.iOS.csproj
new file mode 100644
index 00000000..65f8ea1f
--- /dev/null
+++ b/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/TouchExample.iOS.csproj
@@ -0,0 +1,35 @@
+
+
+ Exe
+ net9.0-ios
+ 12.2
+ com.ogs.touchexample
+ AppIcon
+
+
+
+
+
+
+
+ iPhone Distribution
+ EntitlementsProduction.plist
+
+
+
+ iPhone Developer
+ Entitlements.plist
+
+
+
+
+
+
+
+
+
+ Assets.car
+
+
+
+
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/partial-info.plist b/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/partial-info.plist
new file mode 100644
index 00000000..a19ab726
--- /dev/null
+++ b/Tutorials/MobileDeployment/02-Touch/TouchExample.iOS/partial-info.plist
@@ -0,0 +1,31 @@
+
+
+
+
+ CFBundleIcons
+
+ CFBundlePrimaryIcon
+
+ CFBundleIconFiles
+
+ AppIcon60x60
+
+ CFBundleIconName
+ AppIcon
+
+
+ CFBundleIcons~ipad
+
+ CFBundlePrimaryIcon
+
+ CFBundleIconFiles
+
+ AppIcon60x60
+ AppIcon76x76
+
+ CFBundleIconName
+ AppIcon
+
+
+
+
diff --git a/Tutorials/MobileDeployment/02-Touch/TouchExample.slnx b/Tutorials/MobileDeployment/02-Touch/TouchExample.slnx
new file mode 100644
index 00000000..b863d851
--- /dev/null
+++ b/Tutorials/MobileDeployment/02-Touch/TouchExample.slnx
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/.config/dotnet-tools.json b/Tutorials/MobileDeployment/03-AddIOSProject/.config/dotnet-tools.json
new file mode 100644
index 00000000..7f4505d0
--- /dev/null
+++ b/Tutorials/MobileDeployment/03-AddIOSProject/.config/dotnet-tools.json
@@ -0,0 +1,36 @@
+{
+ "version": 1,
+ "isRoot": true,
+ "tools": {
+ "dotnet-mgcb": {
+ "version": "3.8.4.1",
+ "commands": [
+ "mgcb"
+ ]
+ },
+ "dotnet-mgcb-editor": {
+ "version": "3.8.4.1",
+ "commands": [
+ "mgcb-editor"
+ ]
+ },
+ "dotnet-mgcb-editor-linux": {
+ "version": "3.8.4.1",
+ "commands": [
+ "mgcb-editor-linux"
+ ]
+ },
+ "dotnet-mgcb-editor-windows": {
+ "version": "3.8.4.1",
+ "commands": [
+ "mgcb-editor-windows"
+ ]
+ },
+ "dotnet-mgcb-editor-mac": {
+ "version": "3.8.4.1",
+ "commands": [
+ "mgcb-editor-mac"
+ ]
+ }
+ }
+}
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/Directory.Packages.props b/Tutorials/MobileDeployment/03-AddIOSProject/Directory.Packages.props
new file mode 100644
index 00000000..857df195
--- /dev/null
+++ b/Tutorials/MobileDeployment/03-AddIOSProject/Directory.Packages.props
@@ -0,0 +1,11 @@
+
+
+ true
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/.config/dotnet-tools.json b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/.config/dotnet-tools.json
new file mode 100644
index 00000000..fbedee15
--- /dev/null
+++ b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/.config/dotnet-tools.json
@@ -0,0 +1,36 @@
+{
+ "version": 1,
+ "isRoot": true,
+ "tools": {
+ "dotnet-mgcb": {
+ "version": "3.8.4",
+ "commands": [
+ "mgcb"
+ ]
+ },
+ "dotnet-mgcb-editor": {
+ "version": "3.8.4",
+ "commands": [
+ "mgcb-editor"
+ ]
+ },
+ "dotnet-mgcb-editor-linux": {
+ "version": "3.8.4",
+ "commands": [
+ "mgcb-editor-linux"
+ ]
+ },
+ "dotnet-mgcb-editor-windows": {
+ "version": "3.8.4",
+ "commands": [
+ "mgcb-editor-windows"
+ ]
+ },
+ "dotnet-mgcb-editor-mac": {
+ "version": "3.8.4",
+ "commands": [
+ "mgcb-editor-mac"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/Contents.json b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 00000000..0763ce12
--- /dev/null
+++ b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,93 @@
+{
+ "images": [
+ {
+ "idiom": "iphone",
+ "size": "20x20",
+ "scale": "2x",
+ "filename": "appicon20x20@2x.png"
+ },
+ {
+ "idiom": "ipad",
+ "size": "20x20",
+ "scale": "2x",
+ "filename": "appicon20x20@2x.png"
+ },
+ {
+ "idiom": "iphone",
+ "size": "20x20",
+ "scale": "3x",
+ "filename": "appicon20x20@3x.png"
+ },
+ {
+ "idiom": "iphone",
+ "size": "29x29",
+ "scale": "2x",
+ "filename": "appicon29x29@2x.png"
+ },
+ {
+ "idiom": "ipad",
+ "size": "29x29",
+ "scale": "2x",
+ "filename": "appicon29x29@2x.png"
+ },
+ {
+ "idiom": "iphone",
+ "size": "29x29",
+ "scale": "3x",
+ "filename": "appicon29x29@3x.png"
+ },
+ {
+ "idiom": "iphone",
+ "size": "40x40",
+ "scale": "2x",
+ "filename": "appicon40x40@2x.png"
+ },
+ {
+ "idiom": "ipad",
+ "size": "40x40",
+ "scale": "2x",
+ "filename": "appicon40x40@2x.png"
+ },
+ {
+ "idiom": "iphone",
+ "size": "40x40",
+ "scale": "3x",
+ "filename": "appicon40x40@3x.png"
+ },
+ {
+ "idiom": "iphone",
+ "size": "60x60",
+ "scale": "2x",
+ "filename": "appicon60x60@2x.png"
+ },
+ {
+ "idiom": "iphone",
+ "size": "60x60",
+ "scale": "3x",
+ "filename": "appicon60x60@3x.png"
+ },
+ {
+ "idiom": "ipad",
+ "size": "76x76",
+ "scale": "2x",
+ "filename": "appicon76x76@2x.png"
+ },
+ {
+ "idiom": "ipad",
+ "size": "83.5x83.5",
+ "scale": "2x",
+ "filename": "appicon83.5x83.5@2x.png"
+ },
+ {
+ "idiom": "ios-marketing",
+ "size": "1024x1024",
+ "scale": "1x",
+ "filename": "appiconItunesArtwork.png"
+ }
+ ],
+ "properties": {},
+ "info": {
+ "version": 1,
+ "author": "xcode"
+ }
+}
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon20x20@2x.png b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon20x20@2x.png
new file mode 100644
index 00000000..0d36c4be
Binary files /dev/null and b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon20x20@2x.png differ
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon20x20@3x.png b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon20x20@3x.png
new file mode 100644
index 00000000..13e81e5b
Binary files /dev/null and b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon20x20@3x.png differ
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon29x29@2x.png b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon29x29@2x.png
new file mode 100644
index 00000000..826942c9
Binary files /dev/null and b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon29x29@2x.png differ
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon29x29@3x.png b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon29x29@3x.png
new file mode 100644
index 00000000..eef69b7e
Binary files /dev/null and b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon29x29@3x.png differ
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon40x40@2x.png b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon40x40@2x.png
new file mode 100644
index 00000000..76bf8b6c
Binary files /dev/null and b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon40x40@2x.png differ
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon40x40@3x.png b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon40x40@3x.png
new file mode 100644
index 00000000..09da383e
Binary files /dev/null and b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon40x40@3x.png differ
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon60x60@2x.png b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon60x60@2x.png
new file mode 100644
index 00000000..2a6eba37
Binary files /dev/null and b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon60x60@2x.png differ
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon60x60@3x.png b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon60x60@3x.png
new file mode 100644
index 00000000..fd8c4a04
Binary files /dev/null and b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon60x60@3x.png differ
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon76x76@2x.png b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon76x76@2x.png
new file mode 100644
index 00000000..b3ef476c
Binary files /dev/null and b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon76x76@2x.png differ
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon83.5x83.5@2x.png b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon83.5x83.5@2x.png
new file mode 100644
index 00000000..cb03f365
Binary files /dev/null and b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon83.5x83.5@2x.png differ
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appiconItunesArtwork.png b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appiconItunesArtwork.png
new file mode 100644
index 00000000..3f789e7e
Binary files /dev/null and b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appiconItunesArtwork.png differ
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/Assets.xcassets/Contents.json b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/Assets.xcassets/Contents.json
new file mode 100644
index 00000000..121dee67
--- /dev/null
+++ b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info": {
+ "version": 1,
+ "author": "xcode"
+ }
+}
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/Content/Content.mgcb b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/Content/Content.mgcb
new file mode 100644
index 00000000..f0e047ff
--- /dev/null
+++ b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/Content/Content.mgcb
@@ -0,0 +1,13 @@
+#----------------------------- Global Properties ----------------------------#
+
+/outputDir:bin/$(Platform)
+/intermediateDir:obj/$(Platform)
+/platform:iOS
+/config:
+/profile:Reach
+/compress:False
+
+#-------------------------------- References --------------------------------#
+
+
+#---------------------------------- Content ---------------------------------#
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/Default.png b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/Default.png
new file mode 100644
index 00000000..1f9b909f
Binary files /dev/null and b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/Default.png differ
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/DungeonSlime.iOS.csproj b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/DungeonSlime.iOS.csproj
new file mode 100644
index 00000000..f5fadd38
--- /dev/null
+++ b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/DungeonSlime.iOS.csproj
@@ -0,0 +1,42 @@
+
+
+
+ Exe
+ net9.0-ios
+ 12.2
+ com.monogame.dungeonslime
+ AppIcon
+
+
+
+ iPhone Distribution
+ EntitlementsProduction.plist
+
+
+
+ iPhone Developer
+ Entitlements.plist
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Assets.car
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/Entitlements.plist b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/Entitlements.plist
new file mode 100644
index 00000000..4eac5f62
--- /dev/null
+++ b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/Entitlements.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ get-task-allow
+
+
+
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/EntitlementsProduction.plist b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/EntitlementsProduction.plist
new file mode 100644
index 00000000..efc29758
--- /dev/null
+++ b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/EntitlementsProduction.plist
@@ -0,0 +1,9 @@
+
+
+
+
+
+ get-task-allow
+
+
+
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/Game1.cs b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/Game1.cs
new file mode 100644
index 00000000..f66454cf
--- /dev/null
+++ b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/Game1.cs
@@ -0,0 +1,48 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+using Microsoft.Xna.Framework.Input;
+
+namespace DungeonSlime.iOS;
+
+public class Game1 : Game
+{
+ private GraphicsDeviceManager _graphics;
+ private SpriteBatch _spriteBatch;
+
+ public Game1()
+ {
+ _graphics = new GraphicsDeviceManager(this);
+ Content.RootDirectory = "Content";
+ IsMouseVisible = true;
+ }
+
+ protected override void Initialize()
+ {
+ // TODO: Add your initialization logic here
+
+ base.Initialize();
+ }
+
+ protected override void LoadContent()
+ {
+ _spriteBatch = new SpriteBatch(GraphicsDevice);
+
+ // TODO: use this.Content to load your game content here
+ }
+
+ protected override void Update(GameTime gameTime)
+ {
+ // TODO: Add your update logic here
+
+ base.Update(gameTime);
+ }
+
+ protected override void Draw(GameTime gameTime)
+ {
+ GraphicsDevice.Clear(Color.CornflowerBlue);
+
+ // TODO: Add your drawing code here
+
+ base.Draw(gameTime);
+ }
+}
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/GameThumbnail.png b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/GameThumbnail.png
new file mode 100644
index 00000000..99814c32
Binary files /dev/null and b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/GameThumbnail.png differ
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/Info.plist b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/Info.plist
new file mode 100644
index 00000000..1887bd75
--- /dev/null
+++ b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/Info.plist
@@ -0,0 +1,50 @@
+
+
+
+
+ CFBundleIconName
+ AppIcon
+ CFBundleIcons
+
+ CFBundlePrimaryIcon
+
+ CFBundleIconName
+ AppIcon
+
+
+ CFBundleDisplayName
+ DungeonSlime
+ CFBundleIdentifier
+ com.monogame.dungeonslime
+ MinimumOSVersion
+ 12.2
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ CFBundleName
+ DungeonSlime
+ CFBundleVersion
+ 11
+ CFBundleShortVersionString
+ 1.10
+ UIRequiresFullScreen
+
+ UIStatusBarHidden
+
+ UIRequiredDeviceCapabilities
+
+ opengles-2
+
+ UILaunchStoryboardName
+ LaunchScreen
+ UIDeviceFamily
+
+ 1
+ 2
+
+ CFBundleExecutable
+
+
+
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/LaunchScreen.storyboard b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/LaunchScreen.storyboard
new file mode 100644
index 00000000..5d2e905a
--- /dev/null
+++ b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/LaunchScreen.storyboard
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/Program.cs b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/Program.cs
new file mode 100644
index 00000000..7fd9c6f0
--- /dev/null
+++ b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.iOS/Program.cs
@@ -0,0 +1,31 @@
+using System;
+using Foundation;
+using UIKit;
+
+namespace DungeonSlime.iOS
+{
+ [Register("AppDelegate")]
+ class Program : UIApplicationDelegate
+ {
+ private static Game1 game;
+
+ internal static void RunGame()
+ {
+ game = new Game1();
+ game.Run();
+ }
+
+ ///
+ /// The main entry point for the application.
+ ///
+ static void Main(string[] args)
+ {
+ UIApplication.Main(args, null, typeof(Program));
+ }
+
+ public override void FinishedLaunching(UIApplication app)
+ {
+ RunGame();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.slnx b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.slnx
new file mode 100644
index 00000000..3a0777dc
--- /dev/null
+++ b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime.slnx
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/.config/dotnet-tools.json b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/.config/dotnet-tools.json
new file mode 100644
index 00000000..afd4e2c4
--- /dev/null
+++ b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/.config/dotnet-tools.json
@@ -0,0 +1,36 @@
+{
+ "version": 1,
+ "isRoot": true,
+ "tools": {
+ "dotnet-mgcb": {
+ "version": "3.8.3",
+ "commands": [
+ "mgcb"
+ ]
+ },
+ "dotnet-mgcb-editor": {
+ "version": "3.8.3",
+ "commands": [
+ "mgcb-editor"
+ ]
+ },
+ "dotnet-mgcb-editor-linux": {
+ "version": "3.8.3",
+ "commands": [
+ "mgcb-editor-linux"
+ ]
+ },
+ "dotnet-mgcb-editor-windows": {
+ "version": "3.8.3",
+ "commands": [
+ "mgcb-editor-windows"
+ ]
+ },
+ "dotnet-mgcb-editor-mac": {
+ "version": "3.8.3",
+ "commands": [
+ "mgcb-editor-mac"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Content/Content.mgcb b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Content/Content.mgcb
new file mode 100644
index 00000000..d26ea4f1
--- /dev/null
+++ b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Content/Content.mgcb
@@ -0,0 +1,104 @@
+
+#----------------------------- Global Properties ----------------------------#
+
+/outputDir:bin/$(Platform)
+/intermediateDir:obj/$(Platform)
+/platform:DesktopGL
+/config:
+/profile:Reach
+/compress:False
+
+#-------------------------------- References --------------------------------#
+
+
+#---------------------------------- Content ---------------------------------#
+
+#begin audio/bounce.wav
+/importer:WavImporter
+/processor:SoundEffectProcessor
+/processorParam:Quality=Best
+/build:audio/bounce.wav
+
+#begin audio/collect.wav
+/importer:WavImporter
+/processor:SoundEffectProcessor
+/processorParam:Quality=Best
+/build:audio/collect.wav
+
+#begin audio/theme.ogg
+/importer:OggImporter
+/processor:SongProcessor
+/processorParam:Quality=Best
+/build:audio/theme.ogg
+
+#begin audio/ui.wav
+/importer:WavImporter
+/processor:SoundEffectProcessor
+/processorParam:Quality=Best
+/build:audio/ui.wav
+
+#begin effects/grayscaleEffect.fx
+/importer:EffectImporter
+/processor:EffectProcessor
+/processorParam:DebugMode=Auto
+/build:effects/grayscaleEffect.fx
+
+#begin fonts/04B_30_5x.spritefont
+/importer:FontDescriptionImporter
+/processor:FontDescriptionProcessor
+/processorParam:PremultiplyAlpha=True
+/processorParam:TextureFormat=Compressed
+/build:fonts/04B_30_5x.spritefont
+
+#begin fonts/04b_30.fnt
+/copy:fonts/04b_30.fnt
+
+#begin fonts/04B_30.spritefont
+/importer:FontDescriptionImporter
+/processor:FontDescriptionProcessor
+/processorParam:PremultiplyAlpha=True
+/processorParam:TextureFormat=Compressed
+/build:fonts/04B_30.spritefont
+
+#begin images/atlas-definition.xml
+/copy:images/atlas-definition.xml
+
+#begin images/atlas.png
+/importer:TextureImporter
+/processor:TextureProcessor
+/processorParam:ColorKeyColor=255,0,255,255
+/processorParam:ColorKeyEnabled=True
+/processorParam:GenerateMipmaps=False
+/processorParam:PremultiplyAlpha=True
+/processorParam:ResizeToPowerOfTwo=False
+/processorParam:MakeSquare=False
+/processorParam:TextureFormat=Color
+/build:images/atlas.png
+
+#begin images/background-pattern.png
+/importer:TextureImporter
+/processor:TextureProcessor
+/processorParam:ColorKeyColor=255,0,255,255
+/processorParam:ColorKeyEnabled=True
+/processorParam:GenerateMipmaps=False
+/processorParam:PremultiplyAlpha=True
+/processorParam:ResizeToPowerOfTwo=False
+/processorParam:MakeSquare=False
+/processorParam:TextureFormat=Color
+/build:images/background-pattern.png
+
+#begin images/logo.png
+/importer:TextureImporter
+/processor:TextureProcessor
+/processorParam:ColorKeyColor=255,0,255,255
+/processorParam:ColorKeyEnabled=True
+/processorParam:GenerateMipmaps=False
+/processorParam:PremultiplyAlpha=True
+/processorParam:ResizeToPowerOfTwo=False
+/processorParam:MakeSquare=False
+/processorParam:TextureFormat=Color
+/build:images/logo.png
+
+#begin images/tilemap-definition.xml
+/copy:images/tilemap-definition.xml
+
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Content/audio/bounce.wav b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Content/audio/bounce.wav
new file mode 100644
index 00000000..baa7a47b
Binary files /dev/null and b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Content/audio/bounce.wav differ
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Content/audio/collect.wav b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Content/audio/collect.wav
new file mode 100644
index 00000000..506220de
Binary files /dev/null and b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Content/audio/collect.wav differ
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Content/audio/theme.ogg b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Content/audio/theme.ogg
new file mode 100644
index 00000000..72e1fd3b
Binary files /dev/null and b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Content/audio/theme.ogg differ
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Content/audio/ui.wav b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Content/audio/ui.wav
new file mode 100644
index 00000000..63e8941e
Binary files /dev/null and b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Content/audio/ui.wav differ
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Content/effects/grayscaleEffect.fx b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Content/effects/grayscaleEffect.fx
new file mode 100644
index 00000000..5dd0d8b6
--- /dev/null
+++ b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Content/effects/grayscaleEffect.fx
@@ -0,0 +1,53 @@
+#if OPENGL
+ #define SV_POSITION POSITION
+ #define VS_SHADERMODEL vs_3_0
+ #define PS_SHADERMODEL ps_3_0
+#else
+ #define VS_SHADERMODEL vs_4_0_level_9_1
+ #define PS_SHADERMODEL ps_4_0_level_9_1
+#endif
+
+Texture2D SpriteTexture;
+
+// A value between 0 and 1 that controls the intensity of the grayscale effect.
+// 0 = full color, 1 = full grayscale.
+float Saturation = 1.0;
+
+sampler2D SpriteTextureSampler = sampler_state
+{
+ Texture = ;
+};
+
+struct VertexShaderOutput
+{
+ float4 Position : SV_POSITION;
+ float4 Color : COLOR0;
+ float2 TextureCoordinates : TEXCOORD0;
+};
+
+float4 MainPS(VertexShaderOutput input) : COLOR
+{
+ // Sample the texture
+ float4 color = tex2D(SpriteTextureSampler, input.TextureCoordinates) * input.Color;
+
+ // Calculate the grayscale value based on human perception of colors
+ float grayscale = dot(color.rgb, float3(0.3, 0.59, 0.11));
+
+ // create a grayscale color vector (same value for R, G, and B)
+ float3 grayscaleColor = float3(grayscale, grayscale, grayscale);
+
+ // Linear interpolation between he grayscale color and the original color's
+ // rgb values based on the saturation parameter.
+ float3 finalColor = lerp(grayscale, color.rgb, Saturation);
+
+ // Return the final color with the original alpha value
+ return float4(finalColor, color.a);
+}
+
+technique SpriteDrawing
+{
+ pass P0
+ {
+ PixelShader = compile PS_SHADERMODEL MainPS();
+ }
+};
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Content/fonts/04B_30.spritefont b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Content/fonts/04B_30.spritefont
new file mode 100644
index 00000000..63d4728c
--- /dev/null
+++ b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Content/fonts/04B_30.spritefont
@@ -0,0 +1,16 @@
+
+
+
+ 04B_30.ttf
+ 17.5
+ 0
+ true
+
+
+
+
+ ~
+
+
+
+
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Content/fonts/04B_30.ttf b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Content/fonts/04B_30.ttf
new file mode 100644
index 00000000..4b93740c
Binary files /dev/null and b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Content/fonts/04B_30.ttf differ
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Content/fonts/04B_30_5x.spritefont b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Content/fonts/04B_30_5x.spritefont
new file mode 100644
index 00000000..dd239a53
--- /dev/null
+++ b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Content/fonts/04B_30_5x.spritefont
@@ -0,0 +1,16 @@
+
+
+
+ 04B_30.ttf
+ 87.5
+ 0
+ true
+
+
+
+
+ ~
+
+
+
+
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Content/fonts/04b_30.fnt b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Content/fonts/04b_30.fnt
new file mode 100644
index 00000000..772f8c54
--- /dev/null
+++ b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Content/fonts/04b_30.fnt
@@ -0,0 +1,99 @@
+info face="04b30" size=35 bold=0 italic=0 charset="" unicode=1 stretchH=100 smooth=0 aa=1 padding=0,0,0,0 spacing=1,1 outline=0
+common lineHeight=35 base=31 scaleW=256 scaleH=512 pages=1 packed=0 alphaChnl=0 redChnl=4 greenChnl=4 blueChnl=4
+page id=0 file="../images/atlas.png"
+chars count=95
+char id=32 x=30 y=152 width=3 height=1 xoffset=-1 yoffset=34 xadvance=29 page=0 chnl=15
+char id=33 x=240 y=30 width=14 height=29 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=34 x=102 y=232 width=25 height=15 xoffset=1 yoffset=4 xadvance=29 page=0 chnl=15
+char id=35 x=184 y=0 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=36 x=250 y=150 width=3 height=1 xoffset=-1 yoffset=34 xadvance=29 page=0 chnl=15
+char id=37 x=0 y=34 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=38 x=30 y=150 width=3 height=1 xoffset=-1 yoffset=34 xadvance=29 page=0 chnl=15
+char id=39 x=245 y=202 width=10 height=15 xoffset=1 yoffset=4 xadvance=14 page=0 chnl=15
+char id=40 x=106 y=150 width=16 height=29 xoffset=1 yoffset=2 xadvance=21 page=0 chnl=15
+char id=41 x=123 y=150 width=16 height=29 xoffset=1 yoffset=2 xadvance=21 page=0 chnl=15
+char id=42 x=128 y=232 width=14 height=15 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=43 x=94 y=0 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=44 x=143 y=232 width=10 height=14 xoffset=1 yoffset=19 xadvance=14 page=0 chnl=15
+char id=45 x=154 y=232 width=25 height=11 xoffset=1 yoffset=12 xadvance=29 page=0 chnl=15
+char id=46 x=231 y=228 width=10 height=10 xoffset=1 yoffset=19 xadvance=14 page=0 chnl=15
+char id=47 x=60 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=48 x=90 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=49 x=46 y=150 width=19 height=29 xoffset=1 yoffset=2 xadvance=23 page=0 chnl=15
+char id=50 x=150 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=51 x=180 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=52 x=210 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=53 x=0 y=94 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=54 x=180 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=55 x=60 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=56 x=90 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=57 x=120 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=58 x=234 y=202 width=10 height=25 xoffset=1 yoffset=4 xadvance=14 page=0 chnl=15
+char id=59 x=244 y=0 width=10 height=29 xoffset=1 yoffset=4 xadvance=14 page=0 chnl=15
+char id=60 x=86 y=150 width=19 height=29 xoffset=1 yoffset=2 xadvance=23 page=0 chnl=15
+char id=61 x=182 y=176 width=25 height=25 xoffset=1 yoffset=4 xadvance=29 page=0 chnl=15
+char id=62 x=237 y=120 width=18 height=29 xoffset=1 yoffset=2 xadvance=23 page=0 chnl=15
+char id=63 x=180 y=120 width=28 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=64 x=34 y=150 width=3 height=1 xoffset=-1 yoffset=34 xadvance=29 page=0 chnl=15
+char id=65 x=120 y=120 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=66 x=150 y=120 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=67 x=124 y=0 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=68 x=154 y=0 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=69 x=214 y=0 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=70 x=30 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=71 x=60 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=72 x=90 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=73 x=240 y=90 width=14 height=29 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=74 x=120 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=75 x=150 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=76 x=209 y=120 width=27 height=29 xoffset=1 yoffset=2 xadvance=31 page=0 chnl=15
+char id=77 x=30 y=0 width=31 height=29 xoffset=1 yoffset=2 xadvance=35 page=0 chnl=15
+char id=78 x=210 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=79 x=0 y=64 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=80 x=30 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=81 x=0 y=0 width=29 height=33 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=82 x=120 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=83 x=30 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=84 x=150 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=85 x=180 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=86 x=210 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=87 x=62 y=0 width=31 height=29 xoffset=1 yoffset=2 xadvance=35 page=0 chnl=15
+char id=88 x=0 y=124 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=89 x=30 y=120 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=90 x=60 y=120 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=91 x=240 y=60 width=14 height=29 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=92 x=90 y=120 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=93 x=140 y=150 width=14 height=29 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=94 x=180 y=232 width=16 height=11 xoffset=1 yoffset=4 xadvance=21 page=0 chnl=15
+char id=95 x=0 y=262 width=29 height=10 xoffset=1 yoffset=21 xadvance=33 page=0 chnl=15
+char id=96 x=197 y=228 width=16 height=11 xoffset=1 yoffset=4 xadvance=21 page=0 chnl=15
+char id=97 x=208 y=176 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=98 x=0 y=210 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=99 x=26 y=210 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=100 x=52 y=206 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=101 x=78 y=206 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=102 x=104 y=206 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=103 x=130 y=206 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=104 x=156 y=206 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=105 x=234 y=176 width=12 height=25 xoffset=1 yoffset=6 xadvance=16 page=0 chnl=15
+char id=106 x=182 y=202 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=107 x=208 y=202 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=108 x=78 y=232 width=23 height=25 xoffset=1 yoffset=6 xadvance=27 page=0 chnl=15
+char id=109 x=197 y=150 width=26 height=25 xoffset=1 yoffset=6 xadvance=31 page=0 chnl=15
+char id=110 x=0 y=236 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=111 x=26 y=236 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=112 x=78 y=180 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=113 x=0 y=154 width=25 height=29 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=114 x=52 y=232 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=115 x=224 y=150 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=116 x=0 y=184 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=117 x=26 y=184 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=118 x=52 y=180 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=119 x=170 y=150 width=26 height=25 xoffset=1 yoffset=6 xadvance=31 page=0 chnl=15
+char id=120 x=104 y=180 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=121 x=130 y=180 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=122 x=156 y=180 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=123 x=26 y=154 width=19 height=29 xoffset=1 yoffset=2 xadvance=23 page=0 chnl=15
+char id=124 x=155 y=150 width=14 height=29 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=125 x=66 y=150 width=19 height=29 xoffset=1 yoffset=2 xadvance=23 page=0 chnl=15
+char id=126 x=214 y=228 width=16 height=11 xoffset=1 yoffset=4 xadvance=21 page=0 chnl=15
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Content/images/atlas-definition.xml b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Content/images/atlas-definition.xml
new file mode 100644
index 00000000..21772022
--- /dev/null
+++ b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Content/images/atlas-definition.xml
@@ -0,0 +1,34 @@
+
+
+ images/atlas
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Content/images/atlas.png b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Content/images/atlas.png
new file mode 100644
index 00000000..f7def20f
Binary files /dev/null and b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Content/images/atlas.png differ
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Content/images/background-pattern.png b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Content/images/background-pattern.png
new file mode 100644
index 00000000..2d8d878e
Binary files /dev/null and b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Content/images/background-pattern.png differ
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Content/images/logo.png b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Content/images/logo.png
new file mode 100644
index 00000000..1509036c
Binary files /dev/null and b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Content/images/logo.png differ
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Content/images/tilemap-definition.xml b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Content/images/tilemap-definition.xml
new file mode 100644
index 00000000..85658c60
--- /dev/null
+++ b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Content/images/tilemap-definition.xml
@@ -0,0 +1,15 @@
+
+
+ images/atlas
+
+ 00 01 02 01 02 01 02 01 02 01 02 01 02 01 02 03
+ 04 05 05 06 05 05 06 05 05 06 05 05 06 05 05 07
+ 08 09 09 09 09 09 09 09 09 09 09 09 09 09 09 11
+ 04 09 09 09 09 09 09 09 10 09 09 09 09 10 09 07
+ 08 09 10 09 09 09 09 09 09 09 09 09 09 09 09 11
+ 04 09 09 09 09 09 09 09 09 09 09 09 09 09 09 07
+ 08 10 09 09 09 09 09 09 09 09 10 09 09 09 09 11
+ 04 09 09 09 09 09 10 09 09 09 09 09 09 09 09 07
+ 12 13 14 13 14 13 14 13 14 13 14 13 14 13 14 15
+
+
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/DungeonSlime.csproj b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/DungeonSlime.csproj
new file mode 100644
index 00000000..3dbcf3ce
--- /dev/null
+++ b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/DungeonSlime.csproj
@@ -0,0 +1,33 @@
+
+
+ WinExe
+ net9.0
+ Major
+ false
+ false
+
+
+ app.manifest
+ Icon.ico
+
+
+
+
+
+
+
+ Icon.ico
+
+
+ Icon.bmp
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Game1.cs b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Game1.cs
new file mode 100644
index 00000000..86bfd3e1
--- /dev/null
+++ b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Game1.cs
@@ -0,0 +1,70 @@
+using DungeonSlime.Scenes;
+using Microsoft.Xna.Framework.Media;
+using MonoGameLibrary;
+using MonoGameGum;
+using MonoGameGum.Forms.Controls;
+
+namespace DungeonSlime;
+
+public class Game1 : Core
+{
+ // The background theme song
+ private Song _themeSong;
+
+ public Game1() : base("Dungeon Slime", 1280, 720, false)
+ {
+ }
+
+ protected override void Initialize()
+ {
+ base.Initialize();
+
+ // Start playing the background music
+ Audio.PlaySong(_themeSong);
+
+ // Initialize the Gum UI service
+ InitializeGum();
+
+ // Start the game with the title scene.
+ ChangeScene(new TitleScene());
+ }
+
+ private void InitializeGum()
+ {
+ // Initialize the Gum service
+ GumService.Default.Initialize(this);
+
+ // Tell the Gum service which content manager to use. We will tell it to
+ // use the global content manager from our Core.
+ GumService.Default.ContentLoader.XnaContentManager = Core.Content;
+
+ // Register keyboard input for UI control.
+ FrameworkElement.KeyboardsForUiControl.Add(GumService.Default.Keyboard);
+
+ // Register gamepad input for Ui control.
+ FrameworkElement.GamePadsForUiControl.AddRange(GumService.Default.Gamepads);
+
+ // Customize the tab reverse UI navigation to also trigger when the keyboard
+ // Up arrow key is pushed.
+ FrameworkElement.TabReverseKeyCombos.Add(
+ new KeyCombo() { PushedKey = Microsoft.Xna.Framework.Input.Keys.Up });
+
+ // Customize the tab UI navigation to also trigger when the keyboard
+ // Down arrow key is pushed.
+ FrameworkElement.TabKeyCombos.Add(
+ new KeyCombo() { PushedKey = Microsoft.Xna.Framework.Input.Keys.Down });
+
+ // The assets created for the UI were done so at 1/4th the size to keep the size of the
+ // texture atlas small. So we will set the default canvas size to be 1/4th the size of
+ // the game's resolution then tell gum to zoom in by a factor of 4.
+ GumService.Default.CanvasWidth = GraphicsDevice.PresentationParameters.BackBufferWidth / 4.0f;
+ GumService.Default.CanvasHeight = GraphicsDevice.PresentationParameters.BackBufferHeight / 4.0f;
+ GumService.Default.Renderer.Camera.Zoom = 4.0f;
+ }
+
+ protected override void LoadContent()
+ {
+ // Load the background theme music
+ _themeSong = Content.Load("audio/theme");
+ }
+}
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/GameController.cs b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/GameController.cs
new file mode 100644
index 00000000..92165382
--- /dev/null
+++ b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/GameController.cs
@@ -0,0 +1,79 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Input;
+using MonoGameLibrary;
+using MonoGameLibrary.Input;
+
+namespace DungeonSlime;
+
+///
+/// Provides a game-specific input abstraction that maps physical inputs
+/// to game actions, bridging our input system with game-specific functionality.
+///
+public static class GameController
+{
+ private static KeyboardInfo s_keyboard => Core.Input.Keyboard;
+ private static GamePadInfo s_gamePad => Core.Input.GamePads[(int)PlayerIndex.One];
+
+ ///
+ /// Returns true if the player has triggered the "move up" action.
+ ///
+ public static bool MoveUp()
+ {
+ return s_keyboard.WasKeyJustPressed(Keys.Up) ||
+ s_keyboard.WasKeyJustPressed(Keys.W) ||
+ s_gamePad.WasButtonJustPressed(Buttons.DPadUp) ||
+ s_gamePad.WasButtonJustPressed(Buttons.LeftThumbstickUp);
+ }
+
+ ///
+ /// Returns true if the player has triggered the "move down" action.
+ ///
+ public static bool MoveDown()
+ {
+ return s_keyboard.WasKeyJustPressed(Keys.Down) ||
+ s_keyboard.WasKeyJustPressed(Keys.S) ||
+ s_gamePad.WasButtonJustPressed(Buttons.DPadDown) ||
+ s_gamePad.WasButtonJustPressed(Buttons.LeftThumbstickDown);
+ }
+
+ ///
+ /// Returns true if the player has triggered the "move left" action.
+ ///
+ public static bool MoveLeft()
+ {
+ return s_keyboard.WasKeyJustPressed(Keys.Left) ||
+ s_keyboard.WasKeyJustPressed(Keys.A) ||
+ s_gamePad.WasButtonJustPressed(Buttons.DPadLeft) ||
+ s_gamePad.WasButtonJustPressed(Buttons.LeftThumbstickLeft);
+ }
+
+ ///
+ /// Returns true if the player has triggered the "move right" action.
+ ///
+ public static bool MoveRight()
+ {
+ return s_keyboard.WasKeyJustPressed(Keys.Right) ||
+ s_keyboard.WasKeyJustPressed(Keys.D) ||
+ s_gamePad.WasButtonJustPressed(Buttons.DPadRight) ||
+ s_gamePad.WasButtonJustPressed(Buttons.LeftThumbstickRight);
+ }
+
+ ///
+ /// Returns true if the player has triggered the "pause" action.
+ ///
+ public static bool Pause()
+ {
+ return s_keyboard.WasKeyJustPressed(Keys.Escape) ||
+ s_gamePad.WasButtonJustPressed(Buttons.Start);
+ }
+
+ ///
+ /// Returns true if the player has triggered the "action" button,
+ /// typically used for menu confirmation.
+ ///
+ public static bool Action()
+ {
+ return s_keyboard.WasKeyJustPressed(Keys.Enter) ||
+ s_gamePad.WasButtonJustPressed(Buttons.A);
+ }
+}
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/GameObjects/Bat.cs b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/GameObjects/Bat.cs
new file mode 100644
index 00000000..ddc855ed
--- /dev/null
+++ b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/GameObjects/Bat.cs
@@ -0,0 +1,123 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Audio;
+using MonoGameLibrary;
+using MonoGameLibrary.Graphics;
+
+namespace DungeonSlime.GameObjects;
+
+public class Bat
+{
+ private const float MOVEMENT_SPEED = 5.0f;
+
+ // The velocity of the bat that defines the direction and how much in that
+ // direction to update the bats position each update cycle.
+ private Vector2 _velocity;
+
+ // The AnimatedSprite used when drawing the bat.
+ private AnimatedSprite _sprite;
+
+ // The sound effect to play when the bat bounces off the edge of the room.
+ private SoundEffect _bounceSoundEffect;
+
+ ///
+ /// Gets or Sets the position of the bat.
+ ///
+ public Vector2 Position { get; set; }
+
+ ///
+ /// Creates a new Bat using the specified animated sprite and sound effect.
+ ///
+ /// The AnimatedSprite ot use when drawing the bat.
+ /// The sound effect to play when the bat bounces off a wall.
+ public Bat(AnimatedSprite sprite, SoundEffect bounceSoundEffect)
+ {
+ _sprite = sprite;
+ _bounceSoundEffect = bounceSoundEffect;
+ }
+
+ ///
+ /// Randomizes the velocity of the bat.
+ ///
+ public void RandomizeVelocity()
+ {
+ // Generate a random angle
+ float angle = (float)(Random.Shared.NextDouble() * MathHelper.TwoPi);
+
+ // Convert the angle to a direction vector
+ float x = (float)Math.Cos(angle);
+ float y = (float)Math.Sin(angle);
+ Vector2 direction = new Vector2(x, y);
+
+ // Multiply the direction vector by the movement speed to get the
+ // final velocity
+ _velocity = direction * MOVEMENT_SPEED;
+ }
+
+ ///
+ /// Handles a bounce event when the bat collides with a wall or boundary.
+ ///
+ /// The normal vector of the surface the bat is bouncing against.
+ public void Bounce(Vector2 normal)
+ {
+ Vector2 newPosition = Position;
+
+ // Adjust the position based on the normal to prevent sticking to walls.
+ if (normal.X != 0)
+ {
+ // We are bouncing off a vertical wall (left/right).
+ // Move slightly away from the wall in the direction of the normal.
+ newPosition.X += normal.X * (_sprite.Width * 0.1f);
+ }
+
+ if (normal.Y != 0)
+ {
+ // We are bouncing off a horizontal wall (top/bottom).
+ // Move slightly way from the wall in the direction of the normal.
+ newPosition.Y += normal.Y * (_sprite.Height * 0.1f);
+ }
+
+ // Apply the new position
+ Position = newPosition;
+
+ // Apply reflection based on the normal.
+ _velocity = Vector2.Reflect(_velocity, normal);
+
+ // Play the bounce sound effect.
+ Core.Audio.PlaySoundEffect(_bounceSoundEffect);
+ }
+
+ ///
+ /// Returns a Circle value that represents collision bounds of the bat.
+ ///
+ /// A Circle value.
+ public Circle GetBounds()
+ {
+ int x = (int)(Position.X + _sprite.Width * 0.5f);
+ int y = (int)(Position.Y + _sprite.Height * 0.5f);
+ int radius = (int)(_sprite.Width * 0.25f);
+
+ return new Circle(x, y, radius);
+ }
+
+ ///
+ /// Updates the bat.
+ ///
+ /// A snapshot of the timing values for the current update cycle.
+ public void Update(GameTime gameTime)
+ {
+ // Update the animated sprite
+ _sprite.Update(gameTime);
+
+ // Update the position of the bat based on the velocity.
+ Position += _velocity;
+ }
+
+ ///
+ /// Draws the bat.
+ ///
+ public void Draw()
+ {
+ _sprite.Draw(Core.SpriteBatch, Position);
+ }
+}
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/GameObjects/Slime.cs b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/GameObjects/Slime.cs
new file mode 100644
index 00000000..08b5a63d
--- /dev/null
+++ b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/GameObjects/Slime.cs
@@ -0,0 +1,265 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Xna.Framework;
+using MonoGameLibrary;
+using MonoGameLibrary.Graphics;
+
+namespace DungeonSlime.GameObjects;
+
+public class Slime
+{
+ // A constant value that represents the amount of time to wait between
+ // movement updates.
+ private static readonly TimeSpan s_movementTime = TimeSpan.FromMilliseconds(200);
+
+ // The amount of time that has elapsed since the last movement update.
+ private TimeSpan _movementTimer;
+
+ // Normalized value (0-1) representing progress between movement ticks for visual interpolation
+ private float _movementProgress;
+
+ // The next direction to apply to the head of the slime chain during the
+ // next movement update.
+ private Vector2 _nextDirection;
+
+ // The number of pixels to move the head segment during the movement cycle.
+ private float _stride;
+
+ // Tracks the segments of the slime chain.
+ private List _segments;
+
+ // The AnimatedSprite used when drawing each slime segment
+ private AnimatedSprite _sprite;
+
+ // Buffer to queue inputs input by player during input polling.
+ private Queue _inputBuffer;
+
+ // The maximum size of the buffer queue.
+ private const int MAX_BUFFER_SIZE = 2;
+
+ ///
+ /// Event that is raised if it is detected that the head segment of the slime
+ /// has collided with a body segment.
+ ///
+ public event EventHandler BodyCollision;
+
+ ///
+ /// Creates a new Slime using the specified animated sprite.
+ ///
+ /// The AnimatedSprite to use when drawing the slime.
+ public Slime(AnimatedSprite sprite)
+ {
+ _sprite = sprite;
+ }
+
+ ///
+ /// Initializes the slime, can be used to reset it back to an initial state.
+ ///
+ /// The position the slime should start at.
+ /// The total number of pixels to move the head segment during each movement cycle.
+ public void Initialize(Vector2 startingPosition, float stride)
+ {
+ // Initialize the segment collection.
+ _segments = new List();
+
+ // Set the stride
+ _stride = stride;
+
+ // Create the initial head of the slime chain.
+ SlimeSegment head = new SlimeSegment();
+ head.At = startingPosition;
+ head.To = startingPosition + new Vector2(_stride, 0);
+ head.Direction = Vector2.UnitX;
+
+ // Add it to the segment collection.
+ _segments.Add(head);
+
+ // Set the initial next direction as the same direction the head is
+ // moving.
+ _nextDirection = head.Direction;
+
+ // Zero out the movement timer.
+ _movementTimer = TimeSpan.Zero;
+
+ // initialize the input buffer.
+ _inputBuffer = new Queue(MAX_BUFFER_SIZE);
+ }
+
+ private void HandleInput()
+ {
+ Vector2 potentialNextDirection = Vector2.Zero;
+
+ if (GameController.MoveUp())
+ {
+ potentialNextDirection = -Vector2.UnitY;
+ }
+ else if (GameController.MoveDown())
+ {
+ potentialNextDirection = Vector2.UnitY;
+ }
+ else if (GameController.MoveLeft())
+ {
+ potentialNextDirection = -Vector2.UnitX;
+ }
+ else if (GameController.MoveRight())
+ {
+ potentialNextDirection = Vector2.UnitX;
+ }
+
+ // If a new direction was input, consider adding it to the buffer
+ if (potentialNextDirection != Vector2.Zero && _inputBuffer.Count < MAX_BUFFER_SIZE)
+ {
+ // If the buffer is empty, validate against the current direction;
+ // otherwise, validate against the last buffered direction
+ Vector2 validateAgainst = _inputBuffer.Count > 0 ?
+ _inputBuffer.Last() :
+ _segments[0].Direction;
+
+ // Only allow direction change if it is not reversing the current
+ // direction. This prevents th slime from backing into itself
+ float dot = Vector2.Dot(potentialNextDirection, validateAgainst);
+ if (dot >= 0)
+ {
+ _inputBuffer.Enqueue(potentialNextDirection);
+ }
+ }
+ }
+
+ private void Move()
+ {
+ // Get the next direction from the input buffer if one is available
+ if (_inputBuffer.Count > 0)
+ {
+ _nextDirection = _inputBuffer.Dequeue();
+ }
+
+ // Capture the value of the head segment
+ SlimeSegment head = _segments[0];
+
+ // Update the direction the head is supposed to move in to the
+ // next direction cached.
+ head.Direction = _nextDirection;
+
+ // Update the head's "at" position to be where it was moving "to"
+ head.At = head.To;
+
+ // Update the head's "to" position to the next tile in the direction
+ // it is moving.
+ head.To = head.At + head.Direction * _stride;
+
+ // Insert the new adjusted value for the head at the front of the
+ // segments and remove the tail segment. This effectively moves
+ // the entire chain forward without needing to loop through every
+ // segment and update its "at" and "to" positions.
+ _segments.Insert(0, head);
+ _segments.RemoveAt(_segments.Count - 1);
+
+ // Iterate through all of the segments except the head and check
+ // if they are at the same position as the head. If they are, then
+ // the head is colliding with a body segment and a body collision
+ // has occurred.
+ for (int i = 1; i < _segments.Count; i++)
+ {
+ SlimeSegment segment = _segments[i];
+
+ if (head.At == segment.At)
+ {
+ if (BodyCollision != null)
+ {
+ BodyCollision.Invoke(this, EventArgs.Empty);
+ }
+
+ return;
+ }
+ }
+ }
+
+ ///
+ /// Informs the slime to grow by one segment.
+ ///
+ public void Grow()
+ {
+ // Capture the value of the tail segment
+ SlimeSegment tail = _segments[_segments.Count - 1];
+
+ // Create a new tail segment that is positioned a grid cell in the
+ // reverse direction from the tail moving to the tail.
+ SlimeSegment newTail = new SlimeSegment();
+ newTail.At = tail.To + tail.ReverseDirection * _stride;
+ newTail.To = tail.At;
+ newTail.Direction = Vector2.Normalize(tail.At - newTail.At);
+
+ // Add the new tail segment
+ _segments.Add(newTail);
+ }
+
+ ///
+ /// Updates the slime.
+ ///
+ /// A snapshot of the timing values for the current update cycle.
+ public void Update(GameTime gameTime)
+ {
+ // Update the animated sprite.
+ _sprite.Update(gameTime);
+
+ // Handle any player input
+ HandleInput();
+
+ // Increment the movement timer by the frame elapsed time.
+ _movementTimer += gameTime.ElapsedGameTime;
+
+ // If the movement timer has accumulated enough time to be greater than
+ // the movement time threshold, then perform a full movement.
+ if (_movementTimer >= s_movementTime)
+ {
+ _movementTimer -= s_movementTime;
+ Move();
+ }
+
+ // Update the movement lerp offset amount
+ _movementProgress = (float)(_movementTimer.TotalSeconds / s_movementTime.TotalSeconds);
+ }
+
+ ///
+ /// Draws the slime.
+ ///
+ public void Draw()
+ {
+ // Iterate through each segment and draw it
+ foreach (SlimeSegment segment in _segments)
+ {
+ // Calculate the visual position of the segment at the moment by
+ // lerping between its "at" and "to" position by the movement
+ // offset lerp amount
+ Vector2 pos = Vector2.Lerp(segment.At, segment.To, _movementProgress);
+
+ // Draw the slime sprite at the calculated visual position of this
+ // segment
+ _sprite.Draw(Core.SpriteBatch, pos);
+ }
+ }
+
+ ///
+ /// Returns a Circle value that represents collision bounds of the slime.
+ ///
+ /// A Circle value.
+ public Circle GetBounds()
+ {
+ SlimeSegment head = _segments[0];
+
+ // Calculate the visual position of the head at the moment of this
+ // method call by lerping between the "at" and "to" position by the
+ // movement offset lerp amount
+ Vector2 pos = Vector2.Lerp(head.At, head.To, _movementProgress);
+
+ // Create the bounds using the calculated visual position of the head.
+ Circle bounds = new Circle(
+ (int)(pos.X + (_sprite.Width * 0.5f)),
+ (int)(pos.Y + (_sprite.Height * 0.5f)),
+ (int)(_sprite.Width * 0.5f)
+ );
+
+ return bounds;
+ }
+}
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/GameObjects/SlimeSegment.cs b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/GameObjects/SlimeSegment.cs
new file mode 100644
index 00000000..b00189eb
--- /dev/null
+++ b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/GameObjects/SlimeSegment.cs
@@ -0,0 +1,26 @@
+using Microsoft.Xna.Framework;
+
+namespace DungeonSlime.GameObjects;
+
+public struct SlimeSegment
+{
+ ///
+ /// The position this slime segment is at before the movement cycle occurs.
+ ///
+ public Vector2 At;
+
+ ///
+ /// The position this slime segment should move to during the next movement cycle.
+ ///
+ public Vector2 To;
+
+ ///
+ /// The direction this slime segment is moving.
+ ///
+ public Vector2 Direction;
+
+ ///
+ /// The opposite direction this slime segment is moving.
+ ///
+ public Vector2 ReverseDirection => new Vector2(-Direction.X, -Direction.Y);
+}
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Icon.bmp b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Icon.bmp
new file mode 100644
index 00000000..2b481653
Binary files /dev/null and b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Icon.bmp differ
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Icon.ico b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Icon.ico
new file mode 100644
index 00000000..7d9dec18
Binary files /dev/null and b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Icon.ico differ
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Program.cs b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Program.cs
new file mode 100644
index 00000000..d491c406
--- /dev/null
+++ b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Program.cs
@@ -0,0 +1,2 @@
+using var game = new DungeonSlime.Game1();
+game.Run();
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Scenes/GameScene.cs b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Scenes/GameScene.cs
new file mode 100644
index 00000000..d01a3b03
--- /dev/null
+++ b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Scenes/GameScene.cs
@@ -0,0 +1,428 @@
+using System;
+using DungeonSlime.GameObjects;
+using DungeonSlime.UI;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Audio;
+using Microsoft.Xna.Framework.Graphics;
+using MonoGameGum;
+using MonoGameLibrary;
+using MonoGameLibrary.Graphics;
+using MonoGameLibrary.Scenes;
+
+namespace DungeonSlime.Scenes;
+
+public class GameScene : Scene
+{
+ private enum GameState
+ {
+ Playing,
+ Paused,
+ GameOver
+ }
+
+ // Reference to the slime.
+ private Slime _slime;
+
+ // Reference to the bat.
+ private Bat _bat;
+
+ // Defines the tilemap to draw.
+ private Tilemap _tilemap;
+
+ // Defines the bounds of the room that the slime and bat are contained within.
+ private Rectangle _roomBounds;
+
+ // The sound effect to play when the slime eats a bat.
+ private SoundEffect _collectSoundEffect;
+
+ // Tracks the players score.
+ private int _score;
+
+ private GameSceneUI _ui;
+
+ private GameState _state;
+
+ // The grayscale shader effect.
+ private Effect _grayscaleEffect;
+
+ // The amount of saturation to provide the grayscale shader effect
+ private float _saturation = 1.0f;
+
+ // The speed of the fade to grayscale effect.
+ private const float FADE_SPEED = 0.02f;
+
+ public override void Initialize()
+ {
+ // LoadContent is called during base.Initialize().
+ base.Initialize();
+
+ // During the game scene, we want to disable exit on escape. Instead,
+ // the escape key will be used to return back to the title screen
+ Core.ExitOnEscape = false;
+
+ // Create the room bounds by getting the bounds of the screen then
+ // using the Inflate method to "Deflate" the bounds by the width and
+ // height of a tile so that the bounds only covers the inside room of
+ // the dungeon tilemap.
+ _roomBounds = Core.GraphicsDevice.PresentationParameters.Bounds;
+ _roomBounds.Inflate(-_tilemap.TileWidth, -_tilemap.TileHeight);
+
+ // Subscribe to the slime's BodyCollision event so that a game over
+ // can be triggered when this event is raised.
+ _slime.BodyCollision += OnSlimeBodyCollision;
+
+ // Create any UI elements from the root element created in previous
+ // scenes
+ GumService.Default.Root.Children.Clear();
+
+ // Initialize the user interface for the game scene.
+ InitializeUI();
+
+ // Initialize a new game to be played.
+ InitializeNewGame();
+ }
+
+ private void InitializeUI()
+ {
+ // Clear out any previous UI element incase we came here
+ // from a different scene.
+ GumService.Default.Root.Children.Clear();
+
+ // Create the game scene ui instance.
+ _ui = new GameSceneUI();
+
+ // Subscribe to the events from the game scene ui.
+ _ui.ResumeButtonClick += OnResumeButtonClicked;
+ _ui.RetryButtonClick += OnRetryButtonClicked;
+ _ui.QuitButtonClick += OnQuitButtonClicked;
+ }
+
+ private void OnResumeButtonClicked(object sender, EventArgs args)
+ {
+ // Change the game state back to playing
+ _state = GameState.Playing;
+ }
+
+ private void OnRetryButtonClicked(object sender, EventArgs args)
+ {
+ // Player has chosen to retry, so initialize a new game
+ InitializeNewGame();
+ }
+
+ private void OnQuitButtonClicked(object sender, EventArgs args)
+ {
+ // Player has chosen to quit, so return back to the title scene
+ Core.ChangeScene(new TitleScene());
+ }
+
+ private void InitializeNewGame()
+ {
+ // Calculate the position for the slime, which will be at the center
+ // tile of the tile map.
+ Vector2 slimePos = new Vector2();
+ slimePos.X = (_tilemap.Columns / 2) * _tilemap.TileWidth;
+ slimePos.Y = (_tilemap.Rows / 2) * _tilemap.TileHeight;
+
+ // Initialize the slime
+ _slime.Initialize(slimePos, _tilemap.TileWidth);
+
+ // Initialize the bat
+ _bat.RandomizeVelocity();
+ PositionBatAwayFromSlime();
+
+ // Reset the score
+ _score = 0;
+
+ // Set the game state to playing
+ _state = GameState.Playing;
+ }
+
+ public override void LoadContent()
+ {
+ // Create the texture atlas from the XML configuration file
+ TextureAtlas atlas = TextureAtlas.FromFile(Core.Content, "images/atlas-definition.xml");
+
+ // Create the tilemap from the XML configuration file.
+ _tilemap = Tilemap.FromFile(Content, "images/tilemap-definition.xml");
+ _tilemap.Scale = new Vector2(4.0f, 4.0f);
+
+ // Create the animated sprite for the slime from the atlas.
+ AnimatedSprite slimeAnimation = atlas.CreateAnimatedSprite("slime-animation");
+ slimeAnimation.Scale = new Vector2(4.0f, 4.0f);
+
+ // Create the slime
+ _slime = new Slime(slimeAnimation);
+
+ // Create the animated sprite for the bat from the atlas.
+ AnimatedSprite batAnimation = atlas.CreateAnimatedSprite("bat-animation");
+ batAnimation.Scale = new Vector2(4.0f, 4.0f);
+
+ // Load the bounce sound effect for the bat
+ SoundEffect bounceSoundEffect = Content.Load("audio/bounce");
+
+ // Create the bat
+ _bat = new Bat(batAnimation, bounceSoundEffect);
+
+ // Load the collect sound effect
+ _collectSoundEffect = Content.Load("audio/collect");
+
+ // Load the grayscale effect
+ _grayscaleEffect = Content.Load("effects/grayscaleEffect");
+ }
+
+ public override void Update(GameTime gameTime)
+ {
+ // Ensure the UI is always updated
+ _ui.Update(gameTime);
+
+ if (_state != GameState.Playing)
+ {
+ // The game is in either a paused or game over state, so
+ // gradually decrease the saturation to create the fading grayscale.
+ _saturation = Math.Max(0.0f, _saturation - FADE_SPEED);
+
+ // If its just a game over state, return back
+ if (_state == GameState.GameOver)
+ {
+ return;
+ }
+ }
+
+ // If the pause button is pressed, toggle the pause state
+ if (GameController.Pause())
+ {
+ TogglePause();
+ }
+
+ // At this point, if the game is paused, just return back early
+ if (_state == GameState.Paused)
+ {
+ return;
+ }
+
+ // Update the slime;
+ _slime.Update(gameTime);
+
+ // Update the bat;
+ _bat.Update(gameTime);
+
+ // Perform collision checks
+ CollisionChecks();
+ }
+
+ private void CollisionChecks()
+ {
+ // Capture the current bounds of the slime and bat
+ Circle slimeBounds = _slime.GetBounds();
+ Circle batBounds = _bat.GetBounds();
+
+ // FIrst perform a collision check to see if the slime is colliding with
+ // the bat, which means the slime eats the bat.
+ if (slimeBounds.Intersects(batBounds))
+ {
+ // Move the bat to a new position away from the slime.
+ PositionBatAwayFromSlime();
+
+ // Randomize the velocity of the bat.
+ _bat.RandomizeVelocity();
+
+ // Tell the slime to grow.
+ _slime.Grow();
+
+ // Increment the score.
+ _score += 100;
+
+ // Update the score display on the UI.
+ _ui.UpdateScoreText(_score);
+
+ // Play the collect sound effect
+ Core.Audio.PlaySoundEffect(_collectSoundEffect);
+ }
+
+ // Next check if the slime is colliding with the wall by validating if
+ // it is within the bounds of the room. If it is outside the room
+ // bounds, then it collided with a wall which triggers a game over.
+ if (slimeBounds.Top < _roomBounds.Top ||
+ slimeBounds.Bottom > _roomBounds.Bottom ||
+ slimeBounds.Left < _roomBounds.Left ||
+ slimeBounds.Right > _roomBounds.Right)
+ {
+ GameOver();
+ return;
+ }
+
+ // Finally, check if the bat is colliding with a wall by validating if
+ // it is within the bounds of the room. If it is outside the room
+ // bounds, then it collided with a wall, and the bat should bounce
+ // off of that wall.
+ if (batBounds.Top < _roomBounds.Top)
+ {
+ _bat.Bounce(Vector2.UnitY);
+ }
+ else if (batBounds.Bottom > _roomBounds.Bottom)
+ {
+ _bat.Bounce(-Vector2.UnitY);
+ }
+
+ if (batBounds.Left < _roomBounds.Left)
+ {
+ _bat.Bounce(Vector2.UnitX);
+ }
+ else if (batBounds.Right > _roomBounds.Right)
+ {
+ _bat.Bounce(-Vector2.UnitX);
+ }
+ }
+
+ private void PositionBatAwayFromSlime()
+ {
+ // Calculate the position that is in the center of the bounds
+ // of the room.
+ float roomCenterX = _roomBounds.X + _roomBounds.Width * 0.5f;
+ float roomCenterY = _roomBounds.Y + _roomBounds.Height * 0.5f;
+ Vector2 roomCenter = new Vector2(roomCenterX, roomCenterY);
+
+ // Get the bounds of the slime and calculate the center position
+ Circle slimeBounds = _slime.GetBounds();
+ Vector2 slimeCenter = new Vector2(slimeBounds.X, slimeBounds.Y);
+
+ // Calculate the distance vector from the center of the room to the
+ // center of the slime.
+ Vector2 centerToSlime = slimeCenter - roomCenter;
+
+ // Get the bounds of the bat
+ Circle batBounds = _bat.GetBounds();
+
+ // Calculate the amount of padding we will add to the new position of
+ // the bat to ensure it is not sticking to walls
+ int padding = batBounds.Radius * 2;
+
+ // Calculate the new position of the bat by finding which component of
+ // the center to slime vector (X or Y) is larger and in which direction.
+ Vector2 newBatPosition = Vector2.Zero;
+ if (Math.Abs(centerToSlime.X) > Math.Abs(centerToSlime.Y))
+ {
+ // The slime is closer to either the left or right wall, so the Y
+ // position will be a random position between the top and bottom
+ // walls.
+ newBatPosition.Y = Random.Shared.Next(
+ _roomBounds.Top + padding,
+ _roomBounds.Bottom - padding
+ );
+
+ if (centerToSlime.X > 0)
+ {
+ // The slime is closer to the right side wall, so place the
+ // bat on the left side wall
+ newBatPosition.X = _roomBounds.Left + padding;
+ }
+ else
+ {
+ // The slime is closer ot the left side wall, so place the
+ // bat on the right side wall.
+ newBatPosition.X = _roomBounds.Right - padding * 2;
+ }
+ }
+ else
+ {
+ // The slime is closer to either the top or bottom wall, so the X
+ // position will be a random position between the left and right
+ // walls.
+ newBatPosition.X = Random.Shared.Next(
+ _roomBounds.Left + padding,
+ _roomBounds.Right - padding
+ );
+
+ if (centerToSlime.Y > 0)
+ {
+ // The slime is closer to the top wall, so place the bat on the
+ // bottom wall
+ newBatPosition.Y = _roomBounds.Top + padding;
+ }
+ else
+ {
+ // The slime is closer to the bottom wall, so place the bat on
+ // the top wall.
+ newBatPosition.Y = _roomBounds.Bottom - padding * 2;
+ }
+ }
+
+ // Assign the new bat position
+ _bat.Position = newBatPosition;
+ }
+
+ private void OnSlimeBodyCollision(object sender, EventArgs args)
+ {
+ GameOver();
+ }
+
+ private void TogglePause()
+ {
+ if (_state == GameState.Paused)
+ {
+ // We're now unpausing the game, so hide the pause panel
+ _ui.HidePausePanel();
+
+ // And set the state back to playing
+ _state = GameState.Playing;
+ }
+ else
+ {
+ // We're now pausing the game, so show the pause panel
+ _ui.ShowPausePanel();
+
+ // And set the state to paused
+ _state = GameState.Paused;
+
+ // Set the grayscale effect saturation to 1.0f;
+ _saturation = 1.0f;
+ }
+ }
+
+ private void GameOver()
+ {
+ // Show the game over panel
+ _ui.ShowGameOverPanel();
+
+ // Set the game state to game over
+ _state = GameState.GameOver;
+
+ // Set the grayscale effect saturation to 1.0f;
+ _saturation = 1.0f;
+ }
+
+ public override void Draw(GameTime gameTime)
+ {
+ // Clear the back buffer.
+ Core.GraphicsDevice.Clear(Color.CornflowerBlue);
+
+ if (_state != GameState.Playing)
+ {
+ // We are in a game over state, so apply the saturation parameter.
+ _grayscaleEffect.Parameters["Saturation"].SetValue(_saturation);
+
+ // And begin the sprite batch using the grayscale effect.
+ Core.SpriteBatch.Begin(samplerState: SamplerState.PointClamp, effect: _grayscaleEffect);
+ }
+ else
+ {
+ // Otherwise, just begin the sprite batch as normal.
+ Core.SpriteBatch.Begin(samplerState: SamplerState.PointClamp);
+ }
+
+ // Draw the tilemap
+ _tilemap.Draw(Core.SpriteBatch);
+
+ // Draw the slime.
+ _slime.Draw();
+
+ // Draw the bat.
+ _bat.Draw();
+
+ // Always end the sprite batch when finished.
+ Core.SpriteBatch.End();
+
+ // Draw the UI
+ _ui.Draw();
+ }
+}
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Scenes/TitleScene.cs b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Scenes/TitleScene.cs
new file mode 100644
index 00000000..248c48dd
--- /dev/null
+++ b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/Scenes/TitleScene.cs
@@ -0,0 +1,345 @@
+using System;
+using DungeonSlime.UI;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Audio;
+using Microsoft.Xna.Framework.Graphics;
+using Microsoft.Xna.Framework.Input;
+using MonoGameGum;
+using Gum.Forms.Controls;
+using MonoGameGum.GueDeriving;
+using MonoGameLibrary;
+using MonoGameLibrary.Graphics;
+using MonoGameLibrary.Scenes;
+
+namespace DungeonSlime.Scenes;
+
+public class TitleScene : Scene
+{
+ private const string DUNGEON_TEXT = "Dungeon";
+ private const string SLIME_TEXT = "Slime";
+ private const string PRESS_ENTER_TEXT = "Press Enter To Start";
+
+ // The font to use to render normal text.
+ private SpriteFont _font;
+
+ // The font used to render the title text.
+ private SpriteFont _font5x;
+
+ // The position to draw the dungeon text at.
+ private Vector2 _dungeonTextPos;
+
+ // The origin to set for the dungeon text.
+ private Vector2 _dungeonTextOrigin;
+
+ // The position to draw the slime text at.
+ private Vector2 _slimeTextPos;
+
+ // The origin to set for the slime text.
+ private Vector2 _slimeTextOrigin;
+
+ // The position to draw the press enter text at.
+ private Vector2 _pressEnterPos;
+
+ // The origin to set for the press enter text when drawing it.
+ private Vector2 _pressEnterOrigin;
+
+ // The texture used for the background pattern.
+ private Texture2D _backgroundPattern;
+
+ // The destination rectangle for the background pattern to fill.
+ private Rectangle _backgroundDestination;
+
+ // The offset to apply when drawing the background pattern so it appears to
+ // be scrolling.
+ private Vector2 _backgroundOffset;
+
+ // The speed that the background pattern scrolls.
+ private float _scrollSpeed = 50.0f;
+
+ private SoundEffect _uiSoundEffect;
+ private Panel _titleScreenButtonsPanel;
+ private Panel _optionsPanel;
+
+ // The options button used to open the options menu.
+ private AnimatedButton _optionsButton;
+
+ // The back button used to exit the options menu back to the title menu.
+ private AnimatedButton _optionsBackButton;
+
+ // Reference to the texture atlas that we can pass to UI elements when they
+ // are created.
+ private TextureAtlas _atlas;
+
+ public override void Initialize()
+ {
+ // LoadContent is called during base.Initialize().
+ base.Initialize();
+
+ // While on the title screen, we can enable exit on escape so the player
+ // can close the game by pressing the escape key.
+ Core.ExitOnEscape = true;
+
+ // Set the position and origin for the Dungeon text.
+ Vector2 size = _font5x.MeasureString(DUNGEON_TEXT);
+ _dungeonTextPos = new Vector2(640, 100);
+ _dungeonTextOrigin = size * 0.5f;
+
+ // Set the position and origin for the Slime text.
+ size = _font5x.MeasureString(SLIME_TEXT);
+ _slimeTextPos = new Vector2(757, 207);
+ _slimeTextOrigin = size * 0.5f;
+
+ // Set the position and origin for the press enter text.
+ size = _font.MeasureString(PRESS_ENTER_TEXT);
+ _pressEnterPos = new Vector2(640, 620);
+ _pressEnterOrigin = size * 0.5f;
+
+ // Initialize the offset of the background pattern at zero
+ _backgroundOffset = Vector2.Zero;
+
+ // Set the background pattern destination rectangle to fill the entire
+ // screen background
+ _backgroundDestination = Core.GraphicsDevice.PresentationParameters.Bounds;
+
+ InitializeUI();
+ }
+
+ public override void LoadContent()
+ {
+ // Load the font for the standard text.
+ _font = Core.Content.Load("fonts/04B_30");
+
+ // Load the font for the title text
+ _font5x = Content.Load("fonts/04B_30_5x");
+
+ // Load the background pattern texture.
+ _backgroundPattern = Content.Load("images/background-pattern");
+
+ // Load the sound effect to play when ui actions occur.
+ _uiSoundEffect = Core.Content.Load("audio/ui");
+
+ // Load the texture atlas from the xml configuration file.
+ _atlas = TextureAtlas.FromFile(Core.Content, "images/atlas-definition.xml");
+ }
+
+ private void CreateTitlePanel()
+ {
+ // Create a container to hold all of our buttons
+ _titleScreenButtonsPanel = new Panel();
+ _titleScreenButtonsPanel.Dock(Gum.Wireframe.Dock.Fill);
+ _titleScreenButtonsPanel.AddToRoot();
+
+ AnimatedButton startButton = new AnimatedButton(_atlas);
+ startButton.Anchor(Gum.Wireframe.Anchor.BottomLeft);
+ startButton.Visual.X = 50;
+ startButton.Visual.Y = -12;
+ startButton.Text = "Start";
+ startButton.Click += HandleStartClicked;
+ _titleScreenButtonsPanel.AddChild(startButton);
+
+ _optionsButton = new AnimatedButton(_atlas);
+ _optionsButton.Anchor(Gum.Wireframe.Anchor.BottomRight);
+ _optionsButton.Visual.X = -50;
+ _optionsButton.Visual.Y = -12;
+ _optionsButton.Text = "Options";
+ _optionsButton.Click += HandleOptionsClicked;
+ _titleScreenButtonsPanel.AddChild(_optionsButton);
+
+ startButton.IsFocused = true;
+ }
+
+ private void HandleStartClicked(object sender, EventArgs e)
+ {
+ // A UI interaction occurred, play the sound effect
+ Core.Audio.PlaySoundEffect(_uiSoundEffect);
+
+ // Change to the game scene to start the game.
+ Core.ChangeScene(new GameScene());
+ }
+
+ private void HandleOptionsClicked(object sender, EventArgs e)
+ {
+ // A UI interaction occurred, play the sound effect
+ Core.Audio.PlaySoundEffect(_uiSoundEffect);
+
+ // Set the title panel to be invisible.
+ _titleScreenButtonsPanel.IsVisible = false;
+
+ // Set the options panel to be visible.
+ _optionsPanel.IsVisible = true;
+
+ // Give the back button on the options panel focus.
+ _optionsBackButton.IsFocused = true;
+ }
+
+ private void CreateOptionsPanel()
+ {
+ _optionsPanel = new Panel();
+ _optionsPanel.Dock(Gum.Wireframe.Dock.Fill);
+ _optionsPanel.IsVisible = false;
+ _optionsPanel.AddToRoot();
+
+ TextRuntime optionsText = new TextRuntime();
+ optionsText.X = 10;
+ optionsText.Y = 10;
+ optionsText.Text = "OPTIONS";
+ optionsText.UseCustomFont = true;
+ optionsText.FontScale = 0.5f;
+ optionsText.CustomFontFile = @"fonts/04b_30.fnt";
+ _optionsPanel.AddChild(optionsText);
+
+ OptionsSlider musicSlider = new OptionsSlider(_atlas);
+ musicSlider.Name = "MusicSlider";
+ musicSlider.Text = "MUSIC";
+ musicSlider.Anchor(Gum.Wireframe.Anchor.Top);
+ musicSlider.Visual.Y = 30f;
+ musicSlider.Minimum = 0;
+ musicSlider.Maximum = 1;
+ musicSlider.Value = Core.Audio.SongVolume;
+ musicSlider.SmallChange = .1;
+ musicSlider.LargeChange = .2;
+ musicSlider.ValueChanged += HandleMusicSliderValueChanged;
+ musicSlider.ValueChangeCompleted += HandleMusicSliderValueChangeCompleted;
+ _optionsPanel.AddChild(musicSlider);
+
+ OptionsSlider sfxSlider = new OptionsSlider(_atlas);
+ sfxSlider.Name = "SfxSlider";
+ sfxSlider.Text = "SFX";
+ sfxSlider.Anchor(Gum.Wireframe.Anchor.Top);
+ sfxSlider.Visual.Y = 93;
+ sfxSlider.Minimum = 0;
+ sfxSlider.Maximum = 1;
+ sfxSlider.Value = Core.Audio.SoundEffectVolume;
+ sfxSlider.SmallChange = .1;
+ sfxSlider.LargeChange = .2;
+ sfxSlider.ValueChanged += HandleSfxSliderChanged;
+ sfxSlider.ValueChangeCompleted += HandleSfxSliderChangeCompleted;
+ _optionsPanel.AddChild(sfxSlider);
+
+ _optionsBackButton = new AnimatedButton(_atlas);
+ _optionsBackButton.Text = "BACK";
+ _optionsBackButton.Anchor(Gum.Wireframe.Anchor.BottomRight);
+ _optionsBackButton.X = -28f;
+ _optionsBackButton.Y = -10f;
+ _optionsBackButton.Click += HandleOptionsButtonBack;
+ _optionsPanel.AddChild(_optionsBackButton);
+ }
+
+ private void HandleSfxSliderChanged(object sender, EventArgs args)
+ {
+ // Intentionally not playing the UI sound effect here so that it is not
+ // constantly triggered as the user adjusts the slider's thumb on the
+ // track.
+
+ // Get a reference to the sender as a Slider.
+ var slider = (Slider)sender;
+
+ // Set the global sound effect volume to the value of the slider.;
+ Core.Audio.SoundEffectVolume = (float)slider.Value;
+ }
+
+ private void HandleSfxSliderChangeCompleted(object sender, EventArgs e)
+ {
+ // Play the UI Sound effect so the player can hear the difference in audio.
+ Core.Audio.PlaySoundEffect(_uiSoundEffect);
+ }
+
+ private void HandleMusicSliderValueChanged(object sender, EventArgs args)
+ {
+ // Intentionally not playing the UI sound effect here so that it is not
+ // constantly triggered as the user adjusts the slider's thumb on the
+ // track.
+
+ // Get a reference to the sender as a Slider.
+ var slider = (Slider)sender;
+
+ // Set the global song volume to the value of the slider.
+ Core.Audio.SongVolume = (float)slider.Value;
+ }
+
+ private void HandleMusicSliderValueChangeCompleted(object sender, EventArgs args)
+ {
+ // A UI interaction occurred, play the sound effect
+ Core.Audio.PlaySoundEffect(_uiSoundEffect);
+ }
+
+ private void HandleOptionsButtonBack(object sender, EventArgs e)
+ {
+ // A UI interaction occurred, play the sound effect
+ Core.Audio.PlaySoundEffect(_uiSoundEffect);
+
+ // Set the title panel to be visible.
+ _titleScreenButtonsPanel.IsVisible = true;
+
+ // Set the options panel to be invisible.
+ _optionsPanel.IsVisible = false;
+
+ // Give the options button on the title panel focus since we are coming
+ // back from the options screen.
+ _optionsButton.IsFocused = true;
+ }
+
+ private void InitializeUI()
+ {
+ // Clear out any previous UI in case we came here from
+ // a different screen:
+ GumService.Default.Root.Children.Clear();
+
+ CreateTitlePanel();
+ CreateOptionsPanel();
+ }
+
+ public override void Update(GameTime gameTime)
+ {
+ // Update the offsets for the background pattern wrapping so that it
+ // scrolls down and to the right.
+ float offset = _scrollSpeed * (float)gameTime.ElapsedGameTime.TotalSeconds;
+ _backgroundOffset.X -= offset;
+ _backgroundOffset.Y -= offset;
+
+ // Ensure that the offsets do not go beyond the texture bounds so it is
+ // a seamless wrap
+ _backgroundOffset.X %= _backgroundPattern.Width;
+ _backgroundOffset.Y %= _backgroundPattern.Height;
+
+ GumService.Default.Update(gameTime);
+ }
+
+ public override void Draw(GameTime gameTime)
+ {
+ Core.GraphicsDevice.Clear(new Color(32, 40, 78, 255));
+
+ // Draw the background pattern first using the PointWrap sampler state.
+ Core.SpriteBatch.Begin(samplerState: SamplerState.PointWrap);
+ Core.SpriteBatch.Draw(_backgroundPattern, _backgroundDestination, new Rectangle(_backgroundOffset.ToPoint(), _backgroundDestination.Size), Color.White * 0.5f);
+ Core.SpriteBatch.End();
+
+ if (_titleScreenButtonsPanel.IsVisible)
+ {
+ // Begin the sprite batch to prepare for rendering.
+ Core.SpriteBatch.Begin(samplerState: SamplerState.PointClamp);
+
+ // The color to use for the drop shadow text.
+ Color dropShadowColor = Color.Black * 0.5f;
+
+ // Draw the Dungeon text slightly offset from it is original position and
+ // with a transparent color to give it a drop shadow
+ Core.SpriteBatch.DrawString(_font5x, DUNGEON_TEXT, _dungeonTextPos + new Vector2(10, 10), dropShadowColor, 0.0f, _dungeonTextOrigin, 1.0f, SpriteEffects.None, 1.0f);
+
+ // Draw the Dungeon text on top of that at its original position
+ Core.SpriteBatch.DrawString(_font5x, DUNGEON_TEXT, _dungeonTextPos, Color.White, 0.0f, _dungeonTextOrigin, 1.0f, SpriteEffects.None, 1.0f);
+
+ // Draw the Slime text slightly offset from it is original position and
+ // with a transparent color to give it a drop shadow
+ Core.SpriteBatch.DrawString(_font5x, SLIME_TEXT, _slimeTextPos + new Vector2(10, 10), dropShadowColor, 0.0f, _slimeTextOrigin, 1.0f, SpriteEffects.None, 1.0f);
+
+ // Draw the Slime text on top of that at its original position
+ Core.SpriteBatch.DrawString(_font5x, SLIME_TEXT, _slimeTextPos, Color.White, 0.0f, _slimeTextOrigin, 1.0f, SpriteEffects.None, 1.0f);
+
+ // Always end the sprite batch when finished.
+ Core.SpriteBatch.End();
+ }
+
+ GumService.Default.Draw();
+ }
+}
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/UI/AnimatedButton.cs b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/UI/AnimatedButton.cs
new file mode 100644
index 00000000..56ba8cfc
--- /dev/null
+++ b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/UI/AnimatedButton.cs
@@ -0,0 +1,163 @@
+using System;
+using Gum.DataTypes;
+using Gum.DataTypes.Variables;
+using Gum.Forms.Controls;
+using Gum.Forms.DefaultVisuals;
+using Gum.Graphics.Animation;
+using Gum.Managers;
+using Microsoft.Xna.Framework.Input;
+using MonoGameGum.GueDeriving;
+using MonoGameLibrary.Graphics;
+
+namespace DungeonSlime.UI;
+
+///
+/// A custom button implementation that inherits from Gum's Button class to provide
+/// animated visual feedback when focused.
+///
+internal class AnimatedButton : Button
+{
+ ///
+ /// Creates a new AnimatedButton instance using graphics from the specified texture atlas.
+ ///
+ /// The texture atlas containing button graphics and animations
+ public AnimatedButton(TextureAtlas atlas)
+ {
+ // Each Forms conrol has a general Visual property that
+ // has properties shared by all control types. This Visual
+ // type matches the Forms type. It can be casted to access
+ // controls-specific properties.
+ ButtonVisual buttonVisual = (ButtonVisual)Visual;
+ // Width is relative to children with extra padding, height is fixed
+ buttonVisual.Height = 14f;
+ buttonVisual.HeightUnits = DimensionUnitType.Absolute;
+ buttonVisual.Width = 21f;
+ buttonVisual.WidthUnits = DimensionUnitType.RelativeToChildren;
+
+ // Get a reference to the nine-slice background to display the button graphics
+ // A nine-slice allows the button to stretch while preserving corner appearance
+ NineSliceRuntime background = buttonVisual.Background;
+ background.Texture = atlas.Texture;
+ background.TextureAddress = TextureAddress.Custom;
+ background.Color = Microsoft.Xna.Framework.Color.White;
+ // texture coordinates for the background are set down below
+
+ TextRuntime textInstance = buttonVisual.TextInstance;
+ textInstance.Text = "START";
+ textInstance.Blue = 130;
+ textInstance.Green = 86;
+ textInstance.Red = 70;
+ textInstance.UseCustomFont = true;
+ textInstance.CustomFontFile = "fonts/04b_30.fnt";
+ textInstance.FontScale = 0.25f;
+ textInstance.Anchor(Gum.Wireframe.Anchor.Center);
+ textInstance.Width = 0;
+ textInstance.WidthUnits = DimensionUnitType.RelativeToChildren;
+
+ // Get the texture region for the unfocused button state from the atlas
+ TextureRegion unfocusedTextureRegion = atlas.GetRegion("unfocused-button");
+
+ // Create an animation chain for the unfocused state with a single frame
+ AnimationChain unfocusedAnimation = new AnimationChain();
+ unfocusedAnimation.Name = nameof(unfocusedAnimation);
+ AnimationFrame unfocusedFrame = new AnimationFrame
+ {
+ TopCoordinate = unfocusedTextureRegion.TopTextureCoordinate,
+ BottomCoordinate = unfocusedTextureRegion.BottomTextureCoordinate,
+ LeftCoordinate = unfocusedTextureRegion.LeftTextureCoordinate,
+ RightCoordinate = unfocusedTextureRegion.RightTextureCoordinate,
+ FrameLength = 0.3f,
+ Texture = unfocusedTextureRegion.Texture
+ };
+ unfocusedAnimation.Add(unfocusedFrame);
+
+ // Get the multi-frame animation for the focused button state from the atlas
+ Animation focusedAtlasAnimation = atlas.GetAnimation("focused-button-animation");
+
+ // Create an animation chain for the focused state using all frames from the atlas animation
+ AnimationChain focusedAnimation = new AnimationChain();
+ focusedAnimation.Name = nameof(focusedAnimation);
+ foreach (TextureRegion region in focusedAtlasAnimation.Frames)
+ {
+ AnimationFrame frame = new AnimationFrame
+ {
+ TopCoordinate = region.TopTextureCoordinate,
+ BottomCoordinate = region.BottomTextureCoordinate,
+ LeftCoordinate = region.LeftTextureCoordinate,
+ RightCoordinate = region.RightTextureCoordinate,
+ FrameLength = (float)focusedAtlasAnimation.Delay.TotalSeconds,
+ Texture = region.Texture
+ };
+
+ focusedAnimation.Add(frame);
+ }
+
+ // Assign both animation chains to the nine-slice background
+ background.AnimationChains = new AnimationChainList
+ {
+ unfocusedAnimation,
+ focusedAnimation
+ };
+
+
+ // Reset all state to default so we don't have unexpected variable assignments:
+ buttonVisual.ButtonCategory.ResetAllStates();
+
+ // Get the enabled (default/unfocused) state
+ StateSave enabledState = buttonVisual.States.Enabled;
+ enabledState.Apply = () =>
+ {
+ // When enabled but not focused, use the unfocused animation
+ background.CurrentChainName = unfocusedAnimation.Name;
+ };
+
+ // Create the focused state
+ StateSave focusedState = buttonVisual.States.Focused;
+ focusedState.Apply = () =>
+ {
+ // When focused, use the focused animation and enable animation playback
+ background.CurrentChainName = focusedAnimation.Name;
+ background.Animate = true;
+ };
+
+ // Create the highlighted+focused state (for mouse hover while focused)
+ StateSave highlightedFocused = buttonVisual.States.HighlightedFocused;
+ highlightedFocused.Apply = focusedState.Apply;
+
+ // Create the highlighted state (for mouse hover)
+ // by cloning the enabled state since they appear the same
+ StateSave highlighted = buttonVisual.States.Highlighted;
+ highlighted.Apply = enabledState.Apply;
+
+ // Add event handlers for keyboard input.
+ KeyDown += HandleKeyDown;
+
+ // Add event handler for mouse hover focus.
+ buttonVisual.RollOn += HandleRollOn;
+ }
+
+ ///
+ /// Handles keyboard input for navigation between buttons using left/right keys.
+ ///
+ private void HandleKeyDown(object sender, KeyEventArgs e)
+ {
+ if (e.Key == Keys.Left)
+ {
+ // Left arrow navigates to previous control
+ HandleTab(TabDirection.Up, loop: true);
+ }
+ if (e.Key == Keys.Right)
+ {
+ // Right arrow navigates to next control
+ HandleTab(TabDirection.Down, loop: true);
+ }
+ }
+
+ ///
+ /// Automatically focuses the button when the mouse hovers over it.
+ ///
+ private void HandleRollOn(object sender, EventArgs e)
+ {
+ IsFocused = true;
+ }
+}
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/UI/GameSceneUI.cs b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/UI/GameSceneUI.cs
new file mode 100644
index 00000000..84be9293
--- /dev/null
+++ b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/UI/GameSceneUI.cs
@@ -0,0 +1,340 @@
+using System;
+using Gum.DataTypes;
+using Gum.Managers;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Audio;
+using Microsoft.Xna.Framework.Content;
+using MonoGameGum;
+using Gum.Forms.Controls;
+using MonoGameGum.GueDeriving;
+using MonoGameLibrary;
+using MonoGameLibrary.Graphics;
+
+namespace DungeonSlime.UI;
+
+public class GameSceneUI : ContainerRuntime
+{
+ // The string format to use when updating the text for the score display.
+ private static readonly string s_scoreFormat = "SCORE: {0:D6}";
+
+ // The sound effect to play for auditory feedback of the user interface.
+ private SoundEffect _uiSoundEffect;
+
+ // The pause panel
+ private Panel _pausePanel;
+
+ // The resume button on the pause panel. Field is used to track reference so
+ // focus can be set when the pause panel is shown.
+ private AnimatedButton _resumeButton;
+
+ // The game over panel.
+ private Panel _gameOverPanel;
+
+ // The retry button on the game over panel. Field is used to track reference
+ // so focus can be set when the game over panel is shown.
+ private AnimatedButton _retryButton;
+
+ // The text runtime used to display the players score on the game screen.
+ private TextRuntime _scoreText;
+
+ ///
+ /// Event invoked when the Resume button on the Pause panel is clicked.
+ ///
+ public event EventHandler ResumeButtonClick;
+
+ ///
+ /// Event invoked when the Quit button on either the Pause panel or the
+ /// Game Over panel is clicked.
+ ///
+ public event EventHandler QuitButtonClick;
+
+ ///
+ /// Event invoked when the Retry button on the Game Over panel is clicked.
+ ///
+ public event EventHandler RetryButtonClick;
+
+ public GameSceneUI()
+ {
+ // The game scene UI inherits from ContainerRuntime, so we set its
+ // doc to fill so it fills the entire screen.
+ Dock(Gum.Wireframe.Dock.Fill);
+
+ // Add it to the root element.
+ this.AddToRoot();
+
+ // Get a reference to the content manager that was registered with the
+ // GumService when it was original initialized.
+ ContentManager content = GumService.Default.ContentLoader.XnaContentManager;
+
+ // Use that content manager to load the sound effect and atlas for the
+ // user interface elements
+ _uiSoundEffect = content.Load("audio/ui");
+ TextureAtlas atlas = TextureAtlas.FromFile(content, "images/atlas-definition.xml");
+
+ // Create the text that will display the players score and add it as
+ // a child to this container.
+ _scoreText = CreateScoreText();
+ AddChild(_scoreText);
+
+ // Create the Pause panel that is displayed when the game is paused and
+ // add it as a child to this container
+ _pausePanel = CreatePausePanel(atlas);
+ AddChild(_pausePanel.Visual);
+
+ // Create the Game Over panel that is displayed when a game over occurs
+ // and add it as a child to this container
+ _gameOverPanel = CreateGameOverPanel(atlas);
+ AddChild(_gameOverPanel.Visual);
+ }
+
+ private TextRuntime CreateScoreText()
+ {
+ TextRuntime text = new TextRuntime();
+ text.Anchor(Gum.Wireframe.Anchor.TopLeft);
+ text.WidthUnits = DimensionUnitType.RelativeToChildren;
+ text.X = 20.0f;
+ text.Y = 5.0f;
+ text.UseCustomFont = true;
+ text.CustomFontFile = @"fonts/04b_30.fnt";
+ text.FontScale = 0.25f;
+ text.Text = string.Format(s_scoreFormat, 0);
+
+ return text;
+ }
+
+ private Panel CreatePausePanel(TextureAtlas atlas)
+ {
+ Panel panel = new Panel();
+ panel.Anchor(Gum.Wireframe.Anchor.Center);
+ panel.Visual.WidthUnits = DimensionUnitType.Absolute;
+ panel.Visual.HeightUnits = DimensionUnitType.Absolute;
+ panel.Visual.Width = 264.0f;
+ panel.Visual.Height = 70.0f;
+ panel.IsVisible = false;
+
+ TextureRegion backgroundRegion = atlas.GetRegion("panel-background");
+
+ NineSliceRuntime background = new NineSliceRuntime();
+ background.Dock(Gum.Wireframe.Dock.Fill);
+ background.Texture = backgroundRegion.Texture;
+ background.TextureAddress = TextureAddress.Custom;
+ background.TextureHeight = backgroundRegion.Height;
+ background.TextureWidth = backgroundRegion.Width;
+ background.TextureTop = backgroundRegion.SourceRectangle.Top;
+ background.TextureLeft = backgroundRegion.SourceRectangle.Left;
+ panel.AddChild(background);
+
+ TextRuntime text = new TextRuntime();
+ text.Text = "PAUSED";
+ text.UseCustomFont = true;
+ text.CustomFontFile = "fonts/04b_30.fnt";
+ text.FontScale = 0.5f;
+ text.X = 10.0f;
+ text.Y = 10.0f;
+ panel.AddChild(text);
+
+ _resumeButton = new AnimatedButton(atlas);
+ _resumeButton.Text = "RESUME";
+ _resumeButton.Anchor(Gum.Wireframe.Anchor.BottomLeft);
+ _resumeButton.Visual.X = 9.0f;
+ _resumeButton.Visual.Y = -9.0f;
+
+ _resumeButton.Click += OnResumeButtonClicked;
+ _resumeButton.GotFocus += OnElementGotFocus;
+
+ panel.AddChild(_resumeButton);
+
+ AnimatedButton quitButton = new AnimatedButton(atlas);
+ quitButton.Text = "QUIT";
+ quitButton.Anchor(Gum.Wireframe.Anchor.BottomRight);
+ quitButton.Visual.X = -9.0f;
+ quitButton.Visual.Y = -9.0f;
+
+ quitButton.Click += OnQuitButtonClicked;
+ quitButton.GotFocus += OnElementGotFocus;
+
+ panel.AddChild(quitButton);
+
+ return panel;
+ }
+
+ private Panel CreateGameOverPanel(TextureAtlas atlas)
+ {
+ Panel panel = new Panel();
+ panel.Anchor(Gum.Wireframe.Anchor.Center);
+ panel.Visual.WidthUnits = DimensionUnitType.Absolute;
+ panel.Visual.HeightUnits = DimensionUnitType.Absolute;
+ panel.Visual.Width = 264.0f;
+ panel.Visual.Height = 70.0f;
+ panel.IsVisible = false;
+
+ TextureRegion backgroundRegion = atlas.GetRegion("panel-background");
+
+ NineSliceRuntime background = new NineSliceRuntime();
+ background.Dock(Gum.Wireframe.Dock.Fill);
+ background.Texture = backgroundRegion.Texture;
+ background.TextureAddress = TextureAddress.Custom;
+ background.TextureHeight = backgroundRegion.Height;
+ background.TextureWidth = backgroundRegion.Width;
+ background.TextureTop = backgroundRegion.SourceRectangle.Top;
+ background.TextureLeft = backgroundRegion.SourceRectangle.Left;
+ panel.AddChild(background);
+
+ TextRuntime text = new TextRuntime();
+ text.Text = "GAME OVER";
+ text.WidthUnits = DimensionUnitType.RelativeToChildren;
+ text.UseCustomFont = true;
+ text.CustomFontFile = "fonts/04b_30.fnt";
+ text.FontScale = 0.5f;
+ text.X = 10.0f;
+ text.Y = 10.0f;
+ panel.AddChild(text);
+
+ _retryButton = new AnimatedButton(atlas);
+ _retryButton.Text = "RETRY";
+ _retryButton.Anchor(Gum.Wireframe.Anchor.BottomLeft);
+ _retryButton.Visual.X = 9.0f;
+ _retryButton.Visual.Y = -9.0f;
+
+ _retryButton.Click += OnRetryButtonClicked;
+ _retryButton.GotFocus += OnElementGotFocus;
+
+ panel.AddChild(_retryButton);
+
+ AnimatedButton quitButton = new AnimatedButton(atlas);
+ quitButton.Text = "QUIT";
+ quitButton.Anchor(Gum.Wireframe.Anchor.BottomRight);
+ quitButton.Visual.X = -9.0f;
+ quitButton.Visual.Y = -9.0f;
+
+ quitButton.Click += OnQuitButtonClicked;
+ quitButton.GotFocus += OnElementGotFocus;
+
+ panel.AddChild(quitButton);
+
+ return panel;
+ }
+
+ private void OnResumeButtonClicked(object sender, EventArgs args)
+ {
+ // Button was clicked, play the ui sound effect for auditory feedback.
+ Core.Audio.PlaySoundEffect(_uiSoundEffect);
+
+ // Since the resume button was clicked, we need to hide the pause panel.
+ HidePausePanel();
+
+ // Invoke the ResumeButtonClick event
+ if (ResumeButtonClick != null)
+ {
+ ResumeButtonClick(sender, args);
+ }
+ }
+
+ private void OnRetryButtonClicked(object sender, EventArgs args)
+ {
+ // Button was clicked, play the ui sound effect for auditory feedback.
+ Core.Audio.PlaySoundEffect(_uiSoundEffect);
+
+ // Since the retry button was clicked, we need to hide the game over panel.
+ HideGameOverPanel();
+
+ // Invoke the RetryButtonClick event.
+ if (RetryButtonClick != null)
+ {
+ RetryButtonClick(sender, args);
+ }
+ }
+
+ private void OnQuitButtonClicked(object sender, EventArgs args)
+ {
+ // Button was clicked, play the ui sound effect for auditory feedback.
+ Core.Audio.PlaySoundEffect(_uiSoundEffect);
+
+ // Both panels have a quit button, so hide both panels
+ HidePausePanel();
+ HideGameOverPanel();
+
+ // Invoke the QuitButtonClick event.
+ if (QuitButtonClick != null)
+ {
+ QuitButtonClick(sender, args);
+ }
+ }
+
+ private void OnElementGotFocus(object sender, EventArgs args)
+ {
+ // A ui element that can receive focus has received focus, play the
+ // ui sound effect for auditory feedback.
+ Core.Audio.PlaySoundEffect(_uiSoundEffect);
+ }
+
+ ///
+ /// Updates the text on the score display.
+ ///
+ /// The score to display.
+ public void UpdateScoreText(int score)
+ {
+ _scoreText.Text = string.Format(s_scoreFormat, score);
+ }
+
+ ///
+ /// Tells the game scene ui to show the pause panel.
+ ///
+ public void ShowPausePanel()
+ {
+ _pausePanel.IsVisible = true;
+
+ // Give the resume button focus for keyboard/gamepad input.
+ _resumeButton.IsFocused = true;
+
+ // Ensure the game over panel isn't visible.
+ _gameOverPanel.IsVisible = false;
+ }
+
+ ///
+ /// Tells the game scene ui to hide the pause panel.
+ ///
+ public void HidePausePanel()
+ {
+ _pausePanel.IsVisible = false;
+ }
+
+ ///
+ /// Tells the game scene ui to show the game over panel.
+ ///
+ public void ShowGameOverPanel()
+ {
+ _gameOverPanel.IsVisible = true;
+
+ // Give the retry button focus for keyboard/gamepad input.
+ _retryButton.IsFocused = true;
+
+ // Ensure the pause panel isn't visible.
+ _pausePanel.IsVisible = false;
+ }
+
+ ///
+ /// Tells the game scene ui to hide the game over panel.
+ ///
+ public void HideGameOverPanel()
+ {
+ _gameOverPanel.IsVisible = false;
+ }
+
+ ///
+ /// Updates the game scene ui.
+ ///
+ /// A snapshot of the timing values for the current update cycle.
+ public void Update(GameTime gameTime)
+ {
+ GumService.Default.Update(gameTime);
+ }
+
+ ///
+ /// Draws the game scene ui.
+ ///
+ public void Draw()
+ {
+ GumService.Default.Draw();
+ }
+}
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/UI/OptionsSlider.cs b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/UI/OptionsSlider.cs
new file mode 100644
index 00000000..86d03281
--- /dev/null
+++ b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/UI/OptionsSlider.cs
@@ -0,0 +1,253 @@
+using System;
+using Gum.DataTypes;
+using Gum.DataTypes.Variables;
+using Gum.Managers;
+using Microsoft.Xna.Framework;
+using Gum.Forms.Controls;
+using MonoGameGum.GueDeriving;
+using MonoGameLibrary.Graphics;
+
+namespace DungeonSlime.UI;
+
+///
+/// A custom slider control that inherits from Gum's Slider class.
+///
+public class OptionsSlider : Slider
+{
+ // Reference to the text label that displays the slider's title
+ private TextRuntime _textInstance;
+
+ // Reference to the rectangle that visually represents the current value
+ private ColoredRectangleRuntime _fillRectangle;
+
+ ///
+ /// Gets or sets the text label for this slider.
+ ///
+ public string Text
+ {
+ get => _textInstance.Text;
+ set => _textInstance.Text = value;
+ }
+
+ ///
+ /// Creates a new OptionsSlider instance using graphics from the specified texture atlas.
+ ///
+ /// The texture atlas containing slider graphics.
+ public OptionsSlider(TextureAtlas atlas)
+ {
+ // Create the top-level container for all visual elements
+ ContainerRuntime topLevelContainer = new ContainerRuntime();
+ topLevelContainer.Height = 55f;
+ topLevelContainer.Width = 264f;
+
+ TextureRegion backgroundRegion = atlas.GetRegion("panel-background");
+
+ // Create the background panel that contains everything
+ NineSliceRuntime background = new NineSliceRuntime();
+ background.Texture = atlas.Texture;
+ background.TextureAddress = TextureAddress.Custom;
+ background.TextureHeight = backgroundRegion.Height;
+ background.TextureLeft = backgroundRegion.SourceRectangle.Left;
+ background.TextureTop = backgroundRegion.SourceRectangle.Top;
+ background.TextureWidth = backgroundRegion.Width;
+ background.Dock(Gum.Wireframe.Dock.Fill);
+ topLevelContainer.AddChild(background);
+
+ // Create the title text element
+ _textInstance = new TextRuntime();
+ _textInstance.CustomFontFile = @"fonts/04b_30.fnt";
+ _textInstance.UseCustomFont = true;
+ _textInstance.FontScale = 0.5f;
+ _textInstance.Text = "Replace Me";
+ _textInstance.X = 10f;
+ _textInstance.Y = 10f;
+ _textInstance.WidthUnits = DimensionUnitType.RelativeToChildren;
+ topLevelContainer.AddChild(_textInstance);
+
+ // Create the container for the slider track and decorative elements
+ ContainerRuntime innerContainer = new ContainerRuntime();
+ innerContainer.Height = 13f;
+ innerContainer.Width = 241f;
+ innerContainer.X = 10f;
+ innerContainer.Y = 33f;
+ topLevelContainer.AddChild(innerContainer);
+
+ TextureRegion offBackgroundRegion = atlas.GetRegion("slider-off-background");
+
+ // Create the "OFF" side of the slider (left end)
+ NineSliceRuntime offBackground = new NineSliceRuntime();
+ offBackground.Dock(Gum.Wireframe.Dock.Left);
+ offBackground.Texture = atlas.Texture;
+ offBackground.TextureAddress = TextureAddress.Custom;
+ offBackground.TextureHeight = offBackgroundRegion.Height;
+ offBackground.TextureLeft = offBackgroundRegion.SourceRectangle.Left;
+ offBackground.TextureTop = offBackgroundRegion.SourceRectangle.Top;
+ offBackground.TextureWidth = offBackgroundRegion.Width;
+ offBackground.Width = 28f;
+ offBackground.WidthUnits = DimensionUnitType.Absolute;
+ offBackground.Dock(Gum.Wireframe.Dock.Left);
+ innerContainer.AddChild(offBackground);
+
+ TextureRegion middleBackgroundRegion = atlas.GetRegion("slider-middle-background");
+
+ // Create the middle track portion of the slider
+ NineSliceRuntime middleBackground = new NineSliceRuntime();
+ middleBackground.Dock(Gum.Wireframe.Dock.FillVertically);
+ middleBackground.Texture = middleBackgroundRegion.Texture;
+ middleBackground.TextureAddress = TextureAddress.Custom;
+ middleBackground.TextureHeight = middleBackgroundRegion.Height;
+ middleBackground.TextureLeft = middleBackgroundRegion.SourceRectangle.Left;
+ middleBackground.TextureTop = middleBackgroundRegion.SourceRectangle.Top;
+ middleBackground.TextureWidth = middleBackgroundRegion.Width;
+ middleBackground.Width = 179f;
+ middleBackground.WidthUnits = DimensionUnitType.Absolute;
+ middleBackground.Dock(Gum.Wireframe.Dock.Left);
+ middleBackground.X = 27f;
+ innerContainer.AddChild(middleBackground);
+
+ TextureRegion maxBackgroundRegion = atlas.GetRegion("slider-max-background");
+
+ // Create the "MAX" side of the slider (right end)
+ NineSliceRuntime maxBackground = new NineSliceRuntime();
+ maxBackground.Texture = maxBackgroundRegion.Texture;
+ maxBackground.TextureAddress = TextureAddress.Custom;
+ maxBackground.TextureHeight = maxBackgroundRegion.Height;
+ maxBackground.TextureLeft = maxBackgroundRegion.SourceRectangle.Left;
+ maxBackground.TextureTop = maxBackgroundRegion.SourceRectangle.Top;
+ maxBackground.TextureWidth = maxBackgroundRegion.Width;
+ maxBackground.Width = 36f;
+ maxBackground.WidthUnits = DimensionUnitType.Absolute;
+ maxBackground.Dock(Gum.Wireframe.Dock.Right);
+ innerContainer.AddChild(maxBackground);
+
+ // Create the interactive track that responds to clicks
+ // The special name "TrackInstance" is required for Slider functionality
+ ContainerRuntime trackInstance = new ContainerRuntime();
+ trackInstance.Name = "TrackInstance";
+ trackInstance.Dock(Gum.Wireframe.Dock.Fill);
+ trackInstance.Height = -2f;
+ trackInstance.Width = -2f;
+ middleBackground.AddChild(trackInstance);
+
+ // Create the fill rectangle that visually displays the current value
+ _fillRectangle = new ColoredRectangleRuntime();
+ _fillRectangle.Dock(Gum.Wireframe.Dock.Left);
+ _fillRectangle.Width = 90f; // Default to 90% - will be updated by value changes
+ _fillRectangle.WidthUnits = DimensionUnitType.PercentageOfParent;
+ trackInstance.AddChild(_fillRectangle);
+
+ // Add "OFF" text to the left end
+ TextRuntime offText = new TextRuntime();
+ offText.Red = 70;
+ offText.Green = 86;
+ offText.Blue = 130;
+ offText.CustomFontFile = @"fonts/04b_30.fnt";
+ offText.FontScale = 0.25f;
+ offText.UseCustomFont = true;
+ offText.Text = "OFF";
+ offText.Anchor(Gum.Wireframe.Anchor.Center);
+ offBackground.AddChild(offText);
+
+ // Add "MAX" text to the right end
+ TextRuntime maxText = new TextRuntime();
+ maxText.Red = 70;
+ maxText.Green = 86;
+ maxText.Blue = 130;
+ maxText.CustomFontFile = @"fonts/04b_30.fnt";
+ maxText.FontScale = 0.25f;
+ maxText.UseCustomFont = true;
+ maxText.Text = "MAX";
+ maxText.Anchor(Gum.Wireframe.Anchor.Center);
+ maxBackground.AddChild(maxText);
+
+ // Define colors for focused and unfocused states
+ Color focusedColor = Color.White;
+ Color unfocusedColor = Color.Gray;
+
+ // Create slider state category - Slider.SliderCategoryName is the required name
+ StateSaveCategory sliderCategory = new StateSaveCategory();
+ sliderCategory.Name = Slider.SliderCategoryName;
+ topLevelContainer.AddCategory(sliderCategory);
+
+ // Create the enabled (default/unfocused) state
+ StateSave enabled = new StateSave();
+ enabled.Name = FrameworkElement.EnabledStateName;
+ enabled.Apply = () =>
+ {
+ // When enabled but not focused, use gray coloring for all elements
+ background.Color = unfocusedColor;
+ _textInstance.Color = unfocusedColor;
+ offBackground.Color = unfocusedColor;
+ middleBackground.Color = unfocusedColor;
+ maxBackground.Color = unfocusedColor;
+ _fillRectangle.Color = unfocusedColor;
+ };
+ sliderCategory.States.Add(enabled);
+
+ // Create the focused state
+ StateSave focused = new StateSave();
+ focused.Name = FrameworkElement.FocusedStateName;
+ focused.Apply = () =>
+ {
+ // When focused, use white coloring for all elements
+ background.Color = focusedColor;
+ _textInstance.Color = focusedColor;
+ offBackground.Color = focusedColor;
+ middleBackground.Color = focusedColor;
+ maxBackground.Color = focusedColor;
+ _fillRectangle.Color = focusedColor;
+ };
+ sliderCategory.States.Add(focused);
+
+ // Create the highlighted+focused state by cloning the focused state
+ StateSave highlightedFocused = focused.Clone();
+ highlightedFocused.Name = FrameworkElement.HighlightedFocusedStateName;
+ sliderCategory.States.Add(highlightedFocused);
+
+ // Create the highlighted state by cloning the enabled state
+ StateSave highlighted = enabled.Clone();
+ highlighted.Name = FrameworkElement.HighlightedStateName;
+ sliderCategory.States.Add(highlighted);
+
+ // Assign the configured container as this slider's visual
+ Visual = topLevelContainer;
+
+ // Enable click-to-point functionality for the slider
+ // This allows users to click anywhere on the track to jump to that value
+ IsMoveToPointEnabled = true;
+
+ // Add event handlers
+ Visual.RollOn += HandleRollOn;
+ ValueChanged += HandleValueChanged;
+ ValueChangedByUi += HandleValueChangedByUi;
+ }
+
+ ///
+ /// Automatically focuses the slider when the user interacts with it
+ ///
+ private void HandleValueChangedByUi(object sender, EventArgs e)
+ {
+ IsFocused = true;
+ }
+
+ ///
+ /// Automatically focuses the slider when the mouse hovers over it
+ ///
+ private void HandleRollOn(object sender, EventArgs e)
+ {
+ IsFocused = true;
+ }
+
+ ///
+ /// Updates the fill rectangle width to visually represent the current value
+ ///
+ private void HandleValueChanged(object sender, EventArgs e)
+ {
+ // Calculate the ratio of the current value within its range
+ double ratio = (Value - Minimum) / (Maximum - Minimum);
+
+ // Update the fill rectangle width as a percentage
+ // _fillRectangle uses percentage width units, so we multiply by 100
+ _fillRectangle.Width = 100 * (float)ratio;
+ }
+}
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/app.manifest b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/app.manifest
new file mode 100644
index 00000000..caf45166
--- /dev/null
+++ b/Tutorials/MobileDeployment/03-AddIOSProject/DungeonSlime/app.manifest
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true/pm
+ permonitorv2,permonitor
+
+
+
+
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/MonoGameLibrary/Audio/AudioController.cs b/Tutorials/MobileDeployment/03-AddIOSProject/MonoGameLibrary/Audio/AudioController.cs
new file mode 100644
index 00000000..2c31c166
--- /dev/null
+++ b/Tutorials/MobileDeployment/03-AddIOSProject/MonoGameLibrary/Audio/AudioController.cs
@@ -0,0 +1,282 @@
+using System;
+using System.Collections.Generic;
+using Microsoft.Xna.Framework.Audio;
+using Microsoft.Xna.Framework.Media;
+
+namespace MonoGameLibrary.Audio;
+
+using MediaPlayer = Microsoft.Xna.Framework.Media.MediaPlayer;
+
+public class AudioController : IDisposable
+{
+ // Tracks sound effect instances created so they can be paused, unpaused, and/or disposed.
+ private readonly List _activeSoundEffectInstances;
+
+ // Tracks the volume for song playback when muting and unmuting.
+ private float _previousSongVolume;
+
+ // Tracks the volume for sound effect playback when muting and unmuting.
+ private float _previousSoundEffectVolume;
+
+ ///
+ /// Gets a value that indicates if audio is muted.
+ ///
+ public bool IsMuted { get; private set; }
+
+ ///
+ /// Gets or Sets the global volume of songs.
+ ///
+ ///
+ /// If IsMuted is true, the getter will always return back 0.0f and the
+ /// setter will ignore setting the volume.
+ ///
+ public float SongVolume
+ {
+ get
+ {
+ if (IsMuted)
+ {
+ return 0.0f;
+ }
+
+ return MediaPlayer.Volume;
+ }
+ set
+ {
+ if (IsMuted)
+ {
+ return;
+ }
+
+ MediaPlayer.Volume = Math.Clamp(value, 0.0f, 1.0f);
+ }
+ }
+
+ ///
+ /// Gets or Sets the global volume of sound effects.
+ ///
+ ///
+ /// If IsMuted is true, the getter will always return back 0.0f and the
+ /// setter will ignore setting the volume.
+ ///
+ public float SoundEffectVolume
+ {
+ get
+ {
+ if (IsMuted)
+ {
+ return 0.0f;
+ }
+
+ return SoundEffect.MasterVolume;
+ }
+ set
+ {
+ if (IsMuted)
+ {
+ return;
+ }
+
+ SoundEffect.MasterVolume = Math.Clamp(value, 0.0f, 1.0f);
+ }
+ }
+
+ ///
+ /// Gets a value that indicates if this audio controller has been disposed.
+ ///
+ public bool IsDisposed { get; private set; }
+
+ ///
+ /// Creates a new audio controller instance.
+ ///
+ public AudioController()
+ {
+ _activeSoundEffectInstances = new List();
+ }
+
+ // Finalizer called when object is collected by the garbage collector
+ ~AudioController() => Dispose(false);
+
+ ///
+ /// Updates this audio controller
+ ///
+ public void Update()
+ {
+ int index = 0;
+
+ while (index < _activeSoundEffectInstances.Count)
+ {
+ SoundEffectInstance instance = _activeSoundEffectInstances[index];
+
+ if (instance.State == SoundState.Stopped && !instance.IsDisposed)
+ {
+ instance.Dispose();
+ }
+
+ _activeSoundEffectInstances.RemoveAt(index);
+ }
+ }
+
+ ///
+ /// Plays the given sound effect.
+ ///
+ /// The sound effect to play.
+ /// The sound effect instance created by this method.
+ public SoundEffectInstance PlaySoundEffect(SoundEffect soundEffect)
+ {
+ return PlaySoundEffect(soundEffect, 1.0f, 1.0f, 0.0f, false);
+ }
+
+ ///
+ /// Plays the given sound effect with the specified properties.
+ ///
+ /// The sound effect to play.
+ /// The volume, ranging from 0.0 (silence) to 1.0 (full volume).
+ /// The pitch adjustment, ranging from -1.0 (down an octave) to 0.0 (no change) to 1.0 (up an octave).
+ /// The panning, ranging from -1.0 (left speaker) to 0.0 (centered), 1.0 (right speaker).
+ /// Whether the the sound effect should loop after playback.
+ /// The sound effect instance created by playing the sound effect.
+ /// The sound effect instance created by this method.
+ public SoundEffectInstance PlaySoundEffect(SoundEffect soundEffect, float volume, float pitch, float pan, bool isLooped)
+ {
+ // Create an instance from the sound effect given.
+ SoundEffectInstance soundEffectInstance = soundEffect.CreateInstance();
+
+ // Apply the volume, pitch, pan, and loop values specified.
+ soundEffectInstance.Volume = volume;
+ soundEffectInstance.Pitch = pitch;
+ soundEffectInstance.Pan = pan;
+ soundEffectInstance.IsLooped = isLooped;
+
+ // Tell the instance to play
+ soundEffectInstance.Play();
+
+ // Add it to the active instances for tracking
+ _activeSoundEffectInstances.Add(soundEffectInstance);
+
+ return soundEffectInstance;
+ }
+
+ ///
+ /// Plays the given song.
+ ///
+ /// The song to play.
+ /// Optionally specify if the song should repeat. Default is true.
+ public void PlaySong(Song song, bool isRepeating = true)
+ {
+ // Check if the media player is already playing, if so, stop it.
+ // If we do not stop it, this could cause issues on some platforms
+ if (MediaPlayer.State == MediaState.Playing)
+ {
+ MediaPlayer.Stop();
+ }
+
+ MediaPlayer.Play(song);
+ MediaPlayer.IsRepeating = isRepeating;
+ }
+
+ ///
+ /// Pauses all audio.
+ ///
+ public void PauseAudio()
+ {
+ // Pause any active songs playing
+ MediaPlayer.Pause();
+
+ // Pause any active sound effects
+ foreach (SoundEffectInstance soundEffectInstance in _activeSoundEffectInstances)
+ {
+ soundEffectInstance.Pause();
+ }
+ }
+
+ ///
+ /// Resumes play of all previous paused audio.
+ ///
+ public void ResumeAudio()
+ {
+ // Resume paused music
+ MediaPlayer.Resume();
+
+ // Resume any active sound effects
+ foreach (SoundEffectInstance soundEffectInstance in _activeSoundEffectInstances)
+ {
+ soundEffectInstance.Resume();
+ }
+ }
+
+ ///
+ /// Mutes all audio.
+ ///
+ public void MuteAudio()
+ {
+ // Store the volume so they can be restored during ResumeAudio
+ _previousSongVolume = MediaPlayer.Volume;
+ _previousSoundEffectVolume = SoundEffect.MasterVolume;
+
+ // Set all volumes to 0
+ MediaPlayer.Volume = 0.0f;
+ SoundEffect.MasterVolume = 0.0f;
+
+ IsMuted = true;
+ }
+
+ ///
+ /// Unmutes all audio to the volume level prior to muting.
+ ///
+ public void UnmuteAudio()
+ {
+ // Restore the previous volume values
+ MediaPlayer.Volume = _previousSongVolume;
+ SoundEffect.MasterVolume = _previousSoundEffectVolume;
+
+ IsMuted = false;
+ }
+
+ ///
+ /// Toggles the current audio mute state.
+ ///
+ public void ToggleMute()
+ {
+ if (IsMuted)
+ {
+ UnmuteAudio();
+ }
+ else
+ {
+ MuteAudio();
+ }
+ }
+
+ ///
+ /// Disposes of this audio controller and cleans up resources.
+ ///
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ /// Disposes this audio controller and cleans up resources.
+ ///
+ /// Indicates whether managed resources should be disposed.
+ protected void Dispose(bool disposing)
+ {
+ if (IsDisposed)
+ {
+ return;
+ }
+
+ if (disposing)
+ {
+ foreach (SoundEffectInstance soundEffectInstance in _activeSoundEffectInstances)
+ {
+ soundEffectInstance.Dispose();
+ }
+ _activeSoundEffectInstances.Clear();
+ }
+
+ IsDisposed = true;
+ }
+}
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/MonoGameLibrary/Circle.cs b/Tutorials/MobileDeployment/03-AddIOSProject/MonoGameLibrary/Circle.cs
new file mode 100644
index 00000000..0bb691bc
--- /dev/null
+++ b/Tutorials/MobileDeployment/03-AddIOSProject/MonoGameLibrary/Circle.cs
@@ -0,0 +1,136 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGameLibrary;
+
+public readonly struct Circle : IEquatable
+{
+ private static readonly Circle s_empty = new Circle();
+
+ ///
+ /// The x-coordinate of the center of this circle.
+ ///
+ public readonly int X;
+
+ ///
+ /// The y-coordinate of the center of this circle.
+ ///
+ public readonly int Y;
+
+ ///
+ /// The length, in pixels, from the center of this circle to the edge.
+ ///
+ public readonly int Radius;
+
+ ///
+ /// Gets the location of the center of this circle.
+ ///
+ public readonly Point Location => new Point(X, Y);
+
+ ///
+ /// Gets a circle with X=0, Y=0, and Radius=0.
+ ///
+ public static Circle Empty => s_empty;
+
+ ///
+ /// Gets a value that indicates whether this circle has a radius of 0 and a location of (0, 0).
+ ///
+ public readonly bool IsEmpty => X == 0 && Y == 0 && Radius == 0;
+
+ ///
+ /// Gets the y-coordinate of the highest point on this circle.
+ ///
+ public readonly int Top => Y - Radius;
+
+ ///
+ /// Gets the y-coordinate of the lowest point on this circle.
+ ///
+ public readonly int Bottom => Y + Radius;
+
+ ///
+ /// Gets the x-coordinate of the leftmost point on this circle.
+ ///
+ public readonly int Left => X - Radius;
+
+ ///
+ /// Gets the x-coordinate of the rightmost point on this circle.
+ ///
+ public readonly int Right => X + Radius;
+
+ ///
+ /// Creates a new circle with the specified position and radius.
+ ///
+ /// The x-coordinate of the center of the circle.
+ /// The y-coordinate of the center of the circle..
+ /// The length from the center of the circle to an edge.
+ public Circle(int x, int y, int radius)
+ {
+ X = x;
+ Y = y;
+ Radius = radius;
+ }
+
+ ///
+ /// Creates a new circle with the specified position and radius.
+ ///
+ /// The center of the circle.
+ /// The length from the center of the circle to an edge.
+ public Circle(Point location, int radius)
+ {
+ X = location.X;
+ Y = location.Y;
+ Radius = radius;
+ }
+
+ ///
+ /// Returns a value that indicates whether the specified circle intersects with this circle.
+ ///
+ /// The other circle to check.
+ /// true if the other circle intersects with this circle; otherwise, false.
+ public bool Intersects(Circle other)
+ {
+ int radiiSquared = (this.Radius + other.Radius) * (this.Radius + other.Radius);
+ float distanceSquared = Vector2.DistanceSquared(this.Location.ToVector2(), other.Location.ToVector2());
+ return distanceSquared < radiiSquared;
+ }
+
+ ///
+ /// Returns a value that indicates whether this circle and the specified object are equal
+ ///
+ /// The object to compare with this circle.
+ /// true if this circle and the specified object are equal; otherwise, false.
+ public override readonly bool Equals(object obj) => obj is Circle other && Equals(other);
+
+ ///
+ /// Returns a value that indicates whether this circle and the specified circle are equal.
+ ///
+ /// The circle to compare with this circle.
+ /// true if this circle and the specified circle are equal; otherwise, false.
+ public readonly bool Equals(Circle other) => this.X == other.X &&
+ this.Y == other.Y &&
+ this.Radius == other.Radius;
+
+ ///
+ /// Returns the hash code for this circle.
+ ///
+ /// The hash code for this circle as a 32-bit signed integer.
+ public override readonly int GetHashCode() => HashCode.Combine(X, Y, Radius);
+
+ ///
+ /// Returns a value that indicates if the circle on the left hand side of the equality operator is equal to the
+ /// circle on the right hand side of the equality operator.
+ ///
+ /// The circle on the left hand side of the equality operator.
+ /// The circle on the right hand side of the equality operator.
+ /// true if the two circles are equal; otherwise, false.
+ public static bool operator ==(Circle lhs, Circle rhs) => lhs.Equals(rhs);
+
+ ///
+ /// Returns a value that indicates if the circle on the left hand side of the inequality operator is not equal to the
+ /// circle on the right hand side of the inequality operator.
+ ///
+ /// The circle on the left hand side of the inequality operator.
+ /// The circle on the right hand side fo the inequality operator.
+ /// true if the two circle are not equal; otherwise, false.
+ public static bool operator !=(Circle lhs, Circle rhs) => !lhs.Equals(rhs);
+}
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/MonoGameLibrary/Core.cs b/Tutorials/MobileDeployment/03-AddIOSProject/MonoGameLibrary/Core.cs
new file mode 100644
index 00000000..edb3cb49
--- /dev/null
+++ b/Tutorials/MobileDeployment/03-AddIOSProject/MonoGameLibrary/Core.cs
@@ -0,0 +1,208 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content;
+using Microsoft.Xna.Framework.Graphics;
+using Microsoft.Xna.Framework.Input;
+using MonoGameLibrary.Audio;
+using MonoGameLibrary.Input;
+using MonoGameLibrary.Scenes;
+
+namespace MonoGameLibrary;
+
+public class Core : Game
+{
+ internal static Core s_instance;
+
+ ///
+ /// Gets a reference to the Core instance.
+ ///
+ public static Core Instance => s_instance;
+
+ // The scene that is currently active.
+ private static Scene s_activeScene;
+
+ // The next scene to switch to, if there is one.
+ private static Scene s_nextScene;
+
+ ///
+ /// Gets the graphics device manager to control the presentation of graphics.
+ ///
+ public static GraphicsDeviceManager Graphics { get; private set; }
+
+ ///
+ /// Gets the graphics device used to create graphical resources and perform primitive rendering.
+ ///
+ public static new GraphicsDevice GraphicsDevice { get; private set; }
+
+ ///
+ /// Gets the sprite batch used for all 2D rendering.
+ ///
+ public static SpriteBatch SpriteBatch { get; private set; }
+
+ ///
+ /// Gets the content manager used to load global assets.
+ ///
+ public static new ContentManager Content { get; private set; }
+
+ ///
+ /// Gets a reference to to the input management system.
+ ///
+ public static InputManager Input { get; private set; }
+
+ ///
+ /// Gets or Sets a value that indicates if the game should exit when the esc key on the keyboard is pressed.
+ ///
+ public static bool ExitOnEscape { get; set; }
+
+ ///
+ /// Gets a reference to the audio control system.
+ ///
+ public static AudioController Audio { get; private set; }
+
+ ///
+ /// Creates a new Core instance.
+ ///
+ /// The title to display in the title bar of the game window.
+ /// The initial width, in pixels, of the game window.
+ /// The initial height, in pixels, of the game window.
+ /// Indicates if the game should start in fullscreen mode.
+ public Core(string title, int width, int height, bool fullScreen)
+ {
+ // Ensure that multiple cores are not created.
+ if (s_instance != null)
+ {
+ throw new InvalidOperationException($"Only a single Core instance can be created");
+ }
+
+ // Store reference to engine for global member access.
+ s_instance = this;
+
+ // Create a new graphics device manager.
+ Graphics = new GraphicsDeviceManager(this);
+
+ // Set the graphics defaults
+ Graphics.PreferredBackBufferWidth = width;
+ Graphics.PreferredBackBufferHeight = height;
+ Graphics.IsFullScreen = fullScreen;
+
+ // Apply the graphic presentation changes
+ Graphics.ApplyChanges();
+
+ // Set the window title
+ Window.Title = title;
+
+ // Set the core's content manager to a reference of hte base Game's
+ // content manager.
+ Content = base.Content;
+
+ // Set the root directory for content
+ Content.RootDirectory = "Content";
+
+ // Mouse is visible by default
+ IsMouseVisible = true;
+ }
+
+ protected override void Initialize()
+ {
+ base.Initialize();
+
+ // Set the core's graphics device to a reference of the base Game's
+ // graphics device.
+ GraphicsDevice = base.GraphicsDevice;
+
+ // Create the sprite batch instance.
+ SpriteBatch = new SpriteBatch(GraphicsDevice);
+
+ // Create a new input manager
+ Input = new InputManager();
+
+ // Create a new audio controller.
+ Audio = new AudioController();
+ }
+
+ protected override void UnloadContent()
+ {
+ // Dispose of the audio controller.
+ Audio.Dispose();
+
+ base.UnloadContent();
+ }
+
+ protected override void Update(GameTime gameTime)
+ {
+ // Update the input manager.
+ Input.Update(gameTime);
+
+ // Update the audio controller.
+ Audio.Update();
+
+#if WINDOWS
+ if (ExitOnEscape && Input.Keyboard.WasKeyJustPressed(Keys.Escape))
+ {
+ Exit();
+ }
+#endif
+
+ // if there is a next scene waiting to be switch to, then transition
+ // to that scene
+ if (s_nextScene != null)
+ {
+ TransitionScene();
+ }
+
+ // If there is an active scene, update it.
+ if (s_activeScene != null)
+ {
+ s_activeScene.Update(gameTime);
+ }
+
+ base.Update(gameTime);
+ }
+
+ protected override void Draw(GameTime gameTime)
+ {
+ // If there is an active scene, draw it.
+ if (s_activeScene != null)
+ {
+ s_activeScene.Draw(gameTime);
+ }
+
+ base.Draw(gameTime);
+ }
+
+ public static void ChangeScene(Scene next)
+ {
+ // Only set the next scene value if it is not the same
+ // instance as the currently active scene.
+ if (s_activeScene != next)
+ {
+ s_nextScene = next;
+ }
+ }
+
+ private static void TransitionScene()
+ {
+ // If there is an active scene, dispose of it
+ if (s_activeScene != null)
+ {
+ s_activeScene.Dispose();
+ }
+
+ // Force the garbage collector to collect to ensure memory is cleared
+ GC.Collect();
+
+ // Change the currently active scene to the new scene
+ s_activeScene = s_nextScene;
+
+ // Null out the next scene value so it does not trigger a change over and over.
+ s_nextScene = null;
+
+ // If the active scene now is not null, initialize it.
+ // Remember, just like with Game, the Initialize call also calls the
+ // Scene.LoadContent
+ if (s_activeScene != null)
+ {
+ s_activeScene.Initialize();
+ }
+ }
+}
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/MonoGameLibrary/Graphics/AnimatedSprite.cs b/Tutorials/MobileDeployment/03-AddIOSProject/MonoGameLibrary/Graphics/AnimatedSprite.cs
new file mode 100644
index 00000000..a1a3594e
--- /dev/null
+++ b/Tutorials/MobileDeployment/03-AddIOSProject/MonoGameLibrary/Graphics/AnimatedSprite.cs
@@ -0,0 +1,60 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGameLibrary.Graphics;
+
+public class AnimatedSprite : Sprite
+{
+ private int _currentFrame;
+ private TimeSpan _elapsed;
+ private Animation _animation;
+
+ ///
+ /// Gets or Sets the animation for this animated sprite.
+ ///
+ public Animation Animation
+ {
+ get => _animation;
+ set
+ {
+ _animation = value;
+ Region = _animation.Frames[0];
+ }
+ }
+
+ ///
+ /// Creates a new animated sprite.
+ ///
+ public AnimatedSprite() { }
+
+ ///
+ /// Creates a new animated sprite with the specified frames and delay.
+ ///
+ /// The animation for this animated sprite.
+ public AnimatedSprite(Animation animation)
+ {
+ Animation = animation;
+ }
+
+ ///
+ /// Updates this animated sprite.
+ ///
+ /// A snapshot of the game timing values provided by the framework.
+ public void Update(GameTime gameTime)
+ {
+ _elapsed += gameTime.ElapsedGameTime;
+
+ if (_elapsed >= _animation.Delay)
+ {
+ _elapsed -= _animation.Delay;
+ _currentFrame++;
+
+ if (_currentFrame >= _animation.Frames.Count)
+ {
+ _currentFrame = 0;
+ }
+
+ Region = _animation.Frames[_currentFrame];
+ }
+ }
+}
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/MonoGameLibrary/Graphics/Animation.cs b/Tutorials/MobileDeployment/03-AddIOSProject/MonoGameLibrary/Graphics/Animation.cs
new file mode 100644
index 00000000..44d61b65
--- /dev/null
+++ b/Tutorials/MobileDeployment/03-AddIOSProject/MonoGameLibrary/Graphics/Animation.cs
@@ -0,0 +1,38 @@
+using System;
+using System.Collections.Generic;
+
+namespace MonoGameLibrary.Graphics;
+
+public class Animation
+{
+ ///
+ /// The texture regions that make up the frames of this animation. The order of the regions within the collection
+ /// are the order that the frames should be displayed in.
+ ///
+ public List Frames { get; set; }
+
+ ///
+ /// The amount of time to delay between each frame before moving to the next frame for this animation.
+ ///
+ public TimeSpan Delay { get; set; }
+
+ ///
+ /// Creates a new animation.
+ ///
+ public Animation()
+ {
+ Frames = new List();
+ Delay = TimeSpan.FromMilliseconds(100);
+ }
+
+ ///
+ /// Creates a new animation with the specified frames and delay.
+ ///
+ /// An ordered collection of the frames for this animation.
+ /// The amount of time to delay between each frame of this animation.
+ public Animation(List frames, TimeSpan delay)
+ {
+ Frames = frames;
+ Delay = delay;
+ }
+}
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/MonoGameLibrary/Graphics/Sprite.cs b/Tutorials/MobileDeployment/03-AddIOSProject/MonoGameLibrary/Graphics/Sprite.cs
new file mode 100644
index 00000000..20c44f0b
--- /dev/null
+++ b/Tutorials/MobileDeployment/03-AddIOSProject/MonoGameLibrary/Graphics/Sprite.cs
@@ -0,0 +1,108 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGameLibrary.Graphics;
+
+public class Sprite
+{
+ ///
+ /// Gets or Sets the source texture region represented by this sprite.
+ ///
+ public TextureRegion Region { get; set; }
+
+ ///
+ /// Gets or Sets the color mask to apply when rendering this sprite.
+ ///
+ ///
+ /// Default value is Color.White
+ ///
+ public Color Color { get; set; } = Color.White;
+
+ ///
+ /// Gets or Sets the amount of rotation, in radians, to apply when rendering this sprite.
+ ///
+ ///
+ /// Default value is 0.0f
+ ///
+ public float Rotation { get; set; } = 0.0f;
+
+ ///
+ /// Gets or Sets the scale factor to apply to the x- and y-axes when rendering this sprite.
+ ///
+ ///
+ /// Default value is Vector2.One
+ ///
+ public Vector2 Scale { get; set; } = Vector2.One;
+
+ ///
+ /// Gets or Sets the xy-coordinate origin point, relative to the top-left corner, of this sprite.
+ ///
+ ///
+ /// Default value is Vector2.Zero
+ ///
+ public Vector2 Origin { get; set; } = Vector2.Zero;
+
+ ///
+ /// Gets or Sets the sprite effects to apply when rendering this sprite.
+ ///
+ ///
+ /// Default value is SpriteEffects.None
+ ///
+ public SpriteEffects Effects { get; set; } = SpriteEffects.None;
+
+ ///
+ /// Gets or Sets the layer depth to apply when rendering this sprite.
+ ///
+ ///
+ /// Default value is 0.0f
+ ///
+ public float LayerDepth { get; set; } = 0.0f;
+
+ ///
+ /// Gets the width, in pixels, of this sprite.
+ ///
+ ///
+ /// Width is calculated by multiplying the width of the source texture region by the x-axis scale factor.
+ ///
+ public float Width => Region.Width * Scale.X;
+
+ ///
+ /// Gets the height, in pixels, of this sprite.
+ ///
+ ///
+ /// Height is calculated by multiplying the height of the source texture region by the y-axis scale factor.
+ ///
+ public float Height => Region.Height * Scale.Y;
+
+ ///
+ /// Creates a new sprite.
+ ///
+ public Sprite() { }
+
+ ///
+ /// Creates a new sprite using the specified source texture region.
+ ///
+ /// The texture region to use as the source texture region for this sprite.
+ public Sprite(TextureRegion region)
+ {
+ Region = region;
+ }
+
+ ///
+ /// Sets the origin of this sprite to the center
+ ///
+ public void CenterOrigin()
+ {
+ Origin = new Vector2(Region.Width, Region.Height) * 0.5f;
+ }
+
+ ///
+ /// Submit this sprite for drawing to the current batch.
+ ///
+ /// The SpriteBatch instance used for batching draw calls.
+ /// The xy-coordinate position to render this sprite at.
+ public void Draw(SpriteBatch spriteBatch, Vector2 position)
+ {
+ Region.Draw(spriteBatch, position, Color, Rotation, Origin, Scale, Effects, LayerDepth);
+ }
+}
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/MonoGameLibrary/Graphics/TextureAtlas.cs b/Tutorials/MobileDeployment/03-AddIOSProject/MonoGameLibrary/Graphics/TextureAtlas.cs
new file mode 100644
index 00000000..e48c9abd
--- /dev/null
+++ b/Tutorials/MobileDeployment/03-AddIOSProject/MonoGameLibrary/Graphics/TextureAtlas.cs
@@ -0,0 +1,239 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Xml;
+using System.Xml.Linq;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content;
+using Microsoft.Xna.Framework.Graphics;
+
+
+namespace MonoGameLibrary.Graphics;
+
+public class TextureAtlas
+{
+ private Dictionary _regions;
+
+ // Stores animations added to this atlas.
+ private Dictionary _animations;
+
+ ///
+ /// Gets or Sets the source texture represented by this texture atlas.
+ ///
+ public Texture2D Texture { get; set; }
+
+ ///
+ /// Creates a new texture atlas.
+ ///
+ public TextureAtlas()
+ {
+ _regions = new Dictionary();
+ _animations = new Dictionary();
+ }
+
+ ///
+ /// Creates a new texture atlas instance using the given texture.
+ ///
+ /// The source texture represented by the texture atlas.
+ public TextureAtlas(Texture2D texture)
+ {
+ Texture = texture;
+ _regions = new Dictionary();
+ _animations = new Dictionary();
+ }
+
+ ///
+ /// Creates a new region and adds it to this texture atlas.
+ ///
+ /// The name to give the texture region.
+ /// The top-left x-coordinate position of the region boundary relative to the top-left corner of the source texture boundary.
+ /// The top-left y-coordinate position of the region boundary relative to the top-left corner of the source texture boundary.
+ /// The width, in pixels, of the region.
+ /// The height, in pixels, of the region.
+ public void AddRegion(string name, int x, int y, int width, int height)
+ {
+ TextureRegion region = new TextureRegion(Texture, x, y, width, height);
+ _regions.Add(name, region);
+ }
+
+ ///
+ /// Gets the region from this texture atlas with the specified name.
+ ///
+ /// The name of the region to retrieve.
+ /// The TextureRegion with the specified name.
+ public TextureRegion GetRegion(string name)
+ {
+ return _regions[name];
+ }
+
+ ///
+ /// Removes the region from this texture atlas with the specified name.
+ ///
+ /// The name of the region to remove.
+ ///
+ public bool RemoveRegion(string name)
+ {
+ return _regions.Remove(name);
+ }
+
+ ///
+ /// Removes all regions from this texture atlas.
+ ///
+ public void Clear()
+ {
+ _regions.Clear();
+ }
+
+ ///
+ /// Creates a new sprite using the region from this texture atlas with the specified name.
+ ///
+ /// The name of the region to create the sprite with.
+ /// A new Sprite using the texture region with the specified name.
+ public Sprite CreateSprite(string regionName)
+ {
+ TextureRegion region = GetRegion(regionName);
+ return new Sprite(region);
+ }
+
+ ///
+ /// Adds the given animation to this texture atlas with the specified name.
+ ///
+ /// The name of the animation to add.
+ /// The animation to add.
+ public void AddAnimation(string animationName, Animation animation)
+ {
+ _animations.Add(animationName, animation);
+ }
+
+ ///
+ /// Gets the animation from this texture atlas with the specified name.
+ ///
+ /// The name of the animation to retrieve.
+ /// The animation with the specified name.
+ public Animation GetAnimation(string animationName)
+ {
+ return _animations[animationName];
+ }
+
+ ///
+ /// Removes the animation with the specified name from this texture atlas.
+ ///
+ /// The name of the animation to remove.
+ /// true if the animation is removed successfully; otherwise, false.
+ public bool RemoveAnimation(string animationName)
+ {
+ return _animations.Remove(animationName);
+ }
+
+ ///
+ /// Creates a new animated sprite using the animation from this texture atlas with the specified name.
+ ///
+ /// The name of the animation to use.
+ /// A new AnimatedSprite using the animation with the specified name.
+ public AnimatedSprite CreateAnimatedSprite(string animationName)
+ {
+ Animation animation = GetAnimation(animationName);
+ return new AnimatedSprite(animation);
+ }
+
+ ///
+ /// Creates a new texture atlas based a texture atlas xml configuration file.
+ ///
+ /// The content manager used to load the texture for the atlas.
+ /// The path to the xml file, relative to the content root directory..
+ /// The texture atlas created by this method.
+ public static TextureAtlas FromFile(ContentManager content, string fileName)
+ {
+ TextureAtlas atlas = new TextureAtlas();
+
+ string filePath = Path.Combine(content.RootDirectory, fileName);
+
+ using (Stream stream = TitleContainer.OpenStream(filePath))
+ {
+ using (XmlReader reader = XmlReader.Create(stream))
+ {
+ XDocument doc = XDocument.Load(reader);
+ XElement root = doc.Root;
+
+ // The element contains the content path for the Texture2D to load.
+ // So we will retrieve that value then use the content manager to load the texture.
+ string texturePath = root.Element("Texture").Value;
+ atlas.Texture = content.Load(texturePath);
+
+ // The element contains individual elements, each one describing
+ // a different texture region within the atlas.
+ //
+ // Example:
+ //
+ //
+ //
+ //
+ //
+ // So we retrieve all of the elements then loop through each one
+ // and generate a new TextureRegion instance from it and add it to this atlas.
+ var regions = root.Element("Regions")?.Elements("Region");
+
+ if (regions != null)
+ {
+ foreach (var 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);
+ }
+ }
+ }
+
+ // The element contains individual elements, each one describing
+ // a different animation within the atlas.
+ //
+ // Example:
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ // So we retrieve all of the elements then loop through each one
+ // and generate a new Animation instance from it and add it to this atlas.
+ var animationElements = root.Element("Animations").Elements("Animation");
+
+ if (animationElements != null)
+ {
+ foreach (var animationElement in animationElements)
+ {
+ string name = animationElement.Attribute("name")?.Value;
+ float delayInMilliseconds = float.Parse(animationElement.Attribute("delay")?.Value ?? "0");
+ TimeSpan delay = TimeSpan.FromMilliseconds(delayInMilliseconds);
+
+ List frames = new List();
+
+ var frameElements = animationElement.Elements("Frame");
+
+ if (frameElements != null)
+ {
+ foreach (var frameElement in frameElements)
+ {
+ string regionName = frameElement.Attribute("region").Value;
+ TextureRegion region = atlas.GetRegion(regionName);
+ frames.Add(region);
+ }
+ }
+
+ Animation animation = new Animation(frames, delay);
+ atlas.AddAnimation(name, animation);
+ }
+ }
+
+ return atlas;
+ }
+ }
+ }
+}
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/MonoGameLibrary/Graphics/TextureRegion.cs b/Tutorials/MobileDeployment/03-AddIOSProject/MonoGameLibrary/Graphics/TextureRegion.cs
new file mode 100644
index 00000000..ecd69030
--- /dev/null
+++ b/Tutorials/MobileDeployment/03-AddIOSProject/MonoGameLibrary/Graphics/TextureRegion.cs
@@ -0,0 +1,131 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGameLibrary.Graphics;
+
+///
+/// Represents a rectangular region within a texture.
+///
+public class TextureRegion
+{
+ ///
+ /// Gets or Sets the source texture this texture region is part of.
+ ///
+ public Texture2D Texture { get; set; }
+
+ ///
+ /// Gets or Sets the source rectangle boundary of this texture region within the source texture.
+ ///
+ public Rectangle SourceRectangle { get; set; }
+
+ ///
+ /// Gets the width, in pixels, of this texture region.
+ ///
+ public int Width => SourceRectangle.Width;
+
+ ///
+ /// Gets the height, in pixels, of this texture region.
+ ///
+ public int Height => SourceRectangle.Height;
+
+ ///
+ /// Gets the top normalized texture coordinate of this region.
+ ///
+ public float TopTextureCoordinate => SourceRectangle.Top / (float)Texture.Height;
+
+ ///
+ /// Gets the bottom normalized texture coordinate of this region.
+ ///
+ public float BottomTextureCoordinate => SourceRectangle.Bottom / (float)Texture.Height;
+
+ ///
+ /// Gets the left normalized texture coordinate of this region.
+ ///
+ public float LeftTextureCoordinate => SourceRectangle.Left / (float)Texture.Width;
+
+ ///
+ /// Gets the right normalized texture coordinate of this region.
+ ///
+ public float RightTextureCoordinate => SourceRectangle.Right / (float)Texture.Width;
+
+ ///
+ /// Creates a new texture region.
+ ///
+ public TextureRegion() { }
+
+ ///
+ /// Creates a new texture region using the specified source texture.
+ ///
+ /// The texture to use as the source texture for this texture region.
+ /// The x-coordinate position of the upper-left corner of this texture region relative to the upper-left corner of the source texture.
+ ///
+ /// The width, in pixels, of this texture region.
+ /// The height, in pixels, of this texture region.
+ public TextureRegion(Texture2D texture, int x, int y, int width, int height)
+ {
+ Texture = texture;
+ SourceRectangle = new Rectangle(x, y, width, height);
+ }
+
+ ///
+ /// Submit this texture region for drawing in the current batch.
+ ///
+ /// The spritebatch instance used for batching draw calls.
+ /// The xy-coordinate location to draw this texture region on the screen.
+ /// The color mask to apply when drawing this texture region on screen.
+ public void Draw(SpriteBatch spriteBatch, Vector2 position, Color color)
+ {
+ Draw(spriteBatch, position, color, 0.0f, Vector2.Zero, Vector2.One, SpriteEffects.None, 0.0f);
+ }
+
+ ///
+ /// Submit this texture region for drawing in the current batch.
+ ///
+ /// The spritebatch instance used for batching draw calls.
+ /// The xy-coordinate location to draw this texture region on the screen.
+ /// The color mask to apply when drawing this texture region on screen.
+ /// The amount of rotation, in radians, to apply when drawing this texture region on screen.
+ /// The center of rotation, scaling, and position when drawing this texture region on screen.
+ /// The scale factor to apply when drawing this texture region on screen.
+ /// Specifies if this texture region should be flipped horizontally, vertically, or both when drawing on screen.
+ /// The depth of the layer to use when drawing this texture region on screen.
+ public void Draw(SpriteBatch spriteBatch, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects effects, float layerDepth)
+ {
+ Draw(
+ spriteBatch,
+ position,
+ color,
+ rotation,
+ origin,
+ new Vector2(scale, scale),
+ effects,
+ layerDepth
+ );
+ }
+
+ ///
+ /// Submit this texture region for drawing in the current batch.
+ ///
+ /// The spritebatch instance used for batching draw calls.
+ /// The xy-coordinate location to draw this texture region on the screen.
+ /// The color mask to apply when drawing this texture region on screen.
+ /// The amount of rotation, in radians, to apply when drawing this texture region on screen.
+ /// The center of rotation, scaling, and position when drawing this texture region on screen.
+ /// The amount of scaling to apply to the x- and y-axes when drawing this texture region on screen.
+ /// Specifies if this texture region should be flipped horizontally, vertically, or both when drawing on screen.
+ /// The depth of the layer to use when drawing this texture region on screen.
+ public void Draw(SpriteBatch spriteBatch, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth)
+ {
+ spriteBatch.Draw(
+ Texture,
+ position,
+ SourceRectangle,
+ color,
+ rotation,
+ origin,
+ scale,
+ effects,
+ layerDepth
+ );
+ }
+}
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/MonoGameLibrary/Graphics/Tilemap.cs b/Tutorials/MobileDeployment/03-AddIOSProject/MonoGameLibrary/Graphics/Tilemap.cs
new file mode 100644
index 00000000..96e1ee5e
--- /dev/null
+++ b/Tutorials/MobileDeployment/03-AddIOSProject/MonoGameLibrary/Graphics/Tilemap.cs
@@ -0,0 +1,231 @@
+using System;
+using System.IO;
+using System.Xml;
+using System.Xml.Linq;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGameLibrary.Graphics;
+
+public class Tilemap
+{
+ private readonly Tileset _tileset;
+ private readonly int[] _tiles;
+
+ ///
+ /// Gets the total number of rows in this tilemap.
+ ///
+ public int Rows { get; }
+
+ ///
+ /// Gets the total number of columns in this tilemap.
+ ///
+ public int Columns { get; }
+
+ ///
+ /// Gets the total number of tiles in this tilemap.
+ ///
+ public int Count { get; }
+
+ ///
+ /// Gets or Sets the scale factor to draw each tile at.
+ ///
+ public Vector2 Scale { get; set; }
+
+ ///
+ /// Gets the width, in pixels, each tile is drawn at.
+ ///
+ public float TileWidth => _tileset.TileWidth * Scale.X;
+
+ ///
+ /// Gets the height, in pixels, each tile is drawn at.
+ ///
+ public float TileHeight => _tileset.TileHeight * Scale.Y;
+
+ ///
+ /// Creates a new tilemap.
+ ///
+ /// The tileset used by this tilemap.
+ /// The total number of columns in this tilemap.
+ /// The total number of rows in this tilemap.
+ public Tilemap(Tileset tileset, int columns, int rows)
+ {
+ _tileset = tileset;
+ Rows = rows;
+ Columns = columns;
+ Count = Columns * Rows;
+ Scale = Vector2.One;
+ _tiles = new int[Count];
+ }
+
+ ///
+ /// Sets the tile at the given index in this tilemap to use the tile from
+ /// the tileset at the specified tileset id.
+ ///
+ /// The index of the tile in this tilemap.
+ /// The tileset id of the tile from the tileset to use.
+ public void SetTile(int index, int tilesetID)
+ {
+ _tiles[index] = tilesetID;
+ }
+
+ ///
+ /// Sets the tile at the given column and row in this tilemap to use the tile
+ /// from the tileset at the specified tileset id.
+ ///
+ /// The column of the tile in this tilemap.
+ /// The row of the tile in this tilemap.
+ /// The tileset id of the tile from the tileset to use.
+ public void SetTile(int column, int row, int tilesetID)
+ {
+ int index = row * Columns + column;
+ SetTile(index, tilesetID);
+ }
+
+ ///
+ /// Gets the texture region of the tile from this tilemap at the specified index.
+ ///
+ /// The index of the tile in this tilemap.
+ /// The texture region of the tile from this tilemap at the specified index.
+ public TextureRegion GetTile(int index)
+ {
+ return _tileset.GetTile(_tiles[index]);
+ }
+
+ ///
+ /// Gets the texture region of the tile frm this tilemap at the specified
+ /// column and row.
+ ///
+ /// The column of the tile in this tilemap.
+ /// The row of hte tile in this tilemap.
+ /// The texture region of the tile from this tilemap at the specified column and row.
+ public TextureRegion GetTile(int column, int row)
+ {
+ int index = row * Columns + column;
+ return GetTile(index);
+ }
+
+ ///
+ /// Draws this tilemap using the given sprite batch.
+ ///
+ /// The sprite batch used to draw this tilemap.
+ public void Draw(SpriteBatch spriteBatch)
+ {
+ for (int i = 0; i < Count; i++)
+ {
+ int tileSetIndex = _tiles[i];
+ TextureRegion tile = _tileset.GetTile(tileSetIndex);
+
+ int x = i % Columns;
+ int y = i / Columns;
+
+ Vector2 position = new Vector2(x * TileWidth, y * TileHeight);
+ tile.Draw(spriteBatch, position, Color.White, 0.0f, Vector2.Zero, Scale, SpriteEffects.None, 1.0f);
+ }
+ }
+
+ ///
+ /// Creates a new tilemap based on a tilemap xml configuration file.
+ ///
+ /// The content manager used to load the texture for the tileset.
+ /// The path to the xml file, relative to the content root directory.
+ /// The tilemap created by this method.
+ public static Tilemap FromFile(ContentManager content, string filename)
+ {
+ string filePath = Path.Combine(content.RootDirectory, filename);
+
+ using (Stream stream = TitleContainer.OpenStream(filePath))
+ {
+ using (XmlReader reader = XmlReader.Create(stream))
+ {
+ XDocument doc = XDocument.Load(reader);
+ XElement root = doc.Root;
+
+ // The element contains the information about the tileset
+ // used by the tilemap.
+ //
+ // Example
+ // contentPath
+ //
+ // The region attribute represents the x, y, width, and height
+ // components of the boundary for the texture region within the
+ // texture at the contentPath specified.
+ //
+ // the tileWidth and tileHeight attributes specify the width and
+ // height of each tile in the tileset.
+ //
+ // the contentPath value is the contentPath to the texture to
+ // load that contains the tileset
+ XElement tilesetElement = root.Element("Tileset");
+
+ string regionAttribute = tilesetElement.Attribute("region").Value;
+ string[] split = regionAttribute.Split(" ", StringSplitOptions.RemoveEmptyEntries);
+ int x = int.Parse(split[0]);
+ int y = int.Parse(split[1]);
+ int width = int.Parse(split[2]);
+ int height = int.Parse(split[3]);
+
+ int tileWidth = int.Parse(tilesetElement.Attribute("tileWidth").Value);
+ int tileHeight = int.Parse(tilesetElement.Attribute("tileHeight").Value);
+ string contentPath = tilesetElement.Value;
+
+ // Load the texture 2d at the content path
+ Texture2D texture = content.Load(contentPath);
+
+ // Create the texture region from the texture
+ TextureRegion textureRegion = new TextureRegion(texture, x, y, width, height);
+
+ // Create the tileset using the texture region
+ Tileset tileset = new Tileset(textureRegion, tileWidth, tileHeight);
+
+ // The element contains lines of strings where each line
+ // represents a row in the tilemap. Each line is a space
+ // separated string where each element represents a column in that
+ // row. The value of the column is the id of the tile in the
+ // tileset to draw for that location.
+ //
+ // Example:
+ //
+ // 00 01 01 02
+ // 03 04 04 05
+ // 03 04 04 05
+ // 06 07 07 08
+ //
+ XElement tilesElement = root.Element("Tiles");
+
+ // Split the value of the tiles data into rows by splitting on
+ // the new line character
+ string[] rows = tilesElement.Value.Trim().Split('\n', StringSplitOptions.RemoveEmptyEntries);
+
+ // Split the value of the first row to determine the total number of columns
+ int columnCount = rows[0].Split(" ", StringSplitOptions.RemoveEmptyEntries).Length;
+
+ // Create the tilemap
+ Tilemap tilemap = new Tilemap(tileset, columnCount, rows.Length);
+
+ // Process each row
+ for (int row = 0; row < rows.Length; row++)
+ {
+ // Split the row into individual columns
+ string[] columns = rows[row].Trim().Split(" ", StringSplitOptions.RemoveEmptyEntries);
+
+ // Process each column of the current row
+ for (int column = 0; column < columnCount; column++)
+ {
+ // Get the tileset index for this location
+ int tilesetIndex = int.Parse(columns[column]);
+
+ // Get the texture region of that tile from the tileset
+ TextureRegion region = tileset.GetTile(tilesetIndex);
+
+ // Add that region to the tilemap at the row and column location
+ tilemap.SetTile(column, row, tilesetIndex);
+ }
+ }
+
+ return tilemap;
+ }
+ }
+ }
+}
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/MonoGameLibrary/Graphics/Tileset.cs b/Tutorials/MobileDeployment/03-AddIOSProject/MonoGameLibrary/Graphics/Tileset.cs
new file mode 100644
index 00000000..80c2e65a
--- /dev/null
+++ b/Tutorials/MobileDeployment/03-AddIOSProject/MonoGameLibrary/Graphics/Tileset.cs
@@ -0,0 +1,76 @@
+namespace MonoGameLibrary.Graphics;
+
+public class Tileset
+{
+ private readonly TextureRegion[] _tiles;
+
+ ///
+ /// Gets the width, in pixels, of each tile in this tileset.
+ ///
+ public int TileWidth { get; }
+
+ ///
+ /// Gets the height, in pixels, of each tile in this tileset.
+ ///
+ public int TileHeight { get; }
+
+ ///
+ /// Gets the total number of columns in this tileset.
+ ///
+ public int Columns { get; }
+
+ ///
+ /// Gets the total number of rows in this tileset.
+ ///
+ public int Rows { get; }
+
+ ///
+ /// Gets the total number of tiles in this tileset.
+ ///
+ public int Count { get; }
+
+ ///
+ /// Creates a new tileset based on the given texture region with the specified
+ /// tile width and height.
+ ///
+ /// The texture region that contains the tiles for the tileset.
+ /// The width of each tile in the tileset.
+ /// The height of each tile in the tileset.
+ public Tileset(TextureRegion textureRegion, int tileWidth, int tileHeight)
+ {
+ TileWidth = tileWidth;
+ TileHeight = tileHeight;
+ Columns = textureRegion.Width / tileWidth;
+ Rows = textureRegion.Height / tileHeight;
+ Count = Columns * Rows;
+
+ // Create the texture regions that make up each individual tile
+ _tiles = new TextureRegion[Count];
+
+ for (int i = 0; i < Count; i++)
+ {
+ int x = i % Columns * tileWidth;
+ int y = i / Columns * tileHeight;
+ _tiles[i] = new TextureRegion(textureRegion.Texture, textureRegion.SourceRectangle.X + x, textureRegion.SourceRectangle.Y + y, tileWidth, tileHeight);
+ }
+ }
+
+ ///
+ /// Gets the texture region for the tile from this tileset at the given index.
+ ///
+ /// The index of the texture region in this tile set.
+ /// The texture region for the tile form this tileset at the given index.
+ public TextureRegion GetTile(int index) => _tiles[index];
+
+ ///
+ /// Gets the texture region for the tile from this tileset at the given location.
+ ///
+ /// The column in this tileset of the texture region.
+ /// The row in this tileset of the texture region.
+ /// The texture region for the tile from this tileset at given location.
+ public TextureRegion GetTile(int column, int row)
+ {
+ int index = row * Columns + column;
+ return GetTile(index);
+ }
+}
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/MonoGameLibrary/Input/GamePadInfo.cs b/Tutorials/MobileDeployment/03-AddIOSProject/MonoGameLibrary/Input/GamePadInfo.cs
new file mode 100644
index 00000000..7fd16126
--- /dev/null
+++ b/Tutorials/MobileDeployment/03-AddIOSProject/MonoGameLibrary/Input/GamePadInfo.cs
@@ -0,0 +1,140 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Input;
+
+namespace MonoGameLibrary.Input;
+
+public class GamePadInfo
+{
+ private TimeSpan _vibrationTimeRemaining = TimeSpan.Zero;
+
+ ///
+ /// Gets the index of the player this gamepad is for.
+ ///
+ public PlayerIndex PlayerIndex { get; }
+
+ ///
+ /// Gets the state of input for this gamepad during the previous update cycle.
+ ///
+ public GamePadState PreviousState { get; private set; }
+
+ ///
+ /// Gets the state of input for this gamepad during the current update cycle.
+ ///
+ public GamePadState CurrentState { get; private set; }
+
+ ///
+ /// Gets a value that indicates if this gamepad is currently connected.
+ ///
+ public bool IsConnected => CurrentState.IsConnected;
+
+ ///
+ /// Gets the value of the left thumbstick of this gamepad.
+ ///
+ public Vector2 LeftThumbStick => CurrentState.ThumbSticks.Left;
+
+ ///
+ /// Gets the value of the right thumbstick of this gamepad.
+ ///
+ public Vector2 RightThumbStick => CurrentState.ThumbSticks.Right;
+
+ ///
+ /// Gets the value of the left trigger of this gamepad.
+ ///
+ public float LeftTrigger => CurrentState.Triggers.Left;
+
+ ///
+ /// Gets the value of the right trigger of this gamepad.
+ ///
+ public float RightTrigger => CurrentState.Triggers.Right;
+
+ ///
+ /// Creates a new GamePadInfo for the gamepad connected at the specified player index.
+ ///
+ /// The index of the player for this gamepad.
+ public GamePadInfo(PlayerIndex playerIndex)
+ {
+ PlayerIndex = playerIndex;
+ PreviousState = new GamePadState();
+ CurrentState = GamePad.GetState(playerIndex);
+ }
+
+ ///
+ /// Updates the state information for this gamepad input.
+ ///
+ ///
+ public void Update(GameTime gameTime)
+ {
+ PreviousState = CurrentState;
+ CurrentState = GamePad.GetState(PlayerIndex);
+
+ if (_vibrationTimeRemaining > TimeSpan.Zero)
+ {
+ _vibrationTimeRemaining -= gameTime.ElapsedGameTime;
+
+ if (_vibrationTimeRemaining <= TimeSpan.Zero)
+ {
+ StopVibration();
+ }
+ }
+ }
+
+ ///
+ /// Returns a value that indicates whether the specified gamepad button is current down.
+ ///
+ /// The gamepad button to check.
+ /// true if the specified gamepad button is currently down; otherwise, false.
+ public bool IsButtonDown(Buttons button)
+ {
+ return CurrentState.IsButtonDown(button);
+ }
+
+ ///
+ /// Returns a value that indicates whether the specified gamepad button is currently up.
+ ///
+ /// The gamepad button to check.
+ /// true if the specified gamepad button is currently up; otherwise, false.
+ public bool IsButtonUp(Buttons button)
+ {
+ return CurrentState.IsButtonUp(button);
+ }
+
+ ///
+ /// Returns a value that indicates whether the specified gamepad button was just pressed on the current frame.
+ ///
+ ///
+ /// true if the specified gamepad button was just pressed on the current frame; otherwise, false.
+ public bool WasButtonJustPressed(Buttons button)
+ {
+ return CurrentState.IsButtonDown(button) && PreviousState.IsButtonUp(button);
+ }
+
+ ///
+ /// Returns a value that indicates whether the specified gamepad button was just released on the current frame.
+ ///
+ ///
+ /// true if the specified gamepad button was just released on the current frame; otherwise, false.
+ public bool WasButtonJustReleased(Buttons button)
+ {
+ return CurrentState.IsButtonUp(button) && PreviousState.IsButtonDown(button);
+ }
+
+ ///
+ /// Sets the vibration for all motors of this gamepad.
+ ///
+ /// The strength of the vibration from 0.0f (none) to 1.0f (full).
+ /// The amount of time the vibration should occur.
+ public void SetVibration(float strength, TimeSpan time)
+ {
+ _vibrationTimeRemaining = time;
+ GamePad.SetVibration(PlayerIndex, strength, strength);
+ }
+
+ ///
+ /// Stops the vibration of all motors for this gamepad.
+ ///
+ public void StopVibration()
+ {
+ GamePad.SetVibration(PlayerIndex, 0.0f, 0.0f);
+ }
+}
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/MonoGameLibrary/Input/InputManager.cs b/Tutorials/MobileDeployment/03-AddIOSProject/MonoGameLibrary/Input/InputManager.cs
new file mode 100644
index 00000000..1790eb70
--- /dev/null
+++ b/Tutorials/MobileDeployment/03-AddIOSProject/MonoGameLibrary/Input/InputManager.cs
@@ -0,0 +1,52 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGameLibrary.Input;
+
+public class InputManager
+{
+ ///
+ /// Gets the state information of keyboard input.
+ ///
+ public KeyboardInfo Keyboard { get; private set; }
+
+ ///
+ /// Gets the state information of mouse input.
+ ///
+ public MouseInfo Mouse { get; private set; }
+
+ ///
+ /// Gets the state information of a gamepad.
+ ///
+ public GamePadInfo[] GamePads { get; private set; }
+
+ ///
+ /// Creates a new InputManager.
+ ///
+ /// The game this input manager belongs to.
+ public InputManager()
+ {
+ Keyboard = new KeyboardInfo();
+ Mouse = new MouseInfo();
+
+ GamePads = new GamePadInfo[4];
+ for (int i = 0; i < 4; i++)
+ {
+ GamePads[i] = new GamePadInfo((PlayerIndex)i);
+ }
+ }
+
+ ///
+ /// Updates the state information for the keyboard, mouse, and gamepad inputs.
+ ///
+ /// A snapshot of the timing values for the current frame.
+ public void Update(GameTime gameTime)
+ {
+ Keyboard.Update();
+ Mouse.Update();
+
+ for (int i = 0; i < 4; i++)
+ {
+ GamePads[i].Update(gameTime);
+ }
+ }
+}
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/MonoGameLibrary/Input/KeyboardInfo.cs b/Tutorials/MobileDeployment/03-AddIOSProject/MonoGameLibrary/Input/KeyboardInfo.cs
new file mode 100644
index 00000000..c6770cb0
--- /dev/null
+++ b/Tutorials/MobileDeployment/03-AddIOSProject/MonoGameLibrary/Input/KeyboardInfo.cs
@@ -0,0 +1,74 @@
+using Microsoft.Xna.Framework.Input;
+
+namespace MonoGameLibrary.Input;
+
+public class KeyboardInfo
+{
+ ///
+ /// Gets the state of keyboard input during the previous update cycle.
+ ///
+ public KeyboardState PreviousState { get; private set; }
+
+ ///
+ /// Gets the state of keyboard input during the current input cycle.
+ ///
+ public KeyboardState CurrentState { get; private set; }
+
+ ///
+ /// Creates a new KeyboardInfo
+ ///
+ public KeyboardInfo()
+ {
+ PreviousState = new KeyboardState();
+ CurrentState = Keyboard.GetState();
+ }
+
+ ///
+ /// Updates the state information about keyboard input.
+ ///
+ public void Update()
+ {
+ PreviousState = CurrentState;
+ CurrentState = Keyboard.GetState();
+ }
+
+ ///
+ /// Returns a value that indicates if the specified key is currently down.
+ ///
+ /// The key to check.
+ /// true if the specified key is currently down; otherwise, false.
+ public bool IsKeyDown(Keys key)
+ {
+ return CurrentState.IsKeyDown(key);
+ }
+
+ ///
+ /// Returns a value that indicates whether the specified key is currently up.
+ ///
+ /// The key to check.
+ /// true if the specified key is currently up; otherwise, false.
+ public bool IsKeyUp(Keys key)
+ {
+ return CurrentState.IsKeyUp(key);
+ }
+
+ ///
+ /// Returns a value that indicates if the specified key was just pressed on the current frame.
+ ///
+ /// The key to check.
+ /// true if the specified key was just pressed on the current frame; otherwise, false.
+ public bool WasKeyJustPressed(Keys key)
+ {
+ return CurrentState.IsKeyDown(key) && PreviousState.IsKeyUp(key);
+ }
+
+ ///
+ /// Returns a value that indicates if the specified key was just released on the current frame.
+ ///
+ /// The key to check.
+ /// true if the specified key was just released on the current frame; otherwise, false.
+ public bool WasKeyJustReleased(Keys key)
+ {
+ return CurrentState.IsKeyUp(key) && PreviousState.IsKeyDown(key);
+ }
+}
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/MonoGameLibrary/Input/MouseButton.cs b/Tutorials/MobileDeployment/03-AddIOSProject/MonoGameLibrary/Input/MouseButton.cs
new file mode 100644
index 00000000..5b041f80
--- /dev/null
+++ b/Tutorials/MobileDeployment/03-AddIOSProject/MonoGameLibrary/Input/MouseButton.cs
@@ -0,0 +1,10 @@
+namespace MonoGameLibrary.Input;
+
+public enum MouseButton
+{
+ Left,
+ Middle,
+ Right,
+ XButton1,
+ XButton2
+}
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/MonoGameLibrary/Input/MouseInfo.cs b/Tutorials/MobileDeployment/03-AddIOSProject/MonoGameLibrary/Input/MouseInfo.cs
new file mode 100644
index 00000000..09d6207c
--- /dev/null
+++ b/Tutorials/MobileDeployment/03-AddIOSProject/MonoGameLibrary/Input/MouseInfo.cs
@@ -0,0 +1,208 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Input;
+
+namespace MonoGameLibrary.Input;
+
+public class MouseInfo
+{
+ ///
+ /// The state of mouse input during the previous update cycle.
+ ///
+ public MouseState PreviousState { get; private set; }
+
+ ///
+ /// The state of mouse input during the current update cycle.
+ ///
+ public MouseState CurrentState { get; private set; }
+
+ ///
+ /// Gets or Sets the current position of the mouse cursor in screen space.
+ ///
+ public Point Position
+ {
+ get => CurrentState.Position;
+ set => SetPosition(value.X, value.Y);
+ }
+
+ ///
+ /// Gets or Sets the current x-coordinate position of the mouse cursor in screen space.
+ ///
+ public int X
+ {
+ get => CurrentState.X;
+ set => SetPosition(value, CurrentState.Y);
+ }
+
+ ///
+ /// Gets or Sets the current y-coordinate position of the mouse cursor in screen space.
+ ///
+ public int Y
+ {
+ get => CurrentState.Y;
+ set => SetPosition(CurrentState.X, value);
+ }
+
+ ///
+ /// Gets the difference in the mouse cursor position between the previous and current frame.
+ ///
+ public Point PositionDelta => CurrentState.Position - PreviousState.Position;
+
+ ///
+ /// Gets the difference in the mouse cursor x-position between the previous and current frame.
+ ///
+ public int XDelta => CurrentState.X - PreviousState.X;
+
+ ///
+ /// Gets the difference in the mouse cursor y-position between the previous and current frame.
+ ///
+ public int YDelta => CurrentState.Y - PreviousState.Y;
+
+ ///
+ /// Gets a value that indicates if the mouse cursor moved between the previous and current frames.
+ ///
+ public bool WasMoved => PositionDelta != Point.Zero;
+
+ ///
+ /// Gets the cumulative value of the mouse scroll wheel since the start of the game.
+ ///
+ public int ScrollWheel => CurrentState.ScrollWheelValue;
+
+ ///
+ /// Gets the value of the scroll wheel between the previous and current frame.
+ ///
+ public int ScrollWheelDelta => CurrentState.ScrollWheelValue - PreviousState.ScrollWheelValue;
+
+ ///
+ /// Creates a new MouseInfo.
+ ///
+ public MouseInfo()
+ {
+ PreviousState = new MouseState();
+ CurrentState = Mouse.GetState();
+ }
+
+ ///
+ /// Updates the state information about mouse input.
+ ///
+ public void Update()
+ {
+ PreviousState = CurrentState;
+ CurrentState = Mouse.GetState();
+ }
+
+ ///
+ /// Returns a value that indicates whether the specified mouse button is currently down.
+ ///
+ /// The mouse button to check.
+ /// true if the specified mouse button is currently down; otherwise, false.
+ public bool IsButtonDown(MouseButton button)
+ {
+ switch (button)
+ {
+ case MouseButton.Left:
+ return CurrentState.LeftButton == ButtonState.Pressed;
+ case MouseButton.Middle:
+ return CurrentState.MiddleButton == ButtonState.Pressed;
+ case MouseButton.Right:
+ return CurrentState.RightButton == ButtonState.Pressed;
+ case MouseButton.XButton1:
+ return CurrentState.XButton1 == ButtonState.Pressed;
+ case MouseButton.XButton2:
+ return CurrentState.XButton2 == ButtonState.Pressed;
+ default:
+ return false;
+ }
+ }
+
+ ///
+ /// Returns a value that indicates whether the specified mouse button is current up.
+ ///
+ /// The mouse button to check.
+ /// true if the specified mouse button is currently up; otherwise, false.
+ public bool IsButtonUp(MouseButton button)
+ {
+ switch (button)
+ {
+ case MouseButton.Left:
+ return CurrentState.LeftButton == ButtonState.Released;
+ case MouseButton.Middle:
+ return CurrentState.MiddleButton == ButtonState.Released;
+ case MouseButton.Right:
+ return CurrentState.RightButton == ButtonState.Released;
+ case MouseButton.XButton1:
+ return CurrentState.XButton1 == ButtonState.Released;
+ case MouseButton.XButton2:
+ return CurrentState.XButton2 == ButtonState.Released;
+ default:
+ return false;
+ }
+ }
+
+ ///
+ /// Returns a value that indicates whether the specified mouse button was just pressed on the current frame.
+ ///
+ /// The mouse button to check.
+ /// true if the specified mouse button was just pressed on the current frame; otherwise, false.
+ public bool WasButtonJustPressed(MouseButton button)
+ {
+ switch (button)
+ {
+ case MouseButton.Left:
+ return CurrentState.LeftButton == ButtonState.Pressed && PreviousState.LeftButton == ButtonState.Released;
+ case MouseButton.Middle:
+ return CurrentState.MiddleButton == ButtonState.Pressed && PreviousState.MiddleButton == ButtonState.Released;
+ case MouseButton.Right:
+ return CurrentState.RightButton == ButtonState.Pressed && PreviousState.RightButton == ButtonState.Released;
+ case MouseButton.XButton1:
+ return CurrentState.XButton1 == ButtonState.Pressed && PreviousState.XButton1 == ButtonState.Released;
+ case MouseButton.XButton2:
+ return CurrentState.XButton2 == ButtonState.Pressed && PreviousState.XButton2 == ButtonState.Released;
+ default:
+ return false;
+ }
+ }
+
+ ///
+ /// Returns a value that indicates whether the specified mouse button was just released on the current frame.
+ ///
+ /// The mouse button to check.
+ /// true if the specified mouse button was just released on the current frame; otherwise, false.F
+ public bool WasButtonJustReleased(MouseButton button)
+ {
+ switch (button)
+ {
+ case MouseButton.Left:
+ return CurrentState.LeftButton == ButtonState.Released && PreviousState.LeftButton == ButtonState.Pressed;
+ case MouseButton.Middle:
+ return CurrentState.MiddleButton == ButtonState.Released && PreviousState.MiddleButton == ButtonState.Pressed;
+ case MouseButton.Right:
+ return CurrentState.RightButton == ButtonState.Released && PreviousState.RightButton == ButtonState.Pressed;
+ case MouseButton.XButton1:
+ return CurrentState.XButton1 == ButtonState.Released && PreviousState.XButton1 == ButtonState.Pressed;
+ case MouseButton.XButton2:
+ return CurrentState.XButton2 == ButtonState.Released && PreviousState.XButton2 == ButtonState.Pressed;
+ default:
+ return false;
+ }
+ }
+
+ ///
+ /// Sets the current position of the mouse cursor in screen space and updates the CurrentState with the new position.
+ ///
+ /// The x-coordinate location of the mouse cursor in screen space.
+ /// The y-coordinate location of the mouse cursor in screen space.
+ public void SetPosition(int x, int y)
+ {
+ Mouse.SetPosition(x, y);
+ CurrentState = new MouseState(
+ x,
+ y,
+ CurrentState.ScrollWheelValue,
+ CurrentState.LeftButton,
+ CurrentState.MiddleButton,
+ CurrentState.RightButton,
+ CurrentState.XButton1,
+ CurrentState.XButton2
+ );
+ }
+}
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/MonoGameLibrary/MonoGameLibrary.csproj b/Tutorials/MobileDeployment/03-AddIOSProject/MonoGameLibrary/MonoGameLibrary.csproj
new file mode 100644
index 00000000..2d0d40b2
--- /dev/null
+++ b/Tutorials/MobileDeployment/03-AddIOSProject/MonoGameLibrary/MonoGameLibrary.csproj
@@ -0,0 +1,11 @@
+
+
+ net9.0;net9.0-ios
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/MonoGameLibrary/Scenes/Scene.cs b/Tutorials/MobileDeployment/03-AddIOSProject/MonoGameLibrary/Scenes/Scene.cs
new file mode 100644
index 00000000..627d220f
--- /dev/null
+++ b/Tutorials/MobileDeployment/03-AddIOSProject/MonoGameLibrary/Scenes/Scene.cs
@@ -0,0 +1,104 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content;
+
+namespace MonoGameLibrary.Scenes;
+
+public abstract class Scene : IDisposable
+{
+ ///
+ /// Gets the ContentManager used for loading scene-specific assets.
+ ///
+ ///
+ /// Assets loaded through this ContentManager will be automatically unloaded when this scene ends.
+ ///
+ protected ContentManager Content { get; }
+
+ ///
+ /// Gets a value that indicates if the scene has been disposed of.
+ ///
+ public bool IsDisposed { get; private set; }
+
+ ///
+ /// Creates a new scene instance.
+ ///
+ public Scene()
+ {
+ // Create a content manager for the scene
+ Content = new ContentManager(Core.Content.ServiceProvider);
+
+ // Set the root directory for content to the same as the root directory
+ // for the game's content.
+ Content.RootDirectory = Core.Content.RootDirectory;
+ }
+
+ // Finalizer, called when object is cleaned up by garbage collector.
+ ~Scene() => Dispose(false);
+
+ ///
+ /// Initializes the scene.
+ ///
+ ///
+ /// When overriding this in a derived class, ensure that base.Initialize()
+ /// still called as this is when LoadContent is called.
+ ///
+ public virtual void Initialize()
+ {
+ LoadContent();
+ }
+
+ ///
+ /// Override to provide logic to load content for the scene.
+ ///
+ public virtual void LoadContent() { }
+
+ ///
+ /// Unloads scene-specific content.
+ ///
+ public virtual void UnloadContent()
+ {
+ Content.Unload();
+ }
+
+ ///
+ /// Updates this scene.
+ ///
+ /// A snapshot of the timing values for the current frame.
+ public virtual void Update(GameTime gameTime) { }
+
+ ///
+ /// Draws this scene.
+ ///
+ /// A snapshot of the timing values for the current frame.
+ public virtual void Draw(GameTime gameTime) { }
+
+ ///
+ /// Disposes of this scene.
+ ///
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ /// Disposes of this scene.
+ ///
+ /// '
+ /// Indicates whether managed resources should be disposed. This value is only true when called from the main
+ /// Dispose method. When called from the finalizer, this will be false.
+ ///
+ protected virtual void Dispose(bool disposing)
+ {
+ if (IsDisposed)
+ {
+ return;
+ }
+
+ if (disposing)
+ {
+ UnloadContent();
+ Content.Dispose();
+ }
+ }
+}
diff --git a/Tutorials/MobileDeployment/03-AddIOSProject/README.md b/Tutorials/MobileDeployment/03-AddIOSProject/README.md
new file mode 100644
index 00000000..ca21fe92
--- /dev/null
+++ b/Tutorials/MobileDeployment/03-AddIOSProject/README.md
@@ -0,0 +1,30 @@
+# Chapter 3: Adding IOS Project
+
+This chapter demonstrates how to convert a Windows-only MonoGame project to support iOS platforms.
+
+The chapter covers:
+
+* Converting a single-platform project to multi-platform structure.
+* Creating platform-specific project shells for Windows, and iOS.
+* Configuring conditional package references for each platform.
+* Understanding cross-platform project architecture and naming conventions.
+* Updating third-party libraries for cross-platform compatibility.
+
+## Project Structure
+
+This sample includes:
+
+* **DungeonSlime** - Windows desktop project shell
+* **DungeonSlime.iOS** - iOS mobile project shell
+
+## Prerequisites
+
+* Completed the MonoGame 2D tutorial
+* Development environment set up
+* For iOS: Mac with Xcode and Apple Developer account
+
+## Key Features Demonstrated
+
+* Multi-targeting framework configuration (`net8.0;net8.0-ios`)
+* Platform-specific MonoGame package references
+* Modern .NET project management with Central Package Management
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/.config/dotnet-tools.json b/Tutorials/MobileDeployment/04-AddAndroidProject/.config/dotnet-tools.json
new file mode 100644
index 00000000..7f4505d0
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/.config/dotnet-tools.json
@@ -0,0 +1,36 @@
+{
+ "version": 1,
+ "isRoot": true,
+ "tools": {
+ "dotnet-mgcb": {
+ "version": "3.8.4.1",
+ "commands": [
+ "mgcb"
+ ]
+ },
+ "dotnet-mgcb-editor": {
+ "version": "3.8.4.1",
+ "commands": [
+ "mgcb-editor"
+ ]
+ },
+ "dotnet-mgcb-editor-linux": {
+ "version": "3.8.4.1",
+ "commands": [
+ "mgcb-editor-linux"
+ ]
+ },
+ "dotnet-mgcb-editor-windows": {
+ "version": "3.8.4.1",
+ "commands": [
+ "mgcb-editor-windows"
+ ]
+ },
+ "dotnet-mgcb-editor-mac": {
+ "version": "3.8.4.1",
+ "commands": [
+ "mgcb-editor-mac"
+ ]
+ }
+ }
+}
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/Directory.Packages.props b/Tutorials/MobileDeployment/04-AddAndroidProject/Directory.Packages.props
new file mode 100644
index 00000000..fc28fedb
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/Directory.Packages.props
@@ -0,0 +1,11 @@
+
+
+ true
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/.config/dotnet-tools.json b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/.config/dotnet-tools.json
new file mode 100644
index 00000000..fbedee15
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/.config/dotnet-tools.json
@@ -0,0 +1,36 @@
+{
+ "version": 1,
+ "isRoot": true,
+ "tools": {
+ "dotnet-mgcb": {
+ "version": "3.8.4",
+ "commands": [
+ "mgcb"
+ ]
+ },
+ "dotnet-mgcb-editor": {
+ "version": "3.8.4",
+ "commands": [
+ "mgcb-editor"
+ ]
+ },
+ "dotnet-mgcb-editor-linux": {
+ "version": "3.8.4",
+ "commands": [
+ "mgcb-editor-linux"
+ ]
+ },
+ "dotnet-mgcb-editor-windows": {
+ "version": "3.8.4",
+ "commands": [
+ "mgcb-editor-windows"
+ ]
+ },
+ "dotnet-mgcb-editor-mac": {
+ "version": "3.8.4",
+ "commands": [
+ "mgcb-editor-mac"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Activity1.cs b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Activity1.cs
new file mode 100644
index 00000000..3a192c4c
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Activity1.cs
@@ -0,0 +1,35 @@
+using Android.App;
+using Android.Content.PM;
+using Android.OS;
+using Android.Views;
+using Microsoft.Xna.Framework;
+
+namespace DungeonSlime.Android
+{
+ [Activity(
+ Label = "@string/app_name",
+ MainLauncher = true,
+ Icon = "@drawable/icon",
+ AlwaysRetainTaskState = true,
+ LaunchMode = LaunchMode.SingleInstance,
+ ScreenOrientation = ScreenOrientation.FullUser,
+ ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.Keyboard | ConfigChanges.KeyboardHidden |
+ ConfigChanges.ScreenSize
+ )]
+ public class Activity1 : AndroidGameActivity
+ {
+ private Game1 _game;
+ private View _view;
+
+ protected override void OnCreate(Bundle bundle)
+ {
+ base.OnCreate(bundle);
+
+ _game = new Game1();
+ _view = _game.Services.GetService(typeof(View)) as View;
+
+ SetContentView(_view);
+ _game.Run();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/AndroidManifest.xml b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/AndroidManifest.xml
new file mode 100644
index 00000000..f5c7b036
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/AndroidManifest.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Content/Content.mgcb b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Content/Content.mgcb
new file mode 100644
index 00000000..d35c0c7f
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Content/Content.mgcb
@@ -0,0 +1,104 @@
+
+#----------------------------- Global Properties ----------------------------#
+
+/outputDir:bin/$(Platform)
+/intermediateDir:obj/$(Platform)
+/platform:Android
+/config:
+/profile:Reach
+/compress:False
+
+#-------------------------------- References --------------------------------#
+
+
+#---------------------------------- Content ---------------------------------#
+
+#begin audio/bounce.wav
+/importer:WavImporter
+/processor:SoundEffectProcessor
+/processorParam:Quality=Best
+/build:audio/bounce.wav
+
+#begin audio/collect.wav
+/importer:WavImporter
+/processor:SoundEffectProcessor
+/processorParam:Quality=Best
+/build:audio/collect.wav
+
+#begin audio/theme.ogg
+/importer:OggImporter
+/processor:SongProcessor
+/processorParam:Quality=Best
+/build:audio/theme.ogg
+
+#begin audio/ui.wav
+/importer:WavImporter
+/processor:SoundEffectProcessor
+/processorParam:Quality=Best
+/build:audio/ui.wav
+
+#begin effects/grayscaleEffect.fx
+/importer:EffectImporter
+/processor:EffectProcessor
+/processorParam:DebugMode=Auto
+/build:effects/grayscaleEffect.fx
+
+#begin fonts/04B_30_5x.spritefont
+/importer:FontDescriptionImporter
+/processor:FontDescriptionProcessor
+/processorParam:PremultiplyAlpha=True
+/processorParam:TextureFormat=Compressed
+/build:fonts/04B_30_5x.spritefont
+
+#begin fonts/04b_30.fnt
+/copy:fonts/04b_30.fnt
+
+#begin fonts/04B_30.spritefont
+/importer:FontDescriptionImporter
+/processor:FontDescriptionProcessor
+/processorParam:PremultiplyAlpha=True
+/processorParam:TextureFormat=Compressed
+/build:fonts/04B_30.spritefont
+
+#begin images/atlas-definition.xml
+/copy:images/atlas-definition.xml
+
+#begin images/atlas.png
+/importer:TextureImporter
+/processor:TextureProcessor
+/processorParam:ColorKeyColor=255,0,255,255
+/processorParam:ColorKeyEnabled=True
+/processorParam:GenerateMipmaps=False
+/processorParam:PremultiplyAlpha=True
+/processorParam:ResizeToPowerOfTwo=False
+/processorParam:MakeSquare=False
+/processorParam:TextureFormat=Color
+/build:images/atlas.png
+
+#begin images/background-pattern.png
+/importer:TextureImporter
+/processor:TextureProcessor
+/processorParam:ColorKeyColor=255,0,255,255
+/processorParam:ColorKeyEnabled=True
+/processorParam:GenerateMipmaps=False
+/processorParam:PremultiplyAlpha=True
+/processorParam:ResizeToPowerOfTwo=False
+/processorParam:MakeSquare=False
+/processorParam:TextureFormat=Color
+/build:images/background-pattern.png
+
+#begin images/logo.png
+/importer:TextureImporter
+/processor:TextureProcessor
+/processorParam:ColorKeyColor=255,0,255,255
+/processorParam:ColorKeyEnabled=True
+/processorParam:GenerateMipmaps=False
+/processorParam:PremultiplyAlpha=True
+/processorParam:ResizeToPowerOfTwo=False
+/processorParam:MakeSquare=False
+/processorParam:TextureFormat=Color
+/build:images/logo.png
+
+#begin images/tilemap-definition.xml
+/copy:images/tilemap-definition.xml
+
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Content/audio/bounce.wav b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Content/audio/bounce.wav
new file mode 100644
index 00000000..baa7a47b
Binary files /dev/null and b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Content/audio/bounce.wav differ
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Content/audio/collect.wav b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Content/audio/collect.wav
new file mode 100644
index 00000000..506220de
Binary files /dev/null and b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Content/audio/collect.wav differ
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Content/audio/theme.ogg b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Content/audio/theme.ogg
new file mode 100644
index 00000000..72e1fd3b
Binary files /dev/null and b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Content/audio/theme.ogg differ
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Content/audio/ui.wav b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Content/audio/ui.wav
new file mode 100644
index 00000000..63e8941e
Binary files /dev/null and b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Content/audio/ui.wav differ
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Content/effects/grayscaleEffect.fx b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Content/effects/grayscaleEffect.fx
new file mode 100644
index 00000000..5dd0d8b6
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Content/effects/grayscaleEffect.fx
@@ -0,0 +1,53 @@
+#if OPENGL
+ #define SV_POSITION POSITION
+ #define VS_SHADERMODEL vs_3_0
+ #define PS_SHADERMODEL ps_3_0
+#else
+ #define VS_SHADERMODEL vs_4_0_level_9_1
+ #define PS_SHADERMODEL ps_4_0_level_9_1
+#endif
+
+Texture2D SpriteTexture;
+
+// A value between 0 and 1 that controls the intensity of the grayscale effect.
+// 0 = full color, 1 = full grayscale.
+float Saturation = 1.0;
+
+sampler2D SpriteTextureSampler = sampler_state
+{
+ Texture = ;
+};
+
+struct VertexShaderOutput
+{
+ float4 Position : SV_POSITION;
+ float4 Color : COLOR0;
+ float2 TextureCoordinates : TEXCOORD0;
+};
+
+float4 MainPS(VertexShaderOutput input) : COLOR
+{
+ // Sample the texture
+ float4 color = tex2D(SpriteTextureSampler, input.TextureCoordinates) * input.Color;
+
+ // Calculate the grayscale value based on human perception of colors
+ float grayscale = dot(color.rgb, float3(0.3, 0.59, 0.11));
+
+ // create a grayscale color vector (same value for R, G, and B)
+ float3 grayscaleColor = float3(grayscale, grayscale, grayscale);
+
+ // Linear interpolation between he grayscale color and the original color's
+ // rgb values based on the saturation parameter.
+ float3 finalColor = lerp(grayscale, color.rgb, Saturation);
+
+ // Return the final color with the original alpha value
+ return float4(finalColor, color.a);
+}
+
+technique SpriteDrawing
+{
+ pass P0
+ {
+ PixelShader = compile PS_SHADERMODEL MainPS();
+ }
+};
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Content/fonts/04B_30.spritefont b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Content/fonts/04B_30.spritefont
new file mode 100644
index 00000000..63d4728c
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Content/fonts/04B_30.spritefont
@@ -0,0 +1,16 @@
+
+
+
+ 04B_30.ttf
+ 17.5
+ 0
+ true
+
+
+
+
+ ~
+
+
+
+
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Content/fonts/04B_30.ttf b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Content/fonts/04B_30.ttf
new file mode 100644
index 00000000..4b93740c
Binary files /dev/null and b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Content/fonts/04B_30.ttf differ
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Content/fonts/04B_30_5x.spritefont b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Content/fonts/04B_30_5x.spritefont
new file mode 100644
index 00000000..dd239a53
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Content/fonts/04B_30_5x.spritefont
@@ -0,0 +1,16 @@
+
+
+
+ 04B_30.ttf
+ 87.5
+ 0
+ true
+
+
+
+
+ ~
+
+
+
+
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Content/fonts/04b_30.fnt b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Content/fonts/04b_30.fnt
new file mode 100644
index 00000000..772f8c54
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Content/fonts/04b_30.fnt
@@ -0,0 +1,99 @@
+info face="04b30" size=35 bold=0 italic=0 charset="" unicode=1 stretchH=100 smooth=0 aa=1 padding=0,0,0,0 spacing=1,1 outline=0
+common lineHeight=35 base=31 scaleW=256 scaleH=512 pages=1 packed=0 alphaChnl=0 redChnl=4 greenChnl=4 blueChnl=4
+page id=0 file="../images/atlas.png"
+chars count=95
+char id=32 x=30 y=152 width=3 height=1 xoffset=-1 yoffset=34 xadvance=29 page=0 chnl=15
+char id=33 x=240 y=30 width=14 height=29 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=34 x=102 y=232 width=25 height=15 xoffset=1 yoffset=4 xadvance=29 page=0 chnl=15
+char id=35 x=184 y=0 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=36 x=250 y=150 width=3 height=1 xoffset=-1 yoffset=34 xadvance=29 page=0 chnl=15
+char id=37 x=0 y=34 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=38 x=30 y=150 width=3 height=1 xoffset=-1 yoffset=34 xadvance=29 page=0 chnl=15
+char id=39 x=245 y=202 width=10 height=15 xoffset=1 yoffset=4 xadvance=14 page=0 chnl=15
+char id=40 x=106 y=150 width=16 height=29 xoffset=1 yoffset=2 xadvance=21 page=0 chnl=15
+char id=41 x=123 y=150 width=16 height=29 xoffset=1 yoffset=2 xadvance=21 page=0 chnl=15
+char id=42 x=128 y=232 width=14 height=15 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=43 x=94 y=0 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=44 x=143 y=232 width=10 height=14 xoffset=1 yoffset=19 xadvance=14 page=0 chnl=15
+char id=45 x=154 y=232 width=25 height=11 xoffset=1 yoffset=12 xadvance=29 page=0 chnl=15
+char id=46 x=231 y=228 width=10 height=10 xoffset=1 yoffset=19 xadvance=14 page=0 chnl=15
+char id=47 x=60 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=48 x=90 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=49 x=46 y=150 width=19 height=29 xoffset=1 yoffset=2 xadvance=23 page=0 chnl=15
+char id=50 x=150 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=51 x=180 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=52 x=210 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=53 x=0 y=94 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=54 x=180 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=55 x=60 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=56 x=90 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=57 x=120 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=58 x=234 y=202 width=10 height=25 xoffset=1 yoffset=4 xadvance=14 page=0 chnl=15
+char id=59 x=244 y=0 width=10 height=29 xoffset=1 yoffset=4 xadvance=14 page=0 chnl=15
+char id=60 x=86 y=150 width=19 height=29 xoffset=1 yoffset=2 xadvance=23 page=0 chnl=15
+char id=61 x=182 y=176 width=25 height=25 xoffset=1 yoffset=4 xadvance=29 page=0 chnl=15
+char id=62 x=237 y=120 width=18 height=29 xoffset=1 yoffset=2 xadvance=23 page=0 chnl=15
+char id=63 x=180 y=120 width=28 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=64 x=34 y=150 width=3 height=1 xoffset=-1 yoffset=34 xadvance=29 page=0 chnl=15
+char id=65 x=120 y=120 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=66 x=150 y=120 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=67 x=124 y=0 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=68 x=154 y=0 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=69 x=214 y=0 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=70 x=30 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=71 x=60 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=72 x=90 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=73 x=240 y=90 width=14 height=29 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=74 x=120 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=75 x=150 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=76 x=209 y=120 width=27 height=29 xoffset=1 yoffset=2 xadvance=31 page=0 chnl=15
+char id=77 x=30 y=0 width=31 height=29 xoffset=1 yoffset=2 xadvance=35 page=0 chnl=15
+char id=78 x=210 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=79 x=0 y=64 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=80 x=30 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=81 x=0 y=0 width=29 height=33 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=82 x=120 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=83 x=30 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=84 x=150 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=85 x=180 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=86 x=210 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=87 x=62 y=0 width=31 height=29 xoffset=1 yoffset=2 xadvance=35 page=0 chnl=15
+char id=88 x=0 y=124 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=89 x=30 y=120 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=90 x=60 y=120 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=91 x=240 y=60 width=14 height=29 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=92 x=90 y=120 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=93 x=140 y=150 width=14 height=29 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=94 x=180 y=232 width=16 height=11 xoffset=1 yoffset=4 xadvance=21 page=0 chnl=15
+char id=95 x=0 y=262 width=29 height=10 xoffset=1 yoffset=21 xadvance=33 page=0 chnl=15
+char id=96 x=197 y=228 width=16 height=11 xoffset=1 yoffset=4 xadvance=21 page=0 chnl=15
+char id=97 x=208 y=176 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=98 x=0 y=210 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=99 x=26 y=210 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=100 x=52 y=206 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=101 x=78 y=206 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=102 x=104 y=206 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=103 x=130 y=206 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=104 x=156 y=206 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=105 x=234 y=176 width=12 height=25 xoffset=1 yoffset=6 xadvance=16 page=0 chnl=15
+char id=106 x=182 y=202 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=107 x=208 y=202 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=108 x=78 y=232 width=23 height=25 xoffset=1 yoffset=6 xadvance=27 page=0 chnl=15
+char id=109 x=197 y=150 width=26 height=25 xoffset=1 yoffset=6 xadvance=31 page=0 chnl=15
+char id=110 x=0 y=236 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=111 x=26 y=236 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=112 x=78 y=180 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=113 x=0 y=154 width=25 height=29 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=114 x=52 y=232 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=115 x=224 y=150 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=116 x=0 y=184 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=117 x=26 y=184 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=118 x=52 y=180 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=119 x=170 y=150 width=26 height=25 xoffset=1 yoffset=6 xadvance=31 page=0 chnl=15
+char id=120 x=104 y=180 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=121 x=130 y=180 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=122 x=156 y=180 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=123 x=26 y=154 width=19 height=29 xoffset=1 yoffset=2 xadvance=23 page=0 chnl=15
+char id=124 x=155 y=150 width=14 height=29 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=125 x=66 y=150 width=19 height=29 xoffset=1 yoffset=2 xadvance=23 page=0 chnl=15
+char id=126 x=214 y=228 width=16 height=11 xoffset=1 yoffset=4 xadvance=21 page=0 chnl=15
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Content/images/atlas-definition.xml b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Content/images/atlas-definition.xml
new file mode 100644
index 00000000..21772022
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Content/images/atlas-definition.xml
@@ -0,0 +1,34 @@
+
+
+ images/atlas
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Content/images/atlas.png b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Content/images/atlas.png
new file mode 100644
index 00000000..f7def20f
Binary files /dev/null and b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Content/images/atlas.png differ
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Content/images/background-pattern.png b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Content/images/background-pattern.png
new file mode 100644
index 00000000..2d8d878e
Binary files /dev/null and b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Content/images/background-pattern.png differ
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Content/images/logo.png b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Content/images/logo.png
new file mode 100644
index 00000000..1509036c
Binary files /dev/null and b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Content/images/logo.png differ
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Content/images/tilemap-definition.xml b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Content/images/tilemap-definition.xml
new file mode 100644
index 00000000..85658c60
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Content/images/tilemap-definition.xml
@@ -0,0 +1,15 @@
+
+
+ images/atlas
+
+ 00 01 02 01 02 01 02 01 02 01 02 01 02 01 02 03
+ 04 05 05 06 05 05 06 05 05 06 05 05 06 05 05 07
+ 08 09 09 09 09 09 09 09 09 09 09 09 09 09 09 11
+ 04 09 09 09 09 09 09 09 10 09 09 09 09 10 09 07
+ 08 09 10 09 09 09 09 09 09 09 09 09 09 09 09 11
+ 04 09 09 09 09 09 09 09 09 09 09 09 09 09 09 07
+ 08 10 09 09 09 09 09 09 09 09 10 09 09 09 09 11
+ 04 09 09 09 09 09 10 09 09 09 09 09 09 09 09 07
+ 12 13 14 13 14 13 14 13 14 13 14 13 14 13 14 15
+
+
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/DungeonSlime.Android.csproj b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/DungeonSlime.Android.csproj
new file mode 100644
index 00000000..1f25f538
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/DungeonSlime.Android.csproj
@@ -0,0 +1,26 @@
+
+
+ net9.0-android
+ Exe
+ 23
+ com.companyname.DungeonSlime.Android
+ 1
+ 1.0
+
+
+ False
+
+
+ False
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Game1.cs b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Game1.cs
new file mode 100644
index 00000000..7683bfe4
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Game1.cs
@@ -0,0 +1,84 @@
+using System;
+using DungeonSlime.Scenes;
+using Microsoft.Xna.Framework.Media;
+using MonoGameLibrary;
+using MonoGameGum;
+using Gum.Forms.Controls;
+
+namespace DungeonSlime;
+
+public class Game1 : Core
+{
+ // The background theme song
+ private Song _themeSong;
+
+ public Game1() : base("Dungeon Slime", 1280, 720, false)
+ {
+ Console.WriteLine("🚀 MonoGame initialization started");
+ }
+
+ protected override void Initialize()
+ {
+ try
+ {
+ Console.WriteLine("✅ MonoGame initialisation...");
+
+ base.Initialize();
+
+ // Start playing the background music
+ Audio.PlaySong(_themeSong);
+
+ // Initialize the Gum UI service
+ InitializeGum();
+
+ // Start the game with the title scene.
+ ChangeScene(new TitleScene());
+
+ Console.WriteLine("✅ MonoGame initialisation complete");
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine(e);
+ throw;
+ }
+ }
+
+ private void InitializeGum()
+ {
+ // Initialize the Gum service
+ GumService.Default.Initialize(this);
+
+ // Tell the Gum service which content manager to use. We will tell it to
+ // use the global content manager from our Core.
+ GumService.Default.ContentLoader.XnaContentManager = Core.Content;
+
+ // Register keyboard input for UI control.
+ FrameworkElement.KeyboardsForUiControl.Add(GumService.Default.Keyboard);
+
+ // Register gamepad input for Ui control.
+ FrameworkElement.GamePadsForUiControl.AddRange(GumService.Default.Gamepads);
+
+ // Customize the tab reverse UI navigation to also trigger when the keyboard
+ // Up arrow key is pushed.
+ FrameworkElement.TabReverseKeyCombos.Add(
+ new KeyCombo() { PushedKey = Microsoft.Xna.Framework.Input.Keys.Up });
+
+ // Customize the tab UI navigation to also trigger when the keyboard
+ // Down arrow key is pushed.
+ FrameworkElement.TabKeyCombos.Add(
+ new KeyCombo() { PushedKey = Microsoft.Xna.Framework.Input.Keys.Down });
+
+ // The assets created for the UI were done so at 1/4th the size to keep the size of the
+ // texture atlas small. So we will set the default canvas size to be 1/4th the size of
+ // the game's resolution then tell gum to zoom in by a factor of 4.
+ GumService.Default.CanvasWidth = GraphicsDevice.PresentationParameters.BackBufferWidth / 4.0f;
+ GumService.Default.CanvasHeight = GraphicsDevice.PresentationParameters.BackBufferHeight / 4.0f;
+ GumService.Default.Renderer.Camera.Zoom = 4.0f;
+ }
+
+ protected override void LoadContent()
+ {
+ // Load the background theme music
+ _themeSong = Content.Load("audio/theme");
+ }
+}
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/GameController.cs b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/GameController.cs
new file mode 100644
index 00000000..92165382
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/GameController.cs
@@ -0,0 +1,79 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Input;
+using MonoGameLibrary;
+using MonoGameLibrary.Input;
+
+namespace DungeonSlime;
+
+///
+/// Provides a game-specific input abstraction that maps physical inputs
+/// to game actions, bridging our input system with game-specific functionality.
+///
+public static class GameController
+{
+ private static KeyboardInfo s_keyboard => Core.Input.Keyboard;
+ private static GamePadInfo s_gamePad => Core.Input.GamePads[(int)PlayerIndex.One];
+
+ ///
+ /// Returns true if the player has triggered the "move up" action.
+ ///
+ public static bool MoveUp()
+ {
+ return s_keyboard.WasKeyJustPressed(Keys.Up) ||
+ s_keyboard.WasKeyJustPressed(Keys.W) ||
+ s_gamePad.WasButtonJustPressed(Buttons.DPadUp) ||
+ s_gamePad.WasButtonJustPressed(Buttons.LeftThumbstickUp);
+ }
+
+ ///
+ /// Returns true if the player has triggered the "move down" action.
+ ///
+ public static bool MoveDown()
+ {
+ return s_keyboard.WasKeyJustPressed(Keys.Down) ||
+ s_keyboard.WasKeyJustPressed(Keys.S) ||
+ s_gamePad.WasButtonJustPressed(Buttons.DPadDown) ||
+ s_gamePad.WasButtonJustPressed(Buttons.LeftThumbstickDown);
+ }
+
+ ///
+ /// Returns true if the player has triggered the "move left" action.
+ ///
+ public static bool MoveLeft()
+ {
+ return s_keyboard.WasKeyJustPressed(Keys.Left) ||
+ s_keyboard.WasKeyJustPressed(Keys.A) ||
+ s_gamePad.WasButtonJustPressed(Buttons.DPadLeft) ||
+ s_gamePad.WasButtonJustPressed(Buttons.LeftThumbstickLeft);
+ }
+
+ ///
+ /// Returns true if the player has triggered the "move right" action.
+ ///
+ public static bool MoveRight()
+ {
+ return s_keyboard.WasKeyJustPressed(Keys.Right) ||
+ s_keyboard.WasKeyJustPressed(Keys.D) ||
+ s_gamePad.WasButtonJustPressed(Buttons.DPadRight) ||
+ s_gamePad.WasButtonJustPressed(Buttons.LeftThumbstickRight);
+ }
+
+ ///
+ /// Returns true if the player has triggered the "pause" action.
+ ///
+ public static bool Pause()
+ {
+ return s_keyboard.WasKeyJustPressed(Keys.Escape) ||
+ s_gamePad.WasButtonJustPressed(Buttons.Start);
+ }
+
+ ///
+ /// Returns true if the player has triggered the "action" button,
+ /// typically used for menu confirmation.
+ ///
+ public static bool Action()
+ {
+ return s_keyboard.WasKeyJustPressed(Keys.Enter) ||
+ s_gamePad.WasButtonJustPressed(Buttons.A);
+ }
+}
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/GameObjects/Bat.cs b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/GameObjects/Bat.cs
new file mode 100644
index 00000000..ddc855ed
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/GameObjects/Bat.cs
@@ -0,0 +1,123 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Audio;
+using MonoGameLibrary;
+using MonoGameLibrary.Graphics;
+
+namespace DungeonSlime.GameObjects;
+
+public class Bat
+{
+ private const float MOVEMENT_SPEED = 5.0f;
+
+ // The velocity of the bat that defines the direction and how much in that
+ // direction to update the bats position each update cycle.
+ private Vector2 _velocity;
+
+ // The AnimatedSprite used when drawing the bat.
+ private AnimatedSprite _sprite;
+
+ // The sound effect to play when the bat bounces off the edge of the room.
+ private SoundEffect _bounceSoundEffect;
+
+ ///
+ /// Gets or Sets the position of the bat.
+ ///
+ public Vector2 Position { get; set; }
+
+ ///
+ /// Creates a new Bat using the specified animated sprite and sound effect.
+ ///
+ /// The AnimatedSprite ot use when drawing the bat.
+ /// The sound effect to play when the bat bounces off a wall.
+ public Bat(AnimatedSprite sprite, SoundEffect bounceSoundEffect)
+ {
+ _sprite = sprite;
+ _bounceSoundEffect = bounceSoundEffect;
+ }
+
+ ///
+ /// Randomizes the velocity of the bat.
+ ///
+ public void RandomizeVelocity()
+ {
+ // Generate a random angle
+ float angle = (float)(Random.Shared.NextDouble() * MathHelper.TwoPi);
+
+ // Convert the angle to a direction vector
+ float x = (float)Math.Cos(angle);
+ float y = (float)Math.Sin(angle);
+ Vector2 direction = new Vector2(x, y);
+
+ // Multiply the direction vector by the movement speed to get the
+ // final velocity
+ _velocity = direction * MOVEMENT_SPEED;
+ }
+
+ ///
+ /// Handles a bounce event when the bat collides with a wall or boundary.
+ ///
+ /// The normal vector of the surface the bat is bouncing against.
+ public void Bounce(Vector2 normal)
+ {
+ Vector2 newPosition = Position;
+
+ // Adjust the position based on the normal to prevent sticking to walls.
+ if (normal.X != 0)
+ {
+ // We are bouncing off a vertical wall (left/right).
+ // Move slightly away from the wall in the direction of the normal.
+ newPosition.X += normal.X * (_sprite.Width * 0.1f);
+ }
+
+ if (normal.Y != 0)
+ {
+ // We are bouncing off a horizontal wall (top/bottom).
+ // Move slightly way from the wall in the direction of the normal.
+ newPosition.Y += normal.Y * (_sprite.Height * 0.1f);
+ }
+
+ // Apply the new position
+ Position = newPosition;
+
+ // Apply reflection based on the normal.
+ _velocity = Vector2.Reflect(_velocity, normal);
+
+ // Play the bounce sound effect.
+ Core.Audio.PlaySoundEffect(_bounceSoundEffect);
+ }
+
+ ///
+ /// Returns a Circle value that represents collision bounds of the bat.
+ ///
+ /// A Circle value.
+ public Circle GetBounds()
+ {
+ int x = (int)(Position.X + _sprite.Width * 0.5f);
+ int y = (int)(Position.Y + _sprite.Height * 0.5f);
+ int radius = (int)(_sprite.Width * 0.25f);
+
+ return new Circle(x, y, radius);
+ }
+
+ ///
+ /// Updates the bat.
+ ///
+ /// A snapshot of the timing values for the current update cycle.
+ public void Update(GameTime gameTime)
+ {
+ // Update the animated sprite
+ _sprite.Update(gameTime);
+
+ // Update the position of the bat based on the velocity.
+ Position += _velocity;
+ }
+
+ ///
+ /// Draws the bat.
+ ///
+ public void Draw()
+ {
+ _sprite.Draw(Core.SpriteBatch, Position);
+ }
+}
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/GameObjects/Slime.cs b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/GameObjects/Slime.cs
new file mode 100644
index 00000000..08b5a63d
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/GameObjects/Slime.cs
@@ -0,0 +1,265 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Xna.Framework;
+using MonoGameLibrary;
+using MonoGameLibrary.Graphics;
+
+namespace DungeonSlime.GameObjects;
+
+public class Slime
+{
+ // A constant value that represents the amount of time to wait between
+ // movement updates.
+ private static readonly TimeSpan s_movementTime = TimeSpan.FromMilliseconds(200);
+
+ // The amount of time that has elapsed since the last movement update.
+ private TimeSpan _movementTimer;
+
+ // Normalized value (0-1) representing progress between movement ticks for visual interpolation
+ private float _movementProgress;
+
+ // The next direction to apply to the head of the slime chain during the
+ // next movement update.
+ private Vector2 _nextDirection;
+
+ // The number of pixels to move the head segment during the movement cycle.
+ private float _stride;
+
+ // Tracks the segments of the slime chain.
+ private List _segments;
+
+ // The AnimatedSprite used when drawing each slime segment
+ private AnimatedSprite _sprite;
+
+ // Buffer to queue inputs input by player during input polling.
+ private Queue _inputBuffer;
+
+ // The maximum size of the buffer queue.
+ private const int MAX_BUFFER_SIZE = 2;
+
+ ///
+ /// Event that is raised if it is detected that the head segment of the slime
+ /// has collided with a body segment.
+ ///
+ public event EventHandler BodyCollision;
+
+ ///
+ /// Creates a new Slime using the specified animated sprite.
+ ///
+ /// The AnimatedSprite to use when drawing the slime.
+ public Slime(AnimatedSprite sprite)
+ {
+ _sprite = sprite;
+ }
+
+ ///
+ /// Initializes the slime, can be used to reset it back to an initial state.
+ ///
+ /// The position the slime should start at.
+ /// The total number of pixels to move the head segment during each movement cycle.
+ public void Initialize(Vector2 startingPosition, float stride)
+ {
+ // Initialize the segment collection.
+ _segments = new List();
+
+ // Set the stride
+ _stride = stride;
+
+ // Create the initial head of the slime chain.
+ SlimeSegment head = new SlimeSegment();
+ head.At = startingPosition;
+ head.To = startingPosition + new Vector2(_stride, 0);
+ head.Direction = Vector2.UnitX;
+
+ // Add it to the segment collection.
+ _segments.Add(head);
+
+ // Set the initial next direction as the same direction the head is
+ // moving.
+ _nextDirection = head.Direction;
+
+ // Zero out the movement timer.
+ _movementTimer = TimeSpan.Zero;
+
+ // initialize the input buffer.
+ _inputBuffer = new Queue(MAX_BUFFER_SIZE);
+ }
+
+ private void HandleInput()
+ {
+ Vector2 potentialNextDirection = Vector2.Zero;
+
+ if (GameController.MoveUp())
+ {
+ potentialNextDirection = -Vector2.UnitY;
+ }
+ else if (GameController.MoveDown())
+ {
+ potentialNextDirection = Vector2.UnitY;
+ }
+ else if (GameController.MoveLeft())
+ {
+ potentialNextDirection = -Vector2.UnitX;
+ }
+ else if (GameController.MoveRight())
+ {
+ potentialNextDirection = Vector2.UnitX;
+ }
+
+ // If a new direction was input, consider adding it to the buffer
+ if (potentialNextDirection != Vector2.Zero && _inputBuffer.Count < MAX_BUFFER_SIZE)
+ {
+ // If the buffer is empty, validate against the current direction;
+ // otherwise, validate against the last buffered direction
+ Vector2 validateAgainst = _inputBuffer.Count > 0 ?
+ _inputBuffer.Last() :
+ _segments[0].Direction;
+
+ // Only allow direction change if it is not reversing the current
+ // direction. This prevents th slime from backing into itself
+ float dot = Vector2.Dot(potentialNextDirection, validateAgainst);
+ if (dot >= 0)
+ {
+ _inputBuffer.Enqueue(potentialNextDirection);
+ }
+ }
+ }
+
+ private void Move()
+ {
+ // Get the next direction from the input buffer if one is available
+ if (_inputBuffer.Count > 0)
+ {
+ _nextDirection = _inputBuffer.Dequeue();
+ }
+
+ // Capture the value of the head segment
+ SlimeSegment head = _segments[0];
+
+ // Update the direction the head is supposed to move in to the
+ // next direction cached.
+ head.Direction = _nextDirection;
+
+ // Update the head's "at" position to be where it was moving "to"
+ head.At = head.To;
+
+ // Update the head's "to" position to the next tile in the direction
+ // it is moving.
+ head.To = head.At + head.Direction * _stride;
+
+ // Insert the new adjusted value for the head at the front of the
+ // segments and remove the tail segment. This effectively moves
+ // the entire chain forward without needing to loop through every
+ // segment and update its "at" and "to" positions.
+ _segments.Insert(0, head);
+ _segments.RemoveAt(_segments.Count - 1);
+
+ // Iterate through all of the segments except the head and check
+ // if they are at the same position as the head. If they are, then
+ // the head is colliding with a body segment and a body collision
+ // has occurred.
+ for (int i = 1; i < _segments.Count; i++)
+ {
+ SlimeSegment segment = _segments[i];
+
+ if (head.At == segment.At)
+ {
+ if (BodyCollision != null)
+ {
+ BodyCollision.Invoke(this, EventArgs.Empty);
+ }
+
+ return;
+ }
+ }
+ }
+
+ ///
+ /// Informs the slime to grow by one segment.
+ ///
+ public void Grow()
+ {
+ // Capture the value of the tail segment
+ SlimeSegment tail = _segments[_segments.Count - 1];
+
+ // Create a new tail segment that is positioned a grid cell in the
+ // reverse direction from the tail moving to the tail.
+ SlimeSegment newTail = new SlimeSegment();
+ newTail.At = tail.To + tail.ReverseDirection * _stride;
+ newTail.To = tail.At;
+ newTail.Direction = Vector2.Normalize(tail.At - newTail.At);
+
+ // Add the new tail segment
+ _segments.Add(newTail);
+ }
+
+ ///
+ /// Updates the slime.
+ ///
+ /// A snapshot of the timing values for the current update cycle.
+ public void Update(GameTime gameTime)
+ {
+ // Update the animated sprite.
+ _sprite.Update(gameTime);
+
+ // Handle any player input
+ HandleInput();
+
+ // Increment the movement timer by the frame elapsed time.
+ _movementTimer += gameTime.ElapsedGameTime;
+
+ // If the movement timer has accumulated enough time to be greater than
+ // the movement time threshold, then perform a full movement.
+ if (_movementTimer >= s_movementTime)
+ {
+ _movementTimer -= s_movementTime;
+ Move();
+ }
+
+ // Update the movement lerp offset amount
+ _movementProgress = (float)(_movementTimer.TotalSeconds / s_movementTime.TotalSeconds);
+ }
+
+ ///
+ /// Draws the slime.
+ ///
+ public void Draw()
+ {
+ // Iterate through each segment and draw it
+ foreach (SlimeSegment segment in _segments)
+ {
+ // Calculate the visual position of the segment at the moment by
+ // lerping between its "at" and "to" position by the movement
+ // offset lerp amount
+ Vector2 pos = Vector2.Lerp(segment.At, segment.To, _movementProgress);
+
+ // Draw the slime sprite at the calculated visual position of this
+ // segment
+ _sprite.Draw(Core.SpriteBatch, pos);
+ }
+ }
+
+ ///
+ /// Returns a Circle value that represents collision bounds of the slime.
+ ///
+ /// A Circle value.
+ public Circle GetBounds()
+ {
+ SlimeSegment head = _segments[0];
+
+ // Calculate the visual position of the head at the moment of this
+ // method call by lerping between the "at" and "to" position by the
+ // movement offset lerp amount
+ Vector2 pos = Vector2.Lerp(head.At, head.To, _movementProgress);
+
+ // Create the bounds using the calculated visual position of the head.
+ Circle bounds = new Circle(
+ (int)(pos.X + (_sprite.Width * 0.5f)),
+ (int)(pos.Y + (_sprite.Height * 0.5f)),
+ (int)(_sprite.Width * 0.5f)
+ );
+
+ return bounds;
+ }
+}
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/GameObjects/SlimeSegment.cs b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/GameObjects/SlimeSegment.cs
new file mode 100644
index 00000000..b00189eb
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/GameObjects/SlimeSegment.cs
@@ -0,0 +1,26 @@
+using Microsoft.Xna.Framework;
+
+namespace DungeonSlime.GameObjects;
+
+public struct SlimeSegment
+{
+ ///
+ /// The position this slime segment is at before the movement cycle occurs.
+ ///
+ public Vector2 At;
+
+ ///
+ /// The position this slime segment should move to during the next movement cycle.
+ ///
+ public Vector2 To;
+
+ ///
+ /// The direction this slime segment is moving.
+ ///
+ public Vector2 Direction;
+
+ ///
+ /// The opposite direction this slime segment is moving.
+ ///
+ public Vector2 ReverseDirection => new Vector2(-Direction.X, -Direction.Y);
+}
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Resources/Drawable/Icon.png b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Resources/Drawable/Icon.png
new file mode 100644
index 00000000..25fe0444
Binary files /dev/null and b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Resources/Drawable/Icon.png differ
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Resources/Values/Strings.xml b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Resources/Values/Strings.xml
new file mode 100644
index 00000000..7a59ecad
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Resources/Values/Strings.xml
@@ -0,0 +1,4 @@
+
+
+ DungeonSlime.Android
+
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Scenes/GameScene.cs b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Scenes/GameScene.cs
new file mode 100644
index 00000000..d01a3b03
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Scenes/GameScene.cs
@@ -0,0 +1,428 @@
+using System;
+using DungeonSlime.GameObjects;
+using DungeonSlime.UI;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Audio;
+using Microsoft.Xna.Framework.Graphics;
+using MonoGameGum;
+using MonoGameLibrary;
+using MonoGameLibrary.Graphics;
+using MonoGameLibrary.Scenes;
+
+namespace DungeonSlime.Scenes;
+
+public class GameScene : Scene
+{
+ private enum GameState
+ {
+ Playing,
+ Paused,
+ GameOver
+ }
+
+ // Reference to the slime.
+ private Slime _slime;
+
+ // Reference to the bat.
+ private Bat _bat;
+
+ // Defines the tilemap to draw.
+ private Tilemap _tilemap;
+
+ // Defines the bounds of the room that the slime and bat are contained within.
+ private Rectangle _roomBounds;
+
+ // The sound effect to play when the slime eats a bat.
+ private SoundEffect _collectSoundEffect;
+
+ // Tracks the players score.
+ private int _score;
+
+ private GameSceneUI _ui;
+
+ private GameState _state;
+
+ // The grayscale shader effect.
+ private Effect _grayscaleEffect;
+
+ // The amount of saturation to provide the grayscale shader effect
+ private float _saturation = 1.0f;
+
+ // The speed of the fade to grayscale effect.
+ private const float FADE_SPEED = 0.02f;
+
+ public override void Initialize()
+ {
+ // LoadContent is called during base.Initialize().
+ base.Initialize();
+
+ // During the game scene, we want to disable exit on escape. Instead,
+ // the escape key will be used to return back to the title screen
+ Core.ExitOnEscape = false;
+
+ // Create the room bounds by getting the bounds of the screen then
+ // using the Inflate method to "Deflate" the bounds by the width and
+ // height of a tile so that the bounds only covers the inside room of
+ // the dungeon tilemap.
+ _roomBounds = Core.GraphicsDevice.PresentationParameters.Bounds;
+ _roomBounds.Inflate(-_tilemap.TileWidth, -_tilemap.TileHeight);
+
+ // Subscribe to the slime's BodyCollision event so that a game over
+ // can be triggered when this event is raised.
+ _slime.BodyCollision += OnSlimeBodyCollision;
+
+ // Create any UI elements from the root element created in previous
+ // scenes
+ GumService.Default.Root.Children.Clear();
+
+ // Initialize the user interface for the game scene.
+ InitializeUI();
+
+ // Initialize a new game to be played.
+ InitializeNewGame();
+ }
+
+ private void InitializeUI()
+ {
+ // Clear out any previous UI element incase we came here
+ // from a different scene.
+ GumService.Default.Root.Children.Clear();
+
+ // Create the game scene ui instance.
+ _ui = new GameSceneUI();
+
+ // Subscribe to the events from the game scene ui.
+ _ui.ResumeButtonClick += OnResumeButtonClicked;
+ _ui.RetryButtonClick += OnRetryButtonClicked;
+ _ui.QuitButtonClick += OnQuitButtonClicked;
+ }
+
+ private void OnResumeButtonClicked(object sender, EventArgs args)
+ {
+ // Change the game state back to playing
+ _state = GameState.Playing;
+ }
+
+ private void OnRetryButtonClicked(object sender, EventArgs args)
+ {
+ // Player has chosen to retry, so initialize a new game
+ InitializeNewGame();
+ }
+
+ private void OnQuitButtonClicked(object sender, EventArgs args)
+ {
+ // Player has chosen to quit, so return back to the title scene
+ Core.ChangeScene(new TitleScene());
+ }
+
+ private void InitializeNewGame()
+ {
+ // Calculate the position for the slime, which will be at the center
+ // tile of the tile map.
+ Vector2 slimePos = new Vector2();
+ slimePos.X = (_tilemap.Columns / 2) * _tilemap.TileWidth;
+ slimePos.Y = (_tilemap.Rows / 2) * _tilemap.TileHeight;
+
+ // Initialize the slime
+ _slime.Initialize(slimePos, _tilemap.TileWidth);
+
+ // Initialize the bat
+ _bat.RandomizeVelocity();
+ PositionBatAwayFromSlime();
+
+ // Reset the score
+ _score = 0;
+
+ // Set the game state to playing
+ _state = GameState.Playing;
+ }
+
+ public override void LoadContent()
+ {
+ // Create the texture atlas from the XML configuration file
+ TextureAtlas atlas = TextureAtlas.FromFile(Core.Content, "images/atlas-definition.xml");
+
+ // Create the tilemap from the XML configuration file.
+ _tilemap = Tilemap.FromFile(Content, "images/tilemap-definition.xml");
+ _tilemap.Scale = new Vector2(4.0f, 4.0f);
+
+ // Create the animated sprite for the slime from the atlas.
+ AnimatedSprite slimeAnimation = atlas.CreateAnimatedSprite("slime-animation");
+ slimeAnimation.Scale = new Vector2(4.0f, 4.0f);
+
+ // Create the slime
+ _slime = new Slime(slimeAnimation);
+
+ // Create the animated sprite for the bat from the atlas.
+ AnimatedSprite batAnimation = atlas.CreateAnimatedSprite("bat-animation");
+ batAnimation.Scale = new Vector2(4.0f, 4.0f);
+
+ // Load the bounce sound effect for the bat
+ SoundEffect bounceSoundEffect = Content.Load("audio/bounce");
+
+ // Create the bat
+ _bat = new Bat(batAnimation, bounceSoundEffect);
+
+ // Load the collect sound effect
+ _collectSoundEffect = Content.Load("audio/collect");
+
+ // Load the grayscale effect
+ _grayscaleEffect = Content.Load("effects/grayscaleEffect");
+ }
+
+ public override void Update(GameTime gameTime)
+ {
+ // Ensure the UI is always updated
+ _ui.Update(gameTime);
+
+ if (_state != GameState.Playing)
+ {
+ // The game is in either a paused or game over state, so
+ // gradually decrease the saturation to create the fading grayscale.
+ _saturation = Math.Max(0.0f, _saturation - FADE_SPEED);
+
+ // If its just a game over state, return back
+ if (_state == GameState.GameOver)
+ {
+ return;
+ }
+ }
+
+ // If the pause button is pressed, toggle the pause state
+ if (GameController.Pause())
+ {
+ TogglePause();
+ }
+
+ // At this point, if the game is paused, just return back early
+ if (_state == GameState.Paused)
+ {
+ return;
+ }
+
+ // Update the slime;
+ _slime.Update(gameTime);
+
+ // Update the bat;
+ _bat.Update(gameTime);
+
+ // Perform collision checks
+ CollisionChecks();
+ }
+
+ private void CollisionChecks()
+ {
+ // Capture the current bounds of the slime and bat
+ Circle slimeBounds = _slime.GetBounds();
+ Circle batBounds = _bat.GetBounds();
+
+ // FIrst perform a collision check to see if the slime is colliding with
+ // the bat, which means the slime eats the bat.
+ if (slimeBounds.Intersects(batBounds))
+ {
+ // Move the bat to a new position away from the slime.
+ PositionBatAwayFromSlime();
+
+ // Randomize the velocity of the bat.
+ _bat.RandomizeVelocity();
+
+ // Tell the slime to grow.
+ _slime.Grow();
+
+ // Increment the score.
+ _score += 100;
+
+ // Update the score display on the UI.
+ _ui.UpdateScoreText(_score);
+
+ // Play the collect sound effect
+ Core.Audio.PlaySoundEffect(_collectSoundEffect);
+ }
+
+ // Next check if the slime is colliding with the wall by validating if
+ // it is within the bounds of the room. If it is outside the room
+ // bounds, then it collided with a wall which triggers a game over.
+ if (slimeBounds.Top < _roomBounds.Top ||
+ slimeBounds.Bottom > _roomBounds.Bottom ||
+ slimeBounds.Left < _roomBounds.Left ||
+ slimeBounds.Right > _roomBounds.Right)
+ {
+ GameOver();
+ return;
+ }
+
+ // Finally, check if the bat is colliding with a wall by validating if
+ // it is within the bounds of the room. If it is outside the room
+ // bounds, then it collided with a wall, and the bat should bounce
+ // off of that wall.
+ if (batBounds.Top < _roomBounds.Top)
+ {
+ _bat.Bounce(Vector2.UnitY);
+ }
+ else if (batBounds.Bottom > _roomBounds.Bottom)
+ {
+ _bat.Bounce(-Vector2.UnitY);
+ }
+
+ if (batBounds.Left < _roomBounds.Left)
+ {
+ _bat.Bounce(Vector2.UnitX);
+ }
+ else if (batBounds.Right > _roomBounds.Right)
+ {
+ _bat.Bounce(-Vector2.UnitX);
+ }
+ }
+
+ private void PositionBatAwayFromSlime()
+ {
+ // Calculate the position that is in the center of the bounds
+ // of the room.
+ float roomCenterX = _roomBounds.X + _roomBounds.Width * 0.5f;
+ float roomCenterY = _roomBounds.Y + _roomBounds.Height * 0.5f;
+ Vector2 roomCenter = new Vector2(roomCenterX, roomCenterY);
+
+ // Get the bounds of the slime and calculate the center position
+ Circle slimeBounds = _slime.GetBounds();
+ Vector2 slimeCenter = new Vector2(slimeBounds.X, slimeBounds.Y);
+
+ // Calculate the distance vector from the center of the room to the
+ // center of the slime.
+ Vector2 centerToSlime = slimeCenter - roomCenter;
+
+ // Get the bounds of the bat
+ Circle batBounds = _bat.GetBounds();
+
+ // Calculate the amount of padding we will add to the new position of
+ // the bat to ensure it is not sticking to walls
+ int padding = batBounds.Radius * 2;
+
+ // Calculate the new position of the bat by finding which component of
+ // the center to slime vector (X or Y) is larger and in which direction.
+ Vector2 newBatPosition = Vector2.Zero;
+ if (Math.Abs(centerToSlime.X) > Math.Abs(centerToSlime.Y))
+ {
+ // The slime is closer to either the left or right wall, so the Y
+ // position will be a random position between the top and bottom
+ // walls.
+ newBatPosition.Y = Random.Shared.Next(
+ _roomBounds.Top + padding,
+ _roomBounds.Bottom - padding
+ );
+
+ if (centerToSlime.X > 0)
+ {
+ // The slime is closer to the right side wall, so place the
+ // bat on the left side wall
+ newBatPosition.X = _roomBounds.Left + padding;
+ }
+ else
+ {
+ // The slime is closer ot the left side wall, so place the
+ // bat on the right side wall.
+ newBatPosition.X = _roomBounds.Right - padding * 2;
+ }
+ }
+ else
+ {
+ // The slime is closer to either the top or bottom wall, so the X
+ // position will be a random position between the left and right
+ // walls.
+ newBatPosition.X = Random.Shared.Next(
+ _roomBounds.Left + padding,
+ _roomBounds.Right - padding
+ );
+
+ if (centerToSlime.Y > 0)
+ {
+ // The slime is closer to the top wall, so place the bat on the
+ // bottom wall
+ newBatPosition.Y = _roomBounds.Top + padding;
+ }
+ else
+ {
+ // The slime is closer to the bottom wall, so place the bat on
+ // the top wall.
+ newBatPosition.Y = _roomBounds.Bottom - padding * 2;
+ }
+ }
+
+ // Assign the new bat position
+ _bat.Position = newBatPosition;
+ }
+
+ private void OnSlimeBodyCollision(object sender, EventArgs args)
+ {
+ GameOver();
+ }
+
+ private void TogglePause()
+ {
+ if (_state == GameState.Paused)
+ {
+ // We're now unpausing the game, so hide the pause panel
+ _ui.HidePausePanel();
+
+ // And set the state back to playing
+ _state = GameState.Playing;
+ }
+ else
+ {
+ // We're now pausing the game, so show the pause panel
+ _ui.ShowPausePanel();
+
+ // And set the state to paused
+ _state = GameState.Paused;
+
+ // Set the grayscale effect saturation to 1.0f;
+ _saturation = 1.0f;
+ }
+ }
+
+ private void GameOver()
+ {
+ // Show the game over panel
+ _ui.ShowGameOverPanel();
+
+ // Set the game state to game over
+ _state = GameState.GameOver;
+
+ // Set the grayscale effect saturation to 1.0f;
+ _saturation = 1.0f;
+ }
+
+ public override void Draw(GameTime gameTime)
+ {
+ // Clear the back buffer.
+ Core.GraphicsDevice.Clear(Color.CornflowerBlue);
+
+ if (_state != GameState.Playing)
+ {
+ // We are in a game over state, so apply the saturation parameter.
+ _grayscaleEffect.Parameters["Saturation"].SetValue(_saturation);
+
+ // And begin the sprite batch using the grayscale effect.
+ Core.SpriteBatch.Begin(samplerState: SamplerState.PointClamp, effect: _grayscaleEffect);
+ }
+ else
+ {
+ // Otherwise, just begin the sprite batch as normal.
+ Core.SpriteBatch.Begin(samplerState: SamplerState.PointClamp);
+ }
+
+ // Draw the tilemap
+ _tilemap.Draw(Core.SpriteBatch);
+
+ // Draw the slime.
+ _slime.Draw();
+
+ // Draw the bat.
+ _bat.Draw();
+
+ // Always end the sprite batch when finished.
+ Core.SpriteBatch.End();
+
+ // Draw the UI
+ _ui.Draw();
+ }
+}
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Scenes/TitleScene.cs b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Scenes/TitleScene.cs
new file mode 100644
index 00000000..248c48dd
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/Scenes/TitleScene.cs
@@ -0,0 +1,345 @@
+using System;
+using DungeonSlime.UI;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Audio;
+using Microsoft.Xna.Framework.Graphics;
+using Microsoft.Xna.Framework.Input;
+using MonoGameGum;
+using Gum.Forms.Controls;
+using MonoGameGum.GueDeriving;
+using MonoGameLibrary;
+using MonoGameLibrary.Graphics;
+using MonoGameLibrary.Scenes;
+
+namespace DungeonSlime.Scenes;
+
+public class TitleScene : Scene
+{
+ private const string DUNGEON_TEXT = "Dungeon";
+ private const string SLIME_TEXT = "Slime";
+ private const string PRESS_ENTER_TEXT = "Press Enter To Start";
+
+ // The font to use to render normal text.
+ private SpriteFont _font;
+
+ // The font used to render the title text.
+ private SpriteFont _font5x;
+
+ // The position to draw the dungeon text at.
+ private Vector2 _dungeonTextPos;
+
+ // The origin to set for the dungeon text.
+ private Vector2 _dungeonTextOrigin;
+
+ // The position to draw the slime text at.
+ private Vector2 _slimeTextPos;
+
+ // The origin to set for the slime text.
+ private Vector2 _slimeTextOrigin;
+
+ // The position to draw the press enter text at.
+ private Vector2 _pressEnterPos;
+
+ // The origin to set for the press enter text when drawing it.
+ private Vector2 _pressEnterOrigin;
+
+ // The texture used for the background pattern.
+ private Texture2D _backgroundPattern;
+
+ // The destination rectangle for the background pattern to fill.
+ private Rectangle _backgroundDestination;
+
+ // The offset to apply when drawing the background pattern so it appears to
+ // be scrolling.
+ private Vector2 _backgroundOffset;
+
+ // The speed that the background pattern scrolls.
+ private float _scrollSpeed = 50.0f;
+
+ private SoundEffect _uiSoundEffect;
+ private Panel _titleScreenButtonsPanel;
+ private Panel _optionsPanel;
+
+ // The options button used to open the options menu.
+ private AnimatedButton _optionsButton;
+
+ // The back button used to exit the options menu back to the title menu.
+ private AnimatedButton _optionsBackButton;
+
+ // Reference to the texture atlas that we can pass to UI elements when they
+ // are created.
+ private TextureAtlas _atlas;
+
+ public override void Initialize()
+ {
+ // LoadContent is called during base.Initialize().
+ base.Initialize();
+
+ // While on the title screen, we can enable exit on escape so the player
+ // can close the game by pressing the escape key.
+ Core.ExitOnEscape = true;
+
+ // Set the position and origin for the Dungeon text.
+ Vector2 size = _font5x.MeasureString(DUNGEON_TEXT);
+ _dungeonTextPos = new Vector2(640, 100);
+ _dungeonTextOrigin = size * 0.5f;
+
+ // Set the position and origin for the Slime text.
+ size = _font5x.MeasureString(SLIME_TEXT);
+ _slimeTextPos = new Vector2(757, 207);
+ _slimeTextOrigin = size * 0.5f;
+
+ // Set the position and origin for the press enter text.
+ size = _font.MeasureString(PRESS_ENTER_TEXT);
+ _pressEnterPos = new Vector2(640, 620);
+ _pressEnterOrigin = size * 0.5f;
+
+ // Initialize the offset of the background pattern at zero
+ _backgroundOffset = Vector2.Zero;
+
+ // Set the background pattern destination rectangle to fill the entire
+ // screen background
+ _backgroundDestination = Core.GraphicsDevice.PresentationParameters.Bounds;
+
+ InitializeUI();
+ }
+
+ public override void LoadContent()
+ {
+ // Load the font for the standard text.
+ _font = Core.Content.Load("fonts/04B_30");
+
+ // Load the font for the title text
+ _font5x = Content.Load("fonts/04B_30_5x");
+
+ // Load the background pattern texture.
+ _backgroundPattern = Content.Load("images/background-pattern");
+
+ // Load the sound effect to play when ui actions occur.
+ _uiSoundEffect = Core.Content.Load("audio/ui");
+
+ // Load the texture atlas from the xml configuration file.
+ _atlas = TextureAtlas.FromFile(Core.Content, "images/atlas-definition.xml");
+ }
+
+ private void CreateTitlePanel()
+ {
+ // Create a container to hold all of our buttons
+ _titleScreenButtonsPanel = new Panel();
+ _titleScreenButtonsPanel.Dock(Gum.Wireframe.Dock.Fill);
+ _titleScreenButtonsPanel.AddToRoot();
+
+ AnimatedButton startButton = new AnimatedButton(_atlas);
+ startButton.Anchor(Gum.Wireframe.Anchor.BottomLeft);
+ startButton.Visual.X = 50;
+ startButton.Visual.Y = -12;
+ startButton.Text = "Start";
+ startButton.Click += HandleStartClicked;
+ _titleScreenButtonsPanel.AddChild(startButton);
+
+ _optionsButton = new AnimatedButton(_atlas);
+ _optionsButton.Anchor(Gum.Wireframe.Anchor.BottomRight);
+ _optionsButton.Visual.X = -50;
+ _optionsButton.Visual.Y = -12;
+ _optionsButton.Text = "Options";
+ _optionsButton.Click += HandleOptionsClicked;
+ _titleScreenButtonsPanel.AddChild(_optionsButton);
+
+ startButton.IsFocused = true;
+ }
+
+ private void HandleStartClicked(object sender, EventArgs e)
+ {
+ // A UI interaction occurred, play the sound effect
+ Core.Audio.PlaySoundEffect(_uiSoundEffect);
+
+ // Change to the game scene to start the game.
+ Core.ChangeScene(new GameScene());
+ }
+
+ private void HandleOptionsClicked(object sender, EventArgs e)
+ {
+ // A UI interaction occurred, play the sound effect
+ Core.Audio.PlaySoundEffect(_uiSoundEffect);
+
+ // Set the title panel to be invisible.
+ _titleScreenButtonsPanel.IsVisible = false;
+
+ // Set the options panel to be visible.
+ _optionsPanel.IsVisible = true;
+
+ // Give the back button on the options panel focus.
+ _optionsBackButton.IsFocused = true;
+ }
+
+ private void CreateOptionsPanel()
+ {
+ _optionsPanel = new Panel();
+ _optionsPanel.Dock(Gum.Wireframe.Dock.Fill);
+ _optionsPanel.IsVisible = false;
+ _optionsPanel.AddToRoot();
+
+ TextRuntime optionsText = new TextRuntime();
+ optionsText.X = 10;
+ optionsText.Y = 10;
+ optionsText.Text = "OPTIONS";
+ optionsText.UseCustomFont = true;
+ optionsText.FontScale = 0.5f;
+ optionsText.CustomFontFile = @"fonts/04b_30.fnt";
+ _optionsPanel.AddChild(optionsText);
+
+ OptionsSlider musicSlider = new OptionsSlider(_atlas);
+ musicSlider.Name = "MusicSlider";
+ musicSlider.Text = "MUSIC";
+ musicSlider.Anchor(Gum.Wireframe.Anchor.Top);
+ musicSlider.Visual.Y = 30f;
+ musicSlider.Minimum = 0;
+ musicSlider.Maximum = 1;
+ musicSlider.Value = Core.Audio.SongVolume;
+ musicSlider.SmallChange = .1;
+ musicSlider.LargeChange = .2;
+ musicSlider.ValueChanged += HandleMusicSliderValueChanged;
+ musicSlider.ValueChangeCompleted += HandleMusicSliderValueChangeCompleted;
+ _optionsPanel.AddChild(musicSlider);
+
+ OptionsSlider sfxSlider = new OptionsSlider(_atlas);
+ sfxSlider.Name = "SfxSlider";
+ sfxSlider.Text = "SFX";
+ sfxSlider.Anchor(Gum.Wireframe.Anchor.Top);
+ sfxSlider.Visual.Y = 93;
+ sfxSlider.Minimum = 0;
+ sfxSlider.Maximum = 1;
+ sfxSlider.Value = Core.Audio.SoundEffectVolume;
+ sfxSlider.SmallChange = .1;
+ sfxSlider.LargeChange = .2;
+ sfxSlider.ValueChanged += HandleSfxSliderChanged;
+ sfxSlider.ValueChangeCompleted += HandleSfxSliderChangeCompleted;
+ _optionsPanel.AddChild(sfxSlider);
+
+ _optionsBackButton = new AnimatedButton(_atlas);
+ _optionsBackButton.Text = "BACK";
+ _optionsBackButton.Anchor(Gum.Wireframe.Anchor.BottomRight);
+ _optionsBackButton.X = -28f;
+ _optionsBackButton.Y = -10f;
+ _optionsBackButton.Click += HandleOptionsButtonBack;
+ _optionsPanel.AddChild(_optionsBackButton);
+ }
+
+ private void HandleSfxSliderChanged(object sender, EventArgs args)
+ {
+ // Intentionally not playing the UI sound effect here so that it is not
+ // constantly triggered as the user adjusts the slider's thumb on the
+ // track.
+
+ // Get a reference to the sender as a Slider.
+ var slider = (Slider)sender;
+
+ // Set the global sound effect volume to the value of the slider.;
+ Core.Audio.SoundEffectVolume = (float)slider.Value;
+ }
+
+ private void HandleSfxSliderChangeCompleted(object sender, EventArgs e)
+ {
+ // Play the UI Sound effect so the player can hear the difference in audio.
+ Core.Audio.PlaySoundEffect(_uiSoundEffect);
+ }
+
+ private void HandleMusicSliderValueChanged(object sender, EventArgs args)
+ {
+ // Intentionally not playing the UI sound effect here so that it is not
+ // constantly triggered as the user adjusts the slider's thumb on the
+ // track.
+
+ // Get a reference to the sender as a Slider.
+ var slider = (Slider)sender;
+
+ // Set the global song volume to the value of the slider.
+ Core.Audio.SongVolume = (float)slider.Value;
+ }
+
+ private void HandleMusicSliderValueChangeCompleted(object sender, EventArgs args)
+ {
+ // A UI interaction occurred, play the sound effect
+ Core.Audio.PlaySoundEffect(_uiSoundEffect);
+ }
+
+ private void HandleOptionsButtonBack(object sender, EventArgs e)
+ {
+ // A UI interaction occurred, play the sound effect
+ Core.Audio.PlaySoundEffect(_uiSoundEffect);
+
+ // Set the title panel to be visible.
+ _titleScreenButtonsPanel.IsVisible = true;
+
+ // Set the options panel to be invisible.
+ _optionsPanel.IsVisible = false;
+
+ // Give the options button on the title panel focus since we are coming
+ // back from the options screen.
+ _optionsButton.IsFocused = true;
+ }
+
+ private void InitializeUI()
+ {
+ // Clear out any previous UI in case we came here from
+ // a different screen:
+ GumService.Default.Root.Children.Clear();
+
+ CreateTitlePanel();
+ CreateOptionsPanel();
+ }
+
+ public override void Update(GameTime gameTime)
+ {
+ // Update the offsets for the background pattern wrapping so that it
+ // scrolls down and to the right.
+ float offset = _scrollSpeed * (float)gameTime.ElapsedGameTime.TotalSeconds;
+ _backgroundOffset.X -= offset;
+ _backgroundOffset.Y -= offset;
+
+ // Ensure that the offsets do not go beyond the texture bounds so it is
+ // a seamless wrap
+ _backgroundOffset.X %= _backgroundPattern.Width;
+ _backgroundOffset.Y %= _backgroundPattern.Height;
+
+ GumService.Default.Update(gameTime);
+ }
+
+ public override void Draw(GameTime gameTime)
+ {
+ Core.GraphicsDevice.Clear(new Color(32, 40, 78, 255));
+
+ // Draw the background pattern first using the PointWrap sampler state.
+ Core.SpriteBatch.Begin(samplerState: SamplerState.PointWrap);
+ Core.SpriteBatch.Draw(_backgroundPattern, _backgroundDestination, new Rectangle(_backgroundOffset.ToPoint(), _backgroundDestination.Size), Color.White * 0.5f);
+ Core.SpriteBatch.End();
+
+ if (_titleScreenButtonsPanel.IsVisible)
+ {
+ // Begin the sprite batch to prepare for rendering.
+ Core.SpriteBatch.Begin(samplerState: SamplerState.PointClamp);
+
+ // The color to use for the drop shadow text.
+ Color dropShadowColor = Color.Black * 0.5f;
+
+ // Draw the Dungeon text slightly offset from it is original position and
+ // with a transparent color to give it a drop shadow
+ Core.SpriteBatch.DrawString(_font5x, DUNGEON_TEXT, _dungeonTextPos + new Vector2(10, 10), dropShadowColor, 0.0f, _dungeonTextOrigin, 1.0f, SpriteEffects.None, 1.0f);
+
+ // Draw the Dungeon text on top of that at its original position
+ Core.SpriteBatch.DrawString(_font5x, DUNGEON_TEXT, _dungeonTextPos, Color.White, 0.0f, _dungeonTextOrigin, 1.0f, SpriteEffects.None, 1.0f);
+
+ // Draw the Slime text slightly offset from it is original position and
+ // with a transparent color to give it a drop shadow
+ Core.SpriteBatch.DrawString(_font5x, SLIME_TEXT, _slimeTextPos + new Vector2(10, 10), dropShadowColor, 0.0f, _slimeTextOrigin, 1.0f, SpriteEffects.None, 1.0f);
+
+ // Draw the Slime text on top of that at its original position
+ Core.SpriteBatch.DrawString(_font5x, SLIME_TEXT, _slimeTextPos, Color.White, 0.0f, _slimeTextOrigin, 1.0f, SpriteEffects.None, 1.0f);
+
+ // Always end the sprite batch when finished.
+ Core.SpriteBatch.End();
+ }
+
+ GumService.Default.Draw();
+ }
+}
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/UI/AnimatedButton.cs b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/UI/AnimatedButton.cs
new file mode 100644
index 00000000..34a59c68
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/UI/AnimatedButton.cs
@@ -0,0 +1,175 @@
+using System;
+using Gum.DataTypes;
+using Gum.DataTypes.Variables;
+using Gum.Graphics.Animation;
+using Gum.Managers;
+using Microsoft.Xna.Framework.Input;
+using Gum.Forms.Controls;
+using MonoGameGum.GueDeriving;
+using MonoGameLibrary.Graphics;
+
+namespace DungeonSlime.UI;
+
+///
+/// A custom button implementation that inherits from Gum's Button class to provide
+/// animated visual feedback when focused.
+///
+internal class AnimatedButton : Button
+{
+ ///
+ /// Creates a new AnimatedButton instance using graphics from the specified texture atlas.
+ ///
+ /// The texture atlas containing button graphics and animations
+ public AnimatedButton(TextureAtlas atlas)
+ {
+ // Create the top-level container that will hold all visual elements
+ // Width is relative to children with extra padding, height is fixed
+ ContainerRuntime topLevelContainer = new ContainerRuntime();
+ topLevelContainer.Height = 14f;
+ topLevelContainer.HeightUnits = DimensionUnitType.Absolute;
+ topLevelContainer.Width = 21f;
+ topLevelContainer.WidthUnits = DimensionUnitType.RelativeToChildren;
+
+ // Create the nine-slice background that will display the button graphics
+ // A nine-slice allows the button to stretch while preserving corner appearance
+ NineSliceRuntime nineSliceInstance = new NineSliceRuntime();
+ nineSliceInstance.Height = 0f;
+ nineSliceInstance.Texture = atlas.Texture;
+ nineSliceInstance.TextureAddress = TextureAddress.Custom;
+ nineSliceInstance.Dock(Gum.Wireframe.Dock.Fill);
+ topLevelContainer.Children.Add(nineSliceInstance);
+
+ // Create the text element that will display the button's label
+ TextRuntime textInstance = new TextRuntime();
+ // Name is required so it hooks in to the base Button.Text property
+ textInstance.Name = "TextInstance";
+ textInstance.Text = "START";
+ textInstance.Blue = 130;
+ textInstance.Green = 86;
+ textInstance.Red = 70;
+ textInstance.UseCustomFont = true;
+ textInstance.CustomFontFile = "fonts/04b_30.fnt";
+ textInstance.FontScale = 0.25f;
+ textInstance.Anchor(Gum.Wireframe.Anchor.Center);
+ textInstance.Width = 0;
+ textInstance.WidthUnits = DimensionUnitType.RelativeToChildren;
+ topLevelContainer.Children.Add(textInstance);
+
+ // Get the texture region for the unfocused button state from the atlas
+ TextureRegion unfocusedTextureRegion = atlas.GetRegion("unfocused-button");
+
+ // Create an animation chain for the unfocused state with a single frame
+ AnimationChain unfocusedAnimation = new AnimationChain();
+ unfocusedAnimation.Name = nameof(unfocusedAnimation);
+ AnimationFrame unfocusedFrame = new AnimationFrame
+ {
+ TopCoordinate = unfocusedTextureRegion.TopTextureCoordinate,
+ BottomCoordinate = unfocusedTextureRegion.BottomTextureCoordinate,
+ LeftCoordinate = unfocusedTextureRegion.LeftTextureCoordinate,
+ RightCoordinate = unfocusedTextureRegion.RightTextureCoordinate,
+ FrameLength = 0.3f,
+ Texture = unfocusedTextureRegion.Texture
+ };
+ unfocusedAnimation.Add(unfocusedFrame);
+
+ // Get the multi-frame animation for the focused button state from the atlas
+ Animation focusedAtlasAnimation = atlas.GetAnimation("focused-button-animation");
+
+ // Create an animation chain for the focused state using all frames from the atlas animation
+ AnimationChain focusedAnimation = new AnimationChain();
+ focusedAnimation.Name = nameof(focusedAnimation);
+ foreach (TextureRegion region in focusedAtlasAnimation.Frames)
+ {
+ AnimationFrame frame = new AnimationFrame
+ {
+ TopCoordinate = region.TopTextureCoordinate,
+ BottomCoordinate = region.BottomTextureCoordinate,
+ LeftCoordinate = region.LeftTextureCoordinate,
+ RightCoordinate = region.RightTextureCoordinate,
+ FrameLength = (float)focusedAtlasAnimation.Delay.TotalSeconds,
+ Texture = region.Texture
+ };
+
+ focusedAnimation.Add(frame);
+ }
+
+ // Assign both animation chains to the nine-slice background
+ nineSliceInstance.AnimationChains = new AnimationChainList
+ {
+ unfocusedAnimation,
+ focusedAnimation
+ };
+
+ // Create a state category for button states
+ StateSaveCategory category = new StateSaveCategory();
+ category.Name = Button.ButtonCategoryName;
+ topLevelContainer.AddCategory(category);
+
+ // Create the enabled (default/unfocused) state
+ StateSave enabledState = new StateSave();
+ enabledState.Name = FrameworkElement.EnabledStateName;
+ enabledState.Apply = () =>
+ {
+ // When enabled but not focused, use the unfocused animation
+ nineSliceInstance.CurrentChainName = unfocusedAnimation.Name;
+ };
+ category.States.Add(enabledState);
+
+ // Create the focused state
+ StateSave focusedState = new StateSave();
+ focusedState.Name = FrameworkElement.FocusedStateName;
+ focusedState.Apply = () =>
+ {
+ // When focused, use the focused animation and enable animation playback
+ nineSliceInstance.CurrentChainName = focusedAnimation.Name;
+ nineSliceInstance.Animate = true;
+ };
+ category.States.Add(focusedState);
+
+ // Create the highlighted+focused state (for mouse hover while focused)
+ // by cloning the focused state since they appear the same
+ StateSave highlightedFocused = focusedState.Clone();
+ highlightedFocused.Name = FrameworkElement.HighlightedFocusedStateName;
+ category.States.Add(highlightedFocused);
+
+ // Create the highlighted state (for mouse hover)
+ // by cloning the enabled state since they appear the same
+ StateSave highlighted = enabledState.Clone();
+ highlighted.Name = FrameworkElement.HighlightedStateName;
+ category.States.Add(highlighted);
+
+ // Add event handlers for keyboard input.
+ KeyDown += HandleKeyDown;
+
+ // Add event handler for mouse hover focus.
+ topLevelContainer.RollOn += HandleRollOn;
+
+ // Assign the configured container as this button's visual
+ Visual = topLevelContainer;
+ }
+
+ ///
+ /// Handles keyboard input for navigation between buttons using left/right keys.
+ ///
+ private void HandleKeyDown(object sender, KeyEventArgs e)
+ {
+ if (e.Key == Keys.Left)
+ {
+ // Left arrow navigates to previous control
+ HandleTab(TabDirection.Up, loop: true);
+ }
+ if (e.Key == Keys.Right)
+ {
+ // Right arrow navigates to next control
+ HandleTab(TabDirection.Down, loop: true);
+ }
+ }
+
+ ///
+ /// Automatically focuses the button when the mouse hovers over it.
+ ///
+ private void HandleRollOn(object sender, EventArgs e)
+ {
+ IsFocused = true;
+ }
+}
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/UI/GameSceneUI.cs b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/UI/GameSceneUI.cs
new file mode 100644
index 00000000..84be9293
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/UI/GameSceneUI.cs
@@ -0,0 +1,340 @@
+using System;
+using Gum.DataTypes;
+using Gum.Managers;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Audio;
+using Microsoft.Xna.Framework.Content;
+using MonoGameGum;
+using Gum.Forms.Controls;
+using MonoGameGum.GueDeriving;
+using MonoGameLibrary;
+using MonoGameLibrary.Graphics;
+
+namespace DungeonSlime.UI;
+
+public class GameSceneUI : ContainerRuntime
+{
+ // The string format to use when updating the text for the score display.
+ private static readonly string s_scoreFormat = "SCORE: {0:D6}";
+
+ // The sound effect to play for auditory feedback of the user interface.
+ private SoundEffect _uiSoundEffect;
+
+ // The pause panel
+ private Panel _pausePanel;
+
+ // The resume button on the pause panel. Field is used to track reference so
+ // focus can be set when the pause panel is shown.
+ private AnimatedButton _resumeButton;
+
+ // The game over panel.
+ private Panel _gameOverPanel;
+
+ // The retry button on the game over panel. Field is used to track reference
+ // so focus can be set when the game over panel is shown.
+ private AnimatedButton _retryButton;
+
+ // The text runtime used to display the players score on the game screen.
+ private TextRuntime _scoreText;
+
+ ///
+ /// Event invoked when the Resume button on the Pause panel is clicked.
+ ///
+ public event EventHandler ResumeButtonClick;
+
+ ///
+ /// Event invoked when the Quit button on either the Pause panel or the
+ /// Game Over panel is clicked.
+ ///
+ public event EventHandler QuitButtonClick;
+
+ ///
+ /// Event invoked when the Retry button on the Game Over panel is clicked.
+ ///
+ public event EventHandler RetryButtonClick;
+
+ public GameSceneUI()
+ {
+ // The game scene UI inherits from ContainerRuntime, so we set its
+ // doc to fill so it fills the entire screen.
+ Dock(Gum.Wireframe.Dock.Fill);
+
+ // Add it to the root element.
+ this.AddToRoot();
+
+ // Get a reference to the content manager that was registered with the
+ // GumService when it was original initialized.
+ ContentManager content = GumService.Default.ContentLoader.XnaContentManager;
+
+ // Use that content manager to load the sound effect and atlas for the
+ // user interface elements
+ _uiSoundEffect = content.Load("audio/ui");
+ TextureAtlas atlas = TextureAtlas.FromFile(content, "images/atlas-definition.xml");
+
+ // Create the text that will display the players score and add it as
+ // a child to this container.
+ _scoreText = CreateScoreText();
+ AddChild(_scoreText);
+
+ // Create the Pause panel that is displayed when the game is paused and
+ // add it as a child to this container
+ _pausePanel = CreatePausePanel(atlas);
+ AddChild(_pausePanel.Visual);
+
+ // Create the Game Over panel that is displayed when a game over occurs
+ // and add it as a child to this container
+ _gameOverPanel = CreateGameOverPanel(atlas);
+ AddChild(_gameOverPanel.Visual);
+ }
+
+ private TextRuntime CreateScoreText()
+ {
+ TextRuntime text = new TextRuntime();
+ text.Anchor(Gum.Wireframe.Anchor.TopLeft);
+ text.WidthUnits = DimensionUnitType.RelativeToChildren;
+ text.X = 20.0f;
+ text.Y = 5.0f;
+ text.UseCustomFont = true;
+ text.CustomFontFile = @"fonts/04b_30.fnt";
+ text.FontScale = 0.25f;
+ text.Text = string.Format(s_scoreFormat, 0);
+
+ return text;
+ }
+
+ private Panel CreatePausePanel(TextureAtlas atlas)
+ {
+ Panel panel = new Panel();
+ panel.Anchor(Gum.Wireframe.Anchor.Center);
+ panel.Visual.WidthUnits = DimensionUnitType.Absolute;
+ panel.Visual.HeightUnits = DimensionUnitType.Absolute;
+ panel.Visual.Width = 264.0f;
+ panel.Visual.Height = 70.0f;
+ panel.IsVisible = false;
+
+ TextureRegion backgroundRegion = atlas.GetRegion("panel-background");
+
+ NineSliceRuntime background = new NineSliceRuntime();
+ background.Dock(Gum.Wireframe.Dock.Fill);
+ background.Texture = backgroundRegion.Texture;
+ background.TextureAddress = TextureAddress.Custom;
+ background.TextureHeight = backgroundRegion.Height;
+ background.TextureWidth = backgroundRegion.Width;
+ background.TextureTop = backgroundRegion.SourceRectangle.Top;
+ background.TextureLeft = backgroundRegion.SourceRectangle.Left;
+ panel.AddChild(background);
+
+ TextRuntime text = new TextRuntime();
+ text.Text = "PAUSED";
+ text.UseCustomFont = true;
+ text.CustomFontFile = "fonts/04b_30.fnt";
+ text.FontScale = 0.5f;
+ text.X = 10.0f;
+ text.Y = 10.0f;
+ panel.AddChild(text);
+
+ _resumeButton = new AnimatedButton(atlas);
+ _resumeButton.Text = "RESUME";
+ _resumeButton.Anchor(Gum.Wireframe.Anchor.BottomLeft);
+ _resumeButton.Visual.X = 9.0f;
+ _resumeButton.Visual.Y = -9.0f;
+
+ _resumeButton.Click += OnResumeButtonClicked;
+ _resumeButton.GotFocus += OnElementGotFocus;
+
+ panel.AddChild(_resumeButton);
+
+ AnimatedButton quitButton = new AnimatedButton(atlas);
+ quitButton.Text = "QUIT";
+ quitButton.Anchor(Gum.Wireframe.Anchor.BottomRight);
+ quitButton.Visual.X = -9.0f;
+ quitButton.Visual.Y = -9.0f;
+
+ quitButton.Click += OnQuitButtonClicked;
+ quitButton.GotFocus += OnElementGotFocus;
+
+ panel.AddChild(quitButton);
+
+ return panel;
+ }
+
+ private Panel CreateGameOverPanel(TextureAtlas atlas)
+ {
+ Panel panel = new Panel();
+ panel.Anchor(Gum.Wireframe.Anchor.Center);
+ panel.Visual.WidthUnits = DimensionUnitType.Absolute;
+ panel.Visual.HeightUnits = DimensionUnitType.Absolute;
+ panel.Visual.Width = 264.0f;
+ panel.Visual.Height = 70.0f;
+ panel.IsVisible = false;
+
+ TextureRegion backgroundRegion = atlas.GetRegion("panel-background");
+
+ NineSliceRuntime background = new NineSliceRuntime();
+ background.Dock(Gum.Wireframe.Dock.Fill);
+ background.Texture = backgroundRegion.Texture;
+ background.TextureAddress = TextureAddress.Custom;
+ background.TextureHeight = backgroundRegion.Height;
+ background.TextureWidth = backgroundRegion.Width;
+ background.TextureTop = backgroundRegion.SourceRectangle.Top;
+ background.TextureLeft = backgroundRegion.SourceRectangle.Left;
+ panel.AddChild(background);
+
+ TextRuntime text = new TextRuntime();
+ text.Text = "GAME OVER";
+ text.WidthUnits = DimensionUnitType.RelativeToChildren;
+ text.UseCustomFont = true;
+ text.CustomFontFile = "fonts/04b_30.fnt";
+ text.FontScale = 0.5f;
+ text.X = 10.0f;
+ text.Y = 10.0f;
+ panel.AddChild(text);
+
+ _retryButton = new AnimatedButton(atlas);
+ _retryButton.Text = "RETRY";
+ _retryButton.Anchor(Gum.Wireframe.Anchor.BottomLeft);
+ _retryButton.Visual.X = 9.0f;
+ _retryButton.Visual.Y = -9.0f;
+
+ _retryButton.Click += OnRetryButtonClicked;
+ _retryButton.GotFocus += OnElementGotFocus;
+
+ panel.AddChild(_retryButton);
+
+ AnimatedButton quitButton = new AnimatedButton(atlas);
+ quitButton.Text = "QUIT";
+ quitButton.Anchor(Gum.Wireframe.Anchor.BottomRight);
+ quitButton.Visual.X = -9.0f;
+ quitButton.Visual.Y = -9.0f;
+
+ quitButton.Click += OnQuitButtonClicked;
+ quitButton.GotFocus += OnElementGotFocus;
+
+ panel.AddChild(quitButton);
+
+ return panel;
+ }
+
+ private void OnResumeButtonClicked(object sender, EventArgs args)
+ {
+ // Button was clicked, play the ui sound effect for auditory feedback.
+ Core.Audio.PlaySoundEffect(_uiSoundEffect);
+
+ // Since the resume button was clicked, we need to hide the pause panel.
+ HidePausePanel();
+
+ // Invoke the ResumeButtonClick event
+ if (ResumeButtonClick != null)
+ {
+ ResumeButtonClick(sender, args);
+ }
+ }
+
+ private void OnRetryButtonClicked(object sender, EventArgs args)
+ {
+ // Button was clicked, play the ui sound effect for auditory feedback.
+ Core.Audio.PlaySoundEffect(_uiSoundEffect);
+
+ // Since the retry button was clicked, we need to hide the game over panel.
+ HideGameOverPanel();
+
+ // Invoke the RetryButtonClick event.
+ if (RetryButtonClick != null)
+ {
+ RetryButtonClick(sender, args);
+ }
+ }
+
+ private void OnQuitButtonClicked(object sender, EventArgs args)
+ {
+ // Button was clicked, play the ui sound effect for auditory feedback.
+ Core.Audio.PlaySoundEffect(_uiSoundEffect);
+
+ // Both panels have a quit button, so hide both panels
+ HidePausePanel();
+ HideGameOverPanel();
+
+ // Invoke the QuitButtonClick event.
+ if (QuitButtonClick != null)
+ {
+ QuitButtonClick(sender, args);
+ }
+ }
+
+ private void OnElementGotFocus(object sender, EventArgs args)
+ {
+ // A ui element that can receive focus has received focus, play the
+ // ui sound effect for auditory feedback.
+ Core.Audio.PlaySoundEffect(_uiSoundEffect);
+ }
+
+ ///
+ /// Updates the text on the score display.
+ ///
+ /// The score to display.
+ public void UpdateScoreText(int score)
+ {
+ _scoreText.Text = string.Format(s_scoreFormat, score);
+ }
+
+ ///
+ /// Tells the game scene ui to show the pause panel.
+ ///
+ public void ShowPausePanel()
+ {
+ _pausePanel.IsVisible = true;
+
+ // Give the resume button focus for keyboard/gamepad input.
+ _resumeButton.IsFocused = true;
+
+ // Ensure the game over panel isn't visible.
+ _gameOverPanel.IsVisible = false;
+ }
+
+ ///
+ /// Tells the game scene ui to hide the pause panel.
+ ///
+ public void HidePausePanel()
+ {
+ _pausePanel.IsVisible = false;
+ }
+
+ ///
+ /// Tells the game scene ui to show the game over panel.
+ ///
+ public void ShowGameOverPanel()
+ {
+ _gameOverPanel.IsVisible = true;
+
+ // Give the retry button focus for keyboard/gamepad input.
+ _retryButton.IsFocused = true;
+
+ // Ensure the pause panel isn't visible.
+ _pausePanel.IsVisible = false;
+ }
+
+ ///
+ /// Tells the game scene ui to hide the game over panel.
+ ///
+ public void HideGameOverPanel()
+ {
+ _gameOverPanel.IsVisible = false;
+ }
+
+ ///
+ /// Updates the game scene ui.
+ ///
+ /// A snapshot of the timing values for the current update cycle.
+ public void Update(GameTime gameTime)
+ {
+ GumService.Default.Update(gameTime);
+ }
+
+ ///
+ /// Draws the game scene ui.
+ ///
+ public void Draw()
+ {
+ GumService.Default.Draw();
+ }
+}
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/UI/OptionsSlider.cs b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/UI/OptionsSlider.cs
new file mode 100644
index 00000000..86d03281
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.Android/UI/OptionsSlider.cs
@@ -0,0 +1,253 @@
+using System;
+using Gum.DataTypes;
+using Gum.DataTypes.Variables;
+using Gum.Managers;
+using Microsoft.Xna.Framework;
+using Gum.Forms.Controls;
+using MonoGameGum.GueDeriving;
+using MonoGameLibrary.Graphics;
+
+namespace DungeonSlime.UI;
+
+///
+/// A custom slider control that inherits from Gum's Slider class.
+///
+public class OptionsSlider : Slider
+{
+ // Reference to the text label that displays the slider's title
+ private TextRuntime _textInstance;
+
+ // Reference to the rectangle that visually represents the current value
+ private ColoredRectangleRuntime _fillRectangle;
+
+ ///
+ /// Gets or sets the text label for this slider.
+ ///
+ public string Text
+ {
+ get => _textInstance.Text;
+ set => _textInstance.Text = value;
+ }
+
+ ///
+ /// Creates a new OptionsSlider instance using graphics from the specified texture atlas.
+ ///
+ /// The texture atlas containing slider graphics.
+ public OptionsSlider(TextureAtlas atlas)
+ {
+ // Create the top-level container for all visual elements
+ ContainerRuntime topLevelContainer = new ContainerRuntime();
+ topLevelContainer.Height = 55f;
+ topLevelContainer.Width = 264f;
+
+ TextureRegion backgroundRegion = atlas.GetRegion("panel-background");
+
+ // Create the background panel that contains everything
+ NineSliceRuntime background = new NineSliceRuntime();
+ background.Texture = atlas.Texture;
+ background.TextureAddress = TextureAddress.Custom;
+ background.TextureHeight = backgroundRegion.Height;
+ background.TextureLeft = backgroundRegion.SourceRectangle.Left;
+ background.TextureTop = backgroundRegion.SourceRectangle.Top;
+ background.TextureWidth = backgroundRegion.Width;
+ background.Dock(Gum.Wireframe.Dock.Fill);
+ topLevelContainer.AddChild(background);
+
+ // Create the title text element
+ _textInstance = new TextRuntime();
+ _textInstance.CustomFontFile = @"fonts/04b_30.fnt";
+ _textInstance.UseCustomFont = true;
+ _textInstance.FontScale = 0.5f;
+ _textInstance.Text = "Replace Me";
+ _textInstance.X = 10f;
+ _textInstance.Y = 10f;
+ _textInstance.WidthUnits = DimensionUnitType.RelativeToChildren;
+ topLevelContainer.AddChild(_textInstance);
+
+ // Create the container for the slider track and decorative elements
+ ContainerRuntime innerContainer = new ContainerRuntime();
+ innerContainer.Height = 13f;
+ innerContainer.Width = 241f;
+ innerContainer.X = 10f;
+ innerContainer.Y = 33f;
+ topLevelContainer.AddChild(innerContainer);
+
+ TextureRegion offBackgroundRegion = atlas.GetRegion("slider-off-background");
+
+ // Create the "OFF" side of the slider (left end)
+ NineSliceRuntime offBackground = new NineSliceRuntime();
+ offBackground.Dock(Gum.Wireframe.Dock.Left);
+ offBackground.Texture = atlas.Texture;
+ offBackground.TextureAddress = TextureAddress.Custom;
+ offBackground.TextureHeight = offBackgroundRegion.Height;
+ offBackground.TextureLeft = offBackgroundRegion.SourceRectangle.Left;
+ offBackground.TextureTop = offBackgroundRegion.SourceRectangle.Top;
+ offBackground.TextureWidth = offBackgroundRegion.Width;
+ offBackground.Width = 28f;
+ offBackground.WidthUnits = DimensionUnitType.Absolute;
+ offBackground.Dock(Gum.Wireframe.Dock.Left);
+ innerContainer.AddChild(offBackground);
+
+ TextureRegion middleBackgroundRegion = atlas.GetRegion("slider-middle-background");
+
+ // Create the middle track portion of the slider
+ NineSliceRuntime middleBackground = new NineSliceRuntime();
+ middleBackground.Dock(Gum.Wireframe.Dock.FillVertically);
+ middleBackground.Texture = middleBackgroundRegion.Texture;
+ middleBackground.TextureAddress = TextureAddress.Custom;
+ middleBackground.TextureHeight = middleBackgroundRegion.Height;
+ middleBackground.TextureLeft = middleBackgroundRegion.SourceRectangle.Left;
+ middleBackground.TextureTop = middleBackgroundRegion.SourceRectangle.Top;
+ middleBackground.TextureWidth = middleBackgroundRegion.Width;
+ middleBackground.Width = 179f;
+ middleBackground.WidthUnits = DimensionUnitType.Absolute;
+ middleBackground.Dock(Gum.Wireframe.Dock.Left);
+ middleBackground.X = 27f;
+ innerContainer.AddChild(middleBackground);
+
+ TextureRegion maxBackgroundRegion = atlas.GetRegion("slider-max-background");
+
+ // Create the "MAX" side of the slider (right end)
+ NineSliceRuntime maxBackground = new NineSliceRuntime();
+ maxBackground.Texture = maxBackgroundRegion.Texture;
+ maxBackground.TextureAddress = TextureAddress.Custom;
+ maxBackground.TextureHeight = maxBackgroundRegion.Height;
+ maxBackground.TextureLeft = maxBackgroundRegion.SourceRectangle.Left;
+ maxBackground.TextureTop = maxBackgroundRegion.SourceRectangle.Top;
+ maxBackground.TextureWidth = maxBackgroundRegion.Width;
+ maxBackground.Width = 36f;
+ maxBackground.WidthUnits = DimensionUnitType.Absolute;
+ maxBackground.Dock(Gum.Wireframe.Dock.Right);
+ innerContainer.AddChild(maxBackground);
+
+ // Create the interactive track that responds to clicks
+ // The special name "TrackInstance" is required for Slider functionality
+ ContainerRuntime trackInstance = new ContainerRuntime();
+ trackInstance.Name = "TrackInstance";
+ trackInstance.Dock(Gum.Wireframe.Dock.Fill);
+ trackInstance.Height = -2f;
+ trackInstance.Width = -2f;
+ middleBackground.AddChild(trackInstance);
+
+ // Create the fill rectangle that visually displays the current value
+ _fillRectangle = new ColoredRectangleRuntime();
+ _fillRectangle.Dock(Gum.Wireframe.Dock.Left);
+ _fillRectangle.Width = 90f; // Default to 90% - will be updated by value changes
+ _fillRectangle.WidthUnits = DimensionUnitType.PercentageOfParent;
+ trackInstance.AddChild(_fillRectangle);
+
+ // Add "OFF" text to the left end
+ TextRuntime offText = new TextRuntime();
+ offText.Red = 70;
+ offText.Green = 86;
+ offText.Blue = 130;
+ offText.CustomFontFile = @"fonts/04b_30.fnt";
+ offText.FontScale = 0.25f;
+ offText.UseCustomFont = true;
+ offText.Text = "OFF";
+ offText.Anchor(Gum.Wireframe.Anchor.Center);
+ offBackground.AddChild(offText);
+
+ // Add "MAX" text to the right end
+ TextRuntime maxText = new TextRuntime();
+ maxText.Red = 70;
+ maxText.Green = 86;
+ maxText.Blue = 130;
+ maxText.CustomFontFile = @"fonts/04b_30.fnt";
+ maxText.FontScale = 0.25f;
+ maxText.UseCustomFont = true;
+ maxText.Text = "MAX";
+ maxText.Anchor(Gum.Wireframe.Anchor.Center);
+ maxBackground.AddChild(maxText);
+
+ // Define colors for focused and unfocused states
+ Color focusedColor = Color.White;
+ Color unfocusedColor = Color.Gray;
+
+ // Create slider state category - Slider.SliderCategoryName is the required name
+ StateSaveCategory sliderCategory = new StateSaveCategory();
+ sliderCategory.Name = Slider.SliderCategoryName;
+ topLevelContainer.AddCategory(sliderCategory);
+
+ // Create the enabled (default/unfocused) state
+ StateSave enabled = new StateSave();
+ enabled.Name = FrameworkElement.EnabledStateName;
+ enabled.Apply = () =>
+ {
+ // When enabled but not focused, use gray coloring for all elements
+ background.Color = unfocusedColor;
+ _textInstance.Color = unfocusedColor;
+ offBackground.Color = unfocusedColor;
+ middleBackground.Color = unfocusedColor;
+ maxBackground.Color = unfocusedColor;
+ _fillRectangle.Color = unfocusedColor;
+ };
+ sliderCategory.States.Add(enabled);
+
+ // Create the focused state
+ StateSave focused = new StateSave();
+ focused.Name = FrameworkElement.FocusedStateName;
+ focused.Apply = () =>
+ {
+ // When focused, use white coloring for all elements
+ background.Color = focusedColor;
+ _textInstance.Color = focusedColor;
+ offBackground.Color = focusedColor;
+ middleBackground.Color = focusedColor;
+ maxBackground.Color = focusedColor;
+ _fillRectangle.Color = focusedColor;
+ };
+ sliderCategory.States.Add(focused);
+
+ // Create the highlighted+focused state by cloning the focused state
+ StateSave highlightedFocused = focused.Clone();
+ highlightedFocused.Name = FrameworkElement.HighlightedFocusedStateName;
+ sliderCategory.States.Add(highlightedFocused);
+
+ // Create the highlighted state by cloning the enabled state
+ StateSave highlighted = enabled.Clone();
+ highlighted.Name = FrameworkElement.HighlightedStateName;
+ sliderCategory.States.Add(highlighted);
+
+ // Assign the configured container as this slider's visual
+ Visual = topLevelContainer;
+
+ // Enable click-to-point functionality for the slider
+ // This allows users to click anywhere on the track to jump to that value
+ IsMoveToPointEnabled = true;
+
+ // Add event handlers
+ Visual.RollOn += HandleRollOn;
+ ValueChanged += HandleValueChanged;
+ ValueChangedByUi += HandleValueChangedByUi;
+ }
+
+ ///
+ /// Automatically focuses the slider when the user interacts with it
+ ///
+ private void HandleValueChangedByUi(object sender, EventArgs e)
+ {
+ IsFocused = true;
+ }
+
+ ///
+ /// Automatically focuses the slider when the mouse hovers over it
+ ///
+ private void HandleRollOn(object sender, EventArgs e)
+ {
+ IsFocused = true;
+ }
+
+ ///
+ /// Updates the fill rectangle width to visually represent the current value
+ ///
+ private void HandleValueChanged(object sender, EventArgs e)
+ {
+ // Calculate the ratio of the current value within its range
+ double ratio = (Value - Minimum) / (Maximum - Minimum);
+
+ // Update the fill rectangle width as a percentage
+ // _fillRectangle uses percentage width units, so we multiply by 100
+ _fillRectangle.Width = 100 * (float)ratio;
+ }
+}
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.slnx b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.slnx
new file mode 100644
index 00000000..2ce23d21
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime.slnx
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/.config/dotnet-tools.json b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/.config/dotnet-tools.json
new file mode 100644
index 00000000..afd4e2c4
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/.config/dotnet-tools.json
@@ -0,0 +1,36 @@
+{
+ "version": 1,
+ "isRoot": true,
+ "tools": {
+ "dotnet-mgcb": {
+ "version": "3.8.3",
+ "commands": [
+ "mgcb"
+ ]
+ },
+ "dotnet-mgcb-editor": {
+ "version": "3.8.3",
+ "commands": [
+ "mgcb-editor"
+ ]
+ },
+ "dotnet-mgcb-editor-linux": {
+ "version": "3.8.3",
+ "commands": [
+ "mgcb-editor-linux"
+ ]
+ },
+ "dotnet-mgcb-editor-windows": {
+ "version": "3.8.3",
+ "commands": [
+ "mgcb-editor-windows"
+ ]
+ },
+ "dotnet-mgcb-editor-mac": {
+ "version": "3.8.3",
+ "commands": [
+ "mgcb-editor-mac"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Content/Content.mgcb b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Content/Content.mgcb
new file mode 100644
index 00000000..d26ea4f1
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Content/Content.mgcb
@@ -0,0 +1,104 @@
+
+#----------------------------- Global Properties ----------------------------#
+
+/outputDir:bin/$(Platform)
+/intermediateDir:obj/$(Platform)
+/platform:DesktopGL
+/config:
+/profile:Reach
+/compress:False
+
+#-------------------------------- References --------------------------------#
+
+
+#---------------------------------- Content ---------------------------------#
+
+#begin audio/bounce.wav
+/importer:WavImporter
+/processor:SoundEffectProcessor
+/processorParam:Quality=Best
+/build:audio/bounce.wav
+
+#begin audio/collect.wav
+/importer:WavImporter
+/processor:SoundEffectProcessor
+/processorParam:Quality=Best
+/build:audio/collect.wav
+
+#begin audio/theme.ogg
+/importer:OggImporter
+/processor:SongProcessor
+/processorParam:Quality=Best
+/build:audio/theme.ogg
+
+#begin audio/ui.wav
+/importer:WavImporter
+/processor:SoundEffectProcessor
+/processorParam:Quality=Best
+/build:audio/ui.wav
+
+#begin effects/grayscaleEffect.fx
+/importer:EffectImporter
+/processor:EffectProcessor
+/processorParam:DebugMode=Auto
+/build:effects/grayscaleEffect.fx
+
+#begin fonts/04B_30_5x.spritefont
+/importer:FontDescriptionImporter
+/processor:FontDescriptionProcessor
+/processorParam:PremultiplyAlpha=True
+/processorParam:TextureFormat=Compressed
+/build:fonts/04B_30_5x.spritefont
+
+#begin fonts/04b_30.fnt
+/copy:fonts/04b_30.fnt
+
+#begin fonts/04B_30.spritefont
+/importer:FontDescriptionImporter
+/processor:FontDescriptionProcessor
+/processorParam:PremultiplyAlpha=True
+/processorParam:TextureFormat=Compressed
+/build:fonts/04B_30.spritefont
+
+#begin images/atlas-definition.xml
+/copy:images/atlas-definition.xml
+
+#begin images/atlas.png
+/importer:TextureImporter
+/processor:TextureProcessor
+/processorParam:ColorKeyColor=255,0,255,255
+/processorParam:ColorKeyEnabled=True
+/processorParam:GenerateMipmaps=False
+/processorParam:PremultiplyAlpha=True
+/processorParam:ResizeToPowerOfTwo=False
+/processorParam:MakeSquare=False
+/processorParam:TextureFormat=Color
+/build:images/atlas.png
+
+#begin images/background-pattern.png
+/importer:TextureImporter
+/processor:TextureProcessor
+/processorParam:ColorKeyColor=255,0,255,255
+/processorParam:ColorKeyEnabled=True
+/processorParam:GenerateMipmaps=False
+/processorParam:PremultiplyAlpha=True
+/processorParam:ResizeToPowerOfTwo=False
+/processorParam:MakeSquare=False
+/processorParam:TextureFormat=Color
+/build:images/background-pattern.png
+
+#begin images/logo.png
+/importer:TextureImporter
+/processor:TextureProcessor
+/processorParam:ColorKeyColor=255,0,255,255
+/processorParam:ColorKeyEnabled=True
+/processorParam:GenerateMipmaps=False
+/processorParam:PremultiplyAlpha=True
+/processorParam:ResizeToPowerOfTwo=False
+/processorParam:MakeSquare=False
+/processorParam:TextureFormat=Color
+/build:images/logo.png
+
+#begin images/tilemap-definition.xml
+/copy:images/tilemap-definition.xml
+
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Content/audio/bounce.wav b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Content/audio/bounce.wav
new file mode 100644
index 00000000..baa7a47b
Binary files /dev/null and b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Content/audio/bounce.wav differ
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Content/audio/collect.wav b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Content/audio/collect.wav
new file mode 100644
index 00000000..506220de
Binary files /dev/null and b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Content/audio/collect.wav differ
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Content/audio/theme.ogg b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Content/audio/theme.ogg
new file mode 100644
index 00000000..72e1fd3b
Binary files /dev/null and b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Content/audio/theme.ogg differ
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Content/audio/ui.wav b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Content/audio/ui.wav
new file mode 100644
index 00000000..63e8941e
Binary files /dev/null and b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Content/audio/ui.wav differ
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Content/effects/grayscaleEffect.fx b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Content/effects/grayscaleEffect.fx
new file mode 100644
index 00000000..5dd0d8b6
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Content/effects/grayscaleEffect.fx
@@ -0,0 +1,53 @@
+#if OPENGL
+ #define SV_POSITION POSITION
+ #define VS_SHADERMODEL vs_3_0
+ #define PS_SHADERMODEL ps_3_0
+#else
+ #define VS_SHADERMODEL vs_4_0_level_9_1
+ #define PS_SHADERMODEL ps_4_0_level_9_1
+#endif
+
+Texture2D SpriteTexture;
+
+// A value between 0 and 1 that controls the intensity of the grayscale effect.
+// 0 = full color, 1 = full grayscale.
+float Saturation = 1.0;
+
+sampler2D SpriteTextureSampler = sampler_state
+{
+ Texture = ;
+};
+
+struct VertexShaderOutput
+{
+ float4 Position : SV_POSITION;
+ float4 Color : COLOR0;
+ float2 TextureCoordinates : TEXCOORD0;
+};
+
+float4 MainPS(VertexShaderOutput input) : COLOR
+{
+ // Sample the texture
+ float4 color = tex2D(SpriteTextureSampler, input.TextureCoordinates) * input.Color;
+
+ // Calculate the grayscale value based on human perception of colors
+ float grayscale = dot(color.rgb, float3(0.3, 0.59, 0.11));
+
+ // create a grayscale color vector (same value for R, G, and B)
+ float3 grayscaleColor = float3(grayscale, grayscale, grayscale);
+
+ // Linear interpolation between he grayscale color and the original color's
+ // rgb values based on the saturation parameter.
+ float3 finalColor = lerp(grayscale, color.rgb, Saturation);
+
+ // Return the final color with the original alpha value
+ return float4(finalColor, color.a);
+}
+
+technique SpriteDrawing
+{
+ pass P0
+ {
+ PixelShader = compile PS_SHADERMODEL MainPS();
+ }
+};
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Content/fonts/04B_30.spritefont b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Content/fonts/04B_30.spritefont
new file mode 100644
index 00000000..63d4728c
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Content/fonts/04B_30.spritefont
@@ -0,0 +1,16 @@
+
+
+
+ 04B_30.ttf
+ 17.5
+ 0
+ true
+
+
+
+
+ ~
+
+
+
+
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Content/fonts/04B_30.ttf b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Content/fonts/04B_30.ttf
new file mode 100644
index 00000000..4b93740c
Binary files /dev/null and b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Content/fonts/04B_30.ttf differ
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Content/fonts/04B_30_5x.spritefont b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Content/fonts/04B_30_5x.spritefont
new file mode 100644
index 00000000..dd239a53
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Content/fonts/04B_30_5x.spritefont
@@ -0,0 +1,16 @@
+
+
+
+ 04B_30.ttf
+ 87.5
+ 0
+ true
+
+
+
+
+ ~
+
+
+
+
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Content/fonts/04b_30.fnt b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Content/fonts/04b_30.fnt
new file mode 100644
index 00000000..772f8c54
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Content/fonts/04b_30.fnt
@@ -0,0 +1,99 @@
+info face="04b30" size=35 bold=0 italic=0 charset="" unicode=1 stretchH=100 smooth=0 aa=1 padding=0,0,0,0 spacing=1,1 outline=0
+common lineHeight=35 base=31 scaleW=256 scaleH=512 pages=1 packed=0 alphaChnl=0 redChnl=4 greenChnl=4 blueChnl=4
+page id=0 file="../images/atlas.png"
+chars count=95
+char id=32 x=30 y=152 width=3 height=1 xoffset=-1 yoffset=34 xadvance=29 page=0 chnl=15
+char id=33 x=240 y=30 width=14 height=29 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=34 x=102 y=232 width=25 height=15 xoffset=1 yoffset=4 xadvance=29 page=0 chnl=15
+char id=35 x=184 y=0 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=36 x=250 y=150 width=3 height=1 xoffset=-1 yoffset=34 xadvance=29 page=0 chnl=15
+char id=37 x=0 y=34 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=38 x=30 y=150 width=3 height=1 xoffset=-1 yoffset=34 xadvance=29 page=0 chnl=15
+char id=39 x=245 y=202 width=10 height=15 xoffset=1 yoffset=4 xadvance=14 page=0 chnl=15
+char id=40 x=106 y=150 width=16 height=29 xoffset=1 yoffset=2 xadvance=21 page=0 chnl=15
+char id=41 x=123 y=150 width=16 height=29 xoffset=1 yoffset=2 xadvance=21 page=0 chnl=15
+char id=42 x=128 y=232 width=14 height=15 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=43 x=94 y=0 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=44 x=143 y=232 width=10 height=14 xoffset=1 yoffset=19 xadvance=14 page=0 chnl=15
+char id=45 x=154 y=232 width=25 height=11 xoffset=1 yoffset=12 xadvance=29 page=0 chnl=15
+char id=46 x=231 y=228 width=10 height=10 xoffset=1 yoffset=19 xadvance=14 page=0 chnl=15
+char id=47 x=60 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=48 x=90 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=49 x=46 y=150 width=19 height=29 xoffset=1 yoffset=2 xadvance=23 page=0 chnl=15
+char id=50 x=150 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=51 x=180 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=52 x=210 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=53 x=0 y=94 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=54 x=180 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=55 x=60 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=56 x=90 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=57 x=120 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=58 x=234 y=202 width=10 height=25 xoffset=1 yoffset=4 xadvance=14 page=0 chnl=15
+char id=59 x=244 y=0 width=10 height=29 xoffset=1 yoffset=4 xadvance=14 page=0 chnl=15
+char id=60 x=86 y=150 width=19 height=29 xoffset=1 yoffset=2 xadvance=23 page=0 chnl=15
+char id=61 x=182 y=176 width=25 height=25 xoffset=1 yoffset=4 xadvance=29 page=0 chnl=15
+char id=62 x=237 y=120 width=18 height=29 xoffset=1 yoffset=2 xadvance=23 page=0 chnl=15
+char id=63 x=180 y=120 width=28 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=64 x=34 y=150 width=3 height=1 xoffset=-1 yoffset=34 xadvance=29 page=0 chnl=15
+char id=65 x=120 y=120 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=66 x=150 y=120 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=67 x=124 y=0 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=68 x=154 y=0 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=69 x=214 y=0 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=70 x=30 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=71 x=60 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=72 x=90 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=73 x=240 y=90 width=14 height=29 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=74 x=120 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=75 x=150 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=76 x=209 y=120 width=27 height=29 xoffset=1 yoffset=2 xadvance=31 page=0 chnl=15
+char id=77 x=30 y=0 width=31 height=29 xoffset=1 yoffset=2 xadvance=35 page=0 chnl=15
+char id=78 x=210 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=79 x=0 y=64 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=80 x=30 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=81 x=0 y=0 width=29 height=33 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=82 x=120 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=83 x=30 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=84 x=150 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=85 x=180 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=86 x=210 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=87 x=62 y=0 width=31 height=29 xoffset=1 yoffset=2 xadvance=35 page=0 chnl=15
+char id=88 x=0 y=124 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=89 x=30 y=120 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=90 x=60 y=120 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=91 x=240 y=60 width=14 height=29 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=92 x=90 y=120 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=93 x=140 y=150 width=14 height=29 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=94 x=180 y=232 width=16 height=11 xoffset=1 yoffset=4 xadvance=21 page=0 chnl=15
+char id=95 x=0 y=262 width=29 height=10 xoffset=1 yoffset=21 xadvance=33 page=0 chnl=15
+char id=96 x=197 y=228 width=16 height=11 xoffset=1 yoffset=4 xadvance=21 page=0 chnl=15
+char id=97 x=208 y=176 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=98 x=0 y=210 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=99 x=26 y=210 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=100 x=52 y=206 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=101 x=78 y=206 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=102 x=104 y=206 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=103 x=130 y=206 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=104 x=156 y=206 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=105 x=234 y=176 width=12 height=25 xoffset=1 yoffset=6 xadvance=16 page=0 chnl=15
+char id=106 x=182 y=202 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=107 x=208 y=202 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=108 x=78 y=232 width=23 height=25 xoffset=1 yoffset=6 xadvance=27 page=0 chnl=15
+char id=109 x=197 y=150 width=26 height=25 xoffset=1 yoffset=6 xadvance=31 page=0 chnl=15
+char id=110 x=0 y=236 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=111 x=26 y=236 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=112 x=78 y=180 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=113 x=0 y=154 width=25 height=29 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=114 x=52 y=232 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=115 x=224 y=150 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=116 x=0 y=184 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=117 x=26 y=184 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=118 x=52 y=180 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=119 x=170 y=150 width=26 height=25 xoffset=1 yoffset=6 xadvance=31 page=0 chnl=15
+char id=120 x=104 y=180 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=121 x=130 y=180 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=122 x=156 y=180 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=123 x=26 y=154 width=19 height=29 xoffset=1 yoffset=2 xadvance=23 page=0 chnl=15
+char id=124 x=155 y=150 width=14 height=29 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=125 x=66 y=150 width=19 height=29 xoffset=1 yoffset=2 xadvance=23 page=0 chnl=15
+char id=126 x=214 y=228 width=16 height=11 xoffset=1 yoffset=4 xadvance=21 page=0 chnl=15
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Content/images/atlas-definition.xml b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Content/images/atlas-definition.xml
new file mode 100644
index 00000000..21772022
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Content/images/atlas-definition.xml
@@ -0,0 +1,34 @@
+
+
+ images/atlas
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Content/images/atlas.png b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Content/images/atlas.png
new file mode 100644
index 00000000..f7def20f
Binary files /dev/null and b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Content/images/atlas.png differ
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Content/images/background-pattern.png b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Content/images/background-pattern.png
new file mode 100644
index 00000000..2d8d878e
Binary files /dev/null and b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Content/images/background-pattern.png differ
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Content/images/logo.png b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Content/images/logo.png
new file mode 100644
index 00000000..1509036c
Binary files /dev/null and b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Content/images/logo.png differ
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Content/images/tilemap-definition.xml b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Content/images/tilemap-definition.xml
new file mode 100644
index 00000000..85658c60
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Content/images/tilemap-definition.xml
@@ -0,0 +1,15 @@
+
+
+ images/atlas
+
+ 00 01 02 01 02 01 02 01 02 01 02 01 02 01 02 03
+ 04 05 05 06 05 05 06 05 05 06 05 05 06 05 05 07
+ 08 09 09 09 09 09 09 09 09 09 09 09 09 09 09 11
+ 04 09 09 09 09 09 09 09 10 09 09 09 09 10 09 07
+ 08 09 10 09 09 09 09 09 09 09 09 09 09 09 09 11
+ 04 09 09 09 09 09 09 09 09 09 09 09 09 09 09 07
+ 08 10 09 09 09 09 09 09 09 09 10 09 09 09 09 11
+ 04 09 09 09 09 09 10 09 09 09 09 09 09 09 09 07
+ 12 13 14 13 14 13 14 13 14 13 14 13 14 13 14 15
+
+
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/DungeonSlime.csproj b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/DungeonSlime.csproj
new file mode 100644
index 00000000..3dbcf3ce
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/DungeonSlime.csproj
@@ -0,0 +1,33 @@
+
+
+ WinExe
+ net9.0
+ Major
+ false
+ false
+
+
+ app.manifest
+ Icon.ico
+
+
+
+
+
+
+
+ Icon.ico
+
+
+ Icon.bmp
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Game1.cs b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Game1.cs
new file mode 100644
index 00000000..bf64e6b8
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Game1.cs
@@ -0,0 +1,79 @@
+using System;
+using DungeonSlime.Scenes;
+using Microsoft.Xna.Framework.Media;
+using MonoGameLibrary;
+using MonoGameGum;
+using Gum.Forms.Controls;
+
+namespace DungeonSlime;
+
+public class Game1 : Core
+{
+ // The background theme song
+ private Song _themeSong;
+
+ public Game1() : base("Dungeon Slime", 1280, 720, false)
+ {
+ }
+
+ protected override void Initialize()
+ {
+ try
+ {
+ base.Initialize();
+
+ // Start playing the background music
+ Audio.PlaySong(_themeSong);
+
+ // Initialize the Gum UI service
+ InitializeGum();
+
+ // Start the game with the title scene.
+ ChangeScene(new TitleScene());
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine(e);
+ throw;
+ }
+ }
+
+ private void InitializeGum()
+ {
+ // Initialize the Gum service
+ GumService.Default.Initialize(this);
+
+ // Tell the Gum service which content manager to use. We will tell it to
+ // use the global content manager from our Core.
+ GumService.Default.ContentLoader.XnaContentManager = Core.Content;
+
+ // Register keyboard input for UI control.
+ FrameworkElement.KeyboardsForUiControl.Add(GumService.Default.Keyboard);
+
+ // Register gamepad input for Ui control.
+ FrameworkElement.GamePadsForUiControl.AddRange(GumService.Default.Gamepads);
+
+ // Customize the tab reverse UI navigation to also trigger when the keyboard
+ // Up arrow key is pushed.
+ FrameworkElement.TabReverseKeyCombos.Add(
+ new KeyCombo() { PushedKey = Microsoft.Xna.Framework.Input.Keys.Up });
+
+ // Customize the tab UI navigation to also trigger when the keyboard
+ // Down arrow key is pushed.
+ FrameworkElement.TabKeyCombos.Add(
+ new KeyCombo() { PushedKey = Microsoft.Xna.Framework.Input.Keys.Down });
+
+ // The assets created for the UI were done so at 1/4th the size to keep the size of the
+ // texture atlas small. So we will set the default canvas size to be 1/4th the size of
+ // the game's resolution then tell gum to zoom in by a factor of 4.
+ GumService.Default.CanvasWidth = GraphicsDevice.PresentationParameters.BackBufferWidth / 4.0f;
+ GumService.Default.CanvasHeight = GraphicsDevice.PresentationParameters.BackBufferHeight / 4.0f;
+ GumService.Default.Renderer.Camera.Zoom = 4.0f;
+ }
+
+ protected override void LoadContent()
+ {
+ // Load the background theme music
+ _themeSong = Content.Load("audio/theme");
+ }
+}
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/GameController.cs b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/GameController.cs
new file mode 100644
index 00000000..92165382
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/GameController.cs
@@ -0,0 +1,79 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Input;
+using MonoGameLibrary;
+using MonoGameLibrary.Input;
+
+namespace DungeonSlime;
+
+///
+/// Provides a game-specific input abstraction that maps physical inputs
+/// to game actions, bridging our input system with game-specific functionality.
+///
+public static class GameController
+{
+ private static KeyboardInfo s_keyboard => Core.Input.Keyboard;
+ private static GamePadInfo s_gamePad => Core.Input.GamePads[(int)PlayerIndex.One];
+
+ ///
+ /// Returns true if the player has triggered the "move up" action.
+ ///
+ public static bool MoveUp()
+ {
+ return s_keyboard.WasKeyJustPressed(Keys.Up) ||
+ s_keyboard.WasKeyJustPressed(Keys.W) ||
+ s_gamePad.WasButtonJustPressed(Buttons.DPadUp) ||
+ s_gamePad.WasButtonJustPressed(Buttons.LeftThumbstickUp);
+ }
+
+ ///
+ /// Returns true if the player has triggered the "move down" action.
+ ///
+ public static bool MoveDown()
+ {
+ return s_keyboard.WasKeyJustPressed(Keys.Down) ||
+ s_keyboard.WasKeyJustPressed(Keys.S) ||
+ s_gamePad.WasButtonJustPressed(Buttons.DPadDown) ||
+ s_gamePad.WasButtonJustPressed(Buttons.LeftThumbstickDown);
+ }
+
+ ///
+ /// Returns true if the player has triggered the "move left" action.
+ ///
+ public static bool MoveLeft()
+ {
+ return s_keyboard.WasKeyJustPressed(Keys.Left) ||
+ s_keyboard.WasKeyJustPressed(Keys.A) ||
+ s_gamePad.WasButtonJustPressed(Buttons.DPadLeft) ||
+ s_gamePad.WasButtonJustPressed(Buttons.LeftThumbstickLeft);
+ }
+
+ ///
+ /// Returns true if the player has triggered the "move right" action.
+ ///
+ public static bool MoveRight()
+ {
+ return s_keyboard.WasKeyJustPressed(Keys.Right) ||
+ s_keyboard.WasKeyJustPressed(Keys.D) ||
+ s_gamePad.WasButtonJustPressed(Buttons.DPadRight) ||
+ s_gamePad.WasButtonJustPressed(Buttons.LeftThumbstickRight);
+ }
+
+ ///
+ /// Returns true if the player has triggered the "pause" action.
+ ///
+ public static bool Pause()
+ {
+ return s_keyboard.WasKeyJustPressed(Keys.Escape) ||
+ s_gamePad.WasButtonJustPressed(Buttons.Start);
+ }
+
+ ///
+ /// Returns true if the player has triggered the "action" button,
+ /// typically used for menu confirmation.
+ ///
+ public static bool Action()
+ {
+ return s_keyboard.WasKeyJustPressed(Keys.Enter) ||
+ s_gamePad.WasButtonJustPressed(Buttons.A);
+ }
+}
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/GameObjects/Bat.cs b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/GameObjects/Bat.cs
new file mode 100644
index 00000000..ddc855ed
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/GameObjects/Bat.cs
@@ -0,0 +1,123 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Audio;
+using MonoGameLibrary;
+using MonoGameLibrary.Graphics;
+
+namespace DungeonSlime.GameObjects;
+
+public class Bat
+{
+ private const float MOVEMENT_SPEED = 5.0f;
+
+ // The velocity of the bat that defines the direction and how much in that
+ // direction to update the bats position each update cycle.
+ private Vector2 _velocity;
+
+ // The AnimatedSprite used when drawing the bat.
+ private AnimatedSprite _sprite;
+
+ // The sound effect to play when the bat bounces off the edge of the room.
+ private SoundEffect _bounceSoundEffect;
+
+ ///
+ /// Gets or Sets the position of the bat.
+ ///
+ public Vector2 Position { get; set; }
+
+ ///
+ /// Creates a new Bat using the specified animated sprite and sound effect.
+ ///
+ /// The AnimatedSprite ot use when drawing the bat.
+ /// The sound effect to play when the bat bounces off a wall.
+ public Bat(AnimatedSprite sprite, SoundEffect bounceSoundEffect)
+ {
+ _sprite = sprite;
+ _bounceSoundEffect = bounceSoundEffect;
+ }
+
+ ///
+ /// Randomizes the velocity of the bat.
+ ///
+ public void RandomizeVelocity()
+ {
+ // Generate a random angle
+ float angle = (float)(Random.Shared.NextDouble() * MathHelper.TwoPi);
+
+ // Convert the angle to a direction vector
+ float x = (float)Math.Cos(angle);
+ float y = (float)Math.Sin(angle);
+ Vector2 direction = new Vector2(x, y);
+
+ // Multiply the direction vector by the movement speed to get the
+ // final velocity
+ _velocity = direction * MOVEMENT_SPEED;
+ }
+
+ ///
+ /// Handles a bounce event when the bat collides with a wall or boundary.
+ ///
+ /// The normal vector of the surface the bat is bouncing against.
+ public void Bounce(Vector2 normal)
+ {
+ Vector2 newPosition = Position;
+
+ // Adjust the position based on the normal to prevent sticking to walls.
+ if (normal.X != 0)
+ {
+ // We are bouncing off a vertical wall (left/right).
+ // Move slightly away from the wall in the direction of the normal.
+ newPosition.X += normal.X * (_sprite.Width * 0.1f);
+ }
+
+ if (normal.Y != 0)
+ {
+ // We are bouncing off a horizontal wall (top/bottom).
+ // Move slightly way from the wall in the direction of the normal.
+ newPosition.Y += normal.Y * (_sprite.Height * 0.1f);
+ }
+
+ // Apply the new position
+ Position = newPosition;
+
+ // Apply reflection based on the normal.
+ _velocity = Vector2.Reflect(_velocity, normal);
+
+ // Play the bounce sound effect.
+ Core.Audio.PlaySoundEffect(_bounceSoundEffect);
+ }
+
+ ///
+ /// Returns a Circle value that represents collision bounds of the bat.
+ ///
+ /// A Circle value.
+ public Circle GetBounds()
+ {
+ int x = (int)(Position.X + _sprite.Width * 0.5f);
+ int y = (int)(Position.Y + _sprite.Height * 0.5f);
+ int radius = (int)(_sprite.Width * 0.25f);
+
+ return new Circle(x, y, radius);
+ }
+
+ ///
+ /// Updates the bat.
+ ///
+ /// A snapshot of the timing values for the current update cycle.
+ public void Update(GameTime gameTime)
+ {
+ // Update the animated sprite
+ _sprite.Update(gameTime);
+
+ // Update the position of the bat based on the velocity.
+ Position += _velocity;
+ }
+
+ ///
+ /// Draws the bat.
+ ///
+ public void Draw()
+ {
+ _sprite.Draw(Core.SpriteBatch, Position);
+ }
+}
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/GameObjects/Slime.cs b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/GameObjects/Slime.cs
new file mode 100644
index 00000000..08b5a63d
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/GameObjects/Slime.cs
@@ -0,0 +1,265 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Xna.Framework;
+using MonoGameLibrary;
+using MonoGameLibrary.Graphics;
+
+namespace DungeonSlime.GameObjects;
+
+public class Slime
+{
+ // A constant value that represents the amount of time to wait between
+ // movement updates.
+ private static readonly TimeSpan s_movementTime = TimeSpan.FromMilliseconds(200);
+
+ // The amount of time that has elapsed since the last movement update.
+ private TimeSpan _movementTimer;
+
+ // Normalized value (0-1) representing progress between movement ticks for visual interpolation
+ private float _movementProgress;
+
+ // The next direction to apply to the head of the slime chain during the
+ // next movement update.
+ private Vector2 _nextDirection;
+
+ // The number of pixels to move the head segment during the movement cycle.
+ private float _stride;
+
+ // Tracks the segments of the slime chain.
+ private List _segments;
+
+ // The AnimatedSprite used when drawing each slime segment
+ private AnimatedSprite _sprite;
+
+ // Buffer to queue inputs input by player during input polling.
+ private Queue _inputBuffer;
+
+ // The maximum size of the buffer queue.
+ private const int MAX_BUFFER_SIZE = 2;
+
+ ///
+ /// Event that is raised if it is detected that the head segment of the slime
+ /// has collided with a body segment.
+ ///
+ public event EventHandler BodyCollision;
+
+ ///
+ /// Creates a new Slime using the specified animated sprite.
+ ///
+ /// The AnimatedSprite to use when drawing the slime.
+ public Slime(AnimatedSprite sprite)
+ {
+ _sprite = sprite;
+ }
+
+ ///
+ /// Initializes the slime, can be used to reset it back to an initial state.
+ ///
+ /// The position the slime should start at.
+ /// The total number of pixels to move the head segment during each movement cycle.
+ public void Initialize(Vector2 startingPosition, float stride)
+ {
+ // Initialize the segment collection.
+ _segments = new List();
+
+ // Set the stride
+ _stride = stride;
+
+ // Create the initial head of the slime chain.
+ SlimeSegment head = new SlimeSegment();
+ head.At = startingPosition;
+ head.To = startingPosition + new Vector2(_stride, 0);
+ head.Direction = Vector2.UnitX;
+
+ // Add it to the segment collection.
+ _segments.Add(head);
+
+ // Set the initial next direction as the same direction the head is
+ // moving.
+ _nextDirection = head.Direction;
+
+ // Zero out the movement timer.
+ _movementTimer = TimeSpan.Zero;
+
+ // initialize the input buffer.
+ _inputBuffer = new Queue(MAX_BUFFER_SIZE);
+ }
+
+ private void HandleInput()
+ {
+ Vector2 potentialNextDirection = Vector2.Zero;
+
+ if (GameController.MoveUp())
+ {
+ potentialNextDirection = -Vector2.UnitY;
+ }
+ else if (GameController.MoveDown())
+ {
+ potentialNextDirection = Vector2.UnitY;
+ }
+ else if (GameController.MoveLeft())
+ {
+ potentialNextDirection = -Vector2.UnitX;
+ }
+ else if (GameController.MoveRight())
+ {
+ potentialNextDirection = Vector2.UnitX;
+ }
+
+ // If a new direction was input, consider adding it to the buffer
+ if (potentialNextDirection != Vector2.Zero && _inputBuffer.Count < MAX_BUFFER_SIZE)
+ {
+ // If the buffer is empty, validate against the current direction;
+ // otherwise, validate against the last buffered direction
+ Vector2 validateAgainst = _inputBuffer.Count > 0 ?
+ _inputBuffer.Last() :
+ _segments[0].Direction;
+
+ // Only allow direction change if it is not reversing the current
+ // direction. This prevents th slime from backing into itself
+ float dot = Vector2.Dot(potentialNextDirection, validateAgainst);
+ if (dot >= 0)
+ {
+ _inputBuffer.Enqueue(potentialNextDirection);
+ }
+ }
+ }
+
+ private void Move()
+ {
+ // Get the next direction from the input buffer if one is available
+ if (_inputBuffer.Count > 0)
+ {
+ _nextDirection = _inputBuffer.Dequeue();
+ }
+
+ // Capture the value of the head segment
+ SlimeSegment head = _segments[0];
+
+ // Update the direction the head is supposed to move in to the
+ // next direction cached.
+ head.Direction = _nextDirection;
+
+ // Update the head's "at" position to be where it was moving "to"
+ head.At = head.To;
+
+ // Update the head's "to" position to the next tile in the direction
+ // it is moving.
+ head.To = head.At + head.Direction * _stride;
+
+ // Insert the new adjusted value for the head at the front of the
+ // segments and remove the tail segment. This effectively moves
+ // the entire chain forward without needing to loop through every
+ // segment and update its "at" and "to" positions.
+ _segments.Insert(0, head);
+ _segments.RemoveAt(_segments.Count - 1);
+
+ // Iterate through all of the segments except the head and check
+ // if they are at the same position as the head. If they are, then
+ // the head is colliding with a body segment and a body collision
+ // has occurred.
+ for (int i = 1; i < _segments.Count; i++)
+ {
+ SlimeSegment segment = _segments[i];
+
+ if (head.At == segment.At)
+ {
+ if (BodyCollision != null)
+ {
+ BodyCollision.Invoke(this, EventArgs.Empty);
+ }
+
+ return;
+ }
+ }
+ }
+
+ ///
+ /// Informs the slime to grow by one segment.
+ ///
+ public void Grow()
+ {
+ // Capture the value of the tail segment
+ SlimeSegment tail = _segments[_segments.Count - 1];
+
+ // Create a new tail segment that is positioned a grid cell in the
+ // reverse direction from the tail moving to the tail.
+ SlimeSegment newTail = new SlimeSegment();
+ newTail.At = tail.To + tail.ReverseDirection * _stride;
+ newTail.To = tail.At;
+ newTail.Direction = Vector2.Normalize(tail.At - newTail.At);
+
+ // Add the new tail segment
+ _segments.Add(newTail);
+ }
+
+ ///
+ /// Updates the slime.
+ ///
+ /// A snapshot of the timing values for the current update cycle.
+ public void Update(GameTime gameTime)
+ {
+ // Update the animated sprite.
+ _sprite.Update(gameTime);
+
+ // Handle any player input
+ HandleInput();
+
+ // Increment the movement timer by the frame elapsed time.
+ _movementTimer += gameTime.ElapsedGameTime;
+
+ // If the movement timer has accumulated enough time to be greater than
+ // the movement time threshold, then perform a full movement.
+ if (_movementTimer >= s_movementTime)
+ {
+ _movementTimer -= s_movementTime;
+ Move();
+ }
+
+ // Update the movement lerp offset amount
+ _movementProgress = (float)(_movementTimer.TotalSeconds / s_movementTime.TotalSeconds);
+ }
+
+ ///
+ /// Draws the slime.
+ ///
+ public void Draw()
+ {
+ // Iterate through each segment and draw it
+ foreach (SlimeSegment segment in _segments)
+ {
+ // Calculate the visual position of the segment at the moment by
+ // lerping between its "at" and "to" position by the movement
+ // offset lerp amount
+ Vector2 pos = Vector2.Lerp(segment.At, segment.To, _movementProgress);
+
+ // Draw the slime sprite at the calculated visual position of this
+ // segment
+ _sprite.Draw(Core.SpriteBatch, pos);
+ }
+ }
+
+ ///
+ /// Returns a Circle value that represents collision bounds of the slime.
+ ///
+ /// A Circle value.
+ public Circle GetBounds()
+ {
+ SlimeSegment head = _segments[0];
+
+ // Calculate the visual position of the head at the moment of this
+ // method call by lerping between the "at" and "to" position by the
+ // movement offset lerp amount
+ Vector2 pos = Vector2.Lerp(head.At, head.To, _movementProgress);
+
+ // Create the bounds using the calculated visual position of the head.
+ Circle bounds = new Circle(
+ (int)(pos.X + (_sprite.Width * 0.5f)),
+ (int)(pos.Y + (_sprite.Height * 0.5f)),
+ (int)(_sprite.Width * 0.5f)
+ );
+
+ return bounds;
+ }
+}
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/GameObjects/SlimeSegment.cs b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/GameObjects/SlimeSegment.cs
new file mode 100644
index 00000000..b00189eb
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/GameObjects/SlimeSegment.cs
@@ -0,0 +1,26 @@
+using Microsoft.Xna.Framework;
+
+namespace DungeonSlime.GameObjects;
+
+public struct SlimeSegment
+{
+ ///
+ /// The position this slime segment is at before the movement cycle occurs.
+ ///
+ public Vector2 At;
+
+ ///
+ /// The position this slime segment should move to during the next movement cycle.
+ ///
+ public Vector2 To;
+
+ ///
+ /// The direction this slime segment is moving.
+ ///
+ public Vector2 Direction;
+
+ ///
+ /// The opposite direction this slime segment is moving.
+ ///
+ public Vector2 ReverseDirection => new Vector2(-Direction.X, -Direction.Y);
+}
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Icon.bmp b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Icon.bmp
new file mode 100644
index 00000000..2b481653
Binary files /dev/null and b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Icon.bmp differ
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Icon.ico b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Icon.ico
new file mode 100644
index 00000000..7d9dec18
Binary files /dev/null and b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Icon.ico differ
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Program.cs b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Program.cs
new file mode 100644
index 00000000..d491c406
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Program.cs
@@ -0,0 +1,2 @@
+using var game = new DungeonSlime.Game1();
+game.Run();
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Scenes/GameScene.cs b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Scenes/GameScene.cs
new file mode 100644
index 00000000..d01a3b03
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Scenes/GameScene.cs
@@ -0,0 +1,428 @@
+using System;
+using DungeonSlime.GameObjects;
+using DungeonSlime.UI;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Audio;
+using Microsoft.Xna.Framework.Graphics;
+using MonoGameGum;
+using MonoGameLibrary;
+using MonoGameLibrary.Graphics;
+using MonoGameLibrary.Scenes;
+
+namespace DungeonSlime.Scenes;
+
+public class GameScene : Scene
+{
+ private enum GameState
+ {
+ Playing,
+ Paused,
+ GameOver
+ }
+
+ // Reference to the slime.
+ private Slime _slime;
+
+ // Reference to the bat.
+ private Bat _bat;
+
+ // Defines the tilemap to draw.
+ private Tilemap _tilemap;
+
+ // Defines the bounds of the room that the slime and bat are contained within.
+ private Rectangle _roomBounds;
+
+ // The sound effect to play when the slime eats a bat.
+ private SoundEffect _collectSoundEffect;
+
+ // Tracks the players score.
+ private int _score;
+
+ private GameSceneUI _ui;
+
+ private GameState _state;
+
+ // The grayscale shader effect.
+ private Effect _grayscaleEffect;
+
+ // The amount of saturation to provide the grayscale shader effect
+ private float _saturation = 1.0f;
+
+ // The speed of the fade to grayscale effect.
+ private const float FADE_SPEED = 0.02f;
+
+ public override void Initialize()
+ {
+ // LoadContent is called during base.Initialize().
+ base.Initialize();
+
+ // During the game scene, we want to disable exit on escape. Instead,
+ // the escape key will be used to return back to the title screen
+ Core.ExitOnEscape = false;
+
+ // Create the room bounds by getting the bounds of the screen then
+ // using the Inflate method to "Deflate" the bounds by the width and
+ // height of a tile so that the bounds only covers the inside room of
+ // the dungeon tilemap.
+ _roomBounds = Core.GraphicsDevice.PresentationParameters.Bounds;
+ _roomBounds.Inflate(-_tilemap.TileWidth, -_tilemap.TileHeight);
+
+ // Subscribe to the slime's BodyCollision event so that a game over
+ // can be triggered when this event is raised.
+ _slime.BodyCollision += OnSlimeBodyCollision;
+
+ // Create any UI elements from the root element created in previous
+ // scenes
+ GumService.Default.Root.Children.Clear();
+
+ // Initialize the user interface for the game scene.
+ InitializeUI();
+
+ // Initialize a new game to be played.
+ InitializeNewGame();
+ }
+
+ private void InitializeUI()
+ {
+ // Clear out any previous UI element incase we came here
+ // from a different scene.
+ GumService.Default.Root.Children.Clear();
+
+ // Create the game scene ui instance.
+ _ui = new GameSceneUI();
+
+ // Subscribe to the events from the game scene ui.
+ _ui.ResumeButtonClick += OnResumeButtonClicked;
+ _ui.RetryButtonClick += OnRetryButtonClicked;
+ _ui.QuitButtonClick += OnQuitButtonClicked;
+ }
+
+ private void OnResumeButtonClicked(object sender, EventArgs args)
+ {
+ // Change the game state back to playing
+ _state = GameState.Playing;
+ }
+
+ private void OnRetryButtonClicked(object sender, EventArgs args)
+ {
+ // Player has chosen to retry, so initialize a new game
+ InitializeNewGame();
+ }
+
+ private void OnQuitButtonClicked(object sender, EventArgs args)
+ {
+ // Player has chosen to quit, so return back to the title scene
+ Core.ChangeScene(new TitleScene());
+ }
+
+ private void InitializeNewGame()
+ {
+ // Calculate the position for the slime, which will be at the center
+ // tile of the tile map.
+ Vector2 slimePos = new Vector2();
+ slimePos.X = (_tilemap.Columns / 2) * _tilemap.TileWidth;
+ slimePos.Y = (_tilemap.Rows / 2) * _tilemap.TileHeight;
+
+ // Initialize the slime
+ _slime.Initialize(slimePos, _tilemap.TileWidth);
+
+ // Initialize the bat
+ _bat.RandomizeVelocity();
+ PositionBatAwayFromSlime();
+
+ // Reset the score
+ _score = 0;
+
+ // Set the game state to playing
+ _state = GameState.Playing;
+ }
+
+ public override void LoadContent()
+ {
+ // Create the texture atlas from the XML configuration file
+ TextureAtlas atlas = TextureAtlas.FromFile(Core.Content, "images/atlas-definition.xml");
+
+ // Create the tilemap from the XML configuration file.
+ _tilemap = Tilemap.FromFile(Content, "images/tilemap-definition.xml");
+ _tilemap.Scale = new Vector2(4.0f, 4.0f);
+
+ // Create the animated sprite for the slime from the atlas.
+ AnimatedSprite slimeAnimation = atlas.CreateAnimatedSprite("slime-animation");
+ slimeAnimation.Scale = new Vector2(4.0f, 4.0f);
+
+ // Create the slime
+ _slime = new Slime(slimeAnimation);
+
+ // Create the animated sprite for the bat from the atlas.
+ AnimatedSprite batAnimation = atlas.CreateAnimatedSprite("bat-animation");
+ batAnimation.Scale = new Vector2(4.0f, 4.0f);
+
+ // Load the bounce sound effect for the bat
+ SoundEffect bounceSoundEffect = Content.Load("audio/bounce");
+
+ // Create the bat
+ _bat = new Bat(batAnimation, bounceSoundEffect);
+
+ // Load the collect sound effect
+ _collectSoundEffect = Content.Load("audio/collect");
+
+ // Load the grayscale effect
+ _grayscaleEffect = Content.Load("effects/grayscaleEffect");
+ }
+
+ public override void Update(GameTime gameTime)
+ {
+ // Ensure the UI is always updated
+ _ui.Update(gameTime);
+
+ if (_state != GameState.Playing)
+ {
+ // The game is in either a paused or game over state, so
+ // gradually decrease the saturation to create the fading grayscale.
+ _saturation = Math.Max(0.0f, _saturation - FADE_SPEED);
+
+ // If its just a game over state, return back
+ if (_state == GameState.GameOver)
+ {
+ return;
+ }
+ }
+
+ // If the pause button is pressed, toggle the pause state
+ if (GameController.Pause())
+ {
+ TogglePause();
+ }
+
+ // At this point, if the game is paused, just return back early
+ if (_state == GameState.Paused)
+ {
+ return;
+ }
+
+ // Update the slime;
+ _slime.Update(gameTime);
+
+ // Update the bat;
+ _bat.Update(gameTime);
+
+ // Perform collision checks
+ CollisionChecks();
+ }
+
+ private void CollisionChecks()
+ {
+ // Capture the current bounds of the slime and bat
+ Circle slimeBounds = _slime.GetBounds();
+ Circle batBounds = _bat.GetBounds();
+
+ // FIrst perform a collision check to see if the slime is colliding with
+ // the bat, which means the slime eats the bat.
+ if (slimeBounds.Intersects(batBounds))
+ {
+ // Move the bat to a new position away from the slime.
+ PositionBatAwayFromSlime();
+
+ // Randomize the velocity of the bat.
+ _bat.RandomizeVelocity();
+
+ // Tell the slime to grow.
+ _slime.Grow();
+
+ // Increment the score.
+ _score += 100;
+
+ // Update the score display on the UI.
+ _ui.UpdateScoreText(_score);
+
+ // Play the collect sound effect
+ Core.Audio.PlaySoundEffect(_collectSoundEffect);
+ }
+
+ // Next check if the slime is colliding with the wall by validating if
+ // it is within the bounds of the room. If it is outside the room
+ // bounds, then it collided with a wall which triggers a game over.
+ if (slimeBounds.Top < _roomBounds.Top ||
+ slimeBounds.Bottom > _roomBounds.Bottom ||
+ slimeBounds.Left < _roomBounds.Left ||
+ slimeBounds.Right > _roomBounds.Right)
+ {
+ GameOver();
+ return;
+ }
+
+ // Finally, check if the bat is colliding with a wall by validating if
+ // it is within the bounds of the room. If it is outside the room
+ // bounds, then it collided with a wall, and the bat should bounce
+ // off of that wall.
+ if (batBounds.Top < _roomBounds.Top)
+ {
+ _bat.Bounce(Vector2.UnitY);
+ }
+ else if (batBounds.Bottom > _roomBounds.Bottom)
+ {
+ _bat.Bounce(-Vector2.UnitY);
+ }
+
+ if (batBounds.Left < _roomBounds.Left)
+ {
+ _bat.Bounce(Vector2.UnitX);
+ }
+ else if (batBounds.Right > _roomBounds.Right)
+ {
+ _bat.Bounce(-Vector2.UnitX);
+ }
+ }
+
+ private void PositionBatAwayFromSlime()
+ {
+ // Calculate the position that is in the center of the bounds
+ // of the room.
+ float roomCenterX = _roomBounds.X + _roomBounds.Width * 0.5f;
+ float roomCenterY = _roomBounds.Y + _roomBounds.Height * 0.5f;
+ Vector2 roomCenter = new Vector2(roomCenterX, roomCenterY);
+
+ // Get the bounds of the slime and calculate the center position
+ Circle slimeBounds = _slime.GetBounds();
+ Vector2 slimeCenter = new Vector2(slimeBounds.X, slimeBounds.Y);
+
+ // Calculate the distance vector from the center of the room to the
+ // center of the slime.
+ Vector2 centerToSlime = slimeCenter - roomCenter;
+
+ // Get the bounds of the bat
+ Circle batBounds = _bat.GetBounds();
+
+ // Calculate the amount of padding we will add to the new position of
+ // the bat to ensure it is not sticking to walls
+ int padding = batBounds.Radius * 2;
+
+ // Calculate the new position of the bat by finding which component of
+ // the center to slime vector (X or Y) is larger and in which direction.
+ Vector2 newBatPosition = Vector2.Zero;
+ if (Math.Abs(centerToSlime.X) > Math.Abs(centerToSlime.Y))
+ {
+ // The slime is closer to either the left or right wall, so the Y
+ // position will be a random position between the top and bottom
+ // walls.
+ newBatPosition.Y = Random.Shared.Next(
+ _roomBounds.Top + padding,
+ _roomBounds.Bottom - padding
+ );
+
+ if (centerToSlime.X > 0)
+ {
+ // The slime is closer to the right side wall, so place the
+ // bat on the left side wall
+ newBatPosition.X = _roomBounds.Left + padding;
+ }
+ else
+ {
+ // The slime is closer ot the left side wall, so place the
+ // bat on the right side wall.
+ newBatPosition.X = _roomBounds.Right - padding * 2;
+ }
+ }
+ else
+ {
+ // The slime is closer to either the top or bottom wall, so the X
+ // position will be a random position between the left and right
+ // walls.
+ newBatPosition.X = Random.Shared.Next(
+ _roomBounds.Left + padding,
+ _roomBounds.Right - padding
+ );
+
+ if (centerToSlime.Y > 0)
+ {
+ // The slime is closer to the top wall, so place the bat on the
+ // bottom wall
+ newBatPosition.Y = _roomBounds.Top + padding;
+ }
+ else
+ {
+ // The slime is closer to the bottom wall, so place the bat on
+ // the top wall.
+ newBatPosition.Y = _roomBounds.Bottom - padding * 2;
+ }
+ }
+
+ // Assign the new bat position
+ _bat.Position = newBatPosition;
+ }
+
+ private void OnSlimeBodyCollision(object sender, EventArgs args)
+ {
+ GameOver();
+ }
+
+ private void TogglePause()
+ {
+ if (_state == GameState.Paused)
+ {
+ // We're now unpausing the game, so hide the pause panel
+ _ui.HidePausePanel();
+
+ // And set the state back to playing
+ _state = GameState.Playing;
+ }
+ else
+ {
+ // We're now pausing the game, so show the pause panel
+ _ui.ShowPausePanel();
+
+ // And set the state to paused
+ _state = GameState.Paused;
+
+ // Set the grayscale effect saturation to 1.0f;
+ _saturation = 1.0f;
+ }
+ }
+
+ private void GameOver()
+ {
+ // Show the game over panel
+ _ui.ShowGameOverPanel();
+
+ // Set the game state to game over
+ _state = GameState.GameOver;
+
+ // Set the grayscale effect saturation to 1.0f;
+ _saturation = 1.0f;
+ }
+
+ public override void Draw(GameTime gameTime)
+ {
+ // Clear the back buffer.
+ Core.GraphicsDevice.Clear(Color.CornflowerBlue);
+
+ if (_state != GameState.Playing)
+ {
+ // We are in a game over state, so apply the saturation parameter.
+ _grayscaleEffect.Parameters["Saturation"].SetValue(_saturation);
+
+ // And begin the sprite batch using the grayscale effect.
+ Core.SpriteBatch.Begin(samplerState: SamplerState.PointClamp, effect: _grayscaleEffect);
+ }
+ else
+ {
+ // Otherwise, just begin the sprite batch as normal.
+ Core.SpriteBatch.Begin(samplerState: SamplerState.PointClamp);
+ }
+
+ // Draw the tilemap
+ _tilemap.Draw(Core.SpriteBatch);
+
+ // Draw the slime.
+ _slime.Draw();
+
+ // Draw the bat.
+ _bat.Draw();
+
+ // Always end the sprite batch when finished.
+ Core.SpriteBatch.End();
+
+ // Draw the UI
+ _ui.Draw();
+ }
+}
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Scenes/TitleScene.cs b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Scenes/TitleScene.cs
new file mode 100644
index 00000000..248c48dd
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/Scenes/TitleScene.cs
@@ -0,0 +1,345 @@
+using System;
+using DungeonSlime.UI;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Audio;
+using Microsoft.Xna.Framework.Graphics;
+using Microsoft.Xna.Framework.Input;
+using MonoGameGum;
+using Gum.Forms.Controls;
+using MonoGameGum.GueDeriving;
+using MonoGameLibrary;
+using MonoGameLibrary.Graphics;
+using MonoGameLibrary.Scenes;
+
+namespace DungeonSlime.Scenes;
+
+public class TitleScene : Scene
+{
+ private const string DUNGEON_TEXT = "Dungeon";
+ private const string SLIME_TEXT = "Slime";
+ private const string PRESS_ENTER_TEXT = "Press Enter To Start";
+
+ // The font to use to render normal text.
+ private SpriteFont _font;
+
+ // The font used to render the title text.
+ private SpriteFont _font5x;
+
+ // The position to draw the dungeon text at.
+ private Vector2 _dungeonTextPos;
+
+ // The origin to set for the dungeon text.
+ private Vector2 _dungeonTextOrigin;
+
+ // The position to draw the slime text at.
+ private Vector2 _slimeTextPos;
+
+ // The origin to set for the slime text.
+ private Vector2 _slimeTextOrigin;
+
+ // The position to draw the press enter text at.
+ private Vector2 _pressEnterPos;
+
+ // The origin to set for the press enter text when drawing it.
+ private Vector2 _pressEnterOrigin;
+
+ // The texture used for the background pattern.
+ private Texture2D _backgroundPattern;
+
+ // The destination rectangle for the background pattern to fill.
+ private Rectangle _backgroundDestination;
+
+ // The offset to apply when drawing the background pattern so it appears to
+ // be scrolling.
+ private Vector2 _backgroundOffset;
+
+ // The speed that the background pattern scrolls.
+ private float _scrollSpeed = 50.0f;
+
+ private SoundEffect _uiSoundEffect;
+ private Panel _titleScreenButtonsPanel;
+ private Panel _optionsPanel;
+
+ // The options button used to open the options menu.
+ private AnimatedButton _optionsButton;
+
+ // The back button used to exit the options menu back to the title menu.
+ private AnimatedButton _optionsBackButton;
+
+ // Reference to the texture atlas that we can pass to UI elements when they
+ // are created.
+ private TextureAtlas _atlas;
+
+ public override void Initialize()
+ {
+ // LoadContent is called during base.Initialize().
+ base.Initialize();
+
+ // While on the title screen, we can enable exit on escape so the player
+ // can close the game by pressing the escape key.
+ Core.ExitOnEscape = true;
+
+ // Set the position and origin for the Dungeon text.
+ Vector2 size = _font5x.MeasureString(DUNGEON_TEXT);
+ _dungeonTextPos = new Vector2(640, 100);
+ _dungeonTextOrigin = size * 0.5f;
+
+ // Set the position and origin for the Slime text.
+ size = _font5x.MeasureString(SLIME_TEXT);
+ _slimeTextPos = new Vector2(757, 207);
+ _slimeTextOrigin = size * 0.5f;
+
+ // Set the position and origin for the press enter text.
+ size = _font.MeasureString(PRESS_ENTER_TEXT);
+ _pressEnterPos = new Vector2(640, 620);
+ _pressEnterOrigin = size * 0.5f;
+
+ // Initialize the offset of the background pattern at zero
+ _backgroundOffset = Vector2.Zero;
+
+ // Set the background pattern destination rectangle to fill the entire
+ // screen background
+ _backgroundDestination = Core.GraphicsDevice.PresentationParameters.Bounds;
+
+ InitializeUI();
+ }
+
+ public override void LoadContent()
+ {
+ // Load the font for the standard text.
+ _font = Core.Content.Load("fonts/04B_30");
+
+ // Load the font for the title text
+ _font5x = Content.Load("fonts/04B_30_5x");
+
+ // Load the background pattern texture.
+ _backgroundPattern = Content.Load("images/background-pattern");
+
+ // Load the sound effect to play when ui actions occur.
+ _uiSoundEffect = Core.Content.Load("audio/ui");
+
+ // Load the texture atlas from the xml configuration file.
+ _atlas = TextureAtlas.FromFile(Core.Content, "images/atlas-definition.xml");
+ }
+
+ private void CreateTitlePanel()
+ {
+ // Create a container to hold all of our buttons
+ _titleScreenButtonsPanel = new Panel();
+ _titleScreenButtonsPanel.Dock(Gum.Wireframe.Dock.Fill);
+ _titleScreenButtonsPanel.AddToRoot();
+
+ AnimatedButton startButton = new AnimatedButton(_atlas);
+ startButton.Anchor(Gum.Wireframe.Anchor.BottomLeft);
+ startButton.Visual.X = 50;
+ startButton.Visual.Y = -12;
+ startButton.Text = "Start";
+ startButton.Click += HandleStartClicked;
+ _titleScreenButtonsPanel.AddChild(startButton);
+
+ _optionsButton = new AnimatedButton(_atlas);
+ _optionsButton.Anchor(Gum.Wireframe.Anchor.BottomRight);
+ _optionsButton.Visual.X = -50;
+ _optionsButton.Visual.Y = -12;
+ _optionsButton.Text = "Options";
+ _optionsButton.Click += HandleOptionsClicked;
+ _titleScreenButtonsPanel.AddChild(_optionsButton);
+
+ startButton.IsFocused = true;
+ }
+
+ private void HandleStartClicked(object sender, EventArgs e)
+ {
+ // A UI interaction occurred, play the sound effect
+ Core.Audio.PlaySoundEffect(_uiSoundEffect);
+
+ // Change to the game scene to start the game.
+ Core.ChangeScene(new GameScene());
+ }
+
+ private void HandleOptionsClicked(object sender, EventArgs e)
+ {
+ // A UI interaction occurred, play the sound effect
+ Core.Audio.PlaySoundEffect(_uiSoundEffect);
+
+ // Set the title panel to be invisible.
+ _titleScreenButtonsPanel.IsVisible = false;
+
+ // Set the options panel to be visible.
+ _optionsPanel.IsVisible = true;
+
+ // Give the back button on the options panel focus.
+ _optionsBackButton.IsFocused = true;
+ }
+
+ private void CreateOptionsPanel()
+ {
+ _optionsPanel = new Panel();
+ _optionsPanel.Dock(Gum.Wireframe.Dock.Fill);
+ _optionsPanel.IsVisible = false;
+ _optionsPanel.AddToRoot();
+
+ TextRuntime optionsText = new TextRuntime();
+ optionsText.X = 10;
+ optionsText.Y = 10;
+ optionsText.Text = "OPTIONS";
+ optionsText.UseCustomFont = true;
+ optionsText.FontScale = 0.5f;
+ optionsText.CustomFontFile = @"fonts/04b_30.fnt";
+ _optionsPanel.AddChild(optionsText);
+
+ OptionsSlider musicSlider = new OptionsSlider(_atlas);
+ musicSlider.Name = "MusicSlider";
+ musicSlider.Text = "MUSIC";
+ musicSlider.Anchor(Gum.Wireframe.Anchor.Top);
+ musicSlider.Visual.Y = 30f;
+ musicSlider.Minimum = 0;
+ musicSlider.Maximum = 1;
+ musicSlider.Value = Core.Audio.SongVolume;
+ musicSlider.SmallChange = .1;
+ musicSlider.LargeChange = .2;
+ musicSlider.ValueChanged += HandleMusicSliderValueChanged;
+ musicSlider.ValueChangeCompleted += HandleMusicSliderValueChangeCompleted;
+ _optionsPanel.AddChild(musicSlider);
+
+ OptionsSlider sfxSlider = new OptionsSlider(_atlas);
+ sfxSlider.Name = "SfxSlider";
+ sfxSlider.Text = "SFX";
+ sfxSlider.Anchor(Gum.Wireframe.Anchor.Top);
+ sfxSlider.Visual.Y = 93;
+ sfxSlider.Minimum = 0;
+ sfxSlider.Maximum = 1;
+ sfxSlider.Value = Core.Audio.SoundEffectVolume;
+ sfxSlider.SmallChange = .1;
+ sfxSlider.LargeChange = .2;
+ sfxSlider.ValueChanged += HandleSfxSliderChanged;
+ sfxSlider.ValueChangeCompleted += HandleSfxSliderChangeCompleted;
+ _optionsPanel.AddChild(sfxSlider);
+
+ _optionsBackButton = new AnimatedButton(_atlas);
+ _optionsBackButton.Text = "BACK";
+ _optionsBackButton.Anchor(Gum.Wireframe.Anchor.BottomRight);
+ _optionsBackButton.X = -28f;
+ _optionsBackButton.Y = -10f;
+ _optionsBackButton.Click += HandleOptionsButtonBack;
+ _optionsPanel.AddChild(_optionsBackButton);
+ }
+
+ private void HandleSfxSliderChanged(object sender, EventArgs args)
+ {
+ // Intentionally not playing the UI sound effect here so that it is not
+ // constantly triggered as the user adjusts the slider's thumb on the
+ // track.
+
+ // Get a reference to the sender as a Slider.
+ var slider = (Slider)sender;
+
+ // Set the global sound effect volume to the value of the slider.;
+ Core.Audio.SoundEffectVolume = (float)slider.Value;
+ }
+
+ private void HandleSfxSliderChangeCompleted(object sender, EventArgs e)
+ {
+ // Play the UI Sound effect so the player can hear the difference in audio.
+ Core.Audio.PlaySoundEffect(_uiSoundEffect);
+ }
+
+ private void HandleMusicSliderValueChanged(object sender, EventArgs args)
+ {
+ // Intentionally not playing the UI sound effect here so that it is not
+ // constantly triggered as the user adjusts the slider's thumb on the
+ // track.
+
+ // Get a reference to the sender as a Slider.
+ var slider = (Slider)sender;
+
+ // Set the global song volume to the value of the slider.
+ Core.Audio.SongVolume = (float)slider.Value;
+ }
+
+ private void HandleMusicSliderValueChangeCompleted(object sender, EventArgs args)
+ {
+ // A UI interaction occurred, play the sound effect
+ Core.Audio.PlaySoundEffect(_uiSoundEffect);
+ }
+
+ private void HandleOptionsButtonBack(object sender, EventArgs e)
+ {
+ // A UI interaction occurred, play the sound effect
+ Core.Audio.PlaySoundEffect(_uiSoundEffect);
+
+ // Set the title panel to be visible.
+ _titleScreenButtonsPanel.IsVisible = true;
+
+ // Set the options panel to be invisible.
+ _optionsPanel.IsVisible = false;
+
+ // Give the options button on the title panel focus since we are coming
+ // back from the options screen.
+ _optionsButton.IsFocused = true;
+ }
+
+ private void InitializeUI()
+ {
+ // Clear out any previous UI in case we came here from
+ // a different screen:
+ GumService.Default.Root.Children.Clear();
+
+ CreateTitlePanel();
+ CreateOptionsPanel();
+ }
+
+ public override void Update(GameTime gameTime)
+ {
+ // Update the offsets for the background pattern wrapping so that it
+ // scrolls down and to the right.
+ float offset = _scrollSpeed * (float)gameTime.ElapsedGameTime.TotalSeconds;
+ _backgroundOffset.X -= offset;
+ _backgroundOffset.Y -= offset;
+
+ // Ensure that the offsets do not go beyond the texture bounds so it is
+ // a seamless wrap
+ _backgroundOffset.X %= _backgroundPattern.Width;
+ _backgroundOffset.Y %= _backgroundPattern.Height;
+
+ GumService.Default.Update(gameTime);
+ }
+
+ public override void Draw(GameTime gameTime)
+ {
+ Core.GraphicsDevice.Clear(new Color(32, 40, 78, 255));
+
+ // Draw the background pattern first using the PointWrap sampler state.
+ Core.SpriteBatch.Begin(samplerState: SamplerState.PointWrap);
+ Core.SpriteBatch.Draw(_backgroundPattern, _backgroundDestination, new Rectangle(_backgroundOffset.ToPoint(), _backgroundDestination.Size), Color.White * 0.5f);
+ Core.SpriteBatch.End();
+
+ if (_titleScreenButtonsPanel.IsVisible)
+ {
+ // Begin the sprite batch to prepare for rendering.
+ Core.SpriteBatch.Begin(samplerState: SamplerState.PointClamp);
+
+ // The color to use for the drop shadow text.
+ Color dropShadowColor = Color.Black * 0.5f;
+
+ // Draw the Dungeon text slightly offset from it is original position and
+ // with a transparent color to give it a drop shadow
+ Core.SpriteBatch.DrawString(_font5x, DUNGEON_TEXT, _dungeonTextPos + new Vector2(10, 10), dropShadowColor, 0.0f, _dungeonTextOrigin, 1.0f, SpriteEffects.None, 1.0f);
+
+ // Draw the Dungeon text on top of that at its original position
+ Core.SpriteBatch.DrawString(_font5x, DUNGEON_TEXT, _dungeonTextPos, Color.White, 0.0f, _dungeonTextOrigin, 1.0f, SpriteEffects.None, 1.0f);
+
+ // Draw the Slime text slightly offset from it is original position and
+ // with a transparent color to give it a drop shadow
+ Core.SpriteBatch.DrawString(_font5x, SLIME_TEXT, _slimeTextPos + new Vector2(10, 10), dropShadowColor, 0.0f, _slimeTextOrigin, 1.0f, SpriteEffects.None, 1.0f);
+
+ // Draw the Slime text on top of that at its original position
+ Core.SpriteBatch.DrawString(_font5x, SLIME_TEXT, _slimeTextPos, Color.White, 0.0f, _slimeTextOrigin, 1.0f, SpriteEffects.None, 1.0f);
+
+ // Always end the sprite batch when finished.
+ Core.SpriteBatch.End();
+ }
+
+ GumService.Default.Draw();
+ }
+}
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/UI/AnimatedButton.cs b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/UI/AnimatedButton.cs
new file mode 100644
index 00000000..56ba8cfc
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/UI/AnimatedButton.cs
@@ -0,0 +1,163 @@
+using System;
+using Gum.DataTypes;
+using Gum.DataTypes.Variables;
+using Gum.Forms.Controls;
+using Gum.Forms.DefaultVisuals;
+using Gum.Graphics.Animation;
+using Gum.Managers;
+using Microsoft.Xna.Framework.Input;
+using MonoGameGum.GueDeriving;
+using MonoGameLibrary.Graphics;
+
+namespace DungeonSlime.UI;
+
+///
+/// A custom button implementation that inherits from Gum's Button class to provide
+/// animated visual feedback when focused.
+///
+internal class AnimatedButton : Button
+{
+ ///
+ /// Creates a new AnimatedButton instance using graphics from the specified texture atlas.
+ ///
+ /// The texture atlas containing button graphics and animations
+ public AnimatedButton(TextureAtlas atlas)
+ {
+ // Each Forms conrol has a general Visual property that
+ // has properties shared by all control types. This Visual
+ // type matches the Forms type. It can be casted to access
+ // controls-specific properties.
+ ButtonVisual buttonVisual = (ButtonVisual)Visual;
+ // Width is relative to children with extra padding, height is fixed
+ buttonVisual.Height = 14f;
+ buttonVisual.HeightUnits = DimensionUnitType.Absolute;
+ buttonVisual.Width = 21f;
+ buttonVisual.WidthUnits = DimensionUnitType.RelativeToChildren;
+
+ // Get a reference to the nine-slice background to display the button graphics
+ // A nine-slice allows the button to stretch while preserving corner appearance
+ NineSliceRuntime background = buttonVisual.Background;
+ background.Texture = atlas.Texture;
+ background.TextureAddress = TextureAddress.Custom;
+ background.Color = Microsoft.Xna.Framework.Color.White;
+ // texture coordinates for the background are set down below
+
+ TextRuntime textInstance = buttonVisual.TextInstance;
+ textInstance.Text = "START";
+ textInstance.Blue = 130;
+ textInstance.Green = 86;
+ textInstance.Red = 70;
+ textInstance.UseCustomFont = true;
+ textInstance.CustomFontFile = "fonts/04b_30.fnt";
+ textInstance.FontScale = 0.25f;
+ textInstance.Anchor(Gum.Wireframe.Anchor.Center);
+ textInstance.Width = 0;
+ textInstance.WidthUnits = DimensionUnitType.RelativeToChildren;
+
+ // Get the texture region for the unfocused button state from the atlas
+ TextureRegion unfocusedTextureRegion = atlas.GetRegion("unfocused-button");
+
+ // Create an animation chain for the unfocused state with a single frame
+ AnimationChain unfocusedAnimation = new AnimationChain();
+ unfocusedAnimation.Name = nameof(unfocusedAnimation);
+ AnimationFrame unfocusedFrame = new AnimationFrame
+ {
+ TopCoordinate = unfocusedTextureRegion.TopTextureCoordinate,
+ BottomCoordinate = unfocusedTextureRegion.BottomTextureCoordinate,
+ LeftCoordinate = unfocusedTextureRegion.LeftTextureCoordinate,
+ RightCoordinate = unfocusedTextureRegion.RightTextureCoordinate,
+ FrameLength = 0.3f,
+ Texture = unfocusedTextureRegion.Texture
+ };
+ unfocusedAnimation.Add(unfocusedFrame);
+
+ // Get the multi-frame animation for the focused button state from the atlas
+ Animation focusedAtlasAnimation = atlas.GetAnimation("focused-button-animation");
+
+ // Create an animation chain for the focused state using all frames from the atlas animation
+ AnimationChain focusedAnimation = new AnimationChain();
+ focusedAnimation.Name = nameof(focusedAnimation);
+ foreach (TextureRegion region in focusedAtlasAnimation.Frames)
+ {
+ AnimationFrame frame = new AnimationFrame
+ {
+ TopCoordinate = region.TopTextureCoordinate,
+ BottomCoordinate = region.BottomTextureCoordinate,
+ LeftCoordinate = region.LeftTextureCoordinate,
+ RightCoordinate = region.RightTextureCoordinate,
+ FrameLength = (float)focusedAtlasAnimation.Delay.TotalSeconds,
+ Texture = region.Texture
+ };
+
+ focusedAnimation.Add(frame);
+ }
+
+ // Assign both animation chains to the nine-slice background
+ background.AnimationChains = new AnimationChainList
+ {
+ unfocusedAnimation,
+ focusedAnimation
+ };
+
+
+ // Reset all state to default so we don't have unexpected variable assignments:
+ buttonVisual.ButtonCategory.ResetAllStates();
+
+ // Get the enabled (default/unfocused) state
+ StateSave enabledState = buttonVisual.States.Enabled;
+ enabledState.Apply = () =>
+ {
+ // When enabled but not focused, use the unfocused animation
+ background.CurrentChainName = unfocusedAnimation.Name;
+ };
+
+ // Create the focused state
+ StateSave focusedState = buttonVisual.States.Focused;
+ focusedState.Apply = () =>
+ {
+ // When focused, use the focused animation and enable animation playback
+ background.CurrentChainName = focusedAnimation.Name;
+ background.Animate = true;
+ };
+
+ // Create the highlighted+focused state (for mouse hover while focused)
+ StateSave highlightedFocused = buttonVisual.States.HighlightedFocused;
+ highlightedFocused.Apply = focusedState.Apply;
+
+ // Create the highlighted state (for mouse hover)
+ // by cloning the enabled state since they appear the same
+ StateSave highlighted = buttonVisual.States.Highlighted;
+ highlighted.Apply = enabledState.Apply;
+
+ // Add event handlers for keyboard input.
+ KeyDown += HandleKeyDown;
+
+ // Add event handler for mouse hover focus.
+ buttonVisual.RollOn += HandleRollOn;
+ }
+
+ ///
+ /// Handles keyboard input for navigation between buttons using left/right keys.
+ ///
+ private void HandleKeyDown(object sender, KeyEventArgs e)
+ {
+ if (e.Key == Keys.Left)
+ {
+ // Left arrow navigates to previous control
+ HandleTab(TabDirection.Up, loop: true);
+ }
+ if (e.Key == Keys.Right)
+ {
+ // Right arrow navigates to next control
+ HandleTab(TabDirection.Down, loop: true);
+ }
+ }
+
+ ///
+ /// Automatically focuses the button when the mouse hovers over it.
+ ///
+ private void HandleRollOn(object sender, EventArgs e)
+ {
+ IsFocused = true;
+ }
+}
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/UI/GameSceneUI.cs b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/UI/GameSceneUI.cs
new file mode 100644
index 00000000..84be9293
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/UI/GameSceneUI.cs
@@ -0,0 +1,340 @@
+using System;
+using Gum.DataTypes;
+using Gum.Managers;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Audio;
+using Microsoft.Xna.Framework.Content;
+using MonoGameGum;
+using Gum.Forms.Controls;
+using MonoGameGum.GueDeriving;
+using MonoGameLibrary;
+using MonoGameLibrary.Graphics;
+
+namespace DungeonSlime.UI;
+
+public class GameSceneUI : ContainerRuntime
+{
+ // The string format to use when updating the text for the score display.
+ private static readonly string s_scoreFormat = "SCORE: {0:D6}";
+
+ // The sound effect to play for auditory feedback of the user interface.
+ private SoundEffect _uiSoundEffect;
+
+ // The pause panel
+ private Panel _pausePanel;
+
+ // The resume button on the pause panel. Field is used to track reference so
+ // focus can be set when the pause panel is shown.
+ private AnimatedButton _resumeButton;
+
+ // The game over panel.
+ private Panel _gameOverPanel;
+
+ // The retry button on the game over panel. Field is used to track reference
+ // so focus can be set when the game over panel is shown.
+ private AnimatedButton _retryButton;
+
+ // The text runtime used to display the players score on the game screen.
+ private TextRuntime _scoreText;
+
+ ///
+ /// Event invoked when the Resume button on the Pause panel is clicked.
+ ///
+ public event EventHandler ResumeButtonClick;
+
+ ///
+ /// Event invoked when the Quit button on either the Pause panel or the
+ /// Game Over panel is clicked.
+ ///
+ public event EventHandler QuitButtonClick;
+
+ ///
+ /// Event invoked when the Retry button on the Game Over panel is clicked.
+ ///
+ public event EventHandler RetryButtonClick;
+
+ public GameSceneUI()
+ {
+ // The game scene UI inherits from ContainerRuntime, so we set its
+ // doc to fill so it fills the entire screen.
+ Dock(Gum.Wireframe.Dock.Fill);
+
+ // Add it to the root element.
+ this.AddToRoot();
+
+ // Get a reference to the content manager that was registered with the
+ // GumService when it was original initialized.
+ ContentManager content = GumService.Default.ContentLoader.XnaContentManager;
+
+ // Use that content manager to load the sound effect and atlas for the
+ // user interface elements
+ _uiSoundEffect = content.Load("audio/ui");
+ TextureAtlas atlas = TextureAtlas.FromFile(content, "images/atlas-definition.xml");
+
+ // Create the text that will display the players score and add it as
+ // a child to this container.
+ _scoreText = CreateScoreText();
+ AddChild(_scoreText);
+
+ // Create the Pause panel that is displayed when the game is paused and
+ // add it as a child to this container
+ _pausePanel = CreatePausePanel(atlas);
+ AddChild(_pausePanel.Visual);
+
+ // Create the Game Over panel that is displayed when a game over occurs
+ // and add it as a child to this container
+ _gameOverPanel = CreateGameOverPanel(atlas);
+ AddChild(_gameOverPanel.Visual);
+ }
+
+ private TextRuntime CreateScoreText()
+ {
+ TextRuntime text = new TextRuntime();
+ text.Anchor(Gum.Wireframe.Anchor.TopLeft);
+ text.WidthUnits = DimensionUnitType.RelativeToChildren;
+ text.X = 20.0f;
+ text.Y = 5.0f;
+ text.UseCustomFont = true;
+ text.CustomFontFile = @"fonts/04b_30.fnt";
+ text.FontScale = 0.25f;
+ text.Text = string.Format(s_scoreFormat, 0);
+
+ return text;
+ }
+
+ private Panel CreatePausePanel(TextureAtlas atlas)
+ {
+ Panel panel = new Panel();
+ panel.Anchor(Gum.Wireframe.Anchor.Center);
+ panel.Visual.WidthUnits = DimensionUnitType.Absolute;
+ panel.Visual.HeightUnits = DimensionUnitType.Absolute;
+ panel.Visual.Width = 264.0f;
+ panel.Visual.Height = 70.0f;
+ panel.IsVisible = false;
+
+ TextureRegion backgroundRegion = atlas.GetRegion("panel-background");
+
+ NineSliceRuntime background = new NineSliceRuntime();
+ background.Dock(Gum.Wireframe.Dock.Fill);
+ background.Texture = backgroundRegion.Texture;
+ background.TextureAddress = TextureAddress.Custom;
+ background.TextureHeight = backgroundRegion.Height;
+ background.TextureWidth = backgroundRegion.Width;
+ background.TextureTop = backgroundRegion.SourceRectangle.Top;
+ background.TextureLeft = backgroundRegion.SourceRectangle.Left;
+ panel.AddChild(background);
+
+ TextRuntime text = new TextRuntime();
+ text.Text = "PAUSED";
+ text.UseCustomFont = true;
+ text.CustomFontFile = "fonts/04b_30.fnt";
+ text.FontScale = 0.5f;
+ text.X = 10.0f;
+ text.Y = 10.0f;
+ panel.AddChild(text);
+
+ _resumeButton = new AnimatedButton(atlas);
+ _resumeButton.Text = "RESUME";
+ _resumeButton.Anchor(Gum.Wireframe.Anchor.BottomLeft);
+ _resumeButton.Visual.X = 9.0f;
+ _resumeButton.Visual.Y = -9.0f;
+
+ _resumeButton.Click += OnResumeButtonClicked;
+ _resumeButton.GotFocus += OnElementGotFocus;
+
+ panel.AddChild(_resumeButton);
+
+ AnimatedButton quitButton = new AnimatedButton(atlas);
+ quitButton.Text = "QUIT";
+ quitButton.Anchor(Gum.Wireframe.Anchor.BottomRight);
+ quitButton.Visual.X = -9.0f;
+ quitButton.Visual.Y = -9.0f;
+
+ quitButton.Click += OnQuitButtonClicked;
+ quitButton.GotFocus += OnElementGotFocus;
+
+ panel.AddChild(quitButton);
+
+ return panel;
+ }
+
+ private Panel CreateGameOverPanel(TextureAtlas atlas)
+ {
+ Panel panel = new Panel();
+ panel.Anchor(Gum.Wireframe.Anchor.Center);
+ panel.Visual.WidthUnits = DimensionUnitType.Absolute;
+ panel.Visual.HeightUnits = DimensionUnitType.Absolute;
+ panel.Visual.Width = 264.0f;
+ panel.Visual.Height = 70.0f;
+ panel.IsVisible = false;
+
+ TextureRegion backgroundRegion = atlas.GetRegion("panel-background");
+
+ NineSliceRuntime background = new NineSliceRuntime();
+ background.Dock(Gum.Wireframe.Dock.Fill);
+ background.Texture = backgroundRegion.Texture;
+ background.TextureAddress = TextureAddress.Custom;
+ background.TextureHeight = backgroundRegion.Height;
+ background.TextureWidth = backgroundRegion.Width;
+ background.TextureTop = backgroundRegion.SourceRectangle.Top;
+ background.TextureLeft = backgroundRegion.SourceRectangle.Left;
+ panel.AddChild(background);
+
+ TextRuntime text = new TextRuntime();
+ text.Text = "GAME OVER";
+ text.WidthUnits = DimensionUnitType.RelativeToChildren;
+ text.UseCustomFont = true;
+ text.CustomFontFile = "fonts/04b_30.fnt";
+ text.FontScale = 0.5f;
+ text.X = 10.0f;
+ text.Y = 10.0f;
+ panel.AddChild(text);
+
+ _retryButton = new AnimatedButton(atlas);
+ _retryButton.Text = "RETRY";
+ _retryButton.Anchor(Gum.Wireframe.Anchor.BottomLeft);
+ _retryButton.Visual.X = 9.0f;
+ _retryButton.Visual.Y = -9.0f;
+
+ _retryButton.Click += OnRetryButtonClicked;
+ _retryButton.GotFocus += OnElementGotFocus;
+
+ panel.AddChild(_retryButton);
+
+ AnimatedButton quitButton = new AnimatedButton(atlas);
+ quitButton.Text = "QUIT";
+ quitButton.Anchor(Gum.Wireframe.Anchor.BottomRight);
+ quitButton.Visual.X = -9.0f;
+ quitButton.Visual.Y = -9.0f;
+
+ quitButton.Click += OnQuitButtonClicked;
+ quitButton.GotFocus += OnElementGotFocus;
+
+ panel.AddChild(quitButton);
+
+ return panel;
+ }
+
+ private void OnResumeButtonClicked(object sender, EventArgs args)
+ {
+ // Button was clicked, play the ui sound effect for auditory feedback.
+ Core.Audio.PlaySoundEffect(_uiSoundEffect);
+
+ // Since the resume button was clicked, we need to hide the pause panel.
+ HidePausePanel();
+
+ // Invoke the ResumeButtonClick event
+ if (ResumeButtonClick != null)
+ {
+ ResumeButtonClick(sender, args);
+ }
+ }
+
+ private void OnRetryButtonClicked(object sender, EventArgs args)
+ {
+ // Button was clicked, play the ui sound effect for auditory feedback.
+ Core.Audio.PlaySoundEffect(_uiSoundEffect);
+
+ // Since the retry button was clicked, we need to hide the game over panel.
+ HideGameOverPanel();
+
+ // Invoke the RetryButtonClick event.
+ if (RetryButtonClick != null)
+ {
+ RetryButtonClick(sender, args);
+ }
+ }
+
+ private void OnQuitButtonClicked(object sender, EventArgs args)
+ {
+ // Button was clicked, play the ui sound effect for auditory feedback.
+ Core.Audio.PlaySoundEffect(_uiSoundEffect);
+
+ // Both panels have a quit button, so hide both panels
+ HidePausePanel();
+ HideGameOverPanel();
+
+ // Invoke the QuitButtonClick event.
+ if (QuitButtonClick != null)
+ {
+ QuitButtonClick(sender, args);
+ }
+ }
+
+ private void OnElementGotFocus(object sender, EventArgs args)
+ {
+ // A ui element that can receive focus has received focus, play the
+ // ui sound effect for auditory feedback.
+ Core.Audio.PlaySoundEffect(_uiSoundEffect);
+ }
+
+ ///
+ /// Updates the text on the score display.
+ ///
+ /// The score to display.
+ public void UpdateScoreText(int score)
+ {
+ _scoreText.Text = string.Format(s_scoreFormat, score);
+ }
+
+ ///
+ /// Tells the game scene ui to show the pause panel.
+ ///
+ public void ShowPausePanel()
+ {
+ _pausePanel.IsVisible = true;
+
+ // Give the resume button focus for keyboard/gamepad input.
+ _resumeButton.IsFocused = true;
+
+ // Ensure the game over panel isn't visible.
+ _gameOverPanel.IsVisible = false;
+ }
+
+ ///
+ /// Tells the game scene ui to hide the pause panel.
+ ///
+ public void HidePausePanel()
+ {
+ _pausePanel.IsVisible = false;
+ }
+
+ ///
+ /// Tells the game scene ui to show the game over panel.
+ ///
+ public void ShowGameOverPanel()
+ {
+ _gameOverPanel.IsVisible = true;
+
+ // Give the retry button focus for keyboard/gamepad input.
+ _retryButton.IsFocused = true;
+
+ // Ensure the pause panel isn't visible.
+ _pausePanel.IsVisible = false;
+ }
+
+ ///
+ /// Tells the game scene ui to hide the game over panel.
+ ///
+ public void HideGameOverPanel()
+ {
+ _gameOverPanel.IsVisible = false;
+ }
+
+ ///
+ /// Updates the game scene ui.
+ ///
+ /// A snapshot of the timing values for the current update cycle.
+ public void Update(GameTime gameTime)
+ {
+ GumService.Default.Update(gameTime);
+ }
+
+ ///
+ /// Draws the game scene ui.
+ ///
+ public void Draw()
+ {
+ GumService.Default.Draw();
+ }
+}
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/UI/OptionsSlider.cs b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/UI/OptionsSlider.cs
new file mode 100644
index 00000000..86d03281
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/UI/OptionsSlider.cs
@@ -0,0 +1,253 @@
+using System;
+using Gum.DataTypes;
+using Gum.DataTypes.Variables;
+using Gum.Managers;
+using Microsoft.Xna.Framework;
+using Gum.Forms.Controls;
+using MonoGameGum.GueDeriving;
+using MonoGameLibrary.Graphics;
+
+namespace DungeonSlime.UI;
+
+///
+/// A custom slider control that inherits from Gum's Slider class.
+///
+public class OptionsSlider : Slider
+{
+ // Reference to the text label that displays the slider's title
+ private TextRuntime _textInstance;
+
+ // Reference to the rectangle that visually represents the current value
+ private ColoredRectangleRuntime _fillRectangle;
+
+ ///
+ /// Gets or sets the text label for this slider.
+ ///
+ public string Text
+ {
+ get => _textInstance.Text;
+ set => _textInstance.Text = value;
+ }
+
+ ///
+ /// Creates a new OptionsSlider instance using graphics from the specified texture atlas.
+ ///
+ /// The texture atlas containing slider graphics.
+ public OptionsSlider(TextureAtlas atlas)
+ {
+ // Create the top-level container for all visual elements
+ ContainerRuntime topLevelContainer = new ContainerRuntime();
+ topLevelContainer.Height = 55f;
+ topLevelContainer.Width = 264f;
+
+ TextureRegion backgroundRegion = atlas.GetRegion("panel-background");
+
+ // Create the background panel that contains everything
+ NineSliceRuntime background = new NineSliceRuntime();
+ background.Texture = atlas.Texture;
+ background.TextureAddress = TextureAddress.Custom;
+ background.TextureHeight = backgroundRegion.Height;
+ background.TextureLeft = backgroundRegion.SourceRectangle.Left;
+ background.TextureTop = backgroundRegion.SourceRectangle.Top;
+ background.TextureWidth = backgroundRegion.Width;
+ background.Dock(Gum.Wireframe.Dock.Fill);
+ topLevelContainer.AddChild(background);
+
+ // Create the title text element
+ _textInstance = new TextRuntime();
+ _textInstance.CustomFontFile = @"fonts/04b_30.fnt";
+ _textInstance.UseCustomFont = true;
+ _textInstance.FontScale = 0.5f;
+ _textInstance.Text = "Replace Me";
+ _textInstance.X = 10f;
+ _textInstance.Y = 10f;
+ _textInstance.WidthUnits = DimensionUnitType.RelativeToChildren;
+ topLevelContainer.AddChild(_textInstance);
+
+ // Create the container for the slider track and decorative elements
+ ContainerRuntime innerContainer = new ContainerRuntime();
+ innerContainer.Height = 13f;
+ innerContainer.Width = 241f;
+ innerContainer.X = 10f;
+ innerContainer.Y = 33f;
+ topLevelContainer.AddChild(innerContainer);
+
+ TextureRegion offBackgroundRegion = atlas.GetRegion("slider-off-background");
+
+ // Create the "OFF" side of the slider (left end)
+ NineSliceRuntime offBackground = new NineSliceRuntime();
+ offBackground.Dock(Gum.Wireframe.Dock.Left);
+ offBackground.Texture = atlas.Texture;
+ offBackground.TextureAddress = TextureAddress.Custom;
+ offBackground.TextureHeight = offBackgroundRegion.Height;
+ offBackground.TextureLeft = offBackgroundRegion.SourceRectangle.Left;
+ offBackground.TextureTop = offBackgroundRegion.SourceRectangle.Top;
+ offBackground.TextureWidth = offBackgroundRegion.Width;
+ offBackground.Width = 28f;
+ offBackground.WidthUnits = DimensionUnitType.Absolute;
+ offBackground.Dock(Gum.Wireframe.Dock.Left);
+ innerContainer.AddChild(offBackground);
+
+ TextureRegion middleBackgroundRegion = atlas.GetRegion("slider-middle-background");
+
+ // Create the middle track portion of the slider
+ NineSliceRuntime middleBackground = new NineSliceRuntime();
+ middleBackground.Dock(Gum.Wireframe.Dock.FillVertically);
+ middleBackground.Texture = middleBackgroundRegion.Texture;
+ middleBackground.TextureAddress = TextureAddress.Custom;
+ middleBackground.TextureHeight = middleBackgroundRegion.Height;
+ middleBackground.TextureLeft = middleBackgroundRegion.SourceRectangle.Left;
+ middleBackground.TextureTop = middleBackgroundRegion.SourceRectangle.Top;
+ middleBackground.TextureWidth = middleBackgroundRegion.Width;
+ middleBackground.Width = 179f;
+ middleBackground.WidthUnits = DimensionUnitType.Absolute;
+ middleBackground.Dock(Gum.Wireframe.Dock.Left);
+ middleBackground.X = 27f;
+ innerContainer.AddChild(middleBackground);
+
+ TextureRegion maxBackgroundRegion = atlas.GetRegion("slider-max-background");
+
+ // Create the "MAX" side of the slider (right end)
+ NineSliceRuntime maxBackground = new NineSliceRuntime();
+ maxBackground.Texture = maxBackgroundRegion.Texture;
+ maxBackground.TextureAddress = TextureAddress.Custom;
+ maxBackground.TextureHeight = maxBackgroundRegion.Height;
+ maxBackground.TextureLeft = maxBackgroundRegion.SourceRectangle.Left;
+ maxBackground.TextureTop = maxBackgroundRegion.SourceRectangle.Top;
+ maxBackground.TextureWidth = maxBackgroundRegion.Width;
+ maxBackground.Width = 36f;
+ maxBackground.WidthUnits = DimensionUnitType.Absolute;
+ maxBackground.Dock(Gum.Wireframe.Dock.Right);
+ innerContainer.AddChild(maxBackground);
+
+ // Create the interactive track that responds to clicks
+ // The special name "TrackInstance" is required for Slider functionality
+ ContainerRuntime trackInstance = new ContainerRuntime();
+ trackInstance.Name = "TrackInstance";
+ trackInstance.Dock(Gum.Wireframe.Dock.Fill);
+ trackInstance.Height = -2f;
+ trackInstance.Width = -2f;
+ middleBackground.AddChild(trackInstance);
+
+ // Create the fill rectangle that visually displays the current value
+ _fillRectangle = new ColoredRectangleRuntime();
+ _fillRectangle.Dock(Gum.Wireframe.Dock.Left);
+ _fillRectangle.Width = 90f; // Default to 90% - will be updated by value changes
+ _fillRectangle.WidthUnits = DimensionUnitType.PercentageOfParent;
+ trackInstance.AddChild(_fillRectangle);
+
+ // Add "OFF" text to the left end
+ TextRuntime offText = new TextRuntime();
+ offText.Red = 70;
+ offText.Green = 86;
+ offText.Blue = 130;
+ offText.CustomFontFile = @"fonts/04b_30.fnt";
+ offText.FontScale = 0.25f;
+ offText.UseCustomFont = true;
+ offText.Text = "OFF";
+ offText.Anchor(Gum.Wireframe.Anchor.Center);
+ offBackground.AddChild(offText);
+
+ // Add "MAX" text to the right end
+ TextRuntime maxText = new TextRuntime();
+ maxText.Red = 70;
+ maxText.Green = 86;
+ maxText.Blue = 130;
+ maxText.CustomFontFile = @"fonts/04b_30.fnt";
+ maxText.FontScale = 0.25f;
+ maxText.UseCustomFont = true;
+ maxText.Text = "MAX";
+ maxText.Anchor(Gum.Wireframe.Anchor.Center);
+ maxBackground.AddChild(maxText);
+
+ // Define colors for focused and unfocused states
+ Color focusedColor = Color.White;
+ Color unfocusedColor = Color.Gray;
+
+ // Create slider state category - Slider.SliderCategoryName is the required name
+ StateSaveCategory sliderCategory = new StateSaveCategory();
+ sliderCategory.Name = Slider.SliderCategoryName;
+ topLevelContainer.AddCategory(sliderCategory);
+
+ // Create the enabled (default/unfocused) state
+ StateSave enabled = new StateSave();
+ enabled.Name = FrameworkElement.EnabledStateName;
+ enabled.Apply = () =>
+ {
+ // When enabled but not focused, use gray coloring for all elements
+ background.Color = unfocusedColor;
+ _textInstance.Color = unfocusedColor;
+ offBackground.Color = unfocusedColor;
+ middleBackground.Color = unfocusedColor;
+ maxBackground.Color = unfocusedColor;
+ _fillRectangle.Color = unfocusedColor;
+ };
+ sliderCategory.States.Add(enabled);
+
+ // Create the focused state
+ StateSave focused = new StateSave();
+ focused.Name = FrameworkElement.FocusedStateName;
+ focused.Apply = () =>
+ {
+ // When focused, use white coloring for all elements
+ background.Color = focusedColor;
+ _textInstance.Color = focusedColor;
+ offBackground.Color = focusedColor;
+ middleBackground.Color = focusedColor;
+ maxBackground.Color = focusedColor;
+ _fillRectangle.Color = focusedColor;
+ };
+ sliderCategory.States.Add(focused);
+
+ // Create the highlighted+focused state by cloning the focused state
+ StateSave highlightedFocused = focused.Clone();
+ highlightedFocused.Name = FrameworkElement.HighlightedFocusedStateName;
+ sliderCategory.States.Add(highlightedFocused);
+
+ // Create the highlighted state by cloning the enabled state
+ StateSave highlighted = enabled.Clone();
+ highlighted.Name = FrameworkElement.HighlightedStateName;
+ sliderCategory.States.Add(highlighted);
+
+ // Assign the configured container as this slider's visual
+ Visual = topLevelContainer;
+
+ // Enable click-to-point functionality for the slider
+ // This allows users to click anywhere on the track to jump to that value
+ IsMoveToPointEnabled = true;
+
+ // Add event handlers
+ Visual.RollOn += HandleRollOn;
+ ValueChanged += HandleValueChanged;
+ ValueChangedByUi += HandleValueChangedByUi;
+ }
+
+ ///
+ /// Automatically focuses the slider when the user interacts with it
+ ///
+ private void HandleValueChangedByUi(object sender, EventArgs e)
+ {
+ IsFocused = true;
+ }
+
+ ///
+ /// Automatically focuses the slider when the mouse hovers over it
+ ///
+ private void HandleRollOn(object sender, EventArgs e)
+ {
+ IsFocused = true;
+ }
+
+ ///
+ /// Updates the fill rectangle width to visually represent the current value
+ ///
+ private void HandleValueChanged(object sender, EventArgs e)
+ {
+ // Calculate the ratio of the current value within its range
+ double ratio = (Value - Minimum) / (Maximum - Minimum);
+
+ // Update the fill rectangle width as a percentage
+ // _fillRectangle uses percentage width units, so we multiply by 100
+ _fillRectangle.Width = 100 * (float)ratio;
+ }
+}
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/app.manifest b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/app.manifest
new file mode 100644
index 00000000..0a2d1b66
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/DungeonSlime/app.manifest
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true/PM
+ permonitorv2,permonitor
+
+
+
+
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/MonoGameLibrary/Audio/AudioController.cs b/Tutorials/MobileDeployment/04-AddAndroidProject/MonoGameLibrary/Audio/AudioController.cs
new file mode 100644
index 00000000..1bffd636
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/MonoGameLibrary/Audio/AudioController.cs
@@ -0,0 +1,280 @@
+using System;
+using System.Collections.Generic;
+using Microsoft.Xna.Framework.Audio;
+using Microsoft.Xna.Framework.Media;
+
+namespace MonoGameLibrary.Audio;
+
+public class AudioController : IDisposable
+{
+ // Tracks sound effect instances created so they can be paused, unpaused, and/or disposed.
+ private readonly List _activeSoundEffectInstances;
+
+ // Tracks the volume for song playback when muting and unmuting.
+ private float _previousSongVolume;
+
+ // Tracks the volume for sound effect playback when muting and unmuting.
+ private float _previousSoundEffectVolume;
+
+ ///
+ /// Gets a value that indicates if audio is muted.
+ ///
+ public bool IsMuted { get; private set; }
+
+ ///
+ /// Gets or Sets the global volume of songs.
+ ///
+ ///
+ /// If IsMuted is true, the getter will always return back 0.0f and the
+ /// setter will ignore setting the volume.
+ ///
+ public float SongVolume
+ {
+ get
+ {
+ if (IsMuted)
+ {
+ return 0.0f;
+ }
+
+ return MediaPlayer.Volume;
+ }
+ set
+ {
+ if (IsMuted)
+ {
+ return;
+ }
+
+ MediaPlayer.Volume = Math.Clamp(value, 0.0f, 1.0f);
+ }
+ }
+
+ ///
+ /// Gets or Sets the global volume of sound effects.
+ ///
+ ///
+ /// If IsMuted is true, the getter will always return back 0.0f and the
+ /// setter will ignore setting the volume.
+ ///
+ public float SoundEffectVolume
+ {
+ get
+ {
+ if (IsMuted)
+ {
+ return 0.0f;
+ }
+
+ return SoundEffect.MasterVolume;
+ }
+ set
+ {
+ if (IsMuted)
+ {
+ return;
+ }
+
+ SoundEffect.MasterVolume = Math.Clamp(value, 0.0f, 1.0f);
+ }
+ }
+
+ ///
+ /// Gets a value that indicates if this audio controller has been disposed.
+ ///
+ public bool IsDisposed { get; private set; }
+
+ ///
+ /// Creates a new audio controller instance.
+ ///
+ public AudioController()
+ {
+ _activeSoundEffectInstances = new List();
+ }
+
+ // Finalizer called when object is collected by the garbage collector
+ ~AudioController() => Dispose(false);
+
+ ///
+ /// Updates this audio controller
+ ///
+ public void Update()
+ {
+ int index = 0;
+
+ while (index < _activeSoundEffectInstances.Count)
+ {
+ SoundEffectInstance instance = _activeSoundEffectInstances[index];
+
+ if (instance.State == SoundState.Stopped && !instance.IsDisposed)
+ {
+ instance.Dispose();
+ }
+
+ _activeSoundEffectInstances.RemoveAt(index);
+ }
+ }
+
+ ///
+ /// Plays the given sound effect.
+ ///
+ /// The sound effect to play.
+ /// The sound effect instance created by this method.
+ public SoundEffectInstance PlaySoundEffect(SoundEffect soundEffect)
+ {
+ return PlaySoundEffect(soundEffect, 1.0f, 1.0f, 0.0f, false);
+ }
+
+ ///
+ /// Plays the given sound effect with the specified properties.
+ ///
+ /// The sound effect to play.
+ /// The volume, ranging from 0.0 (silence) to 1.0 (full volume).
+ /// The pitch adjustment, ranging from -1.0 (down an octave) to 0.0 (no change) to 1.0 (up an octave).
+ /// The panning, ranging from -1.0 (left speaker) to 0.0 (centered), 1.0 (right speaker).
+ /// Whether the the sound effect should loop after playback.
+ /// The sound effect instance created by playing the sound effect.
+ /// The sound effect instance created by this method.
+ public SoundEffectInstance PlaySoundEffect(SoundEffect soundEffect, float volume, float pitch, float pan, bool isLooped)
+ {
+ // Create an instance from the sound effect given.
+ SoundEffectInstance soundEffectInstance = soundEffect.CreateInstance();
+
+ // Apply the volume, pitch, pan, and loop values specified.
+ soundEffectInstance.Volume = volume;
+ soundEffectInstance.Pitch = pitch;
+ soundEffectInstance.Pan = pan;
+ soundEffectInstance.IsLooped = isLooped;
+
+ // Tell the instance to play
+ soundEffectInstance.Play();
+
+ // Add it to the active instances for tracking
+ _activeSoundEffectInstances.Add(soundEffectInstance);
+
+ return soundEffectInstance;
+ }
+
+ ///
+ /// Plays the given song.
+ ///
+ /// The song to play.
+ /// Optionally specify if the song should repeat. Default is true.
+ public void PlaySong(Song song, bool isRepeating = true)
+ {
+ // Check if the media player is already playing, if so, stop it.
+ // If we do not stop it, this could cause issues on some platforms
+ if (MediaPlayer.State == MediaState.Playing)
+ {
+ MediaPlayer.Stop();
+ }
+
+ MediaPlayer.Play(song);
+ MediaPlayer.IsRepeating = isRepeating;
+ }
+
+ ///
+ /// Pauses all audio.
+ ///
+ public void PauseAudio()
+ {
+ // Pause any active songs playing
+ MediaPlayer.Pause();
+
+ // Pause any active sound effects
+ foreach (SoundEffectInstance soundEffectInstance in _activeSoundEffectInstances)
+ {
+ soundEffectInstance.Pause();
+ }
+ }
+
+ ///
+ /// Resumes play of all previous paused audio.
+ ///
+ public void ResumeAudio()
+ {
+ // Resume paused music
+ MediaPlayer.Resume();
+
+ // Resume any active sound effects
+ foreach (SoundEffectInstance soundEffectInstance in _activeSoundEffectInstances)
+ {
+ soundEffectInstance.Resume();
+ }
+ }
+
+ ///
+ /// Mutes all audio.
+ ///
+ public void MuteAudio()
+ {
+ // Store the volume so they can be restored during ResumeAudio
+ _previousSongVolume = MediaPlayer.Volume;
+ _previousSoundEffectVolume = SoundEffect.MasterVolume;
+
+ // Set all volumes to 0
+ MediaPlayer.Volume = 0.0f;
+ SoundEffect.MasterVolume = 0.0f;
+
+ IsMuted = true;
+ }
+
+ ///
+ /// Unmutes all audio to the volume level prior to muting.
+ ///
+ public void UnmuteAudio()
+ {
+ // Restore the previous volume values
+ MediaPlayer.Volume = _previousSongVolume;
+ SoundEffect.MasterVolume = _previousSoundEffectVolume;
+
+ IsMuted = false;
+ }
+
+ ///
+ /// Toggles the current audio mute state.
+ ///
+ public void ToggleMute()
+ {
+ if (IsMuted)
+ {
+ UnmuteAudio();
+ }
+ else
+ {
+ MuteAudio();
+ }
+ }
+
+ ///
+ /// Disposes of this audio controller and cleans up resources.
+ ///
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ /// Disposes this audio controller and cleans up resources.
+ ///
+ /// Indicates whether managed resources should be disposed.
+ protected void Dispose(bool disposing)
+ {
+ if (IsDisposed)
+ {
+ return;
+ }
+
+ if (disposing)
+ {
+ foreach (SoundEffectInstance soundEffectInstance in _activeSoundEffectInstances)
+ {
+ soundEffectInstance.Dispose();
+ }
+ _activeSoundEffectInstances.Clear();
+ }
+
+ IsDisposed = true;
+ }
+}
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/MonoGameLibrary/Circle.cs b/Tutorials/MobileDeployment/04-AddAndroidProject/MonoGameLibrary/Circle.cs
new file mode 100644
index 00000000..0bb691bc
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/MonoGameLibrary/Circle.cs
@@ -0,0 +1,136 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGameLibrary;
+
+public readonly struct Circle : IEquatable
+{
+ private static readonly Circle s_empty = new Circle();
+
+ ///
+ /// The x-coordinate of the center of this circle.
+ ///
+ public readonly int X;
+
+ ///
+ /// The y-coordinate of the center of this circle.
+ ///
+ public readonly int Y;
+
+ ///
+ /// The length, in pixels, from the center of this circle to the edge.
+ ///
+ public readonly int Radius;
+
+ ///
+ /// Gets the location of the center of this circle.
+ ///
+ public readonly Point Location => new Point(X, Y);
+
+ ///
+ /// Gets a circle with X=0, Y=0, and Radius=0.
+ ///
+ public static Circle Empty => s_empty;
+
+ ///
+ /// Gets a value that indicates whether this circle has a radius of 0 and a location of (0, 0).
+ ///
+ public readonly bool IsEmpty => X == 0 && Y == 0 && Radius == 0;
+
+ ///
+ /// Gets the y-coordinate of the highest point on this circle.
+ ///
+ public readonly int Top => Y - Radius;
+
+ ///
+ /// Gets the y-coordinate of the lowest point on this circle.
+ ///
+ public readonly int Bottom => Y + Radius;
+
+ ///
+ /// Gets the x-coordinate of the leftmost point on this circle.
+ ///
+ public readonly int Left => X - Radius;
+
+ ///
+ /// Gets the x-coordinate of the rightmost point on this circle.
+ ///
+ public readonly int Right => X + Radius;
+
+ ///
+ /// Creates a new circle with the specified position and radius.
+ ///
+ /// The x-coordinate of the center of the circle.
+ /// The y-coordinate of the center of the circle..
+ /// The length from the center of the circle to an edge.
+ public Circle(int x, int y, int radius)
+ {
+ X = x;
+ Y = y;
+ Radius = radius;
+ }
+
+ ///
+ /// Creates a new circle with the specified position and radius.
+ ///
+ /// The center of the circle.
+ /// The length from the center of the circle to an edge.
+ public Circle(Point location, int radius)
+ {
+ X = location.X;
+ Y = location.Y;
+ Radius = radius;
+ }
+
+ ///
+ /// Returns a value that indicates whether the specified circle intersects with this circle.
+ ///
+ /// The other circle to check.
+ /// true if the other circle intersects with this circle; otherwise, false.
+ public bool Intersects(Circle other)
+ {
+ int radiiSquared = (this.Radius + other.Radius) * (this.Radius + other.Radius);
+ float distanceSquared = Vector2.DistanceSquared(this.Location.ToVector2(), other.Location.ToVector2());
+ return distanceSquared < radiiSquared;
+ }
+
+ ///
+ /// Returns a value that indicates whether this circle and the specified object are equal
+ ///
+ /// The object to compare with this circle.
+ /// true if this circle and the specified object are equal; otherwise, false.
+ public override readonly bool Equals(object obj) => obj is Circle other && Equals(other);
+
+ ///
+ /// Returns a value that indicates whether this circle and the specified circle are equal.
+ ///
+ /// The circle to compare with this circle.
+ /// true if this circle and the specified circle are equal; otherwise, false.
+ public readonly bool Equals(Circle other) => this.X == other.X &&
+ this.Y == other.Y &&
+ this.Radius == other.Radius;
+
+ ///
+ /// Returns the hash code for this circle.
+ ///
+ /// The hash code for this circle as a 32-bit signed integer.
+ public override readonly int GetHashCode() => HashCode.Combine(X, Y, Radius);
+
+ ///
+ /// Returns a value that indicates if the circle on the left hand side of the equality operator is equal to the
+ /// circle on the right hand side of the equality operator.
+ ///
+ /// The circle on the left hand side of the equality operator.
+ /// The circle on the right hand side of the equality operator.
+ /// true if the two circles are equal; otherwise, false.
+ public static bool operator ==(Circle lhs, Circle rhs) => lhs.Equals(rhs);
+
+ ///
+ /// Returns a value that indicates if the circle on the left hand side of the inequality operator is not equal to the
+ /// circle on the right hand side of the inequality operator.
+ ///
+ /// The circle on the left hand side of the inequality operator.
+ /// The circle on the right hand side fo the inequality operator.
+ /// true if the two circle are not equal; otherwise, false.
+ public static bool operator !=(Circle lhs, Circle rhs) => !lhs.Equals(rhs);
+}
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/MonoGameLibrary/Core.cs b/Tutorials/MobileDeployment/04-AddAndroidProject/MonoGameLibrary/Core.cs
new file mode 100644
index 00000000..d83c54fe
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/MonoGameLibrary/Core.cs
@@ -0,0 +1,206 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content;
+using Microsoft.Xna.Framework.Graphics;
+using Microsoft.Xna.Framework.Input;
+using MonoGameLibrary.Audio;
+using MonoGameLibrary.Input;
+using MonoGameLibrary.Scenes;
+
+namespace MonoGameLibrary;
+
+public class Core : Game
+{
+ internal static Core s_instance;
+
+ ///
+ /// Gets a reference to the Core instance.
+ ///
+ public static Core Instance => s_instance;
+
+ // The scene that is currently active.
+ private static Scene s_activeScene;
+
+ // The next scene to switch to, if there is one.
+ private static Scene s_nextScene;
+
+ ///
+ /// Gets the graphics device manager to control the presentation of graphics.
+ ///
+ public static GraphicsDeviceManager Graphics { get; private set; }
+
+ ///
+ /// Gets the graphics device used to create graphical resources and perform primitive rendering.
+ ///
+ public static new GraphicsDevice GraphicsDevice { get; private set; }
+
+ ///
+ /// Gets the sprite batch used for all 2D rendering.
+ ///
+ public static SpriteBatch SpriteBatch { get; private set; }
+
+ ///
+ /// Gets the content manager used to load global assets.
+ ///
+ public static new ContentManager Content { get; private set; }
+
+ ///
+ /// Gets a reference to to the input management system.
+ ///
+ public static InputManager Input { get; private set; }
+
+ ///
+ /// Gets or Sets a value that indicates if the game should exit when the esc key on the keyboard is pressed.
+ ///
+ public static bool ExitOnEscape { get; set; }
+
+ ///
+ /// Gets a reference to the audio control system.
+ ///
+ public static AudioController Audio { get; private set; }
+
+ ///
+ /// Creates a new Core instance.
+ ///
+ /// The title to display in the title bar of the game window.
+ /// The initial width, in pixels, of the game window.
+ /// The initial height, in pixels, of the game window.
+ /// Indicates if the game should start in fullscreen mode.
+ public Core(string title, int width, int height, bool fullScreen)
+ {
+ // Ensure that multiple cores are not created.
+ if (s_instance != null)
+ {
+ throw new InvalidOperationException($"Only a single Core instance can be created");
+ }
+
+ // Store reference to engine for global member access.
+ s_instance = this;
+
+ // Create a new graphics device manager.
+ Graphics = new GraphicsDeviceManager(this);
+
+ // Set the graphics defaults
+ Graphics.PreferredBackBufferWidth = width;
+ Graphics.PreferredBackBufferHeight = height;
+ Graphics.IsFullScreen = fullScreen;
+
+ // Apply the graphic presentation changes
+ Graphics.ApplyChanges();
+
+ // Set the window title
+ Window.Title = title;
+
+ // Set the core's content manager to a reference of hte base Game's
+ // content manager.
+ Content = base.Content;
+
+ // Set the root directory for content
+ Content.RootDirectory = "Content";
+
+ // Mouse is visible by default
+ IsMouseVisible = true;
+ }
+
+ protected override void Initialize()
+ {
+ base.Initialize();
+
+ // Set the core's graphics device to a reference of the base Game's
+ // graphics device.
+ GraphicsDevice = base.GraphicsDevice;
+
+ // Create the sprite batch instance.
+ SpriteBatch = new SpriteBatch(GraphicsDevice);
+
+ // Create a new input manager
+ Input = new InputManager();
+
+ // Create a new audio controller.
+ Audio = new AudioController();
+ }
+
+ protected override void UnloadContent()
+ {
+ // Dispose of the audio controller.
+ Audio.Dispose();
+
+ base.UnloadContent();
+ }
+
+ protected override void Update(GameTime gameTime)
+ {
+ // Update the input manager.
+ Input.Update(gameTime);
+
+ // Update the audio controller.
+ Audio.Update();
+
+ if (ExitOnEscape && Input.Keyboard.WasKeyJustPressed(Keys.Escape))
+ {
+ Exit();
+ }
+
+ // if there is a next scene waiting to be switch to, then transition
+ // to that scene
+ if (s_nextScene != null)
+ {
+ TransitionScene();
+ }
+
+ // If there is an active scene, update it.
+ if (s_activeScene != null)
+ {
+ s_activeScene.Update(gameTime);
+ }
+
+ base.Update(gameTime);
+ }
+
+ protected override void Draw(GameTime gameTime)
+ {
+ // If there is an active scene, draw it.
+ if (s_activeScene != null)
+ {
+ s_activeScene.Draw(gameTime);
+ }
+
+ base.Draw(gameTime);
+ }
+
+ public static void ChangeScene(Scene next)
+ {
+ // Only set the next scene value if it is not the same
+ // instance as the currently active scene.
+ if (s_activeScene != next)
+ {
+ s_nextScene = next;
+ }
+ }
+
+ private static void TransitionScene()
+ {
+ // If there is an active scene, dispose of it
+ if (s_activeScene != null)
+ {
+ s_activeScene.Dispose();
+ }
+
+ // Force the garbage collector to collect to ensure memory is cleared
+ GC.Collect();
+
+ // Change the currently active scene to the new scene
+ s_activeScene = s_nextScene;
+
+ // Null out the next scene value so it does not trigger a change over and over.
+ s_nextScene = null;
+
+ // If the active scene now is not null, initialize it.
+ // Remember, just like with Game, the Initialize call also calls the
+ // Scene.LoadContent
+ if (s_activeScene != null)
+ {
+ s_activeScene.Initialize();
+ }
+ }
+}
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/MonoGameLibrary/Graphics/AnimatedSprite.cs b/Tutorials/MobileDeployment/04-AddAndroidProject/MonoGameLibrary/Graphics/AnimatedSprite.cs
new file mode 100644
index 00000000..a1a3594e
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/MonoGameLibrary/Graphics/AnimatedSprite.cs
@@ -0,0 +1,60 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGameLibrary.Graphics;
+
+public class AnimatedSprite : Sprite
+{
+ private int _currentFrame;
+ private TimeSpan _elapsed;
+ private Animation _animation;
+
+ ///
+ /// Gets or Sets the animation for this animated sprite.
+ ///
+ public Animation Animation
+ {
+ get => _animation;
+ set
+ {
+ _animation = value;
+ Region = _animation.Frames[0];
+ }
+ }
+
+ ///
+ /// Creates a new animated sprite.
+ ///
+ public AnimatedSprite() { }
+
+ ///
+ /// Creates a new animated sprite with the specified frames and delay.
+ ///
+ /// The animation for this animated sprite.
+ public AnimatedSprite(Animation animation)
+ {
+ Animation = animation;
+ }
+
+ ///
+ /// Updates this animated sprite.
+ ///
+ /// A snapshot of the game timing values provided by the framework.
+ public void Update(GameTime gameTime)
+ {
+ _elapsed += gameTime.ElapsedGameTime;
+
+ if (_elapsed >= _animation.Delay)
+ {
+ _elapsed -= _animation.Delay;
+ _currentFrame++;
+
+ if (_currentFrame >= _animation.Frames.Count)
+ {
+ _currentFrame = 0;
+ }
+
+ Region = _animation.Frames[_currentFrame];
+ }
+ }
+}
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/MonoGameLibrary/Graphics/Animation.cs b/Tutorials/MobileDeployment/04-AddAndroidProject/MonoGameLibrary/Graphics/Animation.cs
new file mode 100644
index 00000000..44d61b65
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/MonoGameLibrary/Graphics/Animation.cs
@@ -0,0 +1,38 @@
+using System;
+using System.Collections.Generic;
+
+namespace MonoGameLibrary.Graphics;
+
+public class Animation
+{
+ ///
+ /// The texture regions that make up the frames of this animation. The order of the regions within the collection
+ /// are the order that the frames should be displayed in.
+ ///
+ public List Frames { get; set; }
+
+ ///
+ /// The amount of time to delay between each frame before moving to the next frame for this animation.
+ ///
+ public TimeSpan Delay { get; set; }
+
+ ///
+ /// Creates a new animation.
+ ///
+ public Animation()
+ {
+ Frames = new List();
+ Delay = TimeSpan.FromMilliseconds(100);
+ }
+
+ ///
+ /// Creates a new animation with the specified frames and delay.
+ ///
+ /// An ordered collection of the frames for this animation.
+ /// The amount of time to delay between each frame of this animation.
+ public Animation(List frames, TimeSpan delay)
+ {
+ Frames = frames;
+ Delay = delay;
+ }
+}
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/MonoGameLibrary/Graphics/Sprite.cs b/Tutorials/MobileDeployment/04-AddAndroidProject/MonoGameLibrary/Graphics/Sprite.cs
new file mode 100644
index 00000000..20c44f0b
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/MonoGameLibrary/Graphics/Sprite.cs
@@ -0,0 +1,108 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGameLibrary.Graphics;
+
+public class Sprite
+{
+ ///
+ /// Gets or Sets the source texture region represented by this sprite.
+ ///
+ public TextureRegion Region { get; set; }
+
+ ///
+ /// Gets or Sets the color mask to apply when rendering this sprite.
+ ///
+ ///
+ /// Default value is Color.White
+ ///
+ public Color Color { get; set; } = Color.White;
+
+ ///
+ /// Gets or Sets the amount of rotation, in radians, to apply when rendering this sprite.
+ ///
+ ///
+ /// Default value is 0.0f
+ ///
+ public float Rotation { get; set; } = 0.0f;
+
+ ///
+ /// Gets or Sets the scale factor to apply to the x- and y-axes when rendering this sprite.
+ ///
+ ///
+ /// Default value is Vector2.One
+ ///
+ public Vector2 Scale { get; set; } = Vector2.One;
+
+ ///
+ /// Gets or Sets the xy-coordinate origin point, relative to the top-left corner, of this sprite.
+ ///
+ ///
+ /// Default value is Vector2.Zero
+ ///
+ public Vector2 Origin { get; set; } = Vector2.Zero;
+
+ ///
+ /// Gets or Sets the sprite effects to apply when rendering this sprite.
+ ///
+ ///
+ /// Default value is SpriteEffects.None
+ ///
+ public SpriteEffects Effects { get; set; } = SpriteEffects.None;
+
+ ///
+ /// Gets or Sets the layer depth to apply when rendering this sprite.
+ ///
+ ///
+ /// Default value is 0.0f
+ ///
+ public float LayerDepth { get; set; } = 0.0f;
+
+ ///
+ /// Gets the width, in pixels, of this sprite.
+ ///
+ ///
+ /// Width is calculated by multiplying the width of the source texture region by the x-axis scale factor.
+ ///
+ public float Width => Region.Width * Scale.X;
+
+ ///
+ /// Gets the height, in pixels, of this sprite.
+ ///
+ ///
+ /// Height is calculated by multiplying the height of the source texture region by the y-axis scale factor.
+ ///
+ public float Height => Region.Height * Scale.Y;
+
+ ///
+ /// Creates a new sprite.
+ ///
+ public Sprite() { }
+
+ ///
+ /// Creates a new sprite using the specified source texture region.
+ ///
+ /// The texture region to use as the source texture region for this sprite.
+ public Sprite(TextureRegion region)
+ {
+ Region = region;
+ }
+
+ ///
+ /// Sets the origin of this sprite to the center
+ ///
+ public void CenterOrigin()
+ {
+ Origin = new Vector2(Region.Width, Region.Height) * 0.5f;
+ }
+
+ ///
+ /// Submit this sprite for drawing to the current batch.
+ ///
+ /// The SpriteBatch instance used for batching draw calls.
+ /// The xy-coordinate position to render this sprite at.
+ public void Draw(SpriteBatch spriteBatch, Vector2 position)
+ {
+ Region.Draw(spriteBatch, position, Color, Rotation, Origin, Scale, Effects, LayerDepth);
+ }
+}
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/MonoGameLibrary/Graphics/TextureAtlas.cs b/Tutorials/MobileDeployment/04-AddAndroidProject/MonoGameLibrary/Graphics/TextureAtlas.cs
new file mode 100644
index 00000000..e48c9abd
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/MonoGameLibrary/Graphics/TextureAtlas.cs
@@ -0,0 +1,239 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Xml;
+using System.Xml.Linq;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content;
+using Microsoft.Xna.Framework.Graphics;
+
+
+namespace MonoGameLibrary.Graphics;
+
+public class TextureAtlas
+{
+ private Dictionary _regions;
+
+ // Stores animations added to this atlas.
+ private Dictionary _animations;
+
+ ///
+ /// Gets or Sets the source texture represented by this texture atlas.
+ ///
+ public Texture2D Texture { get; set; }
+
+ ///
+ /// Creates a new texture atlas.
+ ///
+ public TextureAtlas()
+ {
+ _regions = new Dictionary();
+ _animations = new Dictionary();
+ }
+
+ ///
+ /// Creates a new texture atlas instance using the given texture.
+ ///
+ /// The source texture represented by the texture atlas.
+ public TextureAtlas(Texture2D texture)
+ {
+ Texture = texture;
+ _regions = new Dictionary();
+ _animations = new Dictionary();
+ }
+
+ ///
+ /// Creates a new region and adds it to this texture atlas.
+ ///
+ /// The name to give the texture region.
+ /// The top-left x-coordinate position of the region boundary relative to the top-left corner of the source texture boundary.
+ /// The top-left y-coordinate position of the region boundary relative to the top-left corner of the source texture boundary.
+ /// The width, in pixels, of the region.
+ /// The height, in pixels, of the region.
+ public void AddRegion(string name, int x, int y, int width, int height)
+ {
+ TextureRegion region = new TextureRegion(Texture, x, y, width, height);
+ _regions.Add(name, region);
+ }
+
+ ///
+ /// Gets the region from this texture atlas with the specified name.
+ ///
+ /// The name of the region to retrieve.
+ /// The TextureRegion with the specified name.
+ public TextureRegion GetRegion(string name)
+ {
+ return _regions[name];
+ }
+
+ ///
+ /// Removes the region from this texture atlas with the specified name.
+ ///
+ /// The name of the region to remove.
+ ///
+ public bool RemoveRegion(string name)
+ {
+ return _regions.Remove(name);
+ }
+
+ ///
+ /// Removes all regions from this texture atlas.
+ ///
+ public void Clear()
+ {
+ _regions.Clear();
+ }
+
+ ///
+ /// Creates a new sprite using the region from this texture atlas with the specified name.
+ ///
+ /// The name of the region to create the sprite with.
+ /// A new Sprite using the texture region with the specified name.
+ public Sprite CreateSprite(string regionName)
+ {
+ TextureRegion region = GetRegion(regionName);
+ return new Sprite(region);
+ }
+
+ ///
+ /// Adds the given animation to this texture atlas with the specified name.
+ ///
+ /// The name of the animation to add.
+ /// The animation to add.
+ public void AddAnimation(string animationName, Animation animation)
+ {
+ _animations.Add(animationName, animation);
+ }
+
+ ///
+ /// Gets the animation from this texture atlas with the specified name.
+ ///
+ /// The name of the animation to retrieve.
+ /// The animation with the specified name.
+ public Animation GetAnimation(string animationName)
+ {
+ return _animations[animationName];
+ }
+
+ ///
+ /// Removes the animation with the specified name from this texture atlas.
+ ///
+ /// The name of the animation to remove.
+ /// true if the animation is removed successfully; otherwise, false.
+ public bool RemoveAnimation(string animationName)
+ {
+ return _animations.Remove(animationName);
+ }
+
+ ///
+ /// Creates a new animated sprite using the animation from this texture atlas with the specified name.
+ ///
+ /// The name of the animation to use.
+ /// A new AnimatedSprite using the animation with the specified name.
+ public AnimatedSprite CreateAnimatedSprite(string animationName)
+ {
+ Animation animation = GetAnimation(animationName);
+ return new AnimatedSprite(animation);
+ }
+
+ ///
+ /// Creates a new texture atlas based a texture atlas xml configuration file.
+ ///
+ /// The content manager used to load the texture for the atlas.
+ /// The path to the xml file, relative to the content root directory..
+ /// The texture atlas created by this method.
+ public static TextureAtlas FromFile(ContentManager content, string fileName)
+ {
+ TextureAtlas atlas = new TextureAtlas();
+
+ string filePath = Path.Combine(content.RootDirectory, fileName);
+
+ using (Stream stream = TitleContainer.OpenStream(filePath))
+ {
+ using (XmlReader reader = XmlReader.Create(stream))
+ {
+ XDocument doc = XDocument.Load(reader);
+ XElement root = doc.Root;
+
+ // The element contains the content path for the Texture2D to load.
+ // So we will retrieve that value then use the content manager to load the texture.
+ string texturePath = root.Element("Texture").Value;
+ atlas.Texture = content.Load(texturePath);
+
+ // The element contains individual elements, each one describing
+ // a different texture region within the atlas.
+ //
+ // Example:
+ //
+ //
+ //
+ //
+ //
+ // So we retrieve all of the elements then loop through each one
+ // and generate a new TextureRegion instance from it and add it to this atlas.
+ var regions = root.Element("Regions")?.Elements("Region");
+
+ if (regions != null)
+ {
+ foreach (var 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);
+ }
+ }
+ }
+
+ // The element contains individual elements, each one describing
+ // a different animation within the atlas.
+ //
+ // Example:
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ // So we retrieve all of the elements then loop through each one
+ // and generate a new Animation instance from it and add it to this atlas.
+ var animationElements = root.Element("Animations").Elements("Animation");
+
+ if (animationElements != null)
+ {
+ foreach (var animationElement in animationElements)
+ {
+ string name = animationElement.Attribute("name")?.Value;
+ float delayInMilliseconds = float.Parse(animationElement.Attribute("delay")?.Value ?? "0");
+ TimeSpan delay = TimeSpan.FromMilliseconds(delayInMilliseconds);
+
+ List frames = new List();
+
+ var frameElements = animationElement.Elements("Frame");
+
+ if (frameElements != null)
+ {
+ foreach (var frameElement in frameElements)
+ {
+ string regionName = frameElement.Attribute("region").Value;
+ TextureRegion region = atlas.GetRegion(regionName);
+ frames.Add(region);
+ }
+ }
+
+ Animation animation = new Animation(frames, delay);
+ atlas.AddAnimation(name, animation);
+ }
+ }
+
+ return atlas;
+ }
+ }
+ }
+}
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/MonoGameLibrary/Graphics/TextureRegion.cs b/Tutorials/MobileDeployment/04-AddAndroidProject/MonoGameLibrary/Graphics/TextureRegion.cs
new file mode 100644
index 00000000..98edc2da
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/MonoGameLibrary/Graphics/TextureRegion.cs
@@ -0,0 +1,131 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGameLibrary.Graphics;
+
+///
+/// Represents a rectangular region within a texture.
+///
+public class TextureRegion
+{
+ ///
+ /// Gets or Sets the source texture this texture region is part of.
+ ///
+ public Texture2D Texture { get; set; }
+
+ ///
+ /// Gets or Sets the source rectangle boundary of this texture region within the source texture.
+ ///
+ public Rectangle SourceRectangle { get; set; }
+
+ ///
+ /// Gets the width, in pixels, of this texture region.
+ ///
+ public int Width => SourceRectangle.Width;
+
+ ///
+ /// Gets the height, in pixels, of this texture region.
+ ///
+ public int Height => SourceRectangle.Height;
+
+ ///
+ /// Gets the top normalized texture coordinate of this region.
+ ///
+ public float TopTextureCoordinate => SourceRectangle.Top / (float)Texture.Height;
+
+ ///
+ /// Gets the bottom normalized texture coordinate of this region.
+ ///
+ public float BottomTextureCoordinate => SourceRectangle.Bottom / (float)Texture.Height;
+
+ ///
+ /// Gets the left normalized texture coordinate of this region.
+ ///
+ public float LeftTextureCoordinate => SourceRectangle.Left / (float)Texture.Width;
+
+ ///
+ /// Gets the right normalized texture coordinate of this region.
+ ///
+ public float RightTextureCoordinate => SourceRectangle.Right / (float)Texture.Width;
+
+ ///
+ /// Creates a new texture region.
+ ///
+ public TextureRegion() { }
+
+ ///
+ /// Creates a new texture region using the specified source texture.
+ ///
+ /// The texture to use as the source texture for this texture region.
+ /// The x-coordinate position of the upper-left corner of this texture region relative to the upper-left corner of the source texture.
+ /// The y-coordinate position of the upper-left corner of this texture region relative to the upper-left corner of the source texture.
+ /// The width, in pixels, of this texture region.
+ /// The height, in pixels, of this texture region.
+ public TextureRegion(Texture2D texture, int x, int y, int width, int height)
+ {
+ Texture = texture;
+ SourceRectangle = new Rectangle(x, y, width, height);
+ }
+
+ ///
+ /// Submit this texture region for drawing in the current batch.
+ ///
+ /// The spritebatch instance used for batching draw calls.
+ /// The xy-coordinate location to draw this texture region on the screen.
+ /// The color mask to apply when drawing this texture region on screen.
+ public void Draw(SpriteBatch spriteBatch, Vector2 position, Color color)
+ {
+ Draw(spriteBatch, position, color, 0.0f, Vector2.Zero, Vector2.One, SpriteEffects.None, 0.0f);
+ }
+
+ ///
+ /// Submit this texture region for drawing in the current batch.
+ ///
+ /// The spritebatch instance used for batching draw calls.
+ /// The xy-coordinate location to draw this texture region on the screen.
+ /// The color mask to apply when drawing this texture region on screen.
+ /// The amount of rotation, in radians, to apply when drawing this texture region on screen.
+ /// The center of rotation, scaling, and position when drawing this texture region on screen.
+ /// The scale factor to apply when drawing this texture region on screen.
+ /// Specifies if this texture region should be flipped horizontally, vertically, or both when drawing on screen.
+ /// The depth of the layer to use when drawing this texture region on screen.
+ public void Draw(SpriteBatch spriteBatch, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects effects, float layerDepth)
+ {
+ Draw(
+ spriteBatch,
+ position,
+ color,
+ rotation,
+ origin,
+ new Vector2(scale, scale),
+ effects,
+ layerDepth
+ );
+ }
+
+ ///
+ /// Submit this texture region for drawing in the current batch.
+ ///
+ /// The spritebatch instance used for batching draw calls.
+ /// The xy-coordinate location to draw this texture region on the screen.
+ /// The color mask to apply when drawing this texture region on screen.
+ /// The amount of rotation, in radians, to apply when drawing this texture region on screen.
+ /// The center of rotation, scaling, and position when drawing this texture region on screen.
+ /// The amount of scaling to apply to the x- and y-axes when drawing this texture region on screen.
+ /// Specifies if this texture region should be flipped horizontally, vertically, or both when drawing on screen.
+ /// The depth of the layer to use when drawing this texture region on screen.
+ public void Draw(SpriteBatch spriteBatch, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth)
+ {
+ spriteBatch.Draw(
+ Texture,
+ position,
+ SourceRectangle,
+ color,
+ rotation,
+ origin,
+ scale,
+ effects,
+ layerDepth
+ );
+ }
+}
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/MonoGameLibrary/Graphics/Tilemap.cs b/Tutorials/MobileDeployment/04-AddAndroidProject/MonoGameLibrary/Graphics/Tilemap.cs
new file mode 100644
index 00000000..9d5d64ee
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/MonoGameLibrary/Graphics/Tilemap.cs
@@ -0,0 +1,231 @@
+using System;
+using System.IO;
+using System.Xml;
+using System.Xml.Linq;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGameLibrary.Graphics;
+
+public class Tilemap
+{
+ private readonly Tileset _tileset;
+ private readonly int[] _tiles;
+
+ ///
+ /// Gets the total number of rows in this tilemap.
+ ///
+ public int Rows { get; }
+
+ ///
+ /// Gets the total number of columns in this tilemap.
+ ///
+ public int Columns { get; }
+
+ ///
+ /// Gets the total number of tiles in this tilemap.
+ ///
+ public int Count { get; }
+
+ ///
+ /// Gets or Sets the scale factor to draw each tile at.
+ ///
+ public Vector2 Scale { get; set; }
+
+ ///
+ /// Gets the width, in pixels, each tile is drawn at.
+ ///
+ public float TileWidth => _tileset.TileWidth * Scale.X;
+
+ ///
+ /// Gets the height, in pixels, each tile is drawn at.
+ ///
+ public float TileHeight => _tileset.TileHeight * Scale.Y;
+
+ ///
+ /// Creates a new tilemap.
+ ///
+ /// The tileset used by this tilemap.
+ /// The total number of columns in this tilemap.
+ /// The total number of rows in this tilemap.
+ public Tilemap(Tileset tileset, int columns, int rows)
+ {
+ _tileset = tileset;
+ Rows = rows;
+ Columns = columns;
+ Count = Columns * Rows;
+ Scale = Vector2.One;
+ _tiles = new int[Count];
+ }
+
+ ///
+ /// Sets the tile at the given index in this tilemap to use the tile from
+ /// the tileset at the specified tileset id.
+ ///
+ /// The index of the tile in this tilemap.
+ /// The tileset id of the tile from the tileset to use.
+ public void SetTile(int index, int tilesetID)
+ {
+ _tiles[index] = tilesetID;
+ }
+
+ ///
+ /// Sets the tile at the given column and row in this tilemap to use the tile
+ /// from the tileset at the specified tileset id.
+ ///
+ /// The column of the tile in this tilemap.
+ /// The row of the tile in this tilemap.
+ /// The tileset id of the tile from the tileset to use.
+ public void SetTile(int column, int row, int tilesetID)
+ {
+ int index = row * Columns + column;
+ SetTile(index, tilesetID);
+ }
+
+ ///
+ /// Gets the texture region of the tile from this tilemap at the specified index.
+ ///
+ /// The index of the tile in this tilemap.
+ /// The texture region of the tile from this tilemap at the specified index.
+ public TextureRegion GetTile(int index)
+ {
+ return _tileset.GetTile(_tiles[index]);
+ }
+
+ ///
+ /// Gets the texture region of the tile frm this tilemap at the specified
+ /// column and row.
+ ///
+ /// The column of the tile in this tilemap.
+ /// The row of the tile in this tilemap.
+ /// The texture region of the tile from this tilemap at the specified column and row.
+ public TextureRegion GetTile(int column, int row)
+ {
+ int index = row * Columns + column;
+ return GetTile(index);
+ }
+
+ ///
+ /// Draws this tilemap using the given sprite batch.
+ ///
+ /// The sprite batch used to draw this tilemap.
+ public void Draw(SpriteBatch spriteBatch)
+ {
+ for (int i = 0; i < Count; i++)
+ {
+ int tileSetIndex = _tiles[i];
+ TextureRegion tile = _tileset.GetTile(tileSetIndex);
+
+ int x = i % Columns;
+ int y = i / Columns;
+
+ Vector2 position = new Vector2(x * TileWidth, y * TileHeight);
+ tile.Draw(spriteBatch, position, Color.White, 0.0f, Vector2.Zero, Scale, SpriteEffects.None, 1.0f);
+ }
+ }
+
+ ///
+ /// Creates a new tilemap based on a tilemap xml configuration file.
+ ///
+ /// The content manager used to load the texture for the tileset.
+ /// The path to the xml file, relative to the content root directory.
+ /// The tilemap created by this method.
+ public static Tilemap FromFile(ContentManager content, string filename)
+ {
+ string filePath = Path.Combine(content.RootDirectory, filename);
+
+ using (Stream stream = TitleContainer.OpenStream(filePath))
+ {
+ using (XmlReader reader = XmlReader.Create(stream))
+ {
+ XDocument doc = XDocument.Load(reader);
+ XElement root = doc.Root;
+
+ // The element contains the information about the tileset
+ // used by the tilemap.
+ //
+ // Example
+ // contentPath
+ //
+ // The region attribute represents the x, y, width, and height
+ // components of the boundary for the texture region within the
+ // texture at the contentPath specified.
+ //
+ // the tileWidth and tileHeight attributes specify the width and
+ // height of each tile in the tileset.
+ //
+ // the contentPath value is the contentPath to the texture to
+ // load that contains the tileset
+ XElement tilesetElement = root.Element("Tileset");
+
+ string regionAttribute = tilesetElement.Attribute("region").Value;
+ string[] split = regionAttribute.Split(" ", StringSplitOptions.RemoveEmptyEntries);
+ int x = int.Parse(split[0]);
+ int y = int.Parse(split[1]);
+ int width = int.Parse(split[2]);
+ int height = int.Parse(split[3]);
+
+ int tileWidth = int.Parse(tilesetElement.Attribute("tileWidth").Value);
+ int tileHeight = int.Parse(tilesetElement.Attribute("tileHeight").Value);
+ string contentPath = tilesetElement.Value;
+
+ // Load the texture 2d at the content path
+ Texture2D texture = content.Load(contentPath);
+
+ // Create the texture region from the texture
+ TextureRegion textureRegion = new TextureRegion(texture, x, y, width, height);
+
+ // Create the tileset using the texture region
+ Tileset tileset = new Tileset(textureRegion, tileWidth, tileHeight);
+
+ // The element contains lines of strings where each line
+ // represents a row in the tilemap. Each line is a space
+ // separated string where each element represents a column in that
+ // row. The value of the column is the id of the tile in the
+ // tileset to draw for that location.
+ //
+ // Example:
+ //
+ // 00 01 01 02
+ // 03 04 04 05
+ // 03 04 04 05
+ // 06 07 07 08
+ //
+ XElement tilesElement = root.Element("Tiles");
+
+ // Split the value of the tiles data into rows by splitting on
+ // the new line character
+ string[] rows = tilesElement.Value.Trim().Split('\n', StringSplitOptions.RemoveEmptyEntries);
+
+ // Split the value of the first row to determine the total number of columns
+ int columnCount = rows[0].Split(" ", StringSplitOptions.RemoveEmptyEntries).Length;
+
+ // Create the tilemap
+ Tilemap tilemap = new Tilemap(tileset, columnCount, rows.Length);
+
+ // Process each row
+ for (int row = 0; row < rows.Length; row++)
+ {
+ // Split the row into individual columns
+ string[] columns = rows[row].Trim().Split(" ", StringSplitOptions.RemoveEmptyEntries);
+
+ // Process each column of the current row
+ for (int column = 0; column < columnCount; column++)
+ {
+ // Get the tileset index for this location
+ int tilesetIndex = int.Parse(columns[column]);
+
+ // Get the texture region of that tile from the tileset
+ TextureRegion region = tileset.GetTile(tilesetIndex);
+
+ // Add that region to the tilemap at the row and column location
+ tilemap.SetTile(column, row, tilesetIndex);
+ }
+ }
+
+ return tilemap;
+ }
+ }
+ }
+}
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/MonoGameLibrary/Graphics/Tileset.cs b/Tutorials/MobileDeployment/04-AddAndroidProject/MonoGameLibrary/Graphics/Tileset.cs
new file mode 100644
index 00000000..80c2e65a
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/MonoGameLibrary/Graphics/Tileset.cs
@@ -0,0 +1,76 @@
+namespace MonoGameLibrary.Graphics;
+
+public class Tileset
+{
+ private readonly TextureRegion[] _tiles;
+
+ ///
+ /// Gets the width, in pixels, of each tile in this tileset.
+ ///
+ public int TileWidth { get; }
+
+ ///
+ /// Gets the height, in pixels, of each tile in this tileset.
+ ///
+ public int TileHeight { get; }
+
+ ///
+ /// Gets the total number of columns in this tileset.
+ ///
+ public int Columns { get; }
+
+ ///
+ /// Gets the total number of rows in this tileset.
+ ///
+ public int Rows { get; }
+
+ ///
+ /// Gets the total number of tiles in this tileset.
+ ///
+ public int Count { get; }
+
+ ///
+ /// Creates a new tileset based on the given texture region with the specified
+ /// tile width and height.
+ ///
+ /// The texture region that contains the tiles for the tileset.
+ /// The width of each tile in the tileset.
+ /// The height of each tile in the tileset.
+ public Tileset(TextureRegion textureRegion, int tileWidth, int tileHeight)
+ {
+ TileWidth = tileWidth;
+ TileHeight = tileHeight;
+ Columns = textureRegion.Width / tileWidth;
+ Rows = textureRegion.Height / tileHeight;
+ Count = Columns * Rows;
+
+ // Create the texture regions that make up each individual tile
+ _tiles = new TextureRegion[Count];
+
+ for (int i = 0; i < Count; i++)
+ {
+ int x = i % Columns * tileWidth;
+ int y = i / Columns * tileHeight;
+ _tiles[i] = new TextureRegion(textureRegion.Texture, textureRegion.SourceRectangle.X + x, textureRegion.SourceRectangle.Y + y, tileWidth, tileHeight);
+ }
+ }
+
+ ///
+ /// Gets the texture region for the tile from this tileset at the given index.
+ ///
+ /// The index of the texture region in this tile set.
+ /// The texture region for the tile form this tileset at the given index.
+ public TextureRegion GetTile(int index) => _tiles[index];
+
+ ///
+ /// Gets the texture region for the tile from this tileset at the given location.
+ ///
+ /// The column in this tileset of the texture region.
+ /// The row in this tileset of the texture region.
+ /// The texture region for the tile from this tileset at given location.
+ public TextureRegion GetTile(int column, int row)
+ {
+ int index = row * Columns + column;
+ return GetTile(index);
+ }
+}
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/MonoGameLibrary/Input/GamePadInfo.cs b/Tutorials/MobileDeployment/04-AddAndroidProject/MonoGameLibrary/Input/GamePadInfo.cs
new file mode 100644
index 00000000..5e3d0a00
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/MonoGameLibrary/Input/GamePadInfo.cs
@@ -0,0 +1,140 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Input;
+
+namespace MonoGameLibrary.Input;
+
+public class GamePadInfo
+{
+ private TimeSpan _vibrationTimeRemaining = TimeSpan.Zero;
+
+ ///
+ /// Gets the index of the player this gamepad is for.
+ ///
+ public PlayerIndex PlayerIndex { get; }
+
+ ///
+ /// Gets the state of input for this gamepad during the previous update cycle.
+ ///
+ public GamePadState PreviousState { get; private set; }
+
+ ///
+ /// Gets the state of input for this gamepad during the current update cycle.
+ ///
+ public GamePadState CurrentState { get; private set; }
+
+ ///
+ /// Gets a value that indicates if this gamepad is currently connected.
+ ///
+ public bool IsConnected => CurrentState.IsConnected;
+
+ ///
+ /// Gets the value of the left thumbstick of this gamepad.
+ ///
+ public Vector2 LeftThumbStick => CurrentState.ThumbSticks.Left;
+
+ ///
+ /// Gets the value of the right thumbstick of this gamepad.
+ ///
+ public Vector2 RightThumbStick => CurrentState.ThumbSticks.Right;
+
+ ///
+ /// Gets the value of the left trigger of this gamepad.
+ ///
+ public float LeftTrigger => CurrentState.Triggers.Left;
+
+ ///
+ /// Gets the value of the right trigger of this gamepad.
+ ///
+ public float RightTrigger => CurrentState.Triggers.Right;
+
+ ///
+ /// Creates a new GamePadInfo for the gamepad connected at the specified player index.
+ ///
+ /// The index of the player for this gamepad.
+ public GamePadInfo(PlayerIndex playerIndex)
+ {
+ PlayerIndex = playerIndex;
+ PreviousState = new GamePadState();
+ CurrentState = GamePad.GetState(playerIndex);
+ }
+
+ ///
+ /// Updates the state information for this gamepad input.
+ ///
+ ///
+ public void Update(GameTime gameTime)
+ {
+ PreviousState = CurrentState;
+ CurrentState = GamePad.GetState(PlayerIndex);
+
+ if (_vibrationTimeRemaining > TimeSpan.Zero)
+ {
+ _vibrationTimeRemaining -= gameTime.ElapsedGameTime;
+
+ if (_vibrationTimeRemaining <= TimeSpan.Zero)
+ {
+ StopVibration();
+ }
+ }
+ }
+
+ ///
+ /// Returns a value that indicates whether the specified gamepad button is current down.
+ ///
+ /// The gamepad button to check.
+ /// true if the specified gamepad button is currently down; otherwise, false.
+ public bool IsButtonDown(Buttons button)
+ {
+ return CurrentState.IsButtonDown(button);
+ }
+
+ ///
+ /// Returns a value that indicates whether the specified gamepad button is currently up.
+ ///
+ /// The gamepad button to check.
+ /// true if the specified gamepad button is currently up; otherwise, false.
+ public bool IsButtonUp(Buttons button)
+ {
+ return CurrentState.IsButtonUp(button);
+ }
+
+ ///
+ /// Returns a value that indicates whether the specified gamepad button was just pressed on the current frame.
+ ///
+ /// The gamepad button to check.
+ /// true if the specified gamepad button was just pressed on the current frame; otherwise, false.
+ public bool WasButtonJustPressed(Buttons button)
+ {
+ return CurrentState.IsButtonDown(button) && PreviousState.IsButtonUp(button);
+ }
+
+ ///
+ /// Returns a value that indicates whether the specified gamepad button was just released on the current frame.
+ ///
+ /// The gamepad button to check.
+ /// true if the specified gamepad button was just released on the current frame; otherwise, false.
+ public bool WasButtonJustReleased(Buttons button)
+ {
+ return CurrentState.IsButtonUp(button) && PreviousState.IsButtonDown(button);
+ }
+
+ ///
+ /// Sets the vibration for all motors of this gamepad.
+ ///
+ /// The strength of the vibration from 0.0f (none) to 1.0f (full).
+ /// The amount of time the vibration should occur.
+ public void SetVibration(float strength, TimeSpan time)
+ {
+ _vibrationTimeRemaining = time;
+ GamePad.SetVibration(PlayerIndex, strength, strength);
+ }
+
+ ///
+ /// Stops the vibration of all motors for this gamepad.
+ ///
+ public void StopVibration()
+ {
+ GamePad.SetVibration(PlayerIndex, 0.0f, 0.0f);
+ }
+}
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/MonoGameLibrary/Input/InputManager.cs b/Tutorials/MobileDeployment/04-AddAndroidProject/MonoGameLibrary/Input/InputManager.cs
new file mode 100644
index 00000000..1790eb70
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/MonoGameLibrary/Input/InputManager.cs
@@ -0,0 +1,52 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGameLibrary.Input;
+
+public class InputManager
+{
+ ///
+ /// Gets the state information of keyboard input.
+ ///
+ public KeyboardInfo Keyboard { get; private set; }
+
+ ///
+ /// Gets the state information of mouse input.
+ ///
+ public MouseInfo Mouse { get; private set; }
+
+ ///
+ /// Gets the state information of a gamepad.
+ ///
+ public GamePadInfo[] GamePads { get; private set; }
+
+ ///
+ /// Creates a new InputManager.
+ ///
+ /// The game this input manager belongs to.
+ public InputManager()
+ {
+ Keyboard = new KeyboardInfo();
+ Mouse = new MouseInfo();
+
+ GamePads = new GamePadInfo[4];
+ for (int i = 0; i < 4; i++)
+ {
+ GamePads[i] = new GamePadInfo((PlayerIndex)i);
+ }
+ }
+
+ ///
+ /// Updates the state information for the keyboard, mouse, and gamepad inputs.
+ ///
+ /// A snapshot of the timing values for the current frame.
+ public void Update(GameTime gameTime)
+ {
+ Keyboard.Update();
+ Mouse.Update();
+
+ for (int i = 0; i < 4; i++)
+ {
+ GamePads[i].Update(gameTime);
+ }
+ }
+}
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/MonoGameLibrary/Input/KeyboardInfo.cs b/Tutorials/MobileDeployment/04-AddAndroidProject/MonoGameLibrary/Input/KeyboardInfo.cs
new file mode 100644
index 00000000..c6770cb0
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/MonoGameLibrary/Input/KeyboardInfo.cs
@@ -0,0 +1,74 @@
+using Microsoft.Xna.Framework.Input;
+
+namespace MonoGameLibrary.Input;
+
+public class KeyboardInfo
+{
+ ///
+ /// Gets the state of keyboard input during the previous update cycle.
+ ///
+ public KeyboardState PreviousState { get; private set; }
+
+ ///
+ /// Gets the state of keyboard input during the current input cycle.
+ ///
+ public KeyboardState CurrentState { get; private set; }
+
+ ///
+ /// Creates a new KeyboardInfo
+ ///
+ public KeyboardInfo()
+ {
+ PreviousState = new KeyboardState();
+ CurrentState = Keyboard.GetState();
+ }
+
+ ///
+ /// Updates the state information about keyboard input.
+ ///
+ public void Update()
+ {
+ PreviousState = CurrentState;
+ CurrentState = Keyboard.GetState();
+ }
+
+ ///
+ /// Returns a value that indicates if the specified key is currently down.
+ ///
+ /// The key to check.
+ /// true if the specified key is currently down; otherwise, false.
+ public bool IsKeyDown(Keys key)
+ {
+ return CurrentState.IsKeyDown(key);
+ }
+
+ ///
+ /// Returns a value that indicates whether the specified key is currently up.
+ ///
+ /// The key to check.
+ /// true if the specified key is currently up; otherwise, false.
+ public bool IsKeyUp(Keys key)
+ {
+ return CurrentState.IsKeyUp(key);
+ }
+
+ ///
+ /// Returns a value that indicates if the specified key was just pressed on the current frame.
+ ///
+ /// The key to check.
+ /// true if the specified key was just pressed on the current frame; otherwise, false.
+ public bool WasKeyJustPressed(Keys key)
+ {
+ return CurrentState.IsKeyDown(key) && PreviousState.IsKeyUp(key);
+ }
+
+ ///
+ /// Returns a value that indicates if the specified key was just released on the current frame.
+ ///
+ /// The key to check.
+ /// true if the specified key was just released on the current frame; otherwise, false.
+ public bool WasKeyJustReleased(Keys key)
+ {
+ return CurrentState.IsKeyUp(key) && PreviousState.IsKeyDown(key);
+ }
+}
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/MonoGameLibrary/Input/MouseButton.cs b/Tutorials/MobileDeployment/04-AddAndroidProject/MonoGameLibrary/Input/MouseButton.cs
new file mode 100644
index 00000000..5b041f80
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/MonoGameLibrary/Input/MouseButton.cs
@@ -0,0 +1,10 @@
+namespace MonoGameLibrary.Input;
+
+public enum MouseButton
+{
+ Left,
+ Middle,
+ Right,
+ XButton1,
+ XButton2
+}
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/MonoGameLibrary/Input/MouseInfo.cs b/Tutorials/MobileDeployment/04-AddAndroidProject/MonoGameLibrary/Input/MouseInfo.cs
new file mode 100644
index 00000000..fd63e54b
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/MonoGameLibrary/Input/MouseInfo.cs
@@ -0,0 +1,208 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Input;
+
+namespace MonoGameLibrary.Input;
+
+public class MouseInfo
+{
+ ///
+ /// The state of mouse input during the previous update cycle.
+ ///
+ public MouseState PreviousState { get; private set; }
+
+ ///
+ /// The state of mouse input during the current update cycle.
+ ///
+ public MouseState CurrentState { get; private set; }
+
+ ///
+ /// Gets or Sets the current position of the mouse cursor in screen space.
+ ///
+ public Point Position
+ {
+ get => CurrentState.Position;
+ set => SetPosition(value.X, value.Y);
+ }
+
+ ///
+ /// Gets or Sets the current x-coordinate position of the mouse cursor in screen space.
+ ///
+ public int X
+ {
+ get => CurrentState.X;
+ set => SetPosition(value, CurrentState.Y);
+ }
+
+ ///
+ /// Gets or Sets the current y-coordinate position of the mouse cursor in screen space.
+ ///
+ public int Y
+ {
+ get => CurrentState.Y;
+ set => SetPosition(CurrentState.X, value);
+ }
+
+ ///
+ /// Gets the difference in the mouse cursor position between the previous and current frame.
+ ///
+ public Point PositionDelta => CurrentState.Position - PreviousState.Position;
+
+ ///
+ /// Gets the difference in the mouse cursor x-position between the previous and current frame.
+ ///
+ public int XDelta => CurrentState.X - PreviousState.X;
+
+ ///
+ /// Gets the difference in the mouse cursor y-position between the previous and current frame.
+ ///
+ public int YDelta => CurrentState.Y - PreviousState.Y;
+
+ ///
+ /// Gets a value that indicates if the mouse cursor moved between the previous and current frames.
+ ///
+ public bool WasMoved => PositionDelta != Point.Zero;
+
+ ///
+ /// Gets the cumulative value of the mouse scroll wheel since the start of the game.
+ ///
+ public int ScrollWheel => CurrentState.ScrollWheelValue;
+
+ ///
+ /// Gets the value of the scroll wheel between the previous and current frame.
+ ///
+ public int ScrollWheelDelta => CurrentState.ScrollWheelValue - PreviousState.ScrollWheelValue;
+
+ ///
+ /// Creates a new MouseInfo.
+ ///
+ public MouseInfo()
+ {
+ PreviousState = new MouseState();
+ CurrentState = Mouse.GetState();
+ }
+
+ ///
+ /// Updates the state information about mouse input.
+ ///
+ public void Update()
+ {
+ PreviousState = CurrentState;
+ CurrentState = Mouse.GetState();
+ }
+
+ ///
+ /// Returns a value that indicates whether the specified mouse button is currently down.
+ ///
+ /// The mouse button to check.
+ /// true if the specified mouse button is currently down; otherwise, false.
+ public bool IsButtonDown(MouseButton button)
+ {
+ switch (button)
+ {
+ case MouseButton.Left:
+ return CurrentState.LeftButton == ButtonState.Pressed;
+ case MouseButton.Middle:
+ return CurrentState.MiddleButton == ButtonState.Pressed;
+ case MouseButton.Right:
+ return CurrentState.RightButton == ButtonState.Pressed;
+ case MouseButton.XButton1:
+ return CurrentState.XButton1 == ButtonState.Pressed;
+ case MouseButton.XButton2:
+ return CurrentState.XButton2 == ButtonState.Pressed;
+ default:
+ return false;
+ }
+ }
+
+ ///
+ /// Returns a value that indicates whether the specified mouse button is current up.
+ ///
+ /// The mouse button to check.
+ /// true if the specified mouse button is currently up; otherwise, false.
+ public bool IsButtonUp(MouseButton button)
+ {
+ switch (button)
+ {
+ case MouseButton.Left:
+ return CurrentState.LeftButton == ButtonState.Released;
+ case MouseButton.Middle:
+ return CurrentState.MiddleButton == ButtonState.Released;
+ case MouseButton.Right:
+ return CurrentState.RightButton == ButtonState.Released;
+ case MouseButton.XButton1:
+ return CurrentState.XButton1 == ButtonState.Released;
+ case MouseButton.XButton2:
+ return CurrentState.XButton2 == ButtonState.Released;
+ default:
+ return false;
+ }
+ }
+
+ ///
+ /// Returns a value that indicates whether the specified mouse button was just pressed on the current frame.
+ ///
+ /// The mouse button to check.
+ /// true if the specified mouse button was just pressed on the current frame; otherwise, false.
+ public bool WasButtonJustPressed(MouseButton button)
+ {
+ switch (button)
+ {
+ case MouseButton.Left:
+ return CurrentState.LeftButton == ButtonState.Pressed && PreviousState.LeftButton == ButtonState.Released;
+ case MouseButton.Middle:
+ return CurrentState.MiddleButton == ButtonState.Pressed && PreviousState.MiddleButton == ButtonState.Released;
+ case MouseButton.Right:
+ return CurrentState.RightButton == ButtonState.Pressed && PreviousState.RightButton == ButtonState.Released;
+ case MouseButton.XButton1:
+ return CurrentState.XButton1 == ButtonState.Pressed && PreviousState.XButton1 == ButtonState.Released;
+ case MouseButton.XButton2:
+ return CurrentState.XButton2 == ButtonState.Pressed && PreviousState.XButton2 == ButtonState.Released;
+ default:
+ return false;
+ }
+ }
+
+ ///
+ /// Returns a value that indicates whether the specified mouse button was just released on the current frame.
+ ///
+ /// The mouse button to check.
+ /// true if the specified mouse button was just released on the current frame; otherwise, false.
+ public bool WasButtonJustReleased(MouseButton button)
+ {
+ switch (button)
+ {
+ case MouseButton.Left:
+ return CurrentState.LeftButton == ButtonState.Released && PreviousState.LeftButton == ButtonState.Pressed;
+ case MouseButton.Middle:
+ return CurrentState.MiddleButton == ButtonState.Released && PreviousState.MiddleButton == ButtonState.Pressed;
+ case MouseButton.Right:
+ return CurrentState.RightButton == ButtonState.Released && PreviousState.RightButton == ButtonState.Pressed;
+ case MouseButton.XButton1:
+ return CurrentState.XButton1 == ButtonState.Released && PreviousState.XButton1 == ButtonState.Pressed;
+ case MouseButton.XButton2:
+ return CurrentState.XButton2 == ButtonState.Released && PreviousState.XButton2 == ButtonState.Pressed;
+ default:
+ return false;
+ }
+ }
+
+ ///
+ /// Sets the current position of the mouse cursor in screen space and updates the CurrentState with the new position.
+ ///
+ /// The x-coordinate location of the mouse cursor in screen space.
+ /// The y-coordinate location of the mouse cursor in screen space.
+ public void SetPosition(int x, int y)
+ {
+ Mouse.SetPosition(x, y);
+ CurrentState = new MouseState(
+ x,
+ y,
+ CurrentState.ScrollWheelValue,
+ CurrentState.LeftButton,
+ CurrentState.MiddleButton,
+ CurrentState.RightButton,
+ CurrentState.XButton1,
+ CurrentState.XButton2
+ );
+ }
+}
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/MonoGameLibrary/MonoGameLibrary.csproj b/Tutorials/MobileDeployment/04-AddAndroidProject/MonoGameLibrary/MonoGameLibrary.csproj
new file mode 100644
index 00000000..fc6a3e6a
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/MonoGameLibrary/MonoGameLibrary.csproj
@@ -0,0 +1,11 @@
+
+
+ net9.0;net9.0-android
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/MonoGameLibrary/Scenes/Scene.cs b/Tutorials/MobileDeployment/04-AddAndroidProject/MonoGameLibrary/Scenes/Scene.cs
new file mode 100644
index 00000000..627d220f
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/MonoGameLibrary/Scenes/Scene.cs
@@ -0,0 +1,104 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content;
+
+namespace MonoGameLibrary.Scenes;
+
+public abstract class Scene : IDisposable
+{
+ ///
+ /// Gets the ContentManager used for loading scene-specific assets.
+ ///
+ ///
+ /// Assets loaded through this ContentManager will be automatically unloaded when this scene ends.
+ ///
+ protected ContentManager Content { get; }
+
+ ///
+ /// Gets a value that indicates if the scene has been disposed of.
+ ///
+ public bool IsDisposed { get; private set; }
+
+ ///
+ /// Creates a new scene instance.
+ ///
+ public Scene()
+ {
+ // Create a content manager for the scene
+ Content = new ContentManager(Core.Content.ServiceProvider);
+
+ // Set the root directory for content to the same as the root directory
+ // for the game's content.
+ Content.RootDirectory = Core.Content.RootDirectory;
+ }
+
+ // Finalizer, called when object is cleaned up by garbage collector.
+ ~Scene() => Dispose(false);
+
+ ///
+ /// Initializes the scene.
+ ///
+ ///
+ /// When overriding this in a derived class, ensure that base.Initialize()
+ /// still called as this is when LoadContent is called.
+ ///
+ public virtual void Initialize()
+ {
+ LoadContent();
+ }
+
+ ///
+ /// Override to provide logic to load content for the scene.
+ ///
+ public virtual void LoadContent() { }
+
+ ///
+ /// Unloads scene-specific content.
+ ///
+ public virtual void UnloadContent()
+ {
+ Content.Unload();
+ }
+
+ ///
+ /// Updates this scene.
+ ///
+ /// A snapshot of the timing values for the current frame.
+ public virtual void Update(GameTime gameTime) { }
+
+ ///
+ /// Draws this scene.
+ ///
+ /// A snapshot of the timing values for the current frame.
+ public virtual void Draw(GameTime gameTime) { }
+
+ ///
+ /// Disposes of this scene.
+ ///
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ /// Disposes of this scene.
+ ///
+ /// '
+ /// Indicates whether managed resources should be disposed. This value is only true when called from the main
+ /// Dispose method. When called from the finalizer, this will be false.
+ ///
+ protected virtual void Dispose(bool disposing)
+ {
+ if (IsDisposed)
+ {
+ return;
+ }
+
+ if (disposing)
+ {
+ UnloadContent();
+ Content.Dispose();
+ }
+ }
+}
diff --git a/Tutorials/MobileDeployment/04-AddAndroidProject/README.md b/Tutorials/MobileDeployment/04-AddAndroidProject/README.md
new file mode 100644
index 00000000..2424d897
--- /dev/null
+++ b/Tutorials/MobileDeployment/04-AddAndroidProject/README.md
@@ -0,0 +1,30 @@
+# Chapter 4: Add Android Project
+
+This chapter demonstrates how to convert a Windows-only MonoGame project to support Android platforms.
+
+The chapter covers:
+
+* Converting a single-platform project to multi-platform structure.
+* Creating platform-specific project shells for Windows, iOS, and Android.
+* Configuring conditional package references for each platform.
+* Understanding cross-platform project architecture and naming conventions.
+* Updating third-party libraries for cross-platform compatibility.
+
+## Project Structure
+
+This sample includes:
+
+* **DungeonSlime** - Windows desktop project shell
+* **DungeonSlime.Android** - Android mobile project shell
+
+## Prerequisites
+
+* Completed the MonoGame 2D tutorial
+* Development environment set up
+* For Android: Android SDK and development tools
+
+## Key Features Demonstrated
+
+* Multi-targeting framework configuration (`net8.0;net8.0-android`)
+* Platform-specific MonoGame package references
+* Modern .NET project management with Central Package Management
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/.config/dotnet-tools.json b/Tutorials/MobileDeployment/05-SharingLogic/.config/dotnet-tools.json
new file mode 100644
index 00000000..7f4505d0
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/.config/dotnet-tools.json
@@ -0,0 +1,36 @@
+{
+ "version": 1,
+ "isRoot": true,
+ "tools": {
+ "dotnet-mgcb": {
+ "version": "3.8.4.1",
+ "commands": [
+ "mgcb"
+ ]
+ },
+ "dotnet-mgcb-editor": {
+ "version": "3.8.4.1",
+ "commands": [
+ "mgcb-editor"
+ ]
+ },
+ "dotnet-mgcb-editor-linux": {
+ "version": "3.8.4.1",
+ "commands": [
+ "mgcb-editor-linux"
+ ]
+ },
+ "dotnet-mgcb-editor-windows": {
+ "version": "3.8.4.1",
+ "commands": [
+ "mgcb-editor-windows"
+ ]
+ },
+ "dotnet-mgcb-editor-mac": {
+ "version": "3.8.4.1",
+ "commands": [
+ "mgcb-editor-mac"
+ ]
+ }
+ }
+}
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/Directory.Packages.props b/Tutorials/MobileDeployment/05-SharingLogic/Directory.Packages.props
new file mode 100644
index 00000000..364a3dae
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/Directory.Packages.props
@@ -0,0 +1,12 @@
+
+
+ true
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/.config/dotnet-tools.json b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/.config/dotnet-tools.json
new file mode 100644
index 00000000..fbedee15
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/.config/dotnet-tools.json
@@ -0,0 +1,36 @@
+{
+ "version": 1,
+ "isRoot": true,
+ "tools": {
+ "dotnet-mgcb": {
+ "version": "3.8.4",
+ "commands": [
+ "mgcb"
+ ]
+ },
+ "dotnet-mgcb-editor": {
+ "version": "3.8.4",
+ "commands": [
+ "mgcb-editor"
+ ]
+ },
+ "dotnet-mgcb-editor-linux": {
+ "version": "3.8.4",
+ "commands": [
+ "mgcb-editor-linux"
+ ]
+ },
+ "dotnet-mgcb-editor-windows": {
+ "version": "3.8.4",
+ "commands": [
+ "mgcb-editor-windows"
+ ]
+ },
+ "dotnet-mgcb-editor-mac": {
+ "version": "3.8.4",
+ "commands": [
+ "mgcb-editor-mac"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/Activity1.cs b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/Activity1.cs
new file mode 100644
index 00000000..b0aafd05
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/Activity1.cs
@@ -0,0 +1,35 @@
+using Android.App;
+using Android.Content.PM;
+using Android.OS;
+using Android.Views;
+using Microsoft.Xna.Framework;
+
+namespace DungeonSlime.Android
+{
+ [Activity(
+ Label = "@string/app_name",
+ MainLauncher = true,
+ Icon = "@drawable/icon",
+ AlwaysRetainTaskState = true,
+ LaunchMode = LaunchMode.SingleInstance,
+ ScreenOrientation = ScreenOrientation.Landscape,
+ ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.Keyboard | ConfigChanges.KeyboardHidden |
+ ConfigChanges.ScreenSize
+ )]
+ public class Activity1 : AndroidGameActivity
+ {
+ private Game1 _game;
+ private View _view;
+
+ protected override void OnCreate(Bundle bundle)
+ {
+ base.OnCreate(bundle);
+
+ _game = new Game1();
+ _view = _game.Services.GetService(typeof(View)) as View;
+
+ SetContentView(_view);
+ _game.Run();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/AndroidManifest.xml b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/AndroidManifest.xml
new file mode 100644
index 00000000..9461d961
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/AndroidManifest.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/Content/Content.mgcb b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/Content/Content.mgcb
new file mode 100644
index 00000000..57d4a056
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/Content/Content.mgcb
@@ -0,0 +1,105 @@
+
+#----------------------------- Global Properties ----------------------------#
+
+/outputDir:bin/$(Platform)
+/intermediateDir:obj/$(Platform)
+/platform:Android
+/config:
+/profile:Reach
+/compress:False
+
+#-------------------------------- References --------------------------------#
+
+
+#---------------------------------- Content ---------------------------------#
+
+
+#begin audio/bounce.wav
+/importer:WavImporter
+/processor:SoundEffectProcessor
+/processorParam:Quality=Best
+/build:audio/bounce.wav
+
+#begin audio/collect.wav
+/importer:WavImporter
+/processor:SoundEffectProcessor
+/processorParam:Quality=Best
+/build:audio/collect.wav
+
+#begin audio/theme.ogg
+/importer:OggImporter
+/processor:SongProcessor
+/processorParam:Quality=Best
+/build:audio/theme.ogg
+
+#begin audio/ui.wav
+/importer:WavImporter
+/processor:SoundEffectProcessor
+/processorParam:Quality=Best
+/build:audio/ui.wav
+
+#begin effects/grayscaleEffect.fx
+/importer:EffectImporter
+/processor:EffectProcessor
+/processorParam:DebugMode=Auto
+/build:effects/grayscaleEffect.fx
+
+#begin fonts/04B_30_5x.spritefont
+/importer:FontDescriptionImporter
+/processor:FontDescriptionProcessor
+/processorParam:PremultiplyAlpha=True
+/processorParam:TextureFormat=Compressed
+/build:fonts/04B_30_5x.spritefont
+
+#begin fonts/04b_30.fnt
+/copy:fonts/04b_30.fnt
+
+#begin fonts/04B_30.spritefont
+/importer:FontDescriptionImporter
+/processor:FontDescriptionProcessor
+/processorParam:PremultiplyAlpha=True
+/processorParam:TextureFormat=Compressed
+/build:fonts/04B_30.spritefont
+
+#begin images/atlas-definition.xml
+/copy:images/atlas-definition.xml
+
+#begin images/atlas.png
+/importer:TextureImporter
+/processor:TextureProcessor
+/processorParam:ColorKeyColor=255,0,255,255
+/processorParam:ColorKeyEnabled=True
+/processorParam:GenerateMipmaps=False
+/processorParam:PremultiplyAlpha=True
+/processorParam:ResizeToPowerOfTwo=False
+/processorParam:MakeSquare=False
+/processorParam:TextureFormat=Color
+/build:images/atlas.png
+
+#begin images/background-pattern.png
+/importer:TextureImporter
+/processor:TextureProcessor
+/processorParam:ColorKeyColor=255,0,255,255
+/processorParam:ColorKeyEnabled=True
+/processorParam:GenerateMipmaps=False
+/processorParam:PremultiplyAlpha=True
+/processorParam:ResizeToPowerOfTwo=False
+/processorParam:MakeSquare=False
+/processorParam:TextureFormat=Color
+/build:images/background-pattern.png
+
+#begin images/logo.png
+/importer:TextureImporter
+/processor:TextureProcessor
+/processorParam:ColorKeyColor=255,0,255,255
+/processorParam:ColorKeyEnabled=True
+/processorParam:GenerateMipmaps=False
+/processorParam:PremultiplyAlpha=True
+/processorParam:ResizeToPowerOfTwo=False
+/processorParam:MakeSquare=False
+/processorParam:TextureFormat=Color
+/build:images/logo.png
+
+#begin images/tilemap-definition.xml
+/copy:images/tilemap-definition.xml
+
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/Content/audio/bounce.wav b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/Content/audio/bounce.wav
new file mode 100644
index 00000000..baa7a47b
Binary files /dev/null and b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/Content/audio/bounce.wav differ
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/Content/audio/collect.wav b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/Content/audio/collect.wav
new file mode 100644
index 00000000..506220de
Binary files /dev/null and b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/Content/audio/collect.wav differ
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/Content/audio/theme.ogg b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/Content/audio/theme.ogg
new file mode 100644
index 00000000..72e1fd3b
Binary files /dev/null and b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/Content/audio/theme.ogg differ
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/Content/audio/ui.wav b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/Content/audio/ui.wav
new file mode 100644
index 00000000..63e8941e
Binary files /dev/null and b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/Content/audio/ui.wav differ
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/Content/effects/grayscaleEffect.fx b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/Content/effects/grayscaleEffect.fx
new file mode 100644
index 00000000..5dd0d8b6
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/Content/effects/grayscaleEffect.fx
@@ -0,0 +1,53 @@
+#if OPENGL
+ #define SV_POSITION POSITION
+ #define VS_SHADERMODEL vs_3_0
+ #define PS_SHADERMODEL ps_3_0
+#else
+ #define VS_SHADERMODEL vs_4_0_level_9_1
+ #define PS_SHADERMODEL ps_4_0_level_9_1
+#endif
+
+Texture2D SpriteTexture;
+
+// A value between 0 and 1 that controls the intensity of the grayscale effect.
+// 0 = full color, 1 = full grayscale.
+float Saturation = 1.0;
+
+sampler2D SpriteTextureSampler = sampler_state
+{
+ Texture = ;
+};
+
+struct VertexShaderOutput
+{
+ float4 Position : SV_POSITION;
+ float4 Color : COLOR0;
+ float2 TextureCoordinates : TEXCOORD0;
+};
+
+float4 MainPS(VertexShaderOutput input) : COLOR
+{
+ // Sample the texture
+ float4 color = tex2D(SpriteTextureSampler, input.TextureCoordinates) * input.Color;
+
+ // Calculate the grayscale value based on human perception of colors
+ float grayscale = dot(color.rgb, float3(0.3, 0.59, 0.11));
+
+ // create a grayscale color vector (same value for R, G, and B)
+ float3 grayscaleColor = float3(grayscale, grayscale, grayscale);
+
+ // Linear interpolation between he grayscale color and the original color's
+ // rgb values based on the saturation parameter.
+ float3 finalColor = lerp(grayscale, color.rgb, Saturation);
+
+ // Return the final color with the original alpha value
+ return float4(finalColor, color.a);
+}
+
+technique SpriteDrawing
+{
+ pass P0
+ {
+ PixelShader = compile PS_SHADERMODEL MainPS();
+ }
+};
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/Content/fonts/04B_30.spritefont b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/Content/fonts/04B_30.spritefont
new file mode 100644
index 00000000..63d4728c
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/Content/fonts/04B_30.spritefont
@@ -0,0 +1,16 @@
+
+
+
+ 04B_30.ttf
+ 17.5
+ 0
+ true
+
+
+
+
+ ~
+
+
+
+
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/Content/fonts/04B_30.ttf b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/Content/fonts/04B_30.ttf
new file mode 100644
index 00000000..4b93740c
Binary files /dev/null and b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/Content/fonts/04B_30.ttf differ
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/Content/fonts/04B_30_5x.spritefont b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/Content/fonts/04B_30_5x.spritefont
new file mode 100644
index 00000000..dd239a53
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/Content/fonts/04B_30_5x.spritefont
@@ -0,0 +1,16 @@
+
+
+
+ 04B_30.ttf
+ 87.5
+ 0
+ true
+
+
+
+
+ ~
+
+
+
+
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/Content/fonts/04b_30.fnt b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/Content/fonts/04b_30.fnt
new file mode 100644
index 00000000..772f8c54
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/Content/fonts/04b_30.fnt
@@ -0,0 +1,99 @@
+info face="04b30" size=35 bold=0 italic=0 charset="" unicode=1 stretchH=100 smooth=0 aa=1 padding=0,0,0,0 spacing=1,1 outline=0
+common lineHeight=35 base=31 scaleW=256 scaleH=512 pages=1 packed=0 alphaChnl=0 redChnl=4 greenChnl=4 blueChnl=4
+page id=0 file="../images/atlas.png"
+chars count=95
+char id=32 x=30 y=152 width=3 height=1 xoffset=-1 yoffset=34 xadvance=29 page=0 chnl=15
+char id=33 x=240 y=30 width=14 height=29 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=34 x=102 y=232 width=25 height=15 xoffset=1 yoffset=4 xadvance=29 page=0 chnl=15
+char id=35 x=184 y=0 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=36 x=250 y=150 width=3 height=1 xoffset=-1 yoffset=34 xadvance=29 page=0 chnl=15
+char id=37 x=0 y=34 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=38 x=30 y=150 width=3 height=1 xoffset=-1 yoffset=34 xadvance=29 page=0 chnl=15
+char id=39 x=245 y=202 width=10 height=15 xoffset=1 yoffset=4 xadvance=14 page=0 chnl=15
+char id=40 x=106 y=150 width=16 height=29 xoffset=1 yoffset=2 xadvance=21 page=0 chnl=15
+char id=41 x=123 y=150 width=16 height=29 xoffset=1 yoffset=2 xadvance=21 page=0 chnl=15
+char id=42 x=128 y=232 width=14 height=15 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=43 x=94 y=0 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=44 x=143 y=232 width=10 height=14 xoffset=1 yoffset=19 xadvance=14 page=0 chnl=15
+char id=45 x=154 y=232 width=25 height=11 xoffset=1 yoffset=12 xadvance=29 page=0 chnl=15
+char id=46 x=231 y=228 width=10 height=10 xoffset=1 yoffset=19 xadvance=14 page=0 chnl=15
+char id=47 x=60 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=48 x=90 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=49 x=46 y=150 width=19 height=29 xoffset=1 yoffset=2 xadvance=23 page=0 chnl=15
+char id=50 x=150 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=51 x=180 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=52 x=210 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=53 x=0 y=94 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=54 x=180 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=55 x=60 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=56 x=90 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=57 x=120 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=58 x=234 y=202 width=10 height=25 xoffset=1 yoffset=4 xadvance=14 page=0 chnl=15
+char id=59 x=244 y=0 width=10 height=29 xoffset=1 yoffset=4 xadvance=14 page=0 chnl=15
+char id=60 x=86 y=150 width=19 height=29 xoffset=1 yoffset=2 xadvance=23 page=0 chnl=15
+char id=61 x=182 y=176 width=25 height=25 xoffset=1 yoffset=4 xadvance=29 page=0 chnl=15
+char id=62 x=237 y=120 width=18 height=29 xoffset=1 yoffset=2 xadvance=23 page=0 chnl=15
+char id=63 x=180 y=120 width=28 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=64 x=34 y=150 width=3 height=1 xoffset=-1 yoffset=34 xadvance=29 page=0 chnl=15
+char id=65 x=120 y=120 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=66 x=150 y=120 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=67 x=124 y=0 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=68 x=154 y=0 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=69 x=214 y=0 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=70 x=30 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=71 x=60 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=72 x=90 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=73 x=240 y=90 width=14 height=29 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=74 x=120 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=75 x=150 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=76 x=209 y=120 width=27 height=29 xoffset=1 yoffset=2 xadvance=31 page=0 chnl=15
+char id=77 x=30 y=0 width=31 height=29 xoffset=1 yoffset=2 xadvance=35 page=0 chnl=15
+char id=78 x=210 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=79 x=0 y=64 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=80 x=30 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=81 x=0 y=0 width=29 height=33 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=82 x=120 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=83 x=30 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=84 x=150 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=85 x=180 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=86 x=210 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=87 x=62 y=0 width=31 height=29 xoffset=1 yoffset=2 xadvance=35 page=0 chnl=15
+char id=88 x=0 y=124 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=89 x=30 y=120 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=90 x=60 y=120 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=91 x=240 y=60 width=14 height=29 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=92 x=90 y=120 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=93 x=140 y=150 width=14 height=29 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=94 x=180 y=232 width=16 height=11 xoffset=1 yoffset=4 xadvance=21 page=0 chnl=15
+char id=95 x=0 y=262 width=29 height=10 xoffset=1 yoffset=21 xadvance=33 page=0 chnl=15
+char id=96 x=197 y=228 width=16 height=11 xoffset=1 yoffset=4 xadvance=21 page=0 chnl=15
+char id=97 x=208 y=176 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=98 x=0 y=210 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=99 x=26 y=210 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=100 x=52 y=206 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=101 x=78 y=206 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=102 x=104 y=206 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=103 x=130 y=206 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=104 x=156 y=206 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=105 x=234 y=176 width=12 height=25 xoffset=1 yoffset=6 xadvance=16 page=0 chnl=15
+char id=106 x=182 y=202 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=107 x=208 y=202 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=108 x=78 y=232 width=23 height=25 xoffset=1 yoffset=6 xadvance=27 page=0 chnl=15
+char id=109 x=197 y=150 width=26 height=25 xoffset=1 yoffset=6 xadvance=31 page=0 chnl=15
+char id=110 x=0 y=236 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=111 x=26 y=236 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=112 x=78 y=180 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=113 x=0 y=154 width=25 height=29 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=114 x=52 y=232 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=115 x=224 y=150 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=116 x=0 y=184 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=117 x=26 y=184 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=118 x=52 y=180 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=119 x=170 y=150 width=26 height=25 xoffset=1 yoffset=6 xadvance=31 page=0 chnl=15
+char id=120 x=104 y=180 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=121 x=130 y=180 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=122 x=156 y=180 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=123 x=26 y=154 width=19 height=29 xoffset=1 yoffset=2 xadvance=23 page=0 chnl=15
+char id=124 x=155 y=150 width=14 height=29 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=125 x=66 y=150 width=19 height=29 xoffset=1 yoffset=2 xadvance=23 page=0 chnl=15
+char id=126 x=214 y=228 width=16 height=11 xoffset=1 yoffset=4 xadvance=21 page=0 chnl=15
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/Content/images/atlas-definition.xml b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/Content/images/atlas-definition.xml
new file mode 100644
index 00000000..21772022
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/Content/images/atlas-definition.xml
@@ -0,0 +1,34 @@
+
+
+ images/atlas
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/Content/images/atlas.png b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/Content/images/atlas.png
new file mode 100644
index 00000000..f7def20f
Binary files /dev/null and b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/Content/images/atlas.png differ
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/Content/images/background-pattern.png b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/Content/images/background-pattern.png
new file mode 100644
index 00000000..2d8d878e
Binary files /dev/null and b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/Content/images/background-pattern.png differ
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/Content/images/logo.png b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/Content/images/logo.png
new file mode 100644
index 00000000..1509036c
Binary files /dev/null and b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/Content/images/logo.png differ
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/Content/images/tilemap-definition.xml b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/Content/images/tilemap-definition.xml
new file mode 100644
index 00000000..85658c60
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/Content/images/tilemap-definition.xml
@@ -0,0 +1,15 @@
+
+
+ images/atlas
+
+ 00 01 02 01 02 01 02 01 02 01 02 01 02 01 02 03
+ 04 05 05 06 05 05 06 05 05 06 05 05 06 05 05 07
+ 08 09 09 09 09 09 09 09 09 09 09 09 09 09 09 11
+ 04 09 09 09 09 09 09 09 10 09 09 09 09 10 09 07
+ 08 09 10 09 09 09 09 09 09 09 09 09 09 09 09 11
+ 04 09 09 09 09 09 09 09 09 09 09 09 09 09 09 07
+ 08 10 09 09 09 09 09 09 09 09 10 09 09 09 09 11
+ 04 09 09 09 09 09 10 09 09 09 09 09 09 09 09 07
+ 12 13 14 13 14 13 14 13 14 13 14 13 14 13 14 15
+
+
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/DungeonSlime.Android.csproj b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/DungeonSlime.Android.csproj
new file mode 100644
index 00000000..02b73ecc
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/DungeonSlime.Android.csproj
@@ -0,0 +1,26 @@
+
+
+ net9.0-android
+ 23
+ Exe
+ com.monogame.dungeonslime
+ 1
+ 1.0
+
+
+ False
+
+
+ False
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/Resources/Drawable/Icon.png b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/Resources/Drawable/Icon.png
new file mode 100644
index 00000000..25fe0444
Binary files /dev/null and b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/Resources/Drawable/Icon.png differ
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/Resources/Values/Strings.xml b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/Resources/Values/Strings.xml
new file mode 100644
index 00000000..7a59ecad
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Android/Resources/Values/Strings.xml
@@ -0,0 +1,4 @@
+
+
+ DungeonSlime.Android
+
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Common/.config/dotnet-tools.json b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Common/.config/dotnet-tools.json
new file mode 100644
index 00000000..b345335f
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Common/.config/dotnet-tools.json
@@ -0,0 +1,30 @@
+{
+ "version": 1,
+ "isRoot": true,
+ "tools": {
+ "dotnet-mgcb-editor": {
+ "version": "3.8.4",
+ "commands": [
+ "mgcb-editor"
+ ]
+ },
+ "dotnet-mgcb-editor-linux": {
+ "version": "3.8.4",
+ "commands": [
+ "mgcb-editor-linux"
+ ]
+ },
+ "dotnet-mgcb-editor-windows": {
+ "version": "3.8.4",
+ "commands": [
+ "mgcb-editor-windows"
+ ]
+ },
+ "dotnet-mgcb-editor-mac": {
+ "version": "3.8.4",
+ "commands": [
+ "mgcb-editor-mac"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Common/DungeonSlime.Common.csproj b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Common/DungeonSlime.Common.csproj
new file mode 100644
index 00000000..338dff36
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Common/DungeonSlime.Common.csproj
@@ -0,0 +1,20 @@
+
+
+ net9.0;net9.0-ios;net9.0-android
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Common/Game1.cs b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Common/Game1.cs
new file mode 100644
index 00000000..86bfd3e1
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Common/Game1.cs
@@ -0,0 +1,70 @@
+using DungeonSlime.Scenes;
+using Microsoft.Xna.Framework.Media;
+using MonoGameLibrary;
+using MonoGameGum;
+using MonoGameGum.Forms.Controls;
+
+namespace DungeonSlime;
+
+public class Game1 : Core
+{
+ // The background theme song
+ private Song _themeSong;
+
+ public Game1() : base("Dungeon Slime", 1280, 720, false)
+ {
+ }
+
+ protected override void Initialize()
+ {
+ base.Initialize();
+
+ // Start playing the background music
+ Audio.PlaySong(_themeSong);
+
+ // Initialize the Gum UI service
+ InitializeGum();
+
+ // Start the game with the title scene.
+ ChangeScene(new TitleScene());
+ }
+
+ private void InitializeGum()
+ {
+ // Initialize the Gum service
+ GumService.Default.Initialize(this);
+
+ // Tell the Gum service which content manager to use. We will tell it to
+ // use the global content manager from our Core.
+ GumService.Default.ContentLoader.XnaContentManager = Core.Content;
+
+ // Register keyboard input for UI control.
+ FrameworkElement.KeyboardsForUiControl.Add(GumService.Default.Keyboard);
+
+ // Register gamepad input for Ui control.
+ FrameworkElement.GamePadsForUiControl.AddRange(GumService.Default.Gamepads);
+
+ // Customize the tab reverse UI navigation to also trigger when the keyboard
+ // Up arrow key is pushed.
+ FrameworkElement.TabReverseKeyCombos.Add(
+ new KeyCombo() { PushedKey = Microsoft.Xna.Framework.Input.Keys.Up });
+
+ // Customize the tab UI navigation to also trigger when the keyboard
+ // Down arrow key is pushed.
+ FrameworkElement.TabKeyCombos.Add(
+ new KeyCombo() { PushedKey = Microsoft.Xna.Framework.Input.Keys.Down });
+
+ // The assets created for the UI were done so at 1/4th the size to keep the size of the
+ // texture atlas small. So we will set the default canvas size to be 1/4th the size of
+ // the game's resolution then tell gum to zoom in by a factor of 4.
+ GumService.Default.CanvasWidth = GraphicsDevice.PresentationParameters.BackBufferWidth / 4.0f;
+ GumService.Default.CanvasHeight = GraphicsDevice.PresentationParameters.BackBufferHeight / 4.0f;
+ GumService.Default.Renderer.Camera.Zoom = 4.0f;
+ }
+
+ protected override void LoadContent()
+ {
+ // Load the background theme music
+ _themeSong = Content.Load("audio/theme");
+ }
+}
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Common/GameController.cs b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Common/GameController.cs
new file mode 100644
index 00000000..92165382
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Common/GameController.cs
@@ -0,0 +1,79 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Input;
+using MonoGameLibrary;
+using MonoGameLibrary.Input;
+
+namespace DungeonSlime;
+
+///
+/// Provides a game-specific input abstraction that maps physical inputs
+/// to game actions, bridging our input system with game-specific functionality.
+///
+public static class GameController
+{
+ private static KeyboardInfo s_keyboard => Core.Input.Keyboard;
+ private static GamePadInfo s_gamePad => Core.Input.GamePads[(int)PlayerIndex.One];
+
+ ///
+ /// Returns true if the player has triggered the "move up" action.
+ ///
+ public static bool MoveUp()
+ {
+ return s_keyboard.WasKeyJustPressed(Keys.Up) ||
+ s_keyboard.WasKeyJustPressed(Keys.W) ||
+ s_gamePad.WasButtonJustPressed(Buttons.DPadUp) ||
+ s_gamePad.WasButtonJustPressed(Buttons.LeftThumbstickUp);
+ }
+
+ ///
+ /// Returns true if the player has triggered the "move down" action.
+ ///
+ public static bool MoveDown()
+ {
+ return s_keyboard.WasKeyJustPressed(Keys.Down) ||
+ s_keyboard.WasKeyJustPressed(Keys.S) ||
+ s_gamePad.WasButtonJustPressed(Buttons.DPadDown) ||
+ s_gamePad.WasButtonJustPressed(Buttons.LeftThumbstickDown);
+ }
+
+ ///
+ /// Returns true if the player has triggered the "move left" action.
+ ///
+ public static bool MoveLeft()
+ {
+ return s_keyboard.WasKeyJustPressed(Keys.Left) ||
+ s_keyboard.WasKeyJustPressed(Keys.A) ||
+ s_gamePad.WasButtonJustPressed(Buttons.DPadLeft) ||
+ s_gamePad.WasButtonJustPressed(Buttons.LeftThumbstickLeft);
+ }
+
+ ///
+ /// Returns true if the player has triggered the "move right" action.
+ ///
+ public static bool MoveRight()
+ {
+ return s_keyboard.WasKeyJustPressed(Keys.Right) ||
+ s_keyboard.WasKeyJustPressed(Keys.D) ||
+ s_gamePad.WasButtonJustPressed(Buttons.DPadRight) ||
+ s_gamePad.WasButtonJustPressed(Buttons.LeftThumbstickRight);
+ }
+
+ ///
+ /// Returns true if the player has triggered the "pause" action.
+ ///
+ public static bool Pause()
+ {
+ return s_keyboard.WasKeyJustPressed(Keys.Escape) ||
+ s_gamePad.WasButtonJustPressed(Buttons.Start);
+ }
+
+ ///
+ /// Returns true if the player has triggered the "action" button,
+ /// typically used for menu confirmation.
+ ///
+ public static bool Action()
+ {
+ return s_keyboard.WasKeyJustPressed(Keys.Enter) ||
+ s_gamePad.WasButtonJustPressed(Buttons.A);
+ }
+}
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Common/GameObjects/Bat.cs b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Common/GameObjects/Bat.cs
new file mode 100644
index 00000000..ddc855ed
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Common/GameObjects/Bat.cs
@@ -0,0 +1,123 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Audio;
+using MonoGameLibrary;
+using MonoGameLibrary.Graphics;
+
+namespace DungeonSlime.GameObjects;
+
+public class Bat
+{
+ private const float MOVEMENT_SPEED = 5.0f;
+
+ // The velocity of the bat that defines the direction and how much in that
+ // direction to update the bats position each update cycle.
+ private Vector2 _velocity;
+
+ // The AnimatedSprite used when drawing the bat.
+ private AnimatedSprite _sprite;
+
+ // The sound effect to play when the bat bounces off the edge of the room.
+ private SoundEffect _bounceSoundEffect;
+
+ ///
+ /// Gets or Sets the position of the bat.
+ ///
+ public Vector2 Position { get; set; }
+
+ ///
+ /// Creates a new Bat using the specified animated sprite and sound effect.
+ ///
+ /// The AnimatedSprite ot use when drawing the bat.
+ /// The sound effect to play when the bat bounces off a wall.
+ public Bat(AnimatedSprite sprite, SoundEffect bounceSoundEffect)
+ {
+ _sprite = sprite;
+ _bounceSoundEffect = bounceSoundEffect;
+ }
+
+ ///
+ /// Randomizes the velocity of the bat.
+ ///
+ public void RandomizeVelocity()
+ {
+ // Generate a random angle
+ float angle = (float)(Random.Shared.NextDouble() * MathHelper.TwoPi);
+
+ // Convert the angle to a direction vector
+ float x = (float)Math.Cos(angle);
+ float y = (float)Math.Sin(angle);
+ Vector2 direction = new Vector2(x, y);
+
+ // Multiply the direction vector by the movement speed to get the
+ // final velocity
+ _velocity = direction * MOVEMENT_SPEED;
+ }
+
+ ///
+ /// Handles a bounce event when the bat collides with a wall or boundary.
+ ///
+ /// The normal vector of the surface the bat is bouncing against.
+ public void Bounce(Vector2 normal)
+ {
+ Vector2 newPosition = Position;
+
+ // Adjust the position based on the normal to prevent sticking to walls.
+ if (normal.X != 0)
+ {
+ // We are bouncing off a vertical wall (left/right).
+ // Move slightly away from the wall in the direction of the normal.
+ newPosition.X += normal.X * (_sprite.Width * 0.1f);
+ }
+
+ if (normal.Y != 0)
+ {
+ // We are bouncing off a horizontal wall (top/bottom).
+ // Move slightly way from the wall in the direction of the normal.
+ newPosition.Y += normal.Y * (_sprite.Height * 0.1f);
+ }
+
+ // Apply the new position
+ Position = newPosition;
+
+ // Apply reflection based on the normal.
+ _velocity = Vector2.Reflect(_velocity, normal);
+
+ // Play the bounce sound effect.
+ Core.Audio.PlaySoundEffect(_bounceSoundEffect);
+ }
+
+ ///
+ /// Returns a Circle value that represents collision bounds of the bat.
+ ///
+ /// A Circle value.
+ public Circle GetBounds()
+ {
+ int x = (int)(Position.X + _sprite.Width * 0.5f);
+ int y = (int)(Position.Y + _sprite.Height * 0.5f);
+ int radius = (int)(_sprite.Width * 0.25f);
+
+ return new Circle(x, y, radius);
+ }
+
+ ///
+ /// Updates the bat.
+ ///
+ /// A snapshot of the timing values for the current update cycle.
+ public void Update(GameTime gameTime)
+ {
+ // Update the animated sprite
+ _sprite.Update(gameTime);
+
+ // Update the position of the bat based on the velocity.
+ Position += _velocity;
+ }
+
+ ///
+ /// Draws the bat.
+ ///
+ public void Draw()
+ {
+ _sprite.Draw(Core.SpriteBatch, Position);
+ }
+}
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Common/GameObjects/Slime.cs b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Common/GameObjects/Slime.cs
new file mode 100644
index 00000000..08b5a63d
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Common/GameObjects/Slime.cs
@@ -0,0 +1,265 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Xna.Framework;
+using MonoGameLibrary;
+using MonoGameLibrary.Graphics;
+
+namespace DungeonSlime.GameObjects;
+
+public class Slime
+{
+ // A constant value that represents the amount of time to wait between
+ // movement updates.
+ private static readonly TimeSpan s_movementTime = TimeSpan.FromMilliseconds(200);
+
+ // The amount of time that has elapsed since the last movement update.
+ private TimeSpan _movementTimer;
+
+ // Normalized value (0-1) representing progress between movement ticks for visual interpolation
+ private float _movementProgress;
+
+ // The next direction to apply to the head of the slime chain during the
+ // next movement update.
+ private Vector2 _nextDirection;
+
+ // The number of pixels to move the head segment during the movement cycle.
+ private float _stride;
+
+ // Tracks the segments of the slime chain.
+ private List _segments;
+
+ // The AnimatedSprite used when drawing each slime segment
+ private AnimatedSprite _sprite;
+
+ // Buffer to queue inputs input by player during input polling.
+ private Queue _inputBuffer;
+
+ // The maximum size of the buffer queue.
+ private const int MAX_BUFFER_SIZE = 2;
+
+ ///
+ /// Event that is raised if it is detected that the head segment of the slime
+ /// has collided with a body segment.
+ ///
+ public event EventHandler BodyCollision;
+
+ ///
+ /// Creates a new Slime using the specified animated sprite.
+ ///
+ /// The AnimatedSprite to use when drawing the slime.
+ public Slime(AnimatedSprite sprite)
+ {
+ _sprite = sprite;
+ }
+
+ ///
+ /// Initializes the slime, can be used to reset it back to an initial state.
+ ///
+ /// The position the slime should start at.
+ /// The total number of pixels to move the head segment during each movement cycle.
+ public void Initialize(Vector2 startingPosition, float stride)
+ {
+ // Initialize the segment collection.
+ _segments = new List();
+
+ // Set the stride
+ _stride = stride;
+
+ // Create the initial head of the slime chain.
+ SlimeSegment head = new SlimeSegment();
+ head.At = startingPosition;
+ head.To = startingPosition + new Vector2(_stride, 0);
+ head.Direction = Vector2.UnitX;
+
+ // Add it to the segment collection.
+ _segments.Add(head);
+
+ // Set the initial next direction as the same direction the head is
+ // moving.
+ _nextDirection = head.Direction;
+
+ // Zero out the movement timer.
+ _movementTimer = TimeSpan.Zero;
+
+ // initialize the input buffer.
+ _inputBuffer = new Queue(MAX_BUFFER_SIZE);
+ }
+
+ private void HandleInput()
+ {
+ Vector2 potentialNextDirection = Vector2.Zero;
+
+ if (GameController.MoveUp())
+ {
+ potentialNextDirection = -Vector2.UnitY;
+ }
+ else if (GameController.MoveDown())
+ {
+ potentialNextDirection = Vector2.UnitY;
+ }
+ else if (GameController.MoveLeft())
+ {
+ potentialNextDirection = -Vector2.UnitX;
+ }
+ else if (GameController.MoveRight())
+ {
+ potentialNextDirection = Vector2.UnitX;
+ }
+
+ // If a new direction was input, consider adding it to the buffer
+ if (potentialNextDirection != Vector2.Zero && _inputBuffer.Count < MAX_BUFFER_SIZE)
+ {
+ // If the buffer is empty, validate against the current direction;
+ // otherwise, validate against the last buffered direction
+ Vector2 validateAgainst = _inputBuffer.Count > 0 ?
+ _inputBuffer.Last() :
+ _segments[0].Direction;
+
+ // Only allow direction change if it is not reversing the current
+ // direction. This prevents th slime from backing into itself
+ float dot = Vector2.Dot(potentialNextDirection, validateAgainst);
+ if (dot >= 0)
+ {
+ _inputBuffer.Enqueue(potentialNextDirection);
+ }
+ }
+ }
+
+ private void Move()
+ {
+ // Get the next direction from the input buffer if one is available
+ if (_inputBuffer.Count > 0)
+ {
+ _nextDirection = _inputBuffer.Dequeue();
+ }
+
+ // Capture the value of the head segment
+ SlimeSegment head = _segments[0];
+
+ // Update the direction the head is supposed to move in to the
+ // next direction cached.
+ head.Direction = _nextDirection;
+
+ // Update the head's "at" position to be where it was moving "to"
+ head.At = head.To;
+
+ // Update the head's "to" position to the next tile in the direction
+ // it is moving.
+ head.To = head.At + head.Direction * _stride;
+
+ // Insert the new adjusted value for the head at the front of the
+ // segments and remove the tail segment. This effectively moves
+ // the entire chain forward without needing to loop through every
+ // segment and update its "at" and "to" positions.
+ _segments.Insert(0, head);
+ _segments.RemoveAt(_segments.Count - 1);
+
+ // Iterate through all of the segments except the head and check
+ // if they are at the same position as the head. If they are, then
+ // the head is colliding with a body segment and a body collision
+ // has occurred.
+ for (int i = 1; i < _segments.Count; i++)
+ {
+ SlimeSegment segment = _segments[i];
+
+ if (head.At == segment.At)
+ {
+ if (BodyCollision != null)
+ {
+ BodyCollision.Invoke(this, EventArgs.Empty);
+ }
+
+ return;
+ }
+ }
+ }
+
+ ///
+ /// Informs the slime to grow by one segment.
+ ///
+ public void Grow()
+ {
+ // Capture the value of the tail segment
+ SlimeSegment tail = _segments[_segments.Count - 1];
+
+ // Create a new tail segment that is positioned a grid cell in the
+ // reverse direction from the tail moving to the tail.
+ SlimeSegment newTail = new SlimeSegment();
+ newTail.At = tail.To + tail.ReverseDirection * _stride;
+ newTail.To = tail.At;
+ newTail.Direction = Vector2.Normalize(tail.At - newTail.At);
+
+ // Add the new tail segment
+ _segments.Add(newTail);
+ }
+
+ ///
+ /// Updates the slime.
+ ///
+ /// A snapshot of the timing values for the current update cycle.
+ public void Update(GameTime gameTime)
+ {
+ // Update the animated sprite.
+ _sprite.Update(gameTime);
+
+ // Handle any player input
+ HandleInput();
+
+ // Increment the movement timer by the frame elapsed time.
+ _movementTimer += gameTime.ElapsedGameTime;
+
+ // If the movement timer has accumulated enough time to be greater than
+ // the movement time threshold, then perform a full movement.
+ if (_movementTimer >= s_movementTime)
+ {
+ _movementTimer -= s_movementTime;
+ Move();
+ }
+
+ // Update the movement lerp offset amount
+ _movementProgress = (float)(_movementTimer.TotalSeconds / s_movementTime.TotalSeconds);
+ }
+
+ ///
+ /// Draws the slime.
+ ///
+ public void Draw()
+ {
+ // Iterate through each segment and draw it
+ foreach (SlimeSegment segment in _segments)
+ {
+ // Calculate the visual position of the segment at the moment by
+ // lerping between its "at" and "to" position by the movement
+ // offset lerp amount
+ Vector2 pos = Vector2.Lerp(segment.At, segment.To, _movementProgress);
+
+ // Draw the slime sprite at the calculated visual position of this
+ // segment
+ _sprite.Draw(Core.SpriteBatch, pos);
+ }
+ }
+
+ ///
+ /// Returns a Circle value that represents collision bounds of the slime.
+ ///
+ /// A Circle value.
+ public Circle GetBounds()
+ {
+ SlimeSegment head = _segments[0];
+
+ // Calculate the visual position of the head at the moment of this
+ // method call by lerping between the "at" and "to" position by the
+ // movement offset lerp amount
+ Vector2 pos = Vector2.Lerp(head.At, head.To, _movementProgress);
+
+ // Create the bounds using the calculated visual position of the head.
+ Circle bounds = new Circle(
+ (int)(pos.X + (_sprite.Width * 0.5f)),
+ (int)(pos.Y + (_sprite.Height * 0.5f)),
+ (int)(_sprite.Width * 0.5f)
+ );
+
+ return bounds;
+ }
+}
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Common/GameObjects/SlimeSegment.cs b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Common/GameObjects/SlimeSegment.cs
new file mode 100644
index 00000000..b00189eb
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Common/GameObjects/SlimeSegment.cs
@@ -0,0 +1,26 @@
+using Microsoft.Xna.Framework;
+
+namespace DungeonSlime.GameObjects;
+
+public struct SlimeSegment
+{
+ ///
+ /// The position this slime segment is at before the movement cycle occurs.
+ ///
+ public Vector2 At;
+
+ ///
+ /// The position this slime segment should move to during the next movement cycle.
+ ///
+ public Vector2 To;
+
+ ///
+ /// The direction this slime segment is moving.
+ ///
+ public Vector2 Direction;
+
+ ///
+ /// The opposite direction this slime segment is moving.
+ ///
+ public Vector2 ReverseDirection => new Vector2(-Direction.X, -Direction.Y);
+}
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Common/Scenes/GameScene.cs b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Common/Scenes/GameScene.cs
new file mode 100644
index 00000000..d01a3b03
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Common/Scenes/GameScene.cs
@@ -0,0 +1,428 @@
+using System;
+using DungeonSlime.GameObjects;
+using DungeonSlime.UI;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Audio;
+using Microsoft.Xna.Framework.Graphics;
+using MonoGameGum;
+using MonoGameLibrary;
+using MonoGameLibrary.Graphics;
+using MonoGameLibrary.Scenes;
+
+namespace DungeonSlime.Scenes;
+
+public class GameScene : Scene
+{
+ private enum GameState
+ {
+ Playing,
+ Paused,
+ GameOver
+ }
+
+ // Reference to the slime.
+ private Slime _slime;
+
+ // Reference to the bat.
+ private Bat _bat;
+
+ // Defines the tilemap to draw.
+ private Tilemap _tilemap;
+
+ // Defines the bounds of the room that the slime and bat are contained within.
+ private Rectangle _roomBounds;
+
+ // The sound effect to play when the slime eats a bat.
+ private SoundEffect _collectSoundEffect;
+
+ // Tracks the players score.
+ private int _score;
+
+ private GameSceneUI _ui;
+
+ private GameState _state;
+
+ // The grayscale shader effect.
+ private Effect _grayscaleEffect;
+
+ // The amount of saturation to provide the grayscale shader effect
+ private float _saturation = 1.0f;
+
+ // The speed of the fade to grayscale effect.
+ private const float FADE_SPEED = 0.02f;
+
+ public override void Initialize()
+ {
+ // LoadContent is called during base.Initialize().
+ base.Initialize();
+
+ // During the game scene, we want to disable exit on escape. Instead,
+ // the escape key will be used to return back to the title screen
+ Core.ExitOnEscape = false;
+
+ // Create the room bounds by getting the bounds of the screen then
+ // using the Inflate method to "Deflate" the bounds by the width and
+ // height of a tile so that the bounds only covers the inside room of
+ // the dungeon tilemap.
+ _roomBounds = Core.GraphicsDevice.PresentationParameters.Bounds;
+ _roomBounds.Inflate(-_tilemap.TileWidth, -_tilemap.TileHeight);
+
+ // Subscribe to the slime's BodyCollision event so that a game over
+ // can be triggered when this event is raised.
+ _slime.BodyCollision += OnSlimeBodyCollision;
+
+ // Create any UI elements from the root element created in previous
+ // scenes
+ GumService.Default.Root.Children.Clear();
+
+ // Initialize the user interface for the game scene.
+ InitializeUI();
+
+ // Initialize a new game to be played.
+ InitializeNewGame();
+ }
+
+ private void InitializeUI()
+ {
+ // Clear out any previous UI element incase we came here
+ // from a different scene.
+ GumService.Default.Root.Children.Clear();
+
+ // Create the game scene ui instance.
+ _ui = new GameSceneUI();
+
+ // Subscribe to the events from the game scene ui.
+ _ui.ResumeButtonClick += OnResumeButtonClicked;
+ _ui.RetryButtonClick += OnRetryButtonClicked;
+ _ui.QuitButtonClick += OnQuitButtonClicked;
+ }
+
+ private void OnResumeButtonClicked(object sender, EventArgs args)
+ {
+ // Change the game state back to playing
+ _state = GameState.Playing;
+ }
+
+ private void OnRetryButtonClicked(object sender, EventArgs args)
+ {
+ // Player has chosen to retry, so initialize a new game
+ InitializeNewGame();
+ }
+
+ private void OnQuitButtonClicked(object sender, EventArgs args)
+ {
+ // Player has chosen to quit, so return back to the title scene
+ Core.ChangeScene(new TitleScene());
+ }
+
+ private void InitializeNewGame()
+ {
+ // Calculate the position for the slime, which will be at the center
+ // tile of the tile map.
+ Vector2 slimePos = new Vector2();
+ slimePos.X = (_tilemap.Columns / 2) * _tilemap.TileWidth;
+ slimePos.Y = (_tilemap.Rows / 2) * _tilemap.TileHeight;
+
+ // Initialize the slime
+ _slime.Initialize(slimePos, _tilemap.TileWidth);
+
+ // Initialize the bat
+ _bat.RandomizeVelocity();
+ PositionBatAwayFromSlime();
+
+ // Reset the score
+ _score = 0;
+
+ // Set the game state to playing
+ _state = GameState.Playing;
+ }
+
+ public override void LoadContent()
+ {
+ // Create the texture atlas from the XML configuration file
+ TextureAtlas atlas = TextureAtlas.FromFile(Core.Content, "images/atlas-definition.xml");
+
+ // Create the tilemap from the XML configuration file.
+ _tilemap = Tilemap.FromFile(Content, "images/tilemap-definition.xml");
+ _tilemap.Scale = new Vector2(4.0f, 4.0f);
+
+ // Create the animated sprite for the slime from the atlas.
+ AnimatedSprite slimeAnimation = atlas.CreateAnimatedSprite("slime-animation");
+ slimeAnimation.Scale = new Vector2(4.0f, 4.0f);
+
+ // Create the slime
+ _slime = new Slime(slimeAnimation);
+
+ // Create the animated sprite for the bat from the atlas.
+ AnimatedSprite batAnimation = atlas.CreateAnimatedSprite("bat-animation");
+ batAnimation.Scale = new Vector2(4.0f, 4.0f);
+
+ // Load the bounce sound effect for the bat
+ SoundEffect bounceSoundEffect = Content.Load("audio/bounce");
+
+ // Create the bat
+ _bat = new Bat(batAnimation, bounceSoundEffect);
+
+ // Load the collect sound effect
+ _collectSoundEffect = Content.Load("audio/collect");
+
+ // Load the grayscale effect
+ _grayscaleEffect = Content.Load("effects/grayscaleEffect");
+ }
+
+ public override void Update(GameTime gameTime)
+ {
+ // Ensure the UI is always updated
+ _ui.Update(gameTime);
+
+ if (_state != GameState.Playing)
+ {
+ // The game is in either a paused or game over state, so
+ // gradually decrease the saturation to create the fading grayscale.
+ _saturation = Math.Max(0.0f, _saturation - FADE_SPEED);
+
+ // If its just a game over state, return back
+ if (_state == GameState.GameOver)
+ {
+ return;
+ }
+ }
+
+ // If the pause button is pressed, toggle the pause state
+ if (GameController.Pause())
+ {
+ TogglePause();
+ }
+
+ // At this point, if the game is paused, just return back early
+ if (_state == GameState.Paused)
+ {
+ return;
+ }
+
+ // Update the slime;
+ _slime.Update(gameTime);
+
+ // Update the bat;
+ _bat.Update(gameTime);
+
+ // Perform collision checks
+ CollisionChecks();
+ }
+
+ private void CollisionChecks()
+ {
+ // Capture the current bounds of the slime and bat
+ Circle slimeBounds = _slime.GetBounds();
+ Circle batBounds = _bat.GetBounds();
+
+ // FIrst perform a collision check to see if the slime is colliding with
+ // the bat, which means the slime eats the bat.
+ if (slimeBounds.Intersects(batBounds))
+ {
+ // Move the bat to a new position away from the slime.
+ PositionBatAwayFromSlime();
+
+ // Randomize the velocity of the bat.
+ _bat.RandomizeVelocity();
+
+ // Tell the slime to grow.
+ _slime.Grow();
+
+ // Increment the score.
+ _score += 100;
+
+ // Update the score display on the UI.
+ _ui.UpdateScoreText(_score);
+
+ // Play the collect sound effect
+ Core.Audio.PlaySoundEffect(_collectSoundEffect);
+ }
+
+ // Next check if the slime is colliding with the wall by validating if
+ // it is within the bounds of the room. If it is outside the room
+ // bounds, then it collided with a wall which triggers a game over.
+ if (slimeBounds.Top < _roomBounds.Top ||
+ slimeBounds.Bottom > _roomBounds.Bottom ||
+ slimeBounds.Left < _roomBounds.Left ||
+ slimeBounds.Right > _roomBounds.Right)
+ {
+ GameOver();
+ return;
+ }
+
+ // Finally, check if the bat is colliding with a wall by validating if
+ // it is within the bounds of the room. If it is outside the room
+ // bounds, then it collided with a wall, and the bat should bounce
+ // off of that wall.
+ if (batBounds.Top < _roomBounds.Top)
+ {
+ _bat.Bounce(Vector2.UnitY);
+ }
+ else if (batBounds.Bottom > _roomBounds.Bottom)
+ {
+ _bat.Bounce(-Vector2.UnitY);
+ }
+
+ if (batBounds.Left < _roomBounds.Left)
+ {
+ _bat.Bounce(Vector2.UnitX);
+ }
+ else if (batBounds.Right > _roomBounds.Right)
+ {
+ _bat.Bounce(-Vector2.UnitX);
+ }
+ }
+
+ private void PositionBatAwayFromSlime()
+ {
+ // Calculate the position that is in the center of the bounds
+ // of the room.
+ float roomCenterX = _roomBounds.X + _roomBounds.Width * 0.5f;
+ float roomCenterY = _roomBounds.Y + _roomBounds.Height * 0.5f;
+ Vector2 roomCenter = new Vector2(roomCenterX, roomCenterY);
+
+ // Get the bounds of the slime and calculate the center position
+ Circle slimeBounds = _slime.GetBounds();
+ Vector2 slimeCenter = new Vector2(slimeBounds.X, slimeBounds.Y);
+
+ // Calculate the distance vector from the center of the room to the
+ // center of the slime.
+ Vector2 centerToSlime = slimeCenter - roomCenter;
+
+ // Get the bounds of the bat
+ Circle batBounds = _bat.GetBounds();
+
+ // Calculate the amount of padding we will add to the new position of
+ // the bat to ensure it is not sticking to walls
+ int padding = batBounds.Radius * 2;
+
+ // Calculate the new position of the bat by finding which component of
+ // the center to slime vector (X or Y) is larger and in which direction.
+ Vector2 newBatPosition = Vector2.Zero;
+ if (Math.Abs(centerToSlime.X) > Math.Abs(centerToSlime.Y))
+ {
+ // The slime is closer to either the left or right wall, so the Y
+ // position will be a random position between the top and bottom
+ // walls.
+ newBatPosition.Y = Random.Shared.Next(
+ _roomBounds.Top + padding,
+ _roomBounds.Bottom - padding
+ );
+
+ if (centerToSlime.X > 0)
+ {
+ // The slime is closer to the right side wall, so place the
+ // bat on the left side wall
+ newBatPosition.X = _roomBounds.Left + padding;
+ }
+ else
+ {
+ // The slime is closer ot the left side wall, so place the
+ // bat on the right side wall.
+ newBatPosition.X = _roomBounds.Right - padding * 2;
+ }
+ }
+ else
+ {
+ // The slime is closer to either the top or bottom wall, so the X
+ // position will be a random position between the left and right
+ // walls.
+ newBatPosition.X = Random.Shared.Next(
+ _roomBounds.Left + padding,
+ _roomBounds.Right - padding
+ );
+
+ if (centerToSlime.Y > 0)
+ {
+ // The slime is closer to the top wall, so place the bat on the
+ // bottom wall
+ newBatPosition.Y = _roomBounds.Top + padding;
+ }
+ else
+ {
+ // The slime is closer to the bottom wall, so place the bat on
+ // the top wall.
+ newBatPosition.Y = _roomBounds.Bottom - padding * 2;
+ }
+ }
+
+ // Assign the new bat position
+ _bat.Position = newBatPosition;
+ }
+
+ private void OnSlimeBodyCollision(object sender, EventArgs args)
+ {
+ GameOver();
+ }
+
+ private void TogglePause()
+ {
+ if (_state == GameState.Paused)
+ {
+ // We're now unpausing the game, so hide the pause panel
+ _ui.HidePausePanel();
+
+ // And set the state back to playing
+ _state = GameState.Playing;
+ }
+ else
+ {
+ // We're now pausing the game, so show the pause panel
+ _ui.ShowPausePanel();
+
+ // And set the state to paused
+ _state = GameState.Paused;
+
+ // Set the grayscale effect saturation to 1.0f;
+ _saturation = 1.0f;
+ }
+ }
+
+ private void GameOver()
+ {
+ // Show the game over panel
+ _ui.ShowGameOverPanel();
+
+ // Set the game state to game over
+ _state = GameState.GameOver;
+
+ // Set the grayscale effect saturation to 1.0f;
+ _saturation = 1.0f;
+ }
+
+ public override void Draw(GameTime gameTime)
+ {
+ // Clear the back buffer.
+ Core.GraphicsDevice.Clear(Color.CornflowerBlue);
+
+ if (_state != GameState.Playing)
+ {
+ // We are in a game over state, so apply the saturation parameter.
+ _grayscaleEffect.Parameters["Saturation"].SetValue(_saturation);
+
+ // And begin the sprite batch using the grayscale effect.
+ Core.SpriteBatch.Begin(samplerState: SamplerState.PointClamp, effect: _grayscaleEffect);
+ }
+ else
+ {
+ // Otherwise, just begin the sprite batch as normal.
+ Core.SpriteBatch.Begin(samplerState: SamplerState.PointClamp);
+ }
+
+ // Draw the tilemap
+ _tilemap.Draw(Core.SpriteBatch);
+
+ // Draw the slime.
+ _slime.Draw();
+
+ // Draw the bat.
+ _bat.Draw();
+
+ // Always end the sprite batch when finished.
+ Core.SpriteBatch.End();
+
+ // Draw the UI
+ _ui.Draw();
+ }
+}
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Common/Scenes/TitleScene.cs b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Common/Scenes/TitleScene.cs
new file mode 100644
index 00000000..725bc3e8
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Common/Scenes/TitleScene.cs
@@ -0,0 +1,345 @@
+using System;
+using DungeonSlime.UI;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Audio;
+using Microsoft.Xna.Framework.Graphics;
+using Microsoft.Xna.Framework.Input;
+using MonoGameGum;
+using MonoGameGum.Forms.Controls;
+using MonoGameGum.GueDeriving;
+using MonoGameLibrary;
+using MonoGameLibrary.Graphics;
+using MonoGameLibrary.Scenes;
+
+namespace DungeonSlime.Scenes;
+
+public class TitleScene : Scene
+{
+ private const string DUNGEON_TEXT = "Dungeon";
+ private const string SLIME_TEXT = "Slime";
+ private const string PRESS_ENTER_TEXT = "Press Enter To Start";
+
+ // The font to use to render normal text.
+ private SpriteFont _font;
+
+ // The font used to render the title text.
+ private SpriteFont _font5x;
+
+ // The position to draw the dungeon text at.
+ private Vector2 _dungeonTextPos;
+
+ // The origin to set for the dungeon text.
+ private Vector2 _dungeonTextOrigin;
+
+ // The position to draw the slime text at.
+ private Vector2 _slimeTextPos;
+
+ // The origin to set for the slime text.
+ private Vector2 _slimeTextOrigin;
+
+ // The position to draw the press enter text at.
+ private Vector2 _pressEnterPos;
+
+ // The origin to set for the press enter text when drawing it.
+ private Vector2 _pressEnterOrigin;
+
+ // The texture used for the background pattern.
+ private Texture2D _backgroundPattern;
+
+ // The destination rectangle for the background pattern to fill.
+ private Rectangle _backgroundDestination;
+
+ // The offset to apply when drawing the background pattern so it appears to
+ // be scrolling.
+ private Vector2 _backgroundOffset;
+
+ // The speed that the background pattern scrolls.
+ private float _scrollSpeed = 50.0f;
+
+ private SoundEffect _uiSoundEffect;
+ private Panel _titleScreenButtonsPanel;
+ private Panel _optionsPanel;
+
+ // The options button used to open the options menu.
+ private AnimatedButton _optionsButton;
+
+ // The back button used to exit the options menu back to the title menu.
+ private AnimatedButton _optionsBackButton;
+
+ // Reference to the texture atlas that we can pass to UI elements when they
+ // are created.
+ private TextureAtlas _atlas;
+
+ public override void Initialize()
+ {
+ // LoadContent is called during base.Initialize().
+ base.Initialize();
+
+ // While on the title screen, we can enable exit on escape so the player
+ // can close the game by pressing the escape key.
+ Core.ExitOnEscape = true;
+
+ // Set the position and origin for the Dungeon text.
+ Vector2 size = _font5x.MeasureString(DUNGEON_TEXT);
+ _dungeonTextPos = new Vector2(640, 100);
+ _dungeonTextOrigin = size * 0.5f;
+
+ // Set the position and origin for the Slime text.
+ size = _font5x.MeasureString(SLIME_TEXT);
+ _slimeTextPos = new Vector2(757, 207);
+ _slimeTextOrigin = size * 0.5f;
+
+ // Set the position and origin for the press enter text.
+ size = _font.MeasureString(PRESS_ENTER_TEXT);
+ _pressEnterPos = new Vector2(640, 620);
+ _pressEnterOrigin = size * 0.5f;
+
+ // Initialize the offset of the background pattern at zero
+ _backgroundOffset = Vector2.Zero;
+
+ // Set the background pattern destination rectangle to fill the entire
+ // screen background
+ _backgroundDestination = Core.GraphicsDevice.PresentationParameters.Bounds;
+
+ InitializeUI();
+ }
+
+ public override void LoadContent()
+ {
+ // Load the font for the standard text.
+ _font = Core.Content.Load("fonts/04B_30");
+
+ // Load the font for the title text
+ _font5x = Content.Load("fonts/04B_30_5x");
+
+ // Load the background pattern texture.
+ _backgroundPattern = Content.Load("images/background-pattern");
+
+ // Load the sound effect to play when ui actions occur.
+ _uiSoundEffect = Core.Content.Load("audio/ui");
+
+ // Load the texture atlas from the xml configuration file.
+ _atlas = TextureAtlas.FromFile(Core.Content, "images/atlas-definition.xml");
+ }
+
+ private void CreateTitlePanel()
+ {
+ // Create a container to hold all of our buttons
+ _titleScreenButtonsPanel = new Panel();
+ _titleScreenButtonsPanel.Dock(Gum.Wireframe.Dock.Fill);
+ _titleScreenButtonsPanel.AddToRoot();
+
+ AnimatedButton startButton = new AnimatedButton(_atlas);
+ startButton.Anchor(Gum.Wireframe.Anchor.BottomLeft);
+ startButton.Visual.X = 50;
+ startButton.Visual.Y = -12;
+ startButton.Text = "Start";
+ startButton.Click += HandleStartClicked;
+ _titleScreenButtonsPanel.AddChild(startButton);
+
+ _optionsButton = new AnimatedButton(_atlas);
+ _optionsButton.Anchor(Gum.Wireframe.Anchor.BottomRight);
+ _optionsButton.Visual.X = -50;
+ _optionsButton.Visual.Y = -12;
+ _optionsButton.Text = "Options";
+ _optionsButton.Click += HandleOptionsClicked;
+ _titleScreenButtonsPanel.AddChild(_optionsButton);
+
+ startButton.IsFocused = true;
+ }
+
+ private void HandleStartClicked(object sender, EventArgs e)
+ {
+ // A UI interaction occurred, play the sound effect
+ Core.Audio.PlaySoundEffect(_uiSoundEffect);
+
+ // Change to the game scene to start the game.
+ Core.ChangeScene(new GameScene());
+ }
+
+ private void HandleOptionsClicked(object sender, EventArgs e)
+ {
+ // A UI interaction occurred, play the sound effect
+ Core.Audio.PlaySoundEffect(_uiSoundEffect);
+
+ // Set the title panel to be invisible.
+ _titleScreenButtonsPanel.IsVisible = false;
+
+ // Set the options panel to be visible.
+ _optionsPanel.IsVisible = true;
+
+ // Give the back button on the options panel focus.
+ _optionsBackButton.IsFocused = true;
+ }
+
+ private void CreateOptionsPanel()
+ {
+ _optionsPanel = new Panel();
+ _optionsPanel.Dock(Gum.Wireframe.Dock.Fill);
+ _optionsPanel.IsVisible = false;
+ _optionsPanel.AddToRoot();
+
+ TextRuntime optionsText = new TextRuntime();
+ optionsText.X = 10;
+ optionsText.Y = 10;
+ optionsText.Text = "OPTIONS";
+ optionsText.UseCustomFont = true;
+ optionsText.FontScale = 0.5f;
+ optionsText.CustomFontFile = @"fonts/04b_30.fnt";
+ _optionsPanel.AddChild(optionsText);
+
+ OptionsSlider musicSlider = new OptionsSlider(_atlas);
+ musicSlider.Name = "MusicSlider";
+ musicSlider.Text = "MUSIC";
+ musicSlider.Anchor(Gum.Wireframe.Anchor.Top);
+ musicSlider.Visual.Y = 30f;
+ musicSlider.Minimum = 0;
+ musicSlider.Maximum = 1;
+ musicSlider.Value = Core.Audio.SongVolume;
+ musicSlider.SmallChange = .1;
+ musicSlider.LargeChange = .2;
+ musicSlider.ValueChanged += HandleMusicSliderValueChanged;
+ musicSlider.ValueChangeCompleted += HandleMusicSliderValueChangeCompleted;
+ _optionsPanel.AddChild(musicSlider);
+
+ OptionsSlider sfxSlider = new OptionsSlider(_atlas);
+ sfxSlider.Name = "SfxSlider";
+ sfxSlider.Text = "SFX";
+ sfxSlider.Anchor(Gum.Wireframe.Anchor.Top);
+ sfxSlider.Visual.Y = 93;
+ sfxSlider.Minimum = 0;
+ sfxSlider.Maximum = 1;
+ sfxSlider.Value = Core.Audio.SoundEffectVolume;
+ sfxSlider.SmallChange = .1;
+ sfxSlider.LargeChange = .2;
+ sfxSlider.ValueChanged += HandleSfxSliderChanged;
+ sfxSlider.ValueChangeCompleted += HandleSfxSliderChangeCompleted;
+ _optionsPanel.AddChild(sfxSlider);
+
+ _optionsBackButton = new AnimatedButton(_atlas);
+ _optionsBackButton.Text = "BACK";
+ _optionsBackButton.Anchor(Gum.Wireframe.Anchor.BottomRight);
+ _optionsBackButton.X = -28f;
+ _optionsBackButton.Y = -10f;
+ _optionsBackButton.Click += HandleOptionsButtonBack;
+ _optionsPanel.AddChild(_optionsBackButton);
+ }
+
+ private void HandleSfxSliderChanged(object sender, EventArgs args)
+ {
+ // Intentionally not playing the UI sound effect here so that it is not
+ // constantly triggered as the user adjusts the slider's thumb on the
+ // track.
+
+ // Get a reference to the sender as a Slider.
+ var slider = (Slider)sender;
+
+ // Set the global sound effect volume to the value of the slider.;
+ Core.Audio.SoundEffectVolume = (float)slider.Value;
+ }
+
+ private void HandleSfxSliderChangeCompleted(object sender, EventArgs e)
+ {
+ // Play the UI Sound effect so the player can hear the difference in audio.
+ Core.Audio.PlaySoundEffect(_uiSoundEffect);
+ }
+
+ private void HandleMusicSliderValueChanged(object sender, EventArgs args)
+ {
+ // Intentionally not playing the UI sound effect here so that it is not
+ // constantly triggered as the user adjusts the slider's thumb on the
+ // track.
+
+ // Get a reference to the sender as a Slider.
+ var slider = (Slider)sender;
+
+ // Set the global song volume to the value of the slider.
+ Core.Audio.SongVolume = (float)slider.Value;
+ }
+
+ private void HandleMusicSliderValueChangeCompleted(object sender, EventArgs args)
+ {
+ // A UI interaction occurred, play the sound effect
+ Core.Audio.PlaySoundEffect(_uiSoundEffect);
+ }
+
+ private void HandleOptionsButtonBack(object sender, EventArgs e)
+ {
+ // A UI interaction occurred, play the sound effect
+ Core.Audio.PlaySoundEffect(_uiSoundEffect);
+
+ // Set the title panel to be visible.
+ _titleScreenButtonsPanel.IsVisible = true;
+
+ // Set the options panel to be invisible.
+ _optionsPanel.IsVisible = false;
+
+ // Give the options button on the title panel focus since we are coming
+ // back from the options screen.
+ _optionsButton.IsFocused = true;
+ }
+
+ private void InitializeUI()
+ {
+ // Clear out any previous UI in case we came here from
+ // a different screen:
+ GumService.Default.Root.Children.Clear();
+
+ CreateTitlePanel();
+ CreateOptionsPanel();
+ }
+
+ public override void Update(GameTime gameTime)
+ {
+ // Update the offsets for the background pattern wrapping so that it
+ // scrolls down and to the right.
+ float offset = _scrollSpeed * (float)gameTime.ElapsedGameTime.TotalSeconds;
+ _backgroundOffset.X -= offset;
+ _backgroundOffset.Y -= offset;
+
+ // Ensure that the offsets do not go beyond the texture bounds so it is
+ // a seamless wrap
+ _backgroundOffset.X %= _backgroundPattern.Width;
+ _backgroundOffset.Y %= _backgroundPattern.Height;
+
+ GumService.Default.Update(gameTime);
+ }
+
+ public override void Draw(GameTime gameTime)
+ {
+ Core.GraphicsDevice.Clear(new Color(32, 40, 78, 255));
+
+ // Draw the background pattern first using the PointWrap sampler state.
+ Core.SpriteBatch.Begin(samplerState: SamplerState.PointWrap);
+ Core.SpriteBatch.Draw(_backgroundPattern, _backgroundDestination, new Rectangle(_backgroundOffset.ToPoint(), _backgroundDestination.Size), Color.White * 0.5f);
+ Core.SpriteBatch.End();
+
+ if (_titleScreenButtonsPanel.IsVisible)
+ {
+ // Begin the sprite batch to prepare for rendering.
+ Core.SpriteBatch.Begin(samplerState: SamplerState.PointClamp);
+
+ // The color to use for the drop shadow text.
+ Color dropShadowColor = Color.Black * 0.5f;
+
+ // Draw the Dungeon text slightly offset from it is original position and
+ // with a transparent color to give it a drop shadow
+ Core.SpriteBatch.DrawString(_font5x, DUNGEON_TEXT, _dungeonTextPos + new Vector2(10, 10), dropShadowColor, 0.0f, _dungeonTextOrigin, 1.0f, SpriteEffects.None, 1.0f);
+
+ // Draw the Dungeon text on top of that at its original position
+ Core.SpriteBatch.DrawString(_font5x, DUNGEON_TEXT, _dungeonTextPos, Color.White, 0.0f, _dungeonTextOrigin, 1.0f, SpriteEffects.None, 1.0f);
+
+ // Draw the Slime text slightly offset from it is original position and
+ // with a transparent color to give it a drop shadow
+ Core.SpriteBatch.DrawString(_font5x, SLIME_TEXT, _slimeTextPos + new Vector2(10, 10), dropShadowColor, 0.0f, _slimeTextOrigin, 1.0f, SpriteEffects.None, 1.0f);
+
+ // Draw the Slime text on top of that at its original position
+ Core.SpriteBatch.DrawString(_font5x, SLIME_TEXT, _slimeTextPos, Color.White, 0.0f, _slimeTextOrigin, 1.0f, SpriteEffects.None, 1.0f);
+
+ // Always end the sprite batch when finished.
+ Core.SpriteBatch.End();
+ }
+
+ GumService.Default.Draw();
+ }
+}
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Common/UI/AnimatedButton.cs b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Common/UI/AnimatedButton.cs
new file mode 100644
index 00000000..56ba8cfc
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Common/UI/AnimatedButton.cs
@@ -0,0 +1,163 @@
+using System;
+using Gum.DataTypes;
+using Gum.DataTypes.Variables;
+using Gum.Forms.Controls;
+using Gum.Forms.DefaultVisuals;
+using Gum.Graphics.Animation;
+using Gum.Managers;
+using Microsoft.Xna.Framework.Input;
+using MonoGameGum.GueDeriving;
+using MonoGameLibrary.Graphics;
+
+namespace DungeonSlime.UI;
+
+///
+/// A custom button implementation that inherits from Gum's Button class to provide
+/// animated visual feedback when focused.
+///
+internal class AnimatedButton : Button
+{
+ ///
+ /// Creates a new AnimatedButton instance using graphics from the specified texture atlas.
+ ///
+ /// The texture atlas containing button graphics and animations
+ public AnimatedButton(TextureAtlas atlas)
+ {
+ // Each Forms conrol has a general Visual property that
+ // has properties shared by all control types. This Visual
+ // type matches the Forms type. It can be casted to access
+ // controls-specific properties.
+ ButtonVisual buttonVisual = (ButtonVisual)Visual;
+ // Width is relative to children with extra padding, height is fixed
+ buttonVisual.Height = 14f;
+ buttonVisual.HeightUnits = DimensionUnitType.Absolute;
+ buttonVisual.Width = 21f;
+ buttonVisual.WidthUnits = DimensionUnitType.RelativeToChildren;
+
+ // Get a reference to the nine-slice background to display the button graphics
+ // A nine-slice allows the button to stretch while preserving corner appearance
+ NineSliceRuntime background = buttonVisual.Background;
+ background.Texture = atlas.Texture;
+ background.TextureAddress = TextureAddress.Custom;
+ background.Color = Microsoft.Xna.Framework.Color.White;
+ // texture coordinates for the background are set down below
+
+ TextRuntime textInstance = buttonVisual.TextInstance;
+ textInstance.Text = "START";
+ textInstance.Blue = 130;
+ textInstance.Green = 86;
+ textInstance.Red = 70;
+ textInstance.UseCustomFont = true;
+ textInstance.CustomFontFile = "fonts/04b_30.fnt";
+ textInstance.FontScale = 0.25f;
+ textInstance.Anchor(Gum.Wireframe.Anchor.Center);
+ textInstance.Width = 0;
+ textInstance.WidthUnits = DimensionUnitType.RelativeToChildren;
+
+ // Get the texture region for the unfocused button state from the atlas
+ TextureRegion unfocusedTextureRegion = atlas.GetRegion("unfocused-button");
+
+ // Create an animation chain for the unfocused state with a single frame
+ AnimationChain unfocusedAnimation = new AnimationChain();
+ unfocusedAnimation.Name = nameof(unfocusedAnimation);
+ AnimationFrame unfocusedFrame = new AnimationFrame
+ {
+ TopCoordinate = unfocusedTextureRegion.TopTextureCoordinate,
+ BottomCoordinate = unfocusedTextureRegion.BottomTextureCoordinate,
+ LeftCoordinate = unfocusedTextureRegion.LeftTextureCoordinate,
+ RightCoordinate = unfocusedTextureRegion.RightTextureCoordinate,
+ FrameLength = 0.3f,
+ Texture = unfocusedTextureRegion.Texture
+ };
+ unfocusedAnimation.Add(unfocusedFrame);
+
+ // Get the multi-frame animation for the focused button state from the atlas
+ Animation focusedAtlasAnimation = atlas.GetAnimation("focused-button-animation");
+
+ // Create an animation chain for the focused state using all frames from the atlas animation
+ AnimationChain focusedAnimation = new AnimationChain();
+ focusedAnimation.Name = nameof(focusedAnimation);
+ foreach (TextureRegion region in focusedAtlasAnimation.Frames)
+ {
+ AnimationFrame frame = new AnimationFrame
+ {
+ TopCoordinate = region.TopTextureCoordinate,
+ BottomCoordinate = region.BottomTextureCoordinate,
+ LeftCoordinate = region.LeftTextureCoordinate,
+ RightCoordinate = region.RightTextureCoordinate,
+ FrameLength = (float)focusedAtlasAnimation.Delay.TotalSeconds,
+ Texture = region.Texture
+ };
+
+ focusedAnimation.Add(frame);
+ }
+
+ // Assign both animation chains to the nine-slice background
+ background.AnimationChains = new AnimationChainList
+ {
+ unfocusedAnimation,
+ focusedAnimation
+ };
+
+
+ // Reset all state to default so we don't have unexpected variable assignments:
+ buttonVisual.ButtonCategory.ResetAllStates();
+
+ // Get the enabled (default/unfocused) state
+ StateSave enabledState = buttonVisual.States.Enabled;
+ enabledState.Apply = () =>
+ {
+ // When enabled but not focused, use the unfocused animation
+ background.CurrentChainName = unfocusedAnimation.Name;
+ };
+
+ // Create the focused state
+ StateSave focusedState = buttonVisual.States.Focused;
+ focusedState.Apply = () =>
+ {
+ // When focused, use the focused animation and enable animation playback
+ background.CurrentChainName = focusedAnimation.Name;
+ background.Animate = true;
+ };
+
+ // Create the highlighted+focused state (for mouse hover while focused)
+ StateSave highlightedFocused = buttonVisual.States.HighlightedFocused;
+ highlightedFocused.Apply = focusedState.Apply;
+
+ // Create the highlighted state (for mouse hover)
+ // by cloning the enabled state since they appear the same
+ StateSave highlighted = buttonVisual.States.Highlighted;
+ highlighted.Apply = enabledState.Apply;
+
+ // Add event handlers for keyboard input.
+ KeyDown += HandleKeyDown;
+
+ // Add event handler for mouse hover focus.
+ buttonVisual.RollOn += HandleRollOn;
+ }
+
+ ///
+ /// Handles keyboard input for navigation between buttons using left/right keys.
+ ///
+ private void HandleKeyDown(object sender, KeyEventArgs e)
+ {
+ if (e.Key == Keys.Left)
+ {
+ // Left arrow navigates to previous control
+ HandleTab(TabDirection.Up, loop: true);
+ }
+ if (e.Key == Keys.Right)
+ {
+ // Right arrow navigates to next control
+ HandleTab(TabDirection.Down, loop: true);
+ }
+ }
+
+ ///
+ /// Automatically focuses the button when the mouse hovers over it.
+ ///
+ private void HandleRollOn(object sender, EventArgs e)
+ {
+ IsFocused = true;
+ }
+}
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Common/UI/GameSceneUI.cs b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Common/UI/GameSceneUI.cs
new file mode 100644
index 00000000..9d14e27a
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Common/UI/GameSceneUI.cs
@@ -0,0 +1,340 @@
+using System;
+using Gum.DataTypes;
+using Gum.Managers;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Audio;
+using Microsoft.Xna.Framework.Content;
+using MonoGameGum;
+using MonoGameGum.Forms.Controls;
+using MonoGameGum.GueDeriving;
+using MonoGameLibrary;
+using MonoGameLibrary.Graphics;
+
+namespace DungeonSlime.UI;
+
+public class GameSceneUI : ContainerRuntime
+{
+ // The string format to use when updating the text for the score display.
+ private static readonly string s_scoreFormat = "SCORE: {0:D6}";
+
+ // The sound effect to play for auditory feedback of the user interface.
+ private SoundEffect _uiSoundEffect;
+
+ // The pause panel
+ private Panel _pausePanel;
+
+ // The resume button on the pause panel. Field is used to track reference so
+ // focus can be set when the pause panel is shown.
+ private AnimatedButton _resumeButton;
+
+ // The game over panel.
+ private Panel _gameOverPanel;
+
+ // The retry button on the game over panel. Field is used to track reference
+ // so focus can be set when the game over panel is shown.
+ private AnimatedButton _retryButton;
+
+ // The text runtime used to display the players score on the game screen.
+ private TextRuntime _scoreText;
+
+ ///
+ /// Event invoked when the Resume button on the Pause panel is clicked.
+ ///
+ public event EventHandler ResumeButtonClick;
+
+ ///
+ /// Event invoked when the Quit button on either the Pause panel or the
+ /// Game Over panel is clicked.
+ ///
+ public event EventHandler QuitButtonClick;
+
+ ///
+ /// Event invoked when the Retry button on the Game Over panel is clicked.
+ ///
+ public event EventHandler RetryButtonClick;
+
+ public GameSceneUI()
+ {
+ // The game scene UI inherits from ContainerRuntime, so we set its
+ // doc to fill so it fills the entire screen.
+ Dock(Gum.Wireframe.Dock.Fill);
+
+ // Add it to the root element.
+ this.AddToRoot();
+
+ // Get a reference to the content manager that was registered with the
+ // GumService when it was original initialized.
+ ContentManager content = GumService.Default.ContentLoader.XnaContentManager;
+
+ // Use that content manager to load the sound effect and atlas for the
+ // user interface elements
+ _uiSoundEffect = content.Load("audio/ui");
+ TextureAtlas atlas = TextureAtlas.FromFile(content, "images/atlas-definition.xml");
+
+ // Create the text that will display the players score and add it as
+ // a child to this container.
+ _scoreText = CreateScoreText();
+ AddChild(_scoreText);
+
+ // Create the Pause panel that is displayed when the game is paused and
+ // add it as a child to this container
+ _pausePanel = CreatePausePanel(atlas);
+ AddChild(_pausePanel.Visual);
+
+ // Create the Game Over panel that is displayed when a game over occurs
+ // and add it as a child to this container
+ _gameOverPanel = CreateGameOverPanel(atlas);
+ AddChild(_gameOverPanel.Visual);
+ }
+
+ private TextRuntime CreateScoreText()
+ {
+ TextRuntime text = new TextRuntime();
+ text.Anchor(Gum.Wireframe.Anchor.TopLeft);
+ text.WidthUnits = DimensionUnitType.RelativeToChildren;
+ text.X = 20.0f;
+ text.Y = 5.0f;
+ text.UseCustomFont = true;
+ text.CustomFontFile = @"fonts/04b_30.fnt";
+ text.FontScale = 0.25f;
+ text.Text = string.Format(s_scoreFormat, 0);
+
+ return text;
+ }
+
+ private Panel CreatePausePanel(TextureAtlas atlas)
+ {
+ Panel panel = new Panel();
+ panel.Anchor(Gum.Wireframe.Anchor.Center);
+ panel.Visual.WidthUnits = DimensionUnitType.Absolute;
+ panel.Visual.HeightUnits = DimensionUnitType.Absolute;
+ panel.Visual.Width = 264.0f;
+ panel.Visual.Height = 70.0f;
+ panel.IsVisible = false;
+
+ TextureRegion backgroundRegion = atlas.GetRegion("panel-background");
+
+ NineSliceRuntime background = new NineSliceRuntime();
+ background.Dock(Gum.Wireframe.Dock.Fill);
+ background.Texture = backgroundRegion.Texture;
+ background.TextureAddress = TextureAddress.Custom;
+ background.TextureHeight = backgroundRegion.Height;
+ background.TextureWidth = backgroundRegion.Width;
+ background.TextureTop = backgroundRegion.SourceRectangle.Top;
+ background.TextureLeft = backgroundRegion.SourceRectangle.Left;
+ panel.AddChild(background);
+
+ TextRuntime text = new TextRuntime();
+ text.Text = "PAUSED";
+ text.UseCustomFont = true;
+ text.CustomFontFile = "fonts/04b_30.fnt";
+ text.FontScale = 0.5f;
+ text.X = 10.0f;
+ text.Y = 10.0f;
+ panel.AddChild(text);
+
+ _resumeButton = new AnimatedButton(atlas);
+ _resumeButton.Text = "RESUME";
+ _resumeButton.Anchor(Gum.Wireframe.Anchor.BottomLeft);
+ _resumeButton.Visual.X = 9.0f;
+ _resumeButton.Visual.Y = -9.0f;
+
+ _resumeButton.Click += OnResumeButtonClicked;
+ _resumeButton.GotFocus += OnElementGotFocus;
+
+ panel.AddChild(_resumeButton);
+
+ AnimatedButton quitButton = new AnimatedButton(atlas);
+ quitButton.Text = "QUIT";
+ quitButton.Anchor(Gum.Wireframe.Anchor.BottomRight);
+ quitButton.Visual.X = -9.0f;
+ quitButton.Visual.Y = -9.0f;
+
+ quitButton.Click += OnQuitButtonClicked;
+ quitButton.GotFocus += OnElementGotFocus;
+
+ panel.AddChild(quitButton);
+
+ return panel;
+ }
+
+ private Panel CreateGameOverPanel(TextureAtlas atlas)
+ {
+ Panel panel = new Panel();
+ panel.Anchor(Gum.Wireframe.Anchor.Center);
+ panel.Visual.WidthUnits = DimensionUnitType.Absolute;
+ panel.Visual.HeightUnits = DimensionUnitType.Absolute;
+ panel.Visual.Width = 264.0f;
+ panel.Visual.Height = 70.0f;
+ panel.IsVisible = false;
+
+ TextureRegion backgroundRegion = atlas.GetRegion("panel-background");
+
+ NineSliceRuntime background = new NineSliceRuntime();
+ background.Dock(Gum.Wireframe.Dock.Fill);
+ background.Texture = backgroundRegion.Texture;
+ background.TextureAddress = TextureAddress.Custom;
+ background.TextureHeight = backgroundRegion.Height;
+ background.TextureWidth = backgroundRegion.Width;
+ background.TextureTop = backgroundRegion.SourceRectangle.Top;
+ background.TextureLeft = backgroundRegion.SourceRectangle.Left;
+ panel.AddChild(background);
+
+ TextRuntime text = new TextRuntime();
+ text.Text = "GAME OVER";
+ text.WidthUnits = DimensionUnitType.RelativeToChildren;
+ text.UseCustomFont = true;
+ text.CustomFontFile = "fonts/04b_30.fnt";
+ text.FontScale = 0.5f;
+ text.X = 10.0f;
+ text.Y = 10.0f;
+ panel.AddChild(text);
+
+ _retryButton = new AnimatedButton(atlas);
+ _retryButton.Text = "RETRY";
+ _retryButton.Anchor(Gum.Wireframe.Anchor.BottomLeft);
+ _retryButton.Visual.X = 9.0f;
+ _retryButton.Visual.Y = -9.0f;
+
+ _retryButton.Click += OnRetryButtonClicked;
+ _retryButton.GotFocus += OnElementGotFocus;
+
+ panel.AddChild(_retryButton);
+
+ AnimatedButton quitButton = new AnimatedButton(atlas);
+ quitButton.Text = "QUIT";
+ quitButton.Anchor(Gum.Wireframe.Anchor.BottomRight);
+ quitButton.Visual.X = -9.0f;
+ quitButton.Visual.Y = -9.0f;
+
+ quitButton.Click += OnQuitButtonClicked;
+ quitButton.GotFocus += OnElementGotFocus;
+
+ panel.AddChild(quitButton);
+
+ return panel;
+ }
+
+ private void OnResumeButtonClicked(object sender, EventArgs args)
+ {
+ // Button was clicked, play the ui sound effect for auditory feedback.
+ Core.Audio.PlaySoundEffect(_uiSoundEffect);
+
+ // Since the resume button was clicked, we need to hide the pause panel.
+ HidePausePanel();
+
+ // Invoke the ResumeButtonClick event
+ if (ResumeButtonClick != null)
+ {
+ ResumeButtonClick(sender, args);
+ }
+ }
+
+ private void OnRetryButtonClicked(object sender, EventArgs args)
+ {
+ // Button was clicked, play the ui sound effect for auditory feedback.
+ Core.Audio.PlaySoundEffect(_uiSoundEffect);
+
+ // Since the retry button was clicked, we need to hide the game over panel.
+ HideGameOverPanel();
+
+ // Invoke the RetryButtonClick event.
+ if (RetryButtonClick != null)
+ {
+ RetryButtonClick(sender, args);
+ }
+ }
+
+ private void OnQuitButtonClicked(object sender, EventArgs args)
+ {
+ // Button was clicked, play the ui sound effect for auditory feedback.
+ Core.Audio.PlaySoundEffect(_uiSoundEffect);
+
+ // Both panels have a quit button, so hide both panels
+ HidePausePanel();
+ HideGameOverPanel();
+
+ // Invoke the QuitButtonClick event.
+ if (QuitButtonClick != null)
+ {
+ QuitButtonClick(sender, args);
+ }
+ }
+
+ private void OnElementGotFocus(object sender, EventArgs args)
+ {
+ // A ui element that can receive focus has received focus, play the
+ // ui sound effect for auditory feedback.
+ Core.Audio.PlaySoundEffect(_uiSoundEffect);
+ }
+
+ ///
+ /// Updates the text on the score display.
+ ///
+ /// The score to display.
+ public void UpdateScoreText(int score)
+ {
+ _scoreText.Text = string.Format(s_scoreFormat, score);
+ }
+
+ ///
+ /// Tells the game scene ui to show the pause panel.
+ ///
+ public void ShowPausePanel()
+ {
+ _pausePanel.IsVisible = true;
+
+ // Give the resume button focus for keyboard/gamepad input.
+ _resumeButton.IsFocused = true;
+
+ // Ensure the game over panel isn't visible.
+ _gameOverPanel.IsVisible = false;
+ }
+
+ ///
+ /// Tells the game scene ui to hide the pause panel.
+ ///
+ public void HidePausePanel()
+ {
+ _pausePanel.IsVisible = false;
+ }
+
+ ///
+ /// Tells the game scene ui to show the game over panel.
+ ///
+ public void ShowGameOverPanel()
+ {
+ _gameOverPanel.IsVisible = true;
+
+ // Give the retry button focus for keyboard/gamepad input.
+ _retryButton.IsFocused = true;
+
+ // Ensure the pause panel isn't visible.
+ _pausePanel.IsVisible = false;
+ }
+
+ ///
+ /// Tells the game scene ui to hide the game over panel.
+ ///
+ public void HideGameOverPanel()
+ {
+ _gameOverPanel.IsVisible = false;
+ }
+
+ ///
+ /// Updates the game scene ui.
+ ///
+ /// A snapshot of the timing values for the current update cycle.
+ public void Update(GameTime gameTime)
+ {
+ GumService.Default.Update(gameTime);
+ }
+
+ ///
+ /// Draws the game scene ui.
+ ///
+ public void Draw()
+ {
+ GumService.Default.Draw();
+ }
+}
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Common/UI/OptionsSlider.cs b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Common/UI/OptionsSlider.cs
new file mode 100644
index 00000000..a70eaafb
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.Common/UI/OptionsSlider.cs
@@ -0,0 +1,253 @@
+using System;
+using Gum.DataTypes;
+using Gum.DataTypes.Variables;
+using Gum.Managers;
+using Microsoft.Xna.Framework;
+using MonoGameGum.Forms.Controls;
+using MonoGameGum.GueDeriving;
+using MonoGameLibrary.Graphics;
+
+namespace DungeonSlime.UI;
+
+///
+/// A custom slider control that inherits from Gum's Slider class.
+///
+public class OptionsSlider : Slider
+{
+ // Reference to the text label that displays the slider's title
+ private TextRuntime _textInstance;
+
+ // Reference to the rectangle that visually represents the current value
+ private ColoredRectangleRuntime _fillRectangle;
+
+ ///
+ /// Gets or sets the text label for this slider.
+ ///
+ public string Text
+ {
+ get => _textInstance.Text;
+ set => _textInstance.Text = value;
+ }
+
+ ///
+ /// Creates a new OptionsSlider instance using graphics from the specified texture atlas.
+ ///
+ /// The texture atlas containing slider graphics.
+ public OptionsSlider(TextureAtlas atlas)
+ {
+ // Create the top-level container for all visual elements
+ ContainerRuntime topLevelContainer = new ContainerRuntime();
+ topLevelContainer.Height = 55f;
+ topLevelContainer.Width = 264f;
+
+ TextureRegion backgroundRegion = atlas.GetRegion("panel-background");
+
+ // Create the background panel that contains everything
+ NineSliceRuntime background = new NineSliceRuntime();
+ background.Texture = atlas.Texture;
+ background.TextureAddress = TextureAddress.Custom;
+ background.TextureHeight = backgroundRegion.Height;
+ background.TextureLeft = backgroundRegion.SourceRectangle.Left;
+ background.TextureTop = backgroundRegion.SourceRectangle.Top;
+ background.TextureWidth = backgroundRegion.Width;
+ background.Dock(Gum.Wireframe.Dock.Fill);
+ topLevelContainer.AddChild(background);
+
+ // Create the title text element
+ _textInstance = new TextRuntime();
+ _textInstance.CustomFontFile = @"fonts/04b_30.fnt";
+ _textInstance.UseCustomFont = true;
+ _textInstance.FontScale = 0.5f;
+ _textInstance.Text = "Replace Me";
+ _textInstance.X = 10f;
+ _textInstance.Y = 10f;
+ _textInstance.WidthUnits = DimensionUnitType.RelativeToChildren;
+ topLevelContainer.AddChild(_textInstance);
+
+ // Create the container for the slider track and decorative elements
+ ContainerRuntime innerContainer = new ContainerRuntime();
+ innerContainer.Height = 13f;
+ innerContainer.Width = 241f;
+ innerContainer.X = 10f;
+ innerContainer.Y = 33f;
+ topLevelContainer.AddChild(innerContainer);
+
+ TextureRegion offBackgroundRegion = atlas.GetRegion("slider-off-background");
+
+ // Create the "OFF" side of the slider (left end)
+ NineSliceRuntime offBackground = new NineSliceRuntime();
+ offBackground.Dock(Gum.Wireframe.Dock.Left);
+ offBackground.Texture = atlas.Texture;
+ offBackground.TextureAddress = TextureAddress.Custom;
+ offBackground.TextureHeight = offBackgroundRegion.Height;
+ offBackground.TextureLeft = offBackgroundRegion.SourceRectangle.Left;
+ offBackground.TextureTop = offBackgroundRegion.SourceRectangle.Top;
+ offBackground.TextureWidth = offBackgroundRegion.Width;
+ offBackground.Width = 28f;
+ offBackground.WidthUnits = DimensionUnitType.Absolute;
+ offBackground.Dock(Gum.Wireframe.Dock.Left);
+ innerContainer.AddChild(offBackground);
+
+ TextureRegion middleBackgroundRegion = atlas.GetRegion("slider-middle-background");
+
+ // Create the middle track portion of the slider
+ NineSliceRuntime middleBackground = new NineSliceRuntime();
+ middleBackground.Dock(Gum.Wireframe.Dock.FillVertically);
+ middleBackground.Texture = middleBackgroundRegion.Texture;
+ middleBackground.TextureAddress = TextureAddress.Custom;
+ middleBackground.TextureHeight = middleBackgroundRegion.Height;
+ middleBackground.TextureLeft = middleBackgroundRegion.SourceRectangle.Left;
+ middleBackground.TextureTop = middleBackgroundRegion.SourceRectangle.Top;
+ middleBackground.TextureWidth = middleBackgroundRegion.Width;
+ middleBackground.Width = 179f;
+ middleBackground.WidthUnits = DimensionUnitType.Absolute;
+ middleBackground.Dock(Gum.Wireframe.Dock.Left);
+ middleBackground.X = 27f;
+ innerContainer.AddChild(middleBackground);
+
+ TextureRegion maxBackgroundRegion = atlas.GetRegion("slider-max-background");
+
+ // Create the "MAX" side of the slider (right end)
+ NineSliceRuntime maxBackground = new NineSliceRuntime();
+ maxBackground.Texture = maxBackgroundRegion.Texture;
+ maxBackground.TextureAddress = TextureAddress.Custom;
+ maxBackground.TextureHeight = maxBackgroundRegion.Height;
+ maxBackground.TextureLeft = maxBackgroundRegion.SourceRectangle.Left;
+ maxBackground.TextureTop = maxBackgroundRegion.SourceRectangle.Top;
+ maxBackground.TextureWidth = maxBackgroundRegion.Width;
+ maxBackground.Width = 36f;
+ maxBackground.WidthUnits = DimensionUnitType.Absolute;
+ maxBackground.Dock(Gum.Wireframe.Dock.Right);
+ innerContainer.AddChild(maxBackground);
+
+ // Create the interactive track that responds to clicks
+ // The special name "TrackInstance" is required for Slider functionality
+ ContainerRuntime trackInstance = new ContainerRuntime();
+ trackInstance.Name = "TrackInstance";
+ trackInstance.Dock(Gum.Wireframe.Dock.Fill);
+ trackInstance.Height = -2f;
+ trackInstance.Width = -2f;
+ middleBackground.AddChild(trackInstance);
+
+ // Create the fill rectangle that visually displays the current value
+ _fillRectangle = new ColoredRectangleRuntime();
+ _fillRectangle.Dock(Gum.Wireframe.Dock.Left);
+ _fillRectangle.Width = 90f; // Default to 90% - will be updated by value changes
+ _fillRectangle.WidthUnits = DimensionUnitType.PercentageOfParent;
+ trackInstance.AddChild(_fillRectangle);
+
+ // Add "OFF" text to the left end
+ TextRuntime offText = new TextRuntime();
+ offText.Red = 70;
+ offText.Green = 86;
+ offText.Blue = 130;
+ offText.CustomFontFile = @"fonts/04b_30.fnt";
+ offText.FontScale = 0.25f;
+ offText.UseCustomFont = true;
+ offText.Text = "OFF";
+ offText.Anchor(Gum.Wireframe.Anchor.Center);
+ offBackground.AddChild(offText);
+
+ // Add "MAX" text to the right end
+ TextRuntime maxText = new TextRuntime();
+ maxText.Red = 70;
+ maxText.Green = 86;
+ maxText.Blue = 130;
+ maxText.CustomFontFile = @"fonts/04b_30.fnt";
+ maxText.FontScale = 0.25f;
+ maxText.UseCustomFont = true;
+ maxText.Text = "MAX";
+ maxText.Anchor(Gum.Wireframe.Anchor.Center);
+ maxBackground.AddChild(maxText);
+
+ // Define colors for focused and unfocused states
+ Color focusedColor = Color.White;
+ Color unfocusedColor = Color.Gray;
+
+ // Create slider state category - Slider.SliderCategoryName is the required name
+ StateSaveCategory sliderCategory = new StateSaveCategory();
+ sliderCategory.Name = Slider.SliderCategoryName;
+ topLevelContainer.AddCategory(sliderCategory);
+
+ // Create the enabled (default/unfocused) state
+ StateSave enabled = new StateSave();
+ enabled.Name = FrameworkElement.EnabledStateName;
+ enabled.Apply = () =>
+ {
+ // When enabled but not focused, use gray coloring for all elements
+ background.Color = unfocusedColor;
+ _textInstance.Color = unfocusedColor;
+ offBackground.Color = unfocusedColor;
+ middleBackground.Color = unfocusedColor;
+ maxBackground.Color = unfocusedColor;
+ _fillRectangle.Color = unfocusedColor;
+ };
+ sliderCategory.States.Add(enabled);
+
+ // Create the focused state
+ StateSave focused = new StateSave();
+ focused.Name = FrameworkElement.FocusedStateName;
+ focused.Apply = () =>
+ {
+ // When focused, use white coloring for all elements
+ background.Color = focusedColor;
+ _textInstance.Color = focusedColor;
+ offBackground.Color = focusedColor;
+ middleBackground.Color = focusedColor;
+ maxBackground.Color = focusedColor;
+ _fillRectangle.Color = focusedColor;
+ };
+ sliderCategory.States.Add(focused);
+
+ // Create the highlighted+focused state by cloning the focused state
+ StateSave highlightedFocused = focused.Clone();
+ highlightedFocused.Name = FrameworkElement.HighlightedFocusedStateName;
+ sliderCategory.States.Add(highlightedFocused);
+
+ // Create the highlighted state by cloning the enabled state
+ StateSave highlighted = enabled.Clone();
+ highlighted.Name = FrameworkElement.HighlightedStateName;
+ sliderCategory.States.Add(highlighted);
+
+ // Assign the configured container as this slider's visual
+ Visual = topLevelContainer;
+
+ // Enable click-to-point functionality for the slider
+ // This allows users to click anywhere on the track to jump to that value
+ IsMoveToPointEnabled = true;
+
+ // Add event handlers
+ Visual.RollOn += HandleRollOn;
+ ValueChanged += HandleValueChanged;
+ ValueChangedByUi += HandleValueChangedByUi;
+ }
+
+ ///
+ /// Automatically focuses the slider when the user interacts with it
+ ///
+ private void HandleValueChangedByUi(object sender, EventArgs e)
+ {
+ IsFocused = true;
+ }
+
+ ///
+ /// Automatically focuses the slider when the mouse hovers over it
+ ///
+ private void HandleRollOn(object sender, EventArgs e)
+ {
+ IsFocused = true;
+ }
+
+ ///
+ /// Updates the fill rectangle width to visually represent the current value
+ ///
+ private void HandleValueChanged(object sender, EventArgs e)
+ {
+ // Calculate the ratio of the current value within its range
+ double ratio = (Value - Minimum) / (Maximum - Minimum);
+
+ // Update the fill rectangle width as a percentage
+ // _fillRectangle uses percentage width units, so we multiply by 100
+ _fillRectangle.Width = 100 * (float)ratio;
+ }
+}
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/.config/dotnet-tools.json b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/.config/dotnet-tools.json
new file mode 100644
index 00000000..fbedee15
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/.config/dotnet-tools.json
@@ -0,0 +1,36 @@
+{
+ "version": 1,
+ "isRoot": true,
+ "tools": {
+ "dotnet-mgcb": {
+ "version": "3.8.4",
+ "commands": [
+ "mgcb"
+ ]
+ },
+ "dotnet-mgcb-editor": {
+ "version": "3.8.4",
+ "commands": [
+ "mgcb-editor"
+ ]
+ },
+ "dotnet-mgcb-editor-linux": {
+ "version": "3.8.4",
+ "commands": [
+ "mgcb-editor-linux"
+ ]
+ },
+ "dotnet-mgcb-editor-windows": {
+ "version": "3.8.4",
+ "commands": [
+ "mgcb-editor-windows"
+ ]
+ },
+ "dotnet-mgcb-editor-mac": {
+ "version": "3.8.4",
+ "commands": [
+ "mgcb-editor-mac"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/Content/Content.mgcb b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/Content/Content.mgcb
new file mode 100644
index 00000000..4c978bab
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/Content/Content.mgcb
@@ -0,0 +1,102 @@
+#----------------------------- Global Properties ----------------------------#
+
+/outputDir:bin/$(Platform)
+/intermediateDir:obj/$(Platform)
+/platform:iOS
+/config:
+/profile:Reach
+/compress:False
+
+#-------------------------------- References --------------------------------#
+
+
+#---------------------------------- Content ---------------------------------#
+
+#begin audio/bounce.wav
+/importer:WavImporter
+/processor:SoundEffectProcessor
+/processorParam:Quality=Best
+/build:audio/bounce.wav
+
+#begin audio/collect.wav
+/importer:WavImporter
+/processor:SoundEffectProcessor
+/processorParam:Quality=Best
+/build:audio/collect.wav
+
+#begin audio/theme.ogg
+/importer:OggImporter
+/processor:SongProcessor
+/processorParam:Quality=Best
+/build:audio/theme.ogg
+
+#begin audio/ui.wav
+/importer:WavImporter
+/processor:SoundEffectProcessor
+/processorParam:Quality=Best
+/build:audio/ui.wav
+
+#begin effects/grayscaleEffect.fx
+/importer:EffectImporter
+/processor:EffectProcessor
+/processorParam:DebugMode=Auto
+/build:effects/grayscaleEffect.fx
+
+#begin fonts/04B_30_5x.spritefont
+/importer:FontDescriptionImporter
+/processor:FontDescriptionProcessor
+/processorParam:PremultiplyAlpha=True
+/processorParam:TextureFormat=Compressed
+/build:fonts/04B_30_5x.spritefont
+
+#begin fonts/04b_30.fnt
+/copy:fonts/04b_30.fnt
+
+#begin fonts/04B_30.spritefont
+/importer:FontDescriptionImporter
+/processor:FontDescriptionProcessor
+/processorParam:PremultiplyAlpha=True
+/processorParam:TextureFormat=Compressed
+/build:fonts/04B_30.spritefont
+
+#begin images/atlas-definition.xml
+/copy:images/atlas-definition.xml
+
+#begin images/atlas.png
+/importer:TextureImporter
+/processor:TextureProcessor
+/processorParam:ColorKeyColor=255,0,255,255
+/processorParam:ColorKeyEnabled=True
+/processorParam:GenerateMipmaps=False
+/processorParam:PremultiplyAlpha=True
+/processorParam:ResizeToPowerOfTwo=False
+/processorParam:MakeSquare=False
+/processorParam:TextureFormat=Color
+/build:images/atlas.png
+
+#begin images/background-pattern.png
+/importer:TextureImporter
+/processor:TextureProcessor
+/processorParam:ColorKeyColor=255,0,255,255
+/processorParam:ColorKeyEnabled=True
+/processorParam:GenerateMipmaps=False
+/processorParam:PremultiplyAlpha=True
+/processorParam:ResizeToPowerOfTwo=False
+/processorParam:MakeSquare=False
+/processorParam:TextureFormat=Color
+/build:images/background-pattern.png
+
+#begin images/logo.png
+/importer:TextureImporter
+/processor:TextureProcessor
+/processorParam:ColorKeyColor=255,0,255,255
+/processorParam:ColorKeyEnabled=True
+/processorParam:GenerateMipmaps=False
+/processorParam:PremultiplyAlpha=True
+/processorParam:ResizeToPowerOfTwo=False
+/processorParam:MakeSquare=False
+/processorParam:TextureFormat=Color
+/build:images/logo.png
+
+#begin images/tilemap-definition.xml
+/copy:images/tilemap-definition.xml
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/Content/audio/bounce.wav b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/Content/audio/bounce.wav
new file mode 100644
index 00000000..baa7a47b
Binary files /dev/null and b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/Content/audio/bounce.wav differ
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/Content/audio/collect.wav b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/Content/audio/collect.wav
new file mode 100644
index 00000000..506220de
Binary files /dev/null and b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/Content/audio/collect.wav differ
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/Content/audio/theme.ogg b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/Content/audio/theme.ogg
new file mode 100644
index 00000000..72e1fd3b
Binary files /dev/null and b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/Content/audio/theme.ogg differ
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/Content/audio/ui.wav b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/Content/audio/ui.wav
new file mode 100644
index 00000000..63e8941e
Binary files /dev/null and b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/Content/audio/ui.wav differ
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/Content/effects/grayscaleEffect.fx b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/Content/effects/grayscaleEffect.fx
new file mode 100644
index 00000000..5dd0d8b6
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/Content/effects/grayscaleEffect.fx
@@ -0,0 +1,53 @@
+#if OPENGL
+ #define SV_POSITION POSITION
+ #define VS_SHADERMODEL vs_3_0
+ #define PS_SHADERMODEL ps_3_0
+#else
+ #define VS_SHADERMODEL vs_4_0_level_9_1
+ #define PS_SHADERMODEL ps_4_0_level_9_1
+#endif
+
+Texture2D SpriteTexture;
+
+// A value between 0 and 1 that controls the intensity of the grayscale effect.
+// 0 = full color, 1 = full grayscale.
+float Saturation = 1.0;
+
+sampler2D SpriteTextureSampler = sampler_state
+{
+ Texture = ;
+};
+
+struct VertexShaderOutput
+{
+ float4 Position : SV_POSITION;
+ float4 Color : COLOR0;
+ float2 TextureCoordinates : TEXCOORD0;
+};
+
+float4 MainPS(VertexShaderOutput input) : COLOR
+{
+ // Sample the texture
+ float4 color = tex2D(SpriteTextureSampler, input.TextureCoordinates) * input.Color;
+
+ // Calculate the grayscale value based on human perception of colors
+ float grayscale = dot(color.rgb, float3(0.3, 0.59, 0.11));
+
+ // create a grayscale color vector (same value for R, G, and B)
+ float3 grayscaleColor = float3(grayscale, grayscale, grayscale);
+
+ // Linear interpolation between he grayscale color and the original color's
+ // rgb values based on the saturation parameter.
+ float3 finalColor = lerp(grayscale, color.rgb, Saturation);
+
+ // Return the final color with the original alpha value
+ return float4(finalColor, color.a);
+}
+
+technique SpriteDrawing
+{
+ pass P0
+ {
+ PixelShader = compile PS_SHADERMODEL MainPS();
+ }
+};
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/Content/fonts/04B_30.spritefont b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/Content/fonts/04B_30.spritefont
new file mode 100644
index 00000000..63d4728c
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/Content/fonts/04B_30.spritefont
@@ -0,0 +1,16 @@
+
+
+
+ 04B_30.ttf
+ 17.5
+ 0
+ true
+
+
+
+
+ ~
+
+
+
+
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/Content/fonts/04B_30.ttf b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/Content/fonts/04B_30.ttf
new file mode 100644
index 00000000..4b93740c
Binary files /dev/null and b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/Content/fonts/04B_30.ttf differ
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/Content/fonts/04B_30_5x.spritefont b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/Content/fonts/04B_30_5x.spritefont
new file mode 100644
index 00000000..dd239a53
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/Content/fonts/04B_30_5x.spritefont
@@ -0,0 +1,16 @@
+
+
+
+ 04B_30.ttf
+ 87.5
+ 0
+ true
+
+
+
+
+ ~
+
+
+
+
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/Content/fonts/04b_30.fnt b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/Content/fonts/04b_30.fnt
new file mode 100644
index 00000000..772f8c54
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/Content/fonts/04b_30.fnt
@@ -0,0 +1,99 @@
+info face="04b30" size=35 bold=0 italic=0 charset="" unicode=1 stretchH=100 smooth=0 aa=1 padding=0,0,0,0 spacing=1,1 outline=0
+common lineHeight=35 base=31 scaleW=256 scaleH=512 pages=1 packed=0 alphaChnl=0 redChnl=4 greenChnl=4 blueChnl=4
+page id=0 file="../images/atlas.png"
+chars count=95
+char id=32 x=30 y=152 width=3 height=1 xoffset=-1 yoffset=34 xadvance=29 page=0 chnl=15
+char id=33 x=240 y=30 width=14 height=29 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=34 x=102 y=232 width=25 height=15 xoffset=1 yoffset=4 xadvance=29 page=0 chnl=15
+char id=35 x=184 y=0 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=36 x=250 y=150 width=3 height=1 xoffset=-1 yoffset=34 xadvance=29 page=0 chnl=15
+char id=37 x=0 y=34 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=38 x=30 y=150 width=3 height=1 xoffset=-1 yoffset=34 xadvance=29 page=0 chnl=15
+char id=39 x=245 y=202 width=10 height=15 xoffset=1 yoffset=4 xadvance=14 page=0 chnl=15
+char id=40 x=106 y=150 width=16 height=29 xoffset=1 yoffset=2 xadvance=21 page=0 chnl=15
+char id=41 x=123 y=150 width=16 height=29 xoffset=1 yoffset=2 xadvance=21 page=0 chnl=15
+char id=42 x=128 y=232 width=14 height=15 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=43 x=94 y=0 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=44 x=143 y=232 width=10 height=14 xoffset=1 yoffset=19 xadvance=14 page=0 chnl=15
+char id=45 x=154 y=232 width=25 height=11 xoffset=1 yoffset=12 xadvance=29 page=0 chnl=15
+char id=46 x=231 y=228 width=10 height=10 xoffset=1 yoffset=19 xadvance=14 page=0 chnl=15
+char id=47 x=60 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=48 x=90 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=49 x=46 y=150 width=19 height=29 xoffset=1 yoffset=2 xadvance=23 page=0 chnl=15
+char id=50 x=150 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=51 x=180 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=52 x=210 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=53 x=0 y=94 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=54 x=180 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=55 x=60 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=56 x=90 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=57 x=120 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=58 x=234 y=202 width=10 height=25 xoffset=1 yoffset=4 xadvance=14 page=0 chnl=15
+char id=59 x=244 y=0 width=10 height=29 xoffset=1 yoffset=4 xadvance=14 page=0 chnl=15
+char id=60 x=86 y=150 width=19 height=29 xoffset=1 yoffset=2 xadvance=23 page=0 chnl=15
+char id=61 x=182 y=176 width=25 height=25 xoffset=1 yoffset=4 xadvance=29 page=0 chnl=15
+char id=62 x=237 y=120 width=18 height=29 xoffset=1 yoffset=2 xadvance=23 page=0 chnl=15
+char id=63 x=180 y=120 width=28 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=64 x=34 y=150 width=3 height=1 xoffset=-1 yoffset=34 xadvance=29 page=0 chnl=15
+char id=65 x=120 y=120 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=66 x=150 y=120 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=67 x=124 y=0 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=68 x=154 y=0 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=69 x=214 y=0 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=70 x=30 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=71 x=60 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=72 x=90 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=73 x=240 y=90 width=14 height=29 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=74 x=120 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=75 x=150 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=76 x=209 y=120 width=27 height=29 xoffset=1 yoffset=2 xadvance=31 page=0 chnl=15
+char id=77 x=30 y=0 width=31 height=29 xoffset=1 yoffset=2 xadvance=35 page=0 chnl=15
+char id=78 x=210 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=79 x=0 y=64 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=80 x=30 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=81 x=0 y=0 width=29 height=33 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=82 x=120 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=83 x=30 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=84 x=150 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=85 x=180 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=86 x=210 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=87 x=62 y=0 width=31 height=29 xoffset=1 yoffset=2 xadvance=35 page=0 chnl=15
+char id=88 x=0 y=124 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=89 x=30 y=120 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=90 x=60 y=120 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=91 x=240 y=60 width=14 height=29 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=92 x=90 y=120 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=93 x=140 y=150 width=14 height=29 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=94 x=180 y=232 width=16 height=11 xoffset=1 yoffset=4 xadvance=21 page=0 chnl=15
+char id=95 x=0 y=262 width=29 height=10 xoffset=1 yoffset=21 xadvance=33 page=0 chnl=15
+char id=96 x=197 y=228 width=16 height=11 xoffset=1 yoffset=4 xadvance=21 page=0 chnl=15
+char id=97 x=208 y=176 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=98 x=0 y=210 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=99 x=26 y=210 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=100 x=52 y=206 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=101 x=78 y=206 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=102 x=104 y=206 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=103 x=130 y=206 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=104 x=156 y=206 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=105 x=234 y=176 width=12 height=25 xoffset=1 yoffset=6 xadvance=16 page=0 chnl=15
+char id=106 x=182 y=202 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=107 x=208 y=202 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=108 x=78 y=232 width=23 height=25 xoffset=1 yoffset=6 xadvance=27 page=0 chnl=15
+char id=109 x=197 y=150 width=26 height=25 xoffset=1 yoffset=6 xadvance=31 page=0 chnl=15
+char id=110 x=0 y=236 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=111 x=26 y=236 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=112 x=78 y=180 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=113 x=0 y=154 width=25 height=29 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=114 x=52 y=232 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=115 x=224 y=150 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=116 x=0 y=184 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=117 x=26 y=184 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=118 x=52 y=180 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=119 x=170 y=150 width=26 height=25 xoffset=1 yoffset=6 xadvance=31 page=0 chnl=15
+char id=120 x=104 y=180 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=121 x=130 y=180 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=122 x=156 y=180 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=123 x=26 y=154 width=19 height=29 xoffset=1 yoffset=2 xadvance=23 page=0 chnl=15
+char id=124 x=155 y=150 width=14 height=29 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=125 x=66 y=150 width=19 height=29 xoffset=1 yoffset=2 xadvance=23 page=0 chnl=15
+char id=126 x=214 y=228 width=16 height=11 xoffset=1 yoffset=4 xadvance=21 page=0 chnl=15
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/Content/images/atlas-definition.xml b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/Content/images/atlas-definition.xml
new file mode 100644
index 00000000..21772022
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/Content/images/atlas-definition.xml
@@ -0,0 +1,34 @@
+
+
+ images/atlas
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/Content/images/atlas.png b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/Content/images/atlas.png
new file mode 100644
index 00000000..f7def20f
Binary files /dev/null and b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/Content/images/atlas.png differ
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/Content/images/background-pattern.png b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/Content/images/background-pattern.png
new file mode 100644
index 00000000..2d8d878e
Binary files /dev/null and b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/Content/images/background-pattern.png differ
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/Content/images/logo.png b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/Content/images/logo.png
new file mode 100644
index 00000000..1509036c
Binary files /dev/null and b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/Content/images/logo.png differ
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/Content/images/tilemap-definition.xml b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/Content/images/tilemap-definition.xml
new file mode 100644
index 00000000..85658c60
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/Content/images/tilemap-definition.xml
@@ -0,0 +1,15 @@
+
+
+ images/atlas
+
+ 00 01 02 01 02 01 02 01 02 01 02 01 02 01 02 03
+ 04 05 05 06 05 05 06 05 05 06 05 05 06 05 05 07
+ 08 09 09 09 09 09 09 09 09 09 09 09 09 09 09 11
+ 04 09 09 09 09 09 09 09 10 09 09 09 09 10 09 07
+ 08 09 10 09 09 09 09 09 09 09 09 09 09 09 09 11
+ 04 09 09 09 09 09 09 09 09 09 09 09 09 09 09 07
+ 08 10 09 09 09 09 09 09 09 09 10 09 09 09 09 11
+ 04 09 09 09 09 09 10 09 09 09 09 09 09 09 09 07
+ 12 13 14 13 14 13 14 13 14 13 14 13 14 13 14 15
+
+
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/Default.png b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/Default.png
new file mode 100644
index 00000000..1f9b909f
Binary files /dev/null and b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/Default.png differ
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/DungeonSlime.iOS.csproj b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/DungeonSlime.iOS.csproj
new file mode 100644
index 00000000..33f1ee1a
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/DungeonSlime.iOS.csproj
@@ -0,0 +1,18 @@
+
+
+ net9.0-ios
+ Exe
+ 12.2
+ iPhone Developer
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/Entitlements.plist b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/Entitlements.plist
new file mode 100644
index 00000000..9ae59937
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/Entitlements.plist
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/GameThumbnail.png b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/GameThumbnail.png
new file mode 100644
index 00000000..99814c32
Binary files /dev/null and b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/GameThumbnail.png differ
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/Info.plist b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/Info.plist
new file mode 100644
index 00000000..3aee6757
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/Info.plist
@@ -0,0 +1,38 @@
+
+
+
+
+ CFBundleDisplayName
+ DungeonSlime
+ CFBundleIconFiles
+
+ GameThumbnail.png
+
+ CFBundleIdentifier
+ com.monogame.dungeonslime
+ MinimumOSVersion
+ 12.2
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ CFBundleName
+ DungeonSlime.iOS
+ UIRequiresFullScreen
+
+ UIStatusBarHidden
+
+ UILaunchStoryboardName
+ LaunchScreen
+ UIDeviceFamily
+
+ 1
+ 2
+
+ CFBundleShortVersionString
+ 1.0.0
+ CFBundleVersion
+ 1
+
+
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/LaunchScreen.storyboard b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/LaunchScreen.storyboard
new file mode 100644
index 00000000..5d2e905a
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/LaunchScreen.storyboard
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/Program.cs b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/Program.cs
new file mode 100644
index 00000000..7fd9c6f0
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.iOS/Program.cs
@@ -0,0 +1,31 @@
+using System;
+using Foundation;
+using UIKit;
+
+namespace DungeonSlime.iOS
+{
+ [Register("AppDelegate")]
+ class Program : UIApplicationDelegate
+ {
+ private static Game1 game;
+
+ internal static void RunGame()
+ {
+ game = new Game1();
+ game.Run();
+ }
+
+ ///
+ /// The main entry point for the application.
+ ///
+ static void Main(string[] args)
+ {
+ UIApplication.Main(args, null, typeof(Program));
+ }
+
+ public override void FinishedLaunching(UIApplication app)
+ {
+ RunGame();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.slnx b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.slnx
new file mode 100644
index 00000000..9e055e60
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime.slnx
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/.config/dotnet-tools.json b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/.config/dotnet-tools.json
new file mode 100644
index 00000000..afd4e2c4
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/.config/dotnet-tools.json
@@ -0,0 +1,36 @@
+{
+ "version": 1,
+ "isRoot": true,
+ "tools": {
+ "dotnet-mgcb": {
+ "version": "3.8.3",
+ "commands": [
+ "mgcb"
+ ]
+ },
+ "dotnet-mgcb-editor": {
+ "version": "3.8.3",
+ "commands": [
+ "mgcb-editor"
+ ]
+ },
+ "dotnet-mgcb-editor-linux": {
+ "version": "3.8.3",
+ "commands": [
+ "mgcb-editor-linux"
+ ]
+ },
+ "dotnet-mgcb-editor-windows": {
+ "version": "3.8.3",
+ "commands": [
+ "mgcb-editor-windows"
+ ]
+ },
+ "dotnet-mgcb-editor-mac": {
+ "version": "3.8.3",
+ "commands": [
+ "mgcb-editor-mac"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/Content/Content.mgcb b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/Content/Content.mgcb
new file mode 100644
index 00000000..d26ea4f1
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/Content/Content.mgcb
@@ -0,0 +1,104 @@
+
+#----------------------------- Global Properties ----------------------------#
+
+/outputDir:bin/$(Platform)
+/intermediateDir:obj/$(Platform)
+/platform:DesktopGL
+/config:
+/profile:Reach
+/compress:False
+
+#-------------------------------- References --------------------------------#
+
+
+#---------------------------------- Content ---------------------------------#
+
+#begin audio/bounce.wav
+/importer:WavImporter
+/processor:SoundEffectProcessor
+/processorParam:Quality=Best
+/build:audio/bounce.wav
+
+#begin audio/collect.wav
+/importer:WavImporter
+/processor:SoundEffectProcessor
+/processorParam:Quality=Best
+/build:audio/collect.wav
+
+#begin audio/theme.ogg
+/importer:OggImporter
+/processor:SongProcessor
+/processorParam:Quality=Best
+/build:audio/theme.ogg
+
+#begin audio/ui.wav
+/importer:WavImporter
+/processor:SoundEffectProcessor
+/processorParam:Quality=Best
+/build:audio/ui.wav
+
+#begin effects/grayscaleEffect.fx
+/importer:EffectImporter
+/processor:EffectProcessor
+/processorParam:DebugMode=Auto
+/build:effects/grayscaleEffect.fx
+
+#begin fonts/04B_30_5x.spritefont
+/importer:FontDescriptionImporter
+/processor:FontDescriptionProcessor
+/processorParam:PremultiplyAlpha=True
+/processorParam:TextureFormat=Compressed
+/build:fonts/04B_30_5x.spritefont
+
+#begin fonts/04b_30.fnt
+/copy:fonts/04b_30.fnt
+
+#begin fonts/04B_30.spritefont
+/importer:FontDescriptionImporter
+/processor:FontDescriptionProcessor
+/processorParam:PremultiplyAlpha=True
+/processorParam:TextureFormat=Compressed
+/build:fonts/04B_30.spritefont
+
+#begin images/atlas-definition.xml
+/copy:images/atlas-definition.xml
+
+#begin images/atlas.png
+/importer:TextureImporter
+/processor:TextureProcessor
+/processorParam:ColorKeyColor=255,0,255,255
+/processorParam:ColorKeyEnabled=True
+/processorParam:GenerateMipmaps=False
+/processorParam:PremultiplyAlpha=True
+/processorParam:ResizeToPowerOfTwo=False
+/processorParam:MakeSquare=False
+/processorParam:TextureFormat=Color
+/build:images/atlas.png
+
+#begin images/background-pattern.png
+/importer:TextureImporter
+/processor:TextureProcessor
+/processorParam:ColorKeyColor=255,0,255,255
+/processorParam:ColorKeyEnabled=True
+/processorParam:GenerateMipmaps=False
+/processorParam:PremultiplyAlpha=True
+/processorParam:ResizeToPowerOfTwo=False
+/processorParam:MakeSquare=False
+/processorParam:TextureFormat=Color
+/build:images/background-pattern.png
+
+#begin images/logo.png
+/importer:TextureImporter
+/processor:TextureProcessor
+/processorParam:ColorKeyColor=255,0,255,255
+/processorParam:ColorKeyEnabled=True
+/processorParam:GenerateMipmaps=False
+/processorParam:PremultiplyAlpha=True
+/processorParam:ResizeToPowerOfTwo=False
+/processorParam:MakeSquare=False
+/processorParam:TextureFormat=Color
+/build:images/logo.png
+
+#begin images/tilemap-definition.xml
+/copy:images/tilemap-definition.xml
+
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/Content/audio/bounce.wav b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/Content/audio/bounce.wav
new file mode 100644
index 00000000..baa7a47b
Binary files /dev/null and b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/Content/audio/bounce.wav differ
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/Content/audio/collect.wav b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/Content/audio/collect.wav
new file mode 100644
index 00000000..506220de
Binary files /dev/null and b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/Content/audio/collect.wav differ
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/Content/audio/theme.ogg b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/Content/audio/theme.ogg
new file mode 100644
index 00000000..72e1fd3b
Binary files /dev/null and b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/Content/audio/theme.ogg differ
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/Content/audio/ui.wav b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/Content/audio/ui.wav
new file mode 100644
index 00000000..63e8941e
Binary files /dev/null and b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/Content/audio/ui.wav differ
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/Content/effects/grayscaleEffect.fx b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/Content/effects/grayscaleEffect.fx
new file mode 100644
index 00000000..5dd0d8b6
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/Content/effects/grayscaleEffect.fx
@@ -0,0 +1,53 @@
+#if OPENGL
+ #define SV_POSITION POSITION
+ #define VS_SHADERMODEL vs_3_0
+ #define PS_SHADERMODEL ps_3_0
+#else
+ #define VS_SHADERMODEL vs_4_0_level_9_1
+ #define PS_SHADERMODEL ps_4_0_level_9_1
+#endif
+
+Texture2D SpriteTexture;
+
+// A value between 0 and 1 that controls the intensity of the grayscale effect.
+// 0 = full color, 1 = full grayscale.
+float Saturation = 1.0;
+
+sampler2D SpriteTextureSampler = sampler_state
+{
+ Texture = ;
+};
+
+struct VertexShaderOutput
+{
+ float4 Position : SV_POSITION;
+ float4 Color : COLOR0;
+ float2 TextureCoordinates : TEXCOORD0;
+};
+
+float4 MainPS(VertexShaderOutput input) : COLOR
+{
+ // Sample the texture
+ float4 color = tex2D(SpriteTextureSampler, input.TextureCoordinates) * input.Color;
+
+ // Calculate the grayscale value based on human perception of colors
+ float grayscale = dot(color.rgb, float3(0.3, 0.59, 0.11));
+
+ // create a grayscale color vector (same value for R, G, and B)
+ float3 grayscaleColor = float3(grayscale, grayscale, grayscale);
+
+ // Linear interpolation between he grayscale color and the original color's
+ // rgb values based on the saturation parameter.
+ float3 finalColor = lerp(grayscale, color.rgb, Saturation);
+
+ // Return the final color with the original alpha value
+ return float4(finalColor, color.a);
+}
+
+technique SpriteDrawing
+{
+ pass P0
+ {
+ PixelShader = compile PS_SHADERMODEL MainPS();
+ }
+};
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/Content/fonts/04B_30.spritefont b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/Content/fonts/04B_30.spritefont
new file mode 100644
index 00000000..63d4728c
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/Content/fonts/04B_30.spritefont
@@ -0,0 +1,16 @@
+
+
+
+ 04B_30.ttf
+ 17.5
+ 0
+ true
+
+
+
+
+ ~
+
+
+
+
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/Content/fonts/04B_30.ttf b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/Content/fonts/04B_30.ttf
new file mode 100644
index 00000000..4b93740c
Binary files /dev/null and b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/Content/fonts/04B_30.ttf differ
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/Content/fonts/04B_30_5x.spritefont b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/Content/fonts/04B_30_5x.spritefont
new file mode 100644
index 00000000..dd239a53
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/Content/fonts/04B_30_5x.spritefont
@@ -0,0 +1,16 @@
+
+
+
+ 04B_30.ttf
+ 87.5
+ 0
+ true
+
+
+
+
+ ~
+
+
+
+
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/Content/fonts/04b_30.fnt b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/Content/fonts/04b_30.fnt
new file mode 100644
index 00000000..772f8c54
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/Content/fonts/04b_30.fnt
@@ -0,0 +1,99 @@
+info face="04b30" size=35 bold=0 italic=0 charset="" unicode=1 stretchH=100 smooth=0 aa=1 padding=0,0,0,0 spacing=1,1 outline=0
+common lineHeight=35 base=31 scaleW=256 scaleH=512 pages=1 packed=0 alphaChnl=0 redChnl=4 greenChnl=4 blueChnl=4
+page id=0 file="../images/atlas.png"
+chars count=95
+char id=32 x=30 y=152 width=3 height=1 xoffset=-1 yoffset=34 xadvance=29 page=0 chnl=15
+char id=33 x=240 y=30 width=14 height=29 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=34 x=102 y=232 width=25 height=15 xoffset=1 yoffset=4 xadvance=29 page=0 chnl=15
+char id=35 x=184 y=0 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=36 x=250 y=150 width=3 height=1 xoffset=-1 yoffset=34 xadvance=29 page=0 chnl=15
+char id=37 x=0 y=34 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=38 x=30 y=150 width=3 height=1 xoffset=-1 yoffset=34 xadvance=29 page=0 chnl=15
+char id=39 x=245 y=202 width=10 height=15 xoffset=1 yoffset=4 xadvance=14 page=0 chnl=15
+char id=40 x=106 y=150 width=16 height=29 xoffset=1 yoffset=2 xadvance=21 page=0 chnl=15
+char id=41 x=123 y=150 width=16 height=29 xoffset=1 yoffset=2 xadvance=21 page=0 chnl=15
+char id=42 x=128 y=232 width=14 height=15 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=43 x=94 y=0 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=44 x=143 y=232 width=10 height=14 xoffset=1 yoffset=19 xadvance=14 page=0 chnl=15
+char id=45 x=154 y=232 width=25 height=11 xoffset=1 yoffset=12 xadvance=29 page=0 chnl=15
+char id=46 x=231 y=228 width=10 height=10 xoffset=1 yoffset=19 xadvance=14 page=0 chnl=15
+char id=47 x=60 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=48 x=90 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=49 x=46 y=150 width=19 height=29 xoffset=1 yoffset=2 xadvance=23 page=0 chnl=15
+char id=50 x=150 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=51 x=180 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=52 x=210 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=53 x=0 y=94 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=54 x=180 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=55 x=60 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=56 x=90 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=57 x=120 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=58 x=234 y=202 width=10 height=25 xoffset=1 yoffset=4 xadvance=14 page=0 chnl=15
+char id=59 x=244 y=0 width=10 height=29 xoffset=1 yoffset=4 xadvance=14 page=0 chnl=15
+char id=60 x=86 y=150 width=19 height=29 xoffset=1 yoffset=2 xadvance=23 page=0 chnl=15
+char id=61 x=182 y=176 width=25 height=25 xoffset=1 yoffset=4 xadvance=29 page=0 chnl=15
+char id=62 x=237 y=120 width=18 height=29 xoffset=1 yoffset=2 xadvance=23 page=0 chnl=15
+char id=63 x=180 y=120 width=28 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=64 x=34 y=150 width=3 height=1 xoffset=-1 yoffset=34 xadvance=29 page=0 chnl=15
+char id=65 x=120 y=120 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=66 x=150 y=120 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=67 x=124 y=0 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=68 x=154 y=0 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=69 x=214 y=0 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=70 x=30 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=71 x=60 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=72 x=90 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=73 x=240 y=90 width=14 height=29 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=74 x=120 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=75 x=150 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=76 x=209 y=120 width=27 height=29 xoffset=1 yoffset=2 xadvance=31 page=0 chnl=15
+char id=77 x=30 y=0 width=31 height=29 xoffset=1 yoffset=2 xadvance=35 page=0 chnl=15
+char id=78 x=210 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=79 x=0 y=64 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=80 x=30 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=81 x=0 y=0 width=29 height=33 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=82 x=120 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=83 x=30 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=84 x=150 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=85 x=180 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=86 x=210 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=87 x=62 y=0 width=31 height=29 xoffset=1 yoffset=2 xadvance=35 page=0 chnl=15
+char id=88 x=0 y=124 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=89 x=30 y=120 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=90 x=60 y=120 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=91 x=240 y=60 width=14 height=29 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=92 x=90 y=120 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=93 x=140 y=150 width=14 height=29 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=94 x=180 y=232 width=16 height=11 xoffset=1 yoffset=4 xadvance=21 page=0 chnl=15
+char id=95 x=0 y=262 width=29 height=10 xoffset=1 yoffset=21 xadvance=33 page=0 chnl=15
+char id=96 x=197 y=228 width=16 height=11 xoffset=1 yoffset=4 xadvance=21 page=0 chnl=15
+char id=97 x=208 y=176 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=98 x=0 y=210 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=99 x=26 y=210 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=100 x=52 y=206 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=101 x=78 y=206 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=102 x=104 y=206 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=103 x=130 y=206 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=104 x=156 y=206 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=105 x=234 y=176 width=12 height=25 xoffset=1 yoffset=6 xadvance=16 page=0 chnl=15
+char id=106 x=182 y=202 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=107 x=208 y=202 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=108 x=78 y=232 width=23 height=25 xoffset=1 yoffset=6 xadvance=27 page=0 chnl=15
+char id=109 x=197 y=150 width=26 height=25 xoffset=1 yoffset=6 xadvance=31 page=0 chnl=15
+char id=110 x=0 y=236 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=111 x=26 y=236 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=112 x=78 y=180 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=113 x=0 y=154 width=25 height=29 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=114 x=52 y=232 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=115 x=224 y=150 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=116 x=0 y=184 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=117 x=26 y=184 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=118 x=52 y=180 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=119 x=170 y=150 width=26 height=25 xoffset=1 yoffset=6 xadvance=31 page=0 chnl=15
+char id=120 x=104 y=180 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=121 x=130 y=180 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=122 x=156 y=180 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=123 x=26 y=154 width=19 height=29 xoffset=1 yoffset=2 xadvance=23 page=0 chnl=15
+char id=124 x=155 y=150 width=14 height=29 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=125 x=66 y=150 width=19 height=29 xoffset=1 yoffset=2 xadvance=23 page=0 chnl=15
+char id=126 x=214 y=228 width=16 height=11 xoffset=1 yoffset=4 xadvance=21 page=0 chnl=15
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/Content/images/atlas-definition.xml b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/Content/images/atlas-definition.xml
new file mode 100644
index 00000000..21772022
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/Content/images/atlas-definition.xml
@@ -0,0 +1,34 @@
+
+
+ images/atlas
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/Content/images/atlas.png b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/Content/images/atlas.png
new file mode 100644
index 00000000..f7def20f
Binary files /dev/null and b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/Content/images/atlas.png differ
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/Content/images/background-pattern.png b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/Content/images/background-pattern.png
new file mode 100644
index 00000000..2d8d878e
Binary files /dev/null and b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/Content/images/background-pattern.png differ
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/Content/images/logo.png b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/Content/images/logo.png
new file mode 100644
index 00000000..1509036c
Binary files /dev/null and b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/Content/images/logo.png differ
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/Content/images/tilemap-definition.xml b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/Content/images/tilemap-definition.xml
new file mode 100644
index 00000000..85658c60
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/Content/images/tilemap-definition.xml
@@ -0,0 +1,15 @@
+
+
+ images/atlas
+
+ 00 01 02 01 02 01 02 01 02 01 02 01 02 01 02 03
+ 04 05 05 06 05 05 06 05 05 06 05 05 06 05 05 07
+ 08 09 09 09 09 09 09 09 09 09 09 09 09 09 09 11
+ 04 09 09 09 09 09 09 09 10 09 09 09 09 10 09 07
+ 08 09 10 09 09 09 09 09 09 09 09 09 09 09 09 11
+ 04 09 09 09 09 09 09 09 09 09 09 09 09 09 09 07
+ 08 10 09 09 09 09 09 09 09 09 10 09 09 09 09 11
+ 04 09 09 09 09 09 10 09 09 09 09 09 09 09 09 07
+ 12 13 14 13 14 13 14 13 14 13 14 13 14 13 14 15
+
+
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/DungeonSlime.Windows.csproj b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/DungeonSlime.Windows.csproj
new file mode 100644
index 00000000..a5e61145
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/DungeonSlime.Windows.csproj
@@ -0,0 +1,34 @@
+
+
+ WinExe
+ net9.0
+ Major
+ false
+ false
+
+
+ app.manifest
+ Icon.ico
+
+
+
+
+
+
+
+ Icon.ico
+
+
+ Icon.bmp
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/Icon.bmp b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/Icon.bmp
new file mode 100644
index 00000000..2b481653
Binary files /dev/null and b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/Icon.bmp differ
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/Icon.ico b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/Icon.ico
new file mode 100644
index 00000000..7d9dec18
Binary files /dev/null and b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/Icon.ico differ
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/Program.cs b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/Program.cs
new file mode 100644
index 00000000..d491c406
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/Program.cs
@@ -0,0 +1,2 @@
+using var game = new DungeonSlime.Game1();
+game.Run();
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/app.manifest b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/app.manifest
new file mode 100644
index 00000000..caf45166
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/DungeonSlime/app.manifest
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true/pm
+ permonitorv2,permonitor
+
+
+
+
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/MonoGameLibrary/Audio/AudioController.cs b/Tutorials/MobileDeployment/05-SharingLogic/MonoGameLibrary/Audio/AudioController.cs
new file mode 100644
index 00000000..2c31c166
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/MonoGameLibrary/Audio/AudioController.cs
@@ -0,0 +1,282 @@
+using System;
+using System.Collections.Generic;
+using Microsoft.Xna.Framework.Audio;
+using Microsoft.Xna.Framework.Media;
+
+namespace MonoGameLibrary.Audio;
+
+using MediaPlayer = Microsoft.Xna.Framework.Media.MediaPlayer;
+
+public class AudioController : IDisposable
+{
+ // Tracks sound effect instances created so they can be paused, unpaused, and/or disposed.
+ private readonly List _activeSoundEffectInstances;
+
+ // Tracks the volume for song playback when muting and unmuting.
+ private float _previousSongVolume;
+
+ // Tracks the volume for sound effect playback when muting and unmuting.
+ private float _previousSoundEffectVolume;
+
+ ///
+ /// Gets a value that indicates if audio is muted.
+ ///
+ public bool IsMuted { get; private set; }
+
+ ///
+ /// Gets or Sets the global volume of songs.
+ ///
+ ///
+ /// If IsMuted is true, the getter will always return back 0.0f and the
+ /// setter will ignore setting the volume.
+ ///
+ public float SongVolume
+ {
+ get
+ {
+ if (IsMuted)
+ {
+ return 0.0f;
+ }
+
+ return MediaPlayer.Volume;
+ }
+ set
+ {
+ if (IsMuted)
+ {
+ return;
+ }
+
+ MediaPlayer.Volume = Math.Clamp(value, 0.0f, 1.0f);
+ }
+ }
+
+ ///
+ /// Gets or Sets the global volume of sound effects.
+ ///
+ ///
+ /// If IsMuted is true, the getter will always return back 0.0f and the
+ /// setter will ignore setting the volume.
+ ///
+ public float SoundEffectVolume
+ {
+ get
+ {
+ if (IsMuted)
+ {
+ return 0.0f;
+ }
+
+ return SoundEffect.MasterVolume;
+ }
+ set
+ {
+ if (IsMuted)
+ {
+ return;
+ }
+
+ SoundEffect.MasterVolume = Math.Clamp(value, 0.0f, 1.0f);
+ }
+ }
+
+ ///
+ /// Gets a value that indicates if this audio controller has been disposed.
+ ///
+ public bool IsDisposed { get; private set; }
+
+ ///
+ /// Creates a new audio controller instance.
+ ///
+ public AudioController()
+ {
+ _activeSoundEffectInstances = new List();
+ }
+
+ // Finalizer called when object is collected by the garbage collector
+ ~AudioController() => Dispose(false);
+
+ ///
+ /// Updates this audio controller
+ ///
+ public void Update()
+ {
+ int index = 0;
+
+ while (index < _activeSoundEffectInstances.Count)
+ {
+ SoundEffectInstance instance = _activeSoundEffectInstances[index];
+
+ if (instance.State == SoundState.Stopped && !instance.IsDisposed)
+ {
+ instance.Dispose();
+ }
+
+ _activeSoundEffectInstances.RemoveAt(index);
+ }
+ }
+
+ ///
+ /// Plays the given sound effect.
+ ///
+ /// The sound effect to play.
+ /// The sound effect instance created by this method.
+ public SoundEffectInstance PlaySoundEffect(SoundEffect soundEffect)
+ {
+ return PlaySoundEffect(soundEffect, 1.0f, 1.0f, 0.0f, false);
+ }
+
+ ///
+ /// Plays the given sound effect with the specified properties.
+ ///
+ /// The sound effect to play.
+ /// The volume, ranging from 0.0 (silence) to 1.0 (full volume).
+ /// The pitch adjustment, ranging from -1.0 (down an octave) to 0.0 (no change) to 1.0 (up an octave).
+ /// The panning, ranging from -1.0 (left speaker) to 0.0 (centered), 1.0 (right speaker).
+ /// Whether the the sound effect should loop after playback.
+ /// The sound effect instance created by playing the sound effect.
+ /// The sound effect instance created by this method.
+ public SoundEffectInstance PlaySoundEffect(SoundEffect soundEffect, float volume, float pitch, float pan, bool isLooped)
+ {
+ // Create an instance from the sound effect given.
+ SoundEffectInstance soundEffectInstance = soundEffect.CreateInstance();
+
+ // Apply the volume, pitch, pan, and loop values specified.
+ soundEffectInstance.Volume = volume;
+ soundEffectInstance.Pitch = pitch;
+ soundEffectInstance.Pan = pan;
+ soundEffectInstance.IsLooped = isLooped;
+
+ // Tell the instance to play
+ soundEffectInstance.Play();
+
+ // Add it to the active instances for tracking
+ _activeSoundEffectInstances.Add(soundEffectInstance);
+
+ return soundEffectInstance;
+ }
+
+ ///
+ /// Plays the given song.
+ ///
+ /// The song to play.
+ /// Optionally specify if the song should repeat. Default is true.
+ public void PlaySong(Song song, bool isRepeating = true)
+ {
+ // Check if the media player is already playing, if so, stop it.
+ // If we do not stop it, this could cause issues on some platforms
+ if (MediaPlayer.State == MediaState.Playing)
+ {
+ MediaPlayer.Stop();
+ }
+
+ MediaPlayer.Play(song);
+ MediaPlayer.IsRepeating = isRepeating;
+ }
+
+ ///
+ /// Pauses all audio.
+ ///
+ public void PauseAudio()
+ {
+ // Pause any active songs playing
+ MediaPlayer.Pause();
+
+ // Pause any active sound effects
+ foreach (SoundEffectInstance soundEffectInstance in _activeSoundEffectInstances)
+ {
+ soundEffectInstance.Pause();
+ }
+ }
+
+ ///
+ /// Resumes play of all previous paused audio.
+ ///
+ public void ResumeAudio()
+ {
+ // Resume paused music
+ MediaPlayer.Resume();
+
+ // Resume any active sound effects
+ foreach (SoundEffectInstance soundEffectInstance in _activeSoundEffectInstances)
+ {
+ soundEffectInstance.Resume();
+ }
+ }
+
+ ///
+ /// Mutes all audio.
+ ///
+ public void MuteAudio()
+ {
+ // Store the volume so they can be restored during ResumeAudio
+ _previousSongVolume = MediaPlayer.Volume;
+ _previousSoundEffectVolume = SoundEffect.MasterVolume;
+
+ // Set all volumes to 0
+ MediaPlayer.Volume = 0.0f;
+ SoundEffect.MasterVolume = 0.0f;
+
+ IsMuted = true;
+ }
+
+ ///
+ /// Unmutes all audio to the volume level prior to muting.
+ ///
+ public void UnmuteAudio()
+ {
+ // Restore the previous volume values
+ MediaPlayer.Volume = _previousSongVolume;
+ SoundEffect.MasterVolume = _previousSoundEffectVolume;
+
+ IsMuted = false;
+ }
+
+ ///
+ /// Toggles the current audio mute state.
+ ///
+ public void ToggleMute()
+ {
+ if (IsMuted)
+ {
+ UnmuteAudio();
+ }
+ else
+ {
+ MuteAudio();
+ }
+ }
+
+ ///
+ /// Disposes of this audio controller and cleans up resources.
+ ///
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ /// Disposes this audio controller and cleans up resources.
+ ///
+ /// Indicates whether managed resources should be disposed.
+ protected void Dispose(bool disposing)
+ {
+ if (IsDisposed)
+ {
+ return;
+ }
+
+ if (disposing)
+ {
+ foreach (SoundEffectInstance soundEffectInstance in _activeSoundEffectInstances)
+ {
+ soundEffectInstance.Dispose();
+ }
+ _activeSoundEffectInstances.Clear();
+ }
+
+ IsDisposed = true;
+ }
+}
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/MonoGameLibrary/Circle.cs b/Tutorials/MobileDeployment/05-SharingLogic/MonoGameLibrary/Circle.cs
new file mode 100644
index 00000000..0bb691bc
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/MonoGameLibrary/Circle.cs
@@ -0,0 +1,136 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGameLibrary;
+
+public readonly struct Circle : IEquatable
+{
+ private static readonly Circle s_empty = new Circle();
+
+ ///
+ /// The x-coordinate of the center of this circle.
+ ///
+ public readonly int X;
+
+ ///
+ /// The y-coordinate of the center of this circle.
+ ///
+ public readonly int Y;
+
+ ///
+ /// The length, in pixels, from the center of this circle to the edge.
+ ///
+ public readonly int Radius;
+
+ ///
+ /// Gets the location of the center of this circle.
+ ///
+ public readonly Point Location => new Point(X, Y);
+
+ ///
+ /// Gets a circle with X=0, Y=0, and Radius=0.
+ ///
+ public static Circle Empty => s_empty;
+
+ ///
+ /// Gets a value that indicates whether this circle has a radius of 0 and a location of (0, 0).
+ ///
+ public readonly bool IsEmpty => X == 0 && Y == 0 && Radius == 0;
+
+ ///
+ /// Gets the y-coordinate of the highest point on this circle.
+ ///
+ public readonly int Top => Y - Radius;
+
+ ///
+ /// Gets the y-coordinate of the lowest point on this circle.
+ ///
+ public readonly int Bottom => Y + Radius;
+
+ ///
+ /// Gets the x-coordinate of the leftmost point on this circle.
+ ///
+ public readonly int Left => X - Radius;
+
+ ///
+ /// Gets the x-coordinate of the rightmost point on this circle.
+ ///
+ public readonly int Right => X + Radius;
+
+ ///
+ /// Creates a new circle with the specified position and radius.
+ ///
+ /// The x-coordinate of the center of the circle.
+ /// The y-coordinate of the center of the circle..
+ /// The length from the center of the circle to an edge.
+ public Circle(int x, int y, int radius)
+ {
+ X = x;
+ Y = y;
+ Radius = radius;
+ }
+
+ ///
+ /// Creates a new circle with the specified position and radius.
+ ///
+ /// The center of the circle.
+ /// The length from the center of the circle to an edge.
+ public Circle(Point location, int radius)
+ {
+ X = location.X;
+ Y = location.Y;
+ Radius = radius;
+ }
+
+ ///
+ /// Returns a value that indicates whether the specified circle intersects with this circle.
+ ///
+ /// The other circle to check.
+ /// true if the other circle intersects with this circle; otherwise, false.
+ public bool Intersects(Circle other)
+ {
+ int radiiSquared = (this.Radius + other.Radius) * (this.Radius + other.Radius);
+ float distanceSquared = Vector2.DistanceSquared(this.Location.ToVector2(), other.Location.ToVector2());
+ return distanceSquared < radiiSquared;
+ }
+
+ ///
+ /// Returns a value that indicates whether this circle and the specified object are equal
+ ///
+ /// The object to compare with this circle.
+ /// true if this circle and the specified object are equal; otherwise, false.
+ public override readonly bool Equals(object obj) => obj is Circle other && Equals(other);
+
+ ///
+ /// Returns a value that indicates whether this circle and the specified circle are equal.
+ ///
+ /// The circle to compare with this circle.
+ /// true if this circle and the specified circle are equal; otherwise, false.
+ public readonly bool Equals(Circle other) => this.X == other.X &&
+ this.Y == other.Y &&
+ this.Radius == other.Radius;
+
+ ///
+ /// Returns the hash code for this circle.
+ ///
+ /// The hash code for this circle as a 32-bit signed integer.
+ public override readonly int GetHashCode() => HashCode.Combine(X, Y, Radius);
+
+ ///
+ /// Returns a value that indicates if the circle on the left hand side of the equality operator is equal to the
+ /// circle on the right hand side of the equality operator.
+ ///
+ /// The circle on the left hand side of the equality operator.
+ /// The circle on the right hand side of the equality operator.
+ /// true if the two circles are equal; otherwise, false.
+ public static bool operator ==(Circle lhs, Circle rhs) => lhs.Equals(rhs);
+
+ ///
+ /// Returns a value that indicates if the circle on the left hand side of the inequality operator is not equal to the
+ /// circle on the right hand side of the inequality operator.
+ ///
+ /// The circle on the left hand side of the inequality operator.
+ /// The circle on the right hand side fo the inequality operator.
+ /// true if the two circle are not equal; otherwise, false.
+ public static bool operator !=(Circle lhs, Circle rhs) => !lhs.Equals(rhs);
+}
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/MonoGameLibrary/Core.cs b/Tutorials/MobileDeployment/05-SharingLogic/MonoGameLibrary/Core.cs
new file mode 100644
index 00000000..a339155f
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/MonoGameLibrary/Core.cs
@@ -0,0 +1,208 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content;
+using Microsoft.Xna.Framework.Graphics;
+using Microsoft.Xna.Framework.Input;
+using MonoGameLibrary.Audio;
+using MonoGameLibrary.Input;
+using MonoGameLibrary.Scenes;
+
+namespace MonoGameLibrary;
+
+public class Core : Game
+{
+ internal static Core s_instance;
+
+ ///
+ /// Gets a reference to the Core instance.
+ ///
+ public static Core Instance => s_instance;
+
+ // The scene that is currently active.
+ private static Scene s_activeScene;
+
+ // The next scene to switch to, if there is one.
+ private static Scene s_nextScene;
+
+ ///
+ /// Gets the graphics device manager to control the presentation of graphics.
+ ///
+ public static GraphicsDeviceManager Graphics { get; private set; }
+
+ ///
+ /// Gets the graphics device used to create graphical resources and perform primitive rendering.
+ ///
+ public static new GraphicsDevice GraphicsDevice { get; private set; }
+
+ ///
+ /// Gets the sprite batch used for all 2D rendering.
+ ///
+ public static SpriteBatch SpriteBatch { get; private set; }
+
+ ///
+ /// Gets the content manager used to load global assets.
+ ///
+ public static new ContentManager Content { get; private set; }
+
+ ///
+ /// Gets a reference to to the input management system.
+ ///
+ public static InputManager Input { get; private set; }
+
+ ///
+ /// Gets or Sets a value that indicates if the game should exit when the esc key on the keyboard is pressed.
+ ///
+ public static bool ExitOnEscape { get; set; }
+
+ ///
+ /// Gets a reference to the audio control system.
+ ///
+ public static AudioController Audio { get; private set; }
+
+ ///
+ /// Creates a new Core instance.
+ ///
+ /// The title to display in the title bar of the game window.
+ /// The initial width, in pixels, of the game window.
+ /// The initial height, in pixels, of the game window.
+ /// Indicates if the game should start in fullscreen mode.
+ public Core(string title, int width, int height, bool fullScreen)
+ {
+ // Ensure that multiple cores are not created.
+ if (s_instance != null)
+ {
+ throw new InvalidOperationException($"Only a single Core instance can be created");
+ }
+
+ // Store reference to engine for global member access.
+ s_instance = this;
+
+ // Create a new graphics device manager.
+ Graphics = new GraphicsDeviceManager(this);
+
+ // Set the graphics defaults
+ Graphics.PreferredBackBufferWidth = width;
+ Graphics.PreferredBackBufferHeight = height;
+ Graphics.IsFullScreen = fullScreen;
+
+ // Apply the graphic presentation changes
+ Graphics.ApplyChanges();
+
+ // Set the window title
+ Window.Title = title;
+
+ // Set the core's content manager to a reference of hte base Game's
+ // content manager.
+ Content = base.Content;
+
+ // Set the root directory for content
+ Content.RootDirectory = "Content";
+
+ // Mouse is visible by default
+ IsMouseVisible = true;
+ }
+
+ protected override void Initialize()
+ {
+ base.Initialize();
+
+ // Set the core's graphics device to a reference of the base Game's
+ // graphics device.
+ GraphicsDevice = base.GraphicsDevice;
+
+ // Create the sprite batch instance.
+ SpriteBatch = new SpriteBatch(GraphicsDevice);
+
+ // Create a new input manager
+ Input = new InputManager();
+
+ // Create a new audio controller.
+ Audio = new AudioController();
+ }
+
+ protected override void UnloadContent()
+ {
+ // Dispose of the audio controller.
+ Audio.Dispose();
+
+ base.UnloadContent();
+ }
+
+ protected override void Update(GameTime gameTime)
+ {
+ // Update the input manager.
+ Input.Update(gameTime);
+
+ // Update the audio controller.
+ Audio.Update();
+
+#if !IOS
+ if (ExitOnEscape && Input.Keyboard.WasKeyJustPressed(Keys.Escape))
+ {
+ Exit();
+ }
+#endif
+
+ // if there is a next scene waiting to be switch to, then transition
+ // to that scene
+ if (s_nextScene != null)
+ {
+ TransitionScene();
+ }
+
+ // If there is an active scene, update it.
+ if (s_activeScene != null)
+ {
+ s_activeScene.Update(gameTime);
+ }
+
+ base.Update(gameTime);
+ }
+
+ protected override void Draw(GameTime gameTime)
+ {
+ // If there is an active scene, draw it.
+ if (s_activeScene != null)
+ {
+ s_activeScene.Draw(gameTime);
+ }
+
+ base.Draw(gameTime);
+ }
+
+ public static void ChangeScene(Scene next)
+ {
+ // Only set the next scene value if it is not the same
+ // instance as the currently active scene.
+ if (s_activeScene != next)
+ {
+ s_nextScene = next;
+ }
+ }
+
+ private static void TransitionScene()
+ {
+ // If there is an active scene, dispose of it
+ if (s_activeScene != null)
+ {
+ s_activeScene.Dispose();
+ }
+
+ // Force the garbage collector to collect to ensure memory is cleared
+ GC.Collect();
+
+ // Change the currently active scene to the new scene
+ s_activeScene = s_nextScene;
+
+ // Null out the next scene value so it does not trigger a change over and over.
+ s_nextScene = null;
+
+ // If the active scene now is not null, initialize it.
+ // Remember, just like with Game, the Initialize call also calls the
+ // Scene.LoadContent
+ if (s_activeScene != null)
+ {
+ s_activeScene.Initialize();
+ }
+ }
+}
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/MonoGameLibrary/Graphics/AnimatedSprite.cs b/Tutorials/MobileDeployment/05-SharingLogic/MonoGameLibrary/Graphics/AnimatedSprite.cs
new file mode 100644
index 00000000..a1a3594e
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/MonoGameLibrary/Graphics/AnimatedSprite.cs
@@ -0,0 +1,60 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGameLibrary.Graphics;
+
+public class AnimatedSprite : Sprite
+{
+ private int _currentFrame;
+ private TimeSpan _elapsed;
+ private Animation _animation;
+
+ ///
+ /// Gets or Sets the animation for this animated sprite.
+ ///
+ public Animation Animation
+ {
+ get => _animation;
+ set
+ {
+ _animation = value;
+ Region = _animation.Frames[0];
+ }
+ }
+
+ ///
+ /// Creates a new animated sprite.
+ ///
+ public AnimatedSprite() { }
+
+ ///
+ /// Creates a new animated sprite with the specified frames and delay.
+ ///
+ /// The animation for this animated sprite.
+ public AnimatedSprite(Animation animation)
+ {
+ Animation = animation;
+ }
+
+ ///
+ /// Updates this animated sprite.
+ ///
+ /// A snapshot of the game timing values provided by the framework.
+ public void Update(GameTime gameTime)
+ {
+ _elapsed += gameTime.ElapsedGameTime;
+
+ if (_elapsed >= _animation.Delay)
+ {
+ _elapsed -= _animation.Delay;
+ _currentFrame++;
+
+ if (_currentFrame >= _animation.Frames.Count)
+ {
+ _currentFrame = 0;
+ }
+
+ Region = _animation.Frames[_currentFrame];
+ }
+ }
+}
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/MonoGameLibrary/Graphics/Animation.cs b/Tutorials/MobileDeployment/05-SharingLogic/MonoGameLibrary/Graphics/Animation.cs
new file mode 100644
index 00000000..44d61b65
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/MonoGameLibrary/Graphics/Animation.cs
@@ -0,0 +1,38 @@
+using System;
+using System.Collections.Generic;
+
+namespace MonoGameLibrary.Graphics;
+
+public class Animation
+{
+ ///
+ /// The texture regions that make up the frames of this animation. The order of the regions within the collection
+ /// are the order that the frames should be displayed in.
+ ///
+ public List Frames { get; set; }
+
+ ///
+ /// The amount of time to delay between each frame before moving to the next frame for this animation.
+ ///
+ public TimeSpan Delay { get; set; }
+
+ ///
+ /// Creates a new animation.
+ ///
+ public Animation()
+ {
+ Frames = new List();
+ Delay = TimeSpan.FromMilliseconds(100);
+ }
+
+ ///
+ /// Creates a new animation with the specified frames and delay.
+ ///
+ /// An ordered collection of the frames for this animation.
+ /// The amount of time to delay between each frame of this animation.
+ public Animation(List frames, TimeSpan delay)
+ {
+ Frames = frames;
+ Delay = delay;
+ }
+}
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/MonoGameLibrary/Graphics/Sprite.cs b/Tutorials/MobileDeployment/05-SharingLogic/MonoGameLibrary/Graphics/Sprite.cs
new file mode 100644
index 00000000..20c44f0b
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/MonoGameLibrary/Graphics/Sprite.cs
@@ -0,0 +1,108 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGameLibrary.Graphics;
+
+public class Sprite
+{
+ ///
+ /// Gets or Sets the source texture region represented by this sprite.
+ ///
+ public TextureRegion Region { get; set; }
+
+ ///
+ /// Gets or Sets the color mask to apply when rendering this sprite.
+ ///
+ ///
+ /// Default value is Color.White
+ ///
+ public Color Color { get; set; } = Color.White;
+
+ ///
+ /// Gets or Sets the amount of rotation, in radians, to apply when rendering this sprite.
+ ///
+ ///
+ /// Default value is 0.0f
+ ///
+ public float Rotation { get; set; } = 0.0f;
+
+ ///
+ /// Gets or Sets the scale factor to apply to the x- and y-axes when rendering this sprite.
+ ///
+ ///
+ /// Default value is Vector2.One
+ ///
+ public Vector2 Scale { get; set; } = Vector2.One;
+
+ ///
+ /// Gets or Sets the xy-coordinate origin point, relative to the top-left corner, of this sprite.
+ ///
+ ///
+ /// Default value is Vector2.Zero
+ ///
+ public Vector2 Origin { get; set; } = Vector2.Zero;
+
+ ///
+ /// Gets or Sets the sprite effects to apply when rendering this sprite.
+ ///
+ ///
+ /// Default value is SpriteEffects.None
+ ///
+ public SpriteEffects Effects { get; set; } = SpriteEffects.None;
+
+ ///
+ /// Gets or Sets the layer depth to apply when rendering this sprite.
+ ///
+ ///
+ /// Default value is 0.0f
+ ///
+ public float LayerDepth { get; set; } = 0.0f;
+
+ ///
+ /// Gets the width, in pixels, of this sprite.
+ ///
+ ///
+ /// Width is calculated by multiplying the width of the source texture region by the x-axis scale factor.
+ ///
+ public float Width => Region.Width * Scale.X;
+
+ ///
+ /// Gets the height, in pixels, of this sprite.
+ ///
+ ///
+ /// Height is calculated by multiplying the height of the source texture region by the y-axis scale factor.
+ ///
+ public float Height => Region.Height * Scale.Y;
+
+ ///
+ /// Creates a new sprite.
+ ///
+ public Sprite() { }
+
+ ///
+ /// Creates a new sprite using the specified source texture region.
+ ///
+ /// The texture region to use as the source texture region for this sprite.
+ public Sprite(TextureRegion region)
+ {
+ Region = region;
+ }
+
+ ///
+ /// Sets the origin of this sprite to the center
+ ///
+ public void CenterOrigin()
+ {
+ Origin = new Vector2(Region.Width, Region.Height) * 0.5f;
+ }
+
+ ///
+ /// Submit this sprite for drawing to the current batch.
+ ///
+ /// The SpriteBatch instance used for batching draw calls.
+ /// The xy-coordinate position to render this sprite at.
+ public void Draw(SpriteBatch spriteBatch, Vector2 position)
+ {
+ Region.Draw(spriteBatch, position, Color, Rotation, Origin, Scale, Effects, LayerDepth);
+ }
+}
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/MonoGameLibrary/Graphics/TextureAtlas.cs b/Tutorials/MobileDeployment/05-SharingLogic/MonoGameLibrary/Graphics/TextureAtlas.cs
new file mode 100644
index 00000000..e48c9abd
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/MonoGameLibrary/Graphics/TextureAtlas.cs
@@ -0,0 +1,239 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Xml;
+using System.Xml.Linq;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content;
+using Microsoft.Xna.Framework.Graphics;
+
+
+namespace MonoGameLibrary.Graphics;
+
+public class TextureAtlas
+{
+ private Dictionary _regions;
+
+ // Stores animations added to this atlas.
+ private Dictionary _animations;
+
+ ///
+ /// Gets or Sets the source texture represented by this texture atlas.
+ ///
+ public Texture2D Texture { get; set; }
+
+ ///
+ /// Creates a new texture atlas.
+ ///
+ public TextureAtlas()
+ {
+ _regions = new Dictionary();
+ _animations = new Dictionary();
+ }
+
+ ///
+ /// Creates a new texture atlas instance using the given texture.
+ ///
+ /// The source texture represented by the texture atlas.
+ public TextureAtlas(Texture2D texture)
+ {
+ Texture = texture;
+ _regions = new Dictionary();
+ _animations = new Dictionary();
+ }
+
+ ///
+ /// Creates a new region and adds it to this texture atlas.
+ ///
+ /// The name to give the texture region.
+ /// The top-left x-coordinate position of the region boundary relative to the top-left corner of the source texture boundary.
+ /// The top-left y-coordinate position of the region boundary relative to the top-left corner of the source texture boundary.
+ /// The width, in pixels, of the region.
+ /// The height, in pixels, of the region.
+ public void AddRegion(string name, int x, int y, int width, int height)
+ {
+ TextureRegion region = new TextureRegion(Texture, x, y, width, height);
+ _regions.Add(name, region);
+ }
+
+ ///
+ /// Gets the region from this texture atlas with the specified name.
+ ///
+ /// The name of the region to retrieve.
+ /// The TextureRegion with the specified name.
+ public TextureRegion GetRegion(string name)
+ {
+ return _regions[name];
+ }
+
+ ///
+ /// Removes the region from this texture atlas with the specified name.
+ ///
+ /// The name of the region to remove.
+ ///
+ public bool RemoveRegion(string name)
+ {
+ return _regions.Remove(name);
+ }
+
+ ///
+ /// Removes all regions from this texture atlas.
+ ///
+ public void Clear()
+ {
+ _regions.Clear();
+ }
+
+ ///
+ /// Creates a new sprite using the region from this texture atlas with the specified name.
+ ///
+ /// The name of the region to create the sprite with.
+ /// A new Sprite using the texture region with the specified name.
+ public Sprite CreateSprite(string regionName)
+ {
+ TextureRegion region = GetRegion(regionName);
+ return new Sprite(region);
+ }
+
+ ///
+ /// Adds the given animation to this texture atlas with the specified name.
+ ///
+ /// The name of the animation to add.
+ /// The animation to add.
+ public void AddAnimation(string animationName, Animation animation)
+ {
+ _animations.Add(animationName, animation);
+ }
+
+ ///
+ /// Gets the animation from this texture atlas with the specified name.
+ ///
+ /// The name of the animation to retrieve.
+ /// The animation with the specified name.
+ public Animation GetAnimation(string animationName)
+ {
+ return _animations[animationName];
+ }
+
+ ///
+ /// Removes the animation with the specified name from this texture atlas.
+ ///
+ /// The name of the animation to remove.
+ /// true if the animation is removed successfully; otherwise, false.
+ public bool RemoveAnimation(string animationName)
+ {
+ return _animations.Remove(animationName);
+ }
+
+ ///
+ /// Creates a new animated sprite using the animation from this texture atlas with the specified name.
+ ///
+ /// The name of the animation to use.
+ /// A new AnimatedSprite using the animation with the specified name.
+ public AnimatedSprite CreateAnimatedSprite(string animationName)
+ {
+ Animation animation = GetAnimation(animationName);
+ return new AnimatedSprite(animation);
+ }
+
+ ///
+ /// Creates a new texture atlas based a texture atlas xml configuration file.
+ ///
+ /// The content manager used to load the texture for the atlas.
+ /// The path to the xml file, relative to the content root directory..
+ /// The texture atlas created by this method.
+ public static TextureAtlas FromFile(ContentManager content, string fileName)
+ {
+ TextureAtlas atlas = new TextureAtlas();
+
+ string filePath = Path.Combine(content.RootDirectory, fileName);
+
+ using (Stream stream = TitleContainer.OpenStream(filePath))
+ {
+ using (XmlReader reader = XmlReader.Create(stream))
+ {
+ XDocument doc = XDocument.Load(reader);
+ XElement root = doc.Root;
+
+ // The element contains the content path for the Texture2D to load.
+ // So we will retrieve that value then use the content manager to load the texture.
+ string texturePath = root.Element("Texture").Value;
+ atlas.Texture = content.Load(texturePath);
+
+ // The element contains individual elements, each one describing
+ // a different texture region within the atlas.
+ //
+ // Example:
+ //
+ //
+ //
+ //
+ //
+ // So we retrieve all of the elements then loop through each one
+ // and generate a new TextureRegion instance from it and add it to this atlas.
+ var regions = root.Element("Regions")?.Elements("Region");
+
+ if (regions != null)
+ {
+ foreach (var 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);
+ }
+ }
+ }
+
+ // The element contains individual elements, each one describing
+ // a different animation within the atlas.
+ //
+ // Example:
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ // So we retrieve all of the elements then loop through each one
+ // and generate a new Animation instance from it and add it to this atlas.
+ var animationElements = root.Element("Animations").Elements("Animation");
+
+ if (animationElements != null)
+ {
+ foreach (var animationElement in animationElements)
+ {
+ string name = animationElement.Attribute("name")?.Value;
+ float delayInMilliseconds = float.Parse(animationElement.Attribute("delay")?.Value ?? "0");
+ TimeSpan delay = TimeSpan.FromMilliseconds(delayInMilliseconds);
+
+ List frames = new List();
+
+ var frameElements = animationElement.Elements("Frame");
+
+ if (frameElements != null)
+ {
+ foreach (var frameElement in frameElements)
+ {
+ string regionName = frameElement.Attribute("region").Value;
+ TextureRegion region = atlas.GetRegion(regionName);
+ frames.Add(region);
+ }
+ }
+
+ Animation animation = new Animation(frames, delay);
+ atlas.AddAnimation(name, animation);
+ }
+ }
+
+ return atlas;
+ }
+ }
+ }
+}
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/MonoGameLibrary/Graphics/TextureRegion.cs b/Tutorials/MobileDeployment/05-SharingLogic/MonoGameLibrary/Graphics/TextureRegion.cs
new file mode 100644
index 00000000..98edc2da
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/MonoGameLibrary/Graphics/TextureRegion.cs
@@ -0,0 +1,131 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGameLibrary.Graphics;
+
+///
+/// Represents a rectangular region within a texture.
+///
+public class TextureRegion
+{
+ ///
+ /// Gets or Sets the source texture this texture region is part of.
+ ///
+ public Texture2D Texture { get; set; }
+
+ ///
+ /// Gets or Sets the source rectangle boundary of this texture region within the source texture.
+ ///
+ public Rectangle SourceRectangle { get; set; }
+
+ ///
+ /// Gets the width, in pixels, of this texture region.
+ ///
+ public int Width => SourceRectangle.Width;
+
+ ///
+ /// Gets the height, in pixels, of this texture region.
+ ///
+ public int Height => SourceRectangle.Height;
+
+ ///
+ /// Gets the top normalized texture coordinate of this region.
+ ///
+ public float TopTextureCoordinate => SourceRectangle.Top / (float)Texture.Height;
+
+ ///
+ /// Gets the bottom normalized texture coordinate of this region.
+ ///
+ public float BottomTextureCoordinate => SourceRectangle.Bottom / (float)Texture.Height;
+
+ ///
+ /// Gets the left normalized texture coordinate of this region.
+ ///
+ public float LeftTextureCoordinate => SourceRectangle.Left / (float)Texture.Width;
+
+ ///
+ /// Gets the right normalized texture coordinate of this region.
+ ///
+ public float RightTextureCoordinate => SourceRectangle.Right / (float)Texture.Width;
+
+ ///
+ /// Creates a new texture region.
+ ///
+ public TextureRegion() { }
+
+ ///
+ /// Creates a new texture region using the specified source texture.
+ ///
+ /// The texture to use as the source texture for this texture region.
+ /// The x-coordinate position of the upper-left corner of this texture region relative to the upper-left corner of the source texture.
+ /// The y-coordinate position of the upper-left corner of this texture region relative to the upper-left corner of the source texture.
+ /// The width, in pixels, of this texture region.
+ /// The height, in pixels, of this texture region.
+ public TextureRegion(Texture2D texture, int x, int y, int width, int height)
+ {
+ Texture = texture;
+ SourceRectangle = new Rectangle(x, y, width, height);
+ }
+
+ ///
+ /// Submit this texture region for drawing in the current batch.
+ ///
+ /// The spritebatch instance used for batching draw calls.
+ /// The xy-coordinate location to draw this texture region on the screen.
+ /// The color mask to apply when drawing this texture region on screen.
+ public void Draw(SpriteBatch spriteBatch, Vector2 position, Color color)
+ {
+ Draw(spriteBatch, position, color, 0.0f, Vector2.Zero, Vector2.One, SpriteEffects.None, 0.0f);
+ }
+
+ ///
+ /// Submit this texture region for drawing in the current batch.
+ ///
+ /// The spritebatch instance used for batching draw calls.
+ /// The xy-coordinate location to draw this texture region on the screen.
+ /// The color mask to apply when drawing this texture region on screen.
+ /// The amount of rotation, in radians, to apply when drawing this texture region on screen.
+ /// The center of rotation, scaling, and position when drawing this texture region on screen.
+ /// The scale factor to apply when drawing this texture region on screen.
+ /// Specifies if this texture region should be flipped horizontally, vertically, or both when drawing on screen.
+ /// The depth of the layer to use when drawing this texture region on screen.
+ public void Draw(SpriteBatch spriteBatch, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects effects, float layerDepth)
+ {
+ Draw(
+ spriteBatch,
+ position,
+ color,
+ rotation,
+ origin,
+ new Vector2(scale, scale),
+ effects,
+ layerDepth
+ );
+ }
+
+ ///
+ /// Submit this texture region for drawing in the current batch.
+ ///
+ /// The spritebatch instance used for batching draw calls.
+ /// The xy-coordinate location to draw this texture region on the screen.
+ /// The color mask to apply when drawing this texture region on screen.
+ /// The amount of rotation, in radians, to apply when drawing this texture region on screen.
+ /// The center of rotation, scaling, and position when drawing this texture region on screen.
+ /// The amount of scaling to apply to the x- and y-axes when drawing this texture region on screen.
+ /// Specifies if this texture region should be flipped horizontally, vertically, or both when drawing on screen.
+ /// The depth of the layer to use when drawing this texture region on screen.
+ public void Draw(SpriteBatch spriteBatch, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth)
+ {
+ spriteBatch.Draw(
+ Texture,
+ position,
+ SourceRectangle,
+ color,
+ rotation,
+ origin,
+ scale,
+ effects,
+ layerDepth
+ );
+ }
+}
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/MonoGameLibrary/Graphics/Tilemap.cs b/Tutorials/MobileDeployment/05-SharingLogic/MonoGameLibrary/Graphics/Tilemap.cs
new file mode 100644
index 00000000..96e1ee5e
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/MonoGameLibrary/Graphics/Tilemap.cs
@@ -0,0 +1,231 @@
+using System;
+using System.IO;
+using System.Xml;
+using System.Xml.Linq;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGameLibrary.Graphics;
+
+public class Tilemap
+{
+ private readonly Tileset _tileset;
+ private readonly int[] _tiles;
+
+ ///
+ /// Gets the total number of rows in this tilemap.
+ ///
+ public int Rows { get; }
+
+ ///
+ /// Gets the total number of columns in this tilemap.
+ ///
+ public int Columns { get; }
+
+ ///
+ /// Gets the total number of tiles in this tilemap.
+ ///
+ public int Count { get; }
+
+ ///
+ /// Gets or Sets the scale factor to draw each tile at.
+ ///
+ public Vector2 Scale { get; set; }
+
+ ///
+ /// Gets the width, in pixels, each tile is drawn at.
+ ///
+ public float TileWidth => _tileset.TileWidth * Scale.X;
+
+ ///
+ /// Gets the height, in pixels, each tile is drawn at.
+ ///
+ public float TileHeight => _tileset.TileHeight * Scale.Y;
+
+ ///
+ /// Creates a new tilemap.
+ ///
+ /// The tileset used by this tilemap.
+ /// The total number of columns in this tilemap.
+ /// The total number of rows in this tilemap.
+ public Tilemap(Tileset tileset, int columns, int rows)
+ {
+ _tileset = tileset;
+ Rows = rows;
+ Columns = columns;
+ Count = Columns * Rows;
+ Scale = Vector2.One;
+ _tiles = new int[Count];
+ }
+
+ ///
+ /// Sets the tile at the given index in this tilemap to use the tile from
+ /// the tileset at the specified tileset id.
+ ///
+ /// The index of the tile in this tilemap.
+ /// The tileset id of the tile from the tileset to use.
+ public void SetTile(int index, int tilesetID)
+ {
+ _tiles[index] = tilesetID;
+ }
+
+ ///
+ /// Sets the tile at the given column and row in this tilemap to use the tile
+ /// from the tileset at the specified tileset id.
+ ///
+ /// The column of the tile in this tilemap.
+ /// The row of the tile in this tilemap.
+ /// The tileset id of the tile from the tileset to use.
+ public void SetTile(int column, int row, int tilesetID)
+ {
+ int index = row * Columns + column;
+ SetTile(index, tilesetID);
+ }
+
+ ///
+ /// Gets the texture region of the tile from this tilemap at the specified index.
+ ///
+ /// The index of the tile in this tilemap.
+ /// The texture region of the tile from this tilemap at the specified index.
+ public TextureRegion GetTile(int index)
+ {
+ return _tileset.GetTile(_tiles[index]);
+ }
+
+ ///
+ /// Gets the texture region of the tile frm this tilemap at the specified
+ /// column and row.
+ ///
+ /// The column of the tile in this tilemap.
+ /// The row of hte tile in this tilemap.
+ /// The texture region of the tile from this tilemap at the specified column and row.
+ public TextureRegion GetTile(int column, int row)
+ {
+ int index = row * Columns + column;
+ return GetTile(index);
+ }
+
+ ///
+ /// Draws this tilemap using the given sprite batch.
+ ///
+ /// The sprite batch used to draw this tilemap.
+ public void Draw(SpriteBatch spriteBatch)
+ {
+ for (int i = 0; i < Count; i++)
+ {
+ int tileSetIndex = _tiles[i];
+ TextureRegion tile = _tileset.GetTile(tileSetIndex);
+
+ int x = i % Columns;
+ int y = i / Columns;
+
+ Vector2 position = new Vector2(x * TileWidth, y * TileHeight);
+ tile.Draw(spriteBatch, position, Color.White, 0.0f, Vector2.Zero, Scale, SpriteEffects.None, 1.0f);
+ }
+ }
+
+ ///
+ /// Creates a new tilemap based on a tilemap xml configuration file.
+ ///
+ /// The content manager used to load the texture for the tileset.
+ /// The path to the xml file, relative to the content root directory.
+ /// The tilemap created by this method.
+ public static Tilemap FromFile(ContentManager content, string filename)
+ {
+ string filePath = Path.Combine(content.RootDirectory, filename);
+
+ using (Stream stream = TitleContainer.OpenStream(filePath))
+ {
+ using (XmlReader reader = XmlReader.Create(stream))
+ {
+ XDocument doc = XDocument.Load(reader);
+ XElement root = doc.Root;
+
+ // The element contains the information about the tileset
+ // used by the tilemap.
+ //
+ // Example
+ // contentPath
+ //
+ // The region attribute represents the x, y, width, and height
+ // components of the boundary for the texture region within the
+ // texture at the contentPath specified.
+ //
+ // the tileWidth and tileHeight attributes specify the width and
+ // height of each tile in the tileset.
+ //
+ // the contentPath value is the contentPath to the texture to
+ // load that contains the tileset
+ XElement tilesetElement = root.Element("Tileset");
+
+ string regionAttribute = tilesetElement.Attribute("region").Value;
+ string[] split = regionAttribute.Split(" ", StringSplitOptions.RemoveEmptyEntries);
+ int x = int.Parse(split[0]);
+ int y = int.Parse(split[1]);
+ int width = int.Parse(split[2]);
+ int height = int.Parse(split[3]);
+
+ int tileWidth = int.Parse(tilesetElement.Attribute("tileWidth").Value);
+ int tileHeight = int.Parse(tilesetElement.Attribute("tileHeight").Value);
+ string contentPath = tilesetElement.Value;
+
+ // Load the texture 2d at the content path
+ Texture2D texture = content.Load(contentPath);
+
+ // Create the texture region from the texture
+ TextureRegion textureRegion = new TextureRegion(texture, x, y, width, height);
+
+ // Create the tileset using the texture region
+ Tileset tileset = new Tileset(textureRegion, tileWidth, tileHeight);
+
+ // The element contains lines of strings where each line
+ // represents a row in the tilemap. Each line is a space
+ // separated string where each element represents a column in that
+ // row. The value of the column is the id of the tile in the
+ // tileset to draw for that location.
+ //
+ // Example:
+ //
+ // 00 01 01 02
+ // 03 04 04 05
+ // 03 04 04 05
+ // 06 07 07 08
+ //
+ XElement tilesElement = root.Element("Tiles");
+
+ // Split the value of the tiles data into rows by splitting on
+ // the new line character
+ string[] rows = tilesElement.Value.Trim().Split('\n', StringSplitOptions.RemoveEmptyEntries);
+
+ // Split the value of the first row to determine the total number of columns
+ int columnCount = rows[0].Split(" ", StringSplitOptions.RemoveEmptyEntries).Length;
+
+ // Create the tilemap
+ Tilemap tilemap = new Tilemap(tileset, columnCount, rows.Length);
+
+ // Process each row
+ for (int row = 0; row < rows.Length; row++)
+ {
+ // Split the row into individual columns
+ string[] columns = rows[row].Trim().Split(" ", StringSplitOptions.RemoveEmptyEntries);
+
+ // Process each column of the current row
+ for (int column = 0; column < columnCount; column++)
+ {
+ // Get the tileset index for this location
+ int tilesetIndex = int.Parse(columns[column]);
+
+ // Get the texture region of that tile from the tileset
+ TextureRegion region = tileset.GetTile(tilesetIndex);
+
+ // Add that region to the tilemap at the row and column location
+ tilemap.SetTile(column, row, tilesetIndex);
+ }
+ }
+
+ return tilemap;
+ }
+ }
+ }
+}
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/MonoGameLibrary/Graphics/Tileset.cs b/Tutorials/MobileDeployment/05-SharingLogic/MonoGameLibrary/Graphics/Tileset.cs
new file mode 100644
index 00000000..80c2e65a
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/MonoGameLibrary/Graphics/Tileset.cs
@@ -0,0 +1,76 @@
+namespace MonoGameLibrary.Graphics;
+
+public class Tileset
+{
+ private readonly TextureRegion[] _tiles;
+
+ ///
+ /// Gets the width, in pixels, of each tile in this tileset.
+ ///
+ public int TileWidth { get; }
+
+ ///
+ /// Gets the height, in pixels, of each tile in this tileset.
+ ///
+ public int TileHeight { get; }
+
+ ///
+ /// Gets the total number of columns in this tileset.
+ ///
+ public int Columns { get; }
+
+ ///
+ /// Gets the total number of rows in this tileset.
+ ///
+ public int Rows { get; }
+
+ ///
+ /// Gets the total number of tiles in this tileset.
+ ///
+ public int Count { get; }
+
+ ///
+ /// Creates a new tileset based on the given texture region with the specified
+ /// tile width and height.
+ ///
+ /// The texture region that contains the tiles for the tileset.
+ /// The width of each tile in the tileset.
+ /// The height of each tile in the tileset.
+ public Tileset(TextureRegion textureRegion, int tileWidth, int tileHeight)
+ {
+ TileWidth = tileWidth;
+ TileHeight = tileHeight;
+ Columns = textureRegion.Width / tileWidth;
+ Rows = textureRegion.Height / tileHeight;
+ Count = Columns * Rows;
+
+ // Create the texture regions that make up each individual tile
+ _tiles = new TextureRegion[Count];
+
+ for (int i = 0; i < Count; i++)
+ {
+ int x = i % Columns * tileWidth;
+ int y = i / Columns * tileHeight;
+ _tiles[i] = new TextureRegion(textureRegion.Texture, textureRegion.SourceRectangle.X + x, textureRegion.SourceRectangle.Y + y, tileWidth, tileHeight);
+ }
+ }
+
+ ///
+ /// Gets the texture region for the tile from this tileset at the given index.
+ ///
+ /// The index of the texture region in this tile set.
+ /// The texture region for the tile form this tileset at the given index.
+ public TextureRegion GetTile(int index) => _tiles[index];
+
+ ///
+ /// Gets the texture region for the tile from this tileset at the given location.
+ ///
+ /// The column in this tileset of the texture region.
+ /// The row in this tileset of the texture region.
+ /// The texture region for the tile from this tileset at given location.
+ public TextureRegion GetTile(int column, int row)
+ {
+ int index = row * Columns + column;
+ return GetTile(index);
+ }
+}
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/MonoGameLibrary/Input/GamePadInfo.cs b/Tutorials/MobileDeployment/05-SharingLogic/MonoGameLibrary/Input/GamePadInfo.cs
new file mode 100644
index 00000000..5e3d0a00
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/MonoGameLibrary/Input/GamePadInfo.cs
@@ -0,0 +1,140 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Input;
+
+namespace MonoGameLibrary.Input;
+
+public class GamePadInfo
+{
+ private TimeSpan _vibrationTimeRemaining = TimeSpan.Zero;
+
+ ///
+ /// Gets the index of the player this gamepad is for.
+ ///
+ public PlayerIndex PlayerIndex { get; }
+
+ ///
+ /// Gets the state of input for this gamepad during the previous update cycle.
+ ///
+ public GamePadState PreviousState { get; private set; }
+
+ ///
+ /// Gets the state of input for this gamepad during the current update cycle.
+ ///
+ public GamePadState CurrentState { get; private set; }
+
+ ///
+ /// Gets a value that indicates if this gamepad is currently connected.
+ ///
+ public bool IsConnected => CurrentState.IsConnected;
+
+ ///
+ /// Gets the value of the left thumbstick of this gamepad.
+ ///
+ public Vector2 LeftThumbStick => CurrentState.ThumbSticks.Left;
+
+ ///
+ /// Gets the value of the right thumbstick of this gamepad.
+ ///
+ public Vector2 RightThumbStick => CurrentState.ThumbSticks.Right;
+
+ ///
+ /// Gets the value of the left trigger of this gamepad.
+ ///
+ public float LeftTrigger => CurrentState.Triggers.Left;
+
+ ///
+ /// Gets the value of the right trigger of this gamepad.
+ ///
+ public float RightTrigger => CurrentState.Triggers.Right;
+
+ ///
+ /// Creates a new GamePadInfo for the gamepad connected at the specified player index.
+ ///
+ /// The index of the player for this gamepad.
+ public GamePadInfo(PlayerIndex playerIndex)
+ {
+ PlayerIndex = playerIndex;
+ PreviousState = new GamePadState();
+ CurrentState = GamePad.GetState(playerIndex);
+ }
+
+ ///
+ /// Updates the state information for this gamepad input.
+ ///
+ ///
+ public void Update(GameTime gameTime)
+ {
+ PreviousState = CurrentState;
+ CurrentState = GamePad.GetState(PlayerIndex);
+
+ if (_vibrationTimeRemaining > TimeSpan.Zero)
+ {
+ _vibrationTimeRemaining -= gameTime.ElapsedGameTime;
+
+ if (_vibrationTimeRemaining <= TimeSpan.Zero)
+ {
+ StopVibration();
+ }
+ }
+ }
+
+ ///
+ /// Returns a value that indicates whether the specified gamepad button is current down.
+ ///
+ /// The gamepad button to check.
+ /// true if the specified gamepad button is currently down; otherwise, false.
+ public bool IsButtonDown(Buttons button)
+ {
+ return CurrentState.IsButtonDown(button);
+ }
+
+ ///
+ /// Returns a value that indicates whether the specified gamepad button is currently up.
+ ///
+ /// The gamepad button to check.
+ /// true if the specified gamepad button is currently up; otherwise, false.
+ public bool IsButtonUp(Buttons button)
+ {
+ return CurrentState.IsButtonUp(button);
+ }
+
+ ///
+ /// Returns a value that indicates whether the specified gamepad button was just pressed on the current frame.
+ ///
+ /// The gamepad button to check.
+ /// true if the specified gamepad button was just pressed on the current frame; otherwise, false.
+ public bool WasButtonJustPressed(Buttons button)
+ {
+ return CurrentState.IsButtonDown(button) && PreviousState.IsButtonUp(button);
+ }
+
+ ///
+ /// Returns a value that indicates whether the specified gamepad button was just released on the current frame.
+ ///
+ /// The gamepad button to check.
+ /// true if the specified gamepad button was just released on the current frame; otherwise, false.
+ public bool WasButtonJustReleased(Buttons button)
+ {
+ return CurrentState.IsButtonUp(button) && PreviousState.IsButtonDown(button);
+ }
+
+ ///
+ /// Sets the vibration for all motors of this gamepad.
+ ///
+ /// The strength of the vibration from 0.0f (none) to 1.0f (full).
+ /// The amount of time the vibration should occur.
+ public void SetVibration(float strength, TimeSpan time)
+ {
+ _vibrationTimeRemaining = time;
+ GamePad.SetVibration(PlayerIndex, strength, strength);
+ }
+
+ ///
+ /// Stops the vibration of all motors for this gamepad.
+ ///
+ public void StopVibration()
+ {
+ GamePad.SetVibration(PlayerIndex, 0.0f, 0.0f);
+ }
+}
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/MonoGameLibrary/Input/InputManager.cs b/Tutorials/MobileDeployment/05-SharingLogic/MonoGameLibrary/Input/InputManager.cs
new file mode 100644
index 00000000..902dee18
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/MonoGameLibrary/Input/InputManager.cs
@@ -0,0 +1,51 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGameLibrary.Input;
+
+public class InputManager
+{
+ ///
+ /// Gets the state information of keyboard input.
+ ///
+ public KeyboardInfo Keyboard { get; private set; }
+
+ ///
+ /// Gets the state information of mouse input.
+ ///
+ public MouseInfo Mouse { get; private set; }
+
+ ///
+ /// Gets the state information of a gamepad.
+ ///
+ public GamePadInfo[] GamePads { get; private set; }
+
+ ///
+ /// Creates a new InputManager.
+ ///
+ public InputManager()
+ {
+ Keyboard = new KeyboardInfo();
+ Mouse = new MouseInfo();
+
+ GamePads = new GamePadInfo[4];
+ for (int i = 0; i < 4; i++)
+ {
+ GamePads[i] = new GamePadInfo((PlayerIndex)i);
+ }
+ }
+
+ ///
+ /// Updates the state information for the keyboard, mouse, and gamepad inputs.
+ ///
+ /// A snapshot of the timing values for the current frame.
+ public void Update(GameTime gameTime)
+ {
+ Keyboard.Update();
+ Mouse.Update();
+
+ for (int i = 0; i < 4; i++)
+ {
+ GamePads[i].Update(gameTime);
+ }
+ }
+}
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/MonoGameLibrary/Input/KeyboardInfo.cs b/Tutorials/MobileDeployment/05-SharingLogic/MonoGameLibrary/Input/KeyboardInfo.cs
new file mode 100644
index 00000000..c6770cb0
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/MonoGameLibrary/Input/KeyboardInfo.cs
@@ -0,0 +1,74 @@
+using Microsoft.Xna.Framework.Input;
+
+namespace MonoGameLibrary.Input;
+
+public class KeyboardInfo
+{
+ ///
+ /// Gets the state of keyboard input during the previous update cycle.
+ ///
+ public KeyboardState PreviousState { get; private set; }
+
+ ///
+ /// Gets the state of keyboard input during the current input cycle.
+ ///
+ public KeyboardState CurrentState { get; private set; }
+
+ ///
+ /// Creates a new KeyboardInfo
+ ///
+ public KeyboardInfo()
+ {
+ PreviousState = new KeyboardState();
+ CurrentState = Keyboard.GetState();
+ }
+
+ ///
+ /// Updates the state information about keyboard input.
+ ///
+ public void Update()
+ {
+ PreviousState = CurrentState;
+ CurrentState = Keyboard.GetState();
+ }
+
+ ///
+ /// Returns a value that indicates if the specified key is currently down.
+ ///
+ /// The key to check.
+ /// true if the specified key is currently down; otherwise, false.
+ public bool IsKeyDown(Keys key)
+ {
+ return CurrentState.IsKeyDown(key);
+ }
+
+ ///
+ /// Returns a value that indicates whether the specified key is currently up.
+ ///
+ /// The key to check.
+ /// true if the specified key is currently up; otherwise, false.
+ public bool IsKeyUp(Keys key)
+ {
+ return CurrentState.IsKeyUp(key);
+ }
+
+ ///
+ /// Returns a value that indicates if the specified key was just pressed on the current frame.
+ ///
+ /// The key to check.
+ /// true if the specified key was just pressed on the current frame; otherwise, false.
+ public bool WasKeyJustPressed(Keys key)
+ {
+ return CurrentState.IsKeyDown(key) && PreviousState.IsKeyUp(key);
+ }
+
+ ///
+ /// Returns a value that indicates if the specified key was just released on the current frame.
+ ///
+ /// The key to check.
+ /// true if the specified key was just released on the current frame; otherwise, false.
+ public bool WasKeyJustReleased(Keys key)
+ {
+ return CurrentState.IsKeyUp(key) && PreviousState.IsKeyDown(key);
+ }
+}
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/MonoGameLibrary/Input/MouseButton.cs b/Tutorials/MobileDeployment/05-SharingLogic/MonoGameLibrary/Input/MouseButton.cs
new file mode 100644
index 00000000..5b041f80
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/MonoGameLibrary/Input/MouseButton.cs
@@ -0,0 +1,10 @@
+namespace MonoGameLibrary.Input;
+
+public enum MouseButton
+{
+ Left,
+ Middle,
+ Right,
+ XButton1,
+ XButton2
+}
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/MonoGameLibrary/Input/MouseInfo.cs b/Tutorials/MobileDeployment/05-SharingLogic/MonoGameLibrary/Input/MouseInfo.cs
new file mode 100644
index 00000000..09d6207c
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/MonoGameLibrary/Input/MouseInfo.cs
@@ -0,0 +1,208 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Input;
+
+namespace MonoGameLibrary.Input;
+
+public class MouseInfo
+{
+ ///
+ /// The state of mouse input during the previous update cycle.
+ ///
+ public MouseState PreviousState { get; private set; }
+
+ ///
+ /// The state of mouse input during the current update cycle.
+ ///
+ public MouseState CurrentState { get; private set; }
+
+ ///
+ /// Gets or Sets the current position of the mouse cursor in screen space.
+ ///
+ public Point Position
+ {
+ get => CurrentState.Position;
+ set => SetPosition(value.X, value.Y);
+ }
+
+ ///
+ /// Gets or Sets the current x-coordinate position of the mouse cursor in screen space.
+ ///
+ public int X
+ {
+ get => CurrentState.X;
+ set => SetPosition(value, CurrentState.Y);
+ }
+
+ ///
+ /// Gets or Sets the current y-coordinate position of the mouse cursor in screen space.
+ ///
+ public int Y
+ {
+ get => CurrentState.Y;
+ set => SetPosition(CurrentState.X, value);
+ }
+
+ ///
+ /// Gets the difference in the mouse cursor position between the previous and current frame.
+ ///
+ public Point PositionDelta => CurrentState.Position - PreviousState.Position;
+
+ ///
+ /// Gets the difference in the mouse cursor x-position between the previous and current frame.
+ ///
+ public int XDelta => CurrentState.X - PreviousState.X;
+
+ ///
+ /// Gets the difference in the mouse cursor y-position between the previous and current frame.
+ ///
+ public int YDelta => CurrentState.Y - PreviousState.Y;
+
+ ///
+ /// Gets a value that indicates if the mouse cursor moved between the previous and current frames.
+ ///
+ public bool WasMoved => PositionDelta != Point.Zero;
+
+ ///
+ /// Gets the cumulative value of the mouse scroll wheel since the start of the game.
+ ///
+ public int ScrollWheel => CurrentState.ScrollWheelValue;
+
+ ///
+ /// Gets the value of the scroll wheel between the previous and current frame.
+ ///
+ public int ScrollWheelDelta => CurrentState.ScrollWheelValue - PreviousState.ScrollWheelValue;
+
+ ///
+ /// Creates a new MouseInfo.
+ ///
+ public MouseInfo()
+ {
+ PreviousState = new MouseState();
+ CurrentState = Mouse.GetState();
+ }
+
+ ///
+ /// Updates the state information about mouse input.
+ ///
+ public void Update()
+ {
+ PreviousState = CurrentState;
+ CurrentState = Mouse.GetState();
+ }
+
+ ///
+ /// Returns a value that indicates whether the specified mouse button is currently down.
+ ///
+ /// The mouse button to check.
+ /// true if the specified mouse button is currently down; otherwise, false.
+ public bool IsButtonDown(MouseButton button)
+ {
+ switch (button)
+ {
+ case MouseButton.Left:
+ return CurrentState.LeftButton == ButtonState.Pressed;
+ case MouseButton.Middle:
+ return CurrentState.MiddleButton == ButtonState.Pressed;
+ case MouseButton.Right:
+ return CurrentState.RightButton == ButtonState.Pressed;
+ case MouseButton.XButton1:
+ return CurrentState.XButton1 == ButtonState.Pressed;
+ case MouseButton.XButton2:
+ return CurrentState.XButton2 == ButtonState.Pressed;
+ default:
+ return false;
+ }
+ }
+
+ ///
+ /// Returns a value that indicates whether the specified mouse button is current up.
+ ///
+ /// The mouse button to check.
+ /// true if the specified mouse button is currently up; otherwise, false.
+ public bool IsButtonUp(MouseButton button)
+ {
+ switch (button)
+ {
+ case MouseButton.Left:
+ return CurrentState.LeftButton == ButtonState.Released;
+ case MouseButton.Middle:
+ return CurrentState.MiddleButton == ButtonState.Released;
+ case MouseButton.Right:
+ return CurrentState.RightButton == ButtonState.Released;
+ case MouseButton.XButton1:
+ return CurrentState.XButton1 == ButtonState.Released;
+ case MouseButton.XButton2:
+ return CurrentState.XButton2 == ButtonState.Released;
+ default:
+ return false;
+ }
+ }
+
+ ///
+ /// Returns a value that indicates whether the specified mouse button was just pressed on the current frame.
+ ///
+ /// The mouse button to check.
+ /// true if the specified mouse button was just pressed on the current frame; otherwise, false.
+ public bool WasButtonJustPressed(MouseButton button)
+ {
+ switch (button)
+ {
+ case MouseButton.Left:
+ return CurrentState.LeftButton == ButtonState.Pressed && PreviousState.LeftButton == ButtonState.Released;
+ case MouseButton.Middle:
+ return CurrentState.MiddleButton == ButtonState.Pressed && PreviousState.MiddleButton == ButtonState.Released;
+ case MouseButton.Right:
+ return CurrentState.RightButton == ButtonState.Pressed && PreviousState.RightButton == ButtonState.Released;
+ case MouseButton.XButton1:
+ return CurrentState.XButton1 == ButtonState.Pressed && PreviousState.XButton1 == ButtonState.Released;
+ case MouseButton.XButton2:
+ return CurrentState.XButton2 == ButtonState.Pressed && PreviousState.XButton2 == ButtonState.Released;
+ default:
+ return false;
+ }
+ }
+
+ ///
+ /// Returns a value that indicates whether the specified mouse button was just released on the current frame.
+ ///
+ /// The mouse button to check.
+ /// true if the specified mouse button was just released on the current frame; otherwise, false.F
+ public bool WasButtonJustReleased(MouseButton button)
+ {
+ switch (button)
+ {
+ case MouseButton.Left:
+ return CurrentState.LeftButton == ButtonState.Released && PreviousState.LeftButton == ButtonState.Pressed;
+ case MouseButton.Middle:
+ return CurrentState.MiddleButton == ButtonState.Released && PreviousState.MiddleButton == ButtonState.Pressed;
+ case MouseButton.Right:
+ return CurrentState.RightButton == ButtonState.Released && PreviousState.RightButton == ButtonState.Pressed;
+ case MouseButton.XButton1:
+ return CurrentState.XButton1 == ButtonState.Released && PreviousState.XButton1 == ButtonState.Pressed;
+ case MouseButton.XButton2:
+ return CurrentState.XButton2 == ButtonState.Released && PreviousState.XButton2 == ButtonState.Pressed;
+ default:
+ return false;
+ }
+ }
+
+ ///
+ /// Sets the current position of the mouse cursor in screen space and updates the CurrentState with the new position.
+ ///
+ /// The x-coordinate location of the mouse cursor in screen space.
+ /// The y-coordinate location of the mouse cursor in screen space.
+ public void SetPosition(int x, int y)
+ {
+ Mouse.SetPosition(x, y);
+ CurrentState = new MouseState(
+ x,
+ y,
+ CurrentState.ScrollWheelValue,
+ CurrentState.LeftButton,
+ CurrentState.MiddleButton,
+ CurrentState.RightButton,
+ CurrentState.XButton1,
+ CurrentState.XButton2
+ );
+ }
+}
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/MonoGameLibrary/MonoGameLibrary.csproj b/Tutorials/MobileDeployment/05-SharingLogic/MonoGameLibrary/MonoGameLibrary.csproj
new file mode 100644
index 00000000..b0e2438e
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/MonoGameLibrary/MonoGameLibrary.csproj
@@ -0,0 +1,14 @@
+
+
+ net9.0;net9.0-ios;net9.0-android
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/MonoGameLibrary/Scenes/Scene.cs b/Tutorials/MobileDeployment/05-SharingLogic/MonoGameLibrary/Scenes/Scene.cs
new file mode 100644
index 00000000..627d220f
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/MonoGameLibrary/Scenes/Scene.cs
@@ -0,0 +1,104 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content;
+
+namespace MonoGameLibrary.Scenes;
+
+public abstract class Scene : IDisposable
+{
+ ///
+ /// Gets the ContentManager used for loading scene-specific assets.
+ ///
+ ///
+ /// Assets loaded through this ContentManager will be automatically unloaded when this scene ends.
+ ///
+ protected ContentManager Content { get; }
+
+ ///
+ /// Gets a value that indicates if the scene has been disposed of.
+ ///
+ public bool IsDisposed { get; private set; }
+
+ ///
+ /// Creates a new scene instance.
+ ///
+ public Scene()
+ {
+ // Create a content manager for the scene
+ Content = new ContentManager(Core.Content.ServiceProvider);
+
+ // Set the root directory for content to the same as the root directory
+ // for the game's content.
+ Content.RootDirectory = Core.Content.RootDirectory;
+ }
+
+ // Finalizer, called when object is cleaned up by garbage collector.
+ ~Scene() => Dispose(false);
+
+ ///
+ /// Initializes the scene.
+ ///
+ ///
+ /// When overriding this in a derived class, ensure that base.Initialize()
+ /// still called as this is when LoadContent is called.
+ ///
+ public virtual void Initialize()
+ {
+ LoadContent();
+ }
+
+ ///
+ /// Override to provide logic to load content for the scene.
+ ///
+ public virtual void LoadContent() { }
+
+ ///
+ /// Unloads scene-specific content.
+ ///
+ public virtual void UnloadContent()
+ {
+ Content.Unload();
+ }
+
+ ///
+ /// Updates this scene.
+ ///
+ /// A snapshot of the timing values for the current frame.
+ public virtual void Update(GameTime gameTime) { }
+
+ ///
+ /// Draws this scene.
+ ///
+ /// A snapshot of the timing values for the current frame.
+ public virtual void Draw(GameTime gameTime) { }
+
+ ///
+ /// Disposes of this scene.
+ ///
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ /// Disposes of this scene.
+ ///
+ /// '
+ /// Indicates whether managed resources should be disposed. This value is only true when called from the main
+ /// Dispose method. When called from the finalizer, this will be false.
+ ///
+ protected virtual void Dispose(bool disposing)
+ {
+ if (IsDisposed)
+ {
+ return;
+ }
+
+ if (disposing)
+ {
+ UnloadContent();
+ Content.Dispose();
+ }
+ }
+}
diff --git a/Tutorials/MobileDeployment/05-SharingLogic/README.md b/Tutorials/MobileDeployment/05-SharingLogic/README.md
new file mode 100644
index 00000000..2511a893
--- /dev/null
+++ b/Tutorials/MobileDeployment/05-SharingLogic/README.md
@@ -0,0 +1,35 @@
+# Chapter 5: Sharing Logic and Simplifying Changes
+
+This chapter demonstrates how to convert a Windows-only MonoGame project to support iOS and Android platforms using a shared codebase architecture.
+
+The chapter covers:
+
+* Converting a single-platform project to multi-platform structure.
+* Setting up a shared common library with multi-targeting.
+* Creating platform-specific project shells for Windows, iOS, and Android.
+* Configuring conditional package references for each platform.
+* Understanding cross-platform project architecture and naming conventions.
+* Updating third-party libraries for cross-platform compatibility.
+
+## Project Structure
+
+This sample includes:
+
+* **DungeonSlime.Common** - Shared game logic library with multi-targeting support
+* **DungeonSlime.Windows** - Windows desktop project shell
+* **DungeonSlime.iOS** - iOS mobile project shell
+* **DungeonSlime.Android** - Android mobile project shell
+
+## Prerequisites
+
+* Completed the MonoGame 2D tutorial
+* Development environment set up
+* For iOS: Mac with Xcode and Apple Developer account
+* For Android: Android SDK and development tools
+
+## Key Features Demonstrated
+
+* Multi-targeting framework configuration (`net8.0;net8.0-ios;net8.0-android`)
+* Platform-specific MonoGame package references
+* Shared code architecture for cross-platform development
+* Modern .NET project management with Central Package Management
diff --git a/Tutorials/MobileDeployment/06-Publishing/.config/dotnet-tools.json b/Tutorials/MobileDeployment/06-Publishing/.config/dotnet-tools.json
new file mode 100644
index 00000000..7f4505d0
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/.config/dotnet-tools.json
@@ -0,0 +1,36 @@
+{
+ "version": 1,
+ "isRoot": true,
+ "tools": {
+ "dotnet-mgcb": {
+ "version": "3.8.4.1",
+ "commands": [
+ "mgcb"
+ ]
+ },
+ "dotnet-mgcb-editor": {
+ "version": "3.8.4.1",
+ "commands": [
+ "mgcb-editor"
+ ]
+ },
+ "dotnet-mgcb-editor-linux": {
+ "version": "3.8.4.1",
+ "commands": [
+ "mgcb-editor-linux"
+ ]
+ },
+ "dotnet-mgcb-editor-windows": {
+ "version": "3.8.4.1",
+ "commands": [
+ "mgcb-editor-windows"
+ ]
+ },
+ "dotnet-mgcb-editor-mac": {
+ "version": "3.8.4.1",
+ "commands": [
+ "mgcb-editor-mac"
+ ]
+ }
+ }
+}
diff --git a/Tutorials/MobileDeployment/06-Publishing/Directory.Packages.props b/Tutorials/MobileDeployment/06-Publishing/Directory.Packages.props
new file mode 100644
index 00000000..4f656aec
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/Directory.Packages.props
@@ -0,0 +1,13 @@
+
+
+ true
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/.config/dotnet-tools.json b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/.config/dotnet-tools.json
new file mode 100644
index 00000000..fbedee15
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/.config/dotnet-tools.json
@@ -0,0 +1,36 @@
+{
+ "version": 1,
+ "isRoot": true,
+ "tools": {
+ "dotnet-mgcb": {
+ "version": "3.8.4",
+ "commands": [
+ "mgcb"
+ ]
+ },
+ "dotnet-mgcb-editor": {
+ "version": "3.8.4",
+ "commands": [
+ "mgcb-editor"
+ ]
+ },
+ "dotnet-mgcb-editor-linux": {
+ "version": "3.8.4",
+ "commands": [
+ "mgcb-editor-linux"
+ ]
+ },
+ "dotnet-mgcb-editor-windows": {
+ "version": "3.8.4",
+ "commands": [
+ "mgcb-editor-windows"
+ ]
+ },
+ "dotnet-mgcb-editor-mac": {
+ "version": "3.8.4",
+ "commands": [
+ "mgcb-editor-mac"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/Activity1.cs b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/Activity1.cs
new file mode 100644
index 00000000..b0aafd05
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/Activity1.cs
@@ -0,0 +1,35 @@
+using Android.App;
+using Android.Content.PM;
+using Android.OS;
+using Android.Views;
+using Microsoft.Xna.Framework;
+
+namespace DungeonSlime.Android
+{
+ [Activity(
+ Label = "@string/app_name",
+ MainLauncher = true,
+ Icon = "@drawable/icon",
+ AlwaysRetainTaskState = true,
+ LaunchMode = LaunchMode.SingleInstance,
+ ScreenOrientation = ScreenOrientation.Landscape,
+ ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.Keyboard | ConfigChanges.KeyboardHidden |
+ ConfigChanges.ScreenSize
+ )]
+ public class Activity1 : AndroidGameActivity
+ {
+ private Game1 _game;
+ private View _view;
+
+ protected override void OnCreate(Bundle bundle)
+ {
+ base.OnCreate(bundle);
+
+ _game = new Game1();
+ _view = _game.Services.GetService(typeof(View)) as View;
+
+ SetContentView(_view);
+ _game.Run();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/AndroidManifest.xml b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/AndroidManifest.xml
new file mode 100644
index 00000000..9461d961
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/AndroidManifest.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/Content/Content.mgcb b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/Content/Content.mgcb
new file mode 100644
index 00000000..57d4a056
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/Content/Content.mgcb
@@ -0,0 +1,105 @@
+
+#----------------------------- Global Properties ----------------------------#
+
+/outputDir:bin/$(Platform)
+/intermediateDir:obj/$(Platform)
+/platform:Android
+/config:
+/profile:Reach
+/compress:False
+
+#-------------------------------- References --------------------------------#
+
+
+#---------------------------------- Content ---------------------------------#
+
+
+#begin audio/bounce.wav
+/importer:WavImporter
+/processor:SoundEffectProcessor
+/processorParam:Quality=Best
+/build:audio/bounce.wav
+
+#begin audio/collect.wav
+/importer:WavImporter
+/processor:SoundEffectProcessor
+/processorParam:Quality=Best
+/build:audio/collect.wav
+
+#begin audio/theme.ogg
+/importer:OggImporter
+/processor:SongProcessor
+/processorParam:Quality=Best
+/build:audio/theme.ogg
+
+#begin audio/ui.wav
+/importer:WavImporter
+/processor:SoundEffectProcessor
+/processorParam:Quality=Best
+/build:audio/ui.wav
+
+#begin effects/grayscaleEffect.fx
+/importer:EffectImporter
+/processor:EffectProcessor
+/processorParam:DebugMode=Auto
+/build:effects/grayscaleEffect.fx
+
+#begin fonts/04B_30_5x.spritefont
+/importer:FontDescriptionImporter
+/processor:FontDescriptionProcessor
+/processorParam:PremultiplyAlpha=True
+/processorParam:TextureFormat=Compressed
+/build:fonts/04B_30_5x.spritefont
+
+#begin fonts/04b_30.fnt
+/copy:fonts/04b_30.fnt
+
+#begin fonts/04B_30.spritefont
+/importer:FontDescriptionImporter
+/processor:FontDescriptionProcessor
+/processorParam:PremultiplyAlpha=True
+/processorParam:TextureFormat=Compressed
+/build:fonts/04B_30.spritefont
+
+#begin images/atlas-definition.xml
+/copy:images/atlas-definition.xml
+
+#begin images/atlas.png
+/importer:TextureImporter
+/processor:TextureProcessor
+/processorParam:ColorKeyColor=255,0,255,255
+/processorParam:ColorKeyEnabled=True
+/processorParam:GenerateMipmaps=False
+/processorParam:PremultiplyAlpha=True
+/processorParam:ResizeToPowerOfTwo=False
+/processorParam:MakeSquare=False
+/processorParam:TextureFormat=Color
+/build:images/atlas.png
+
+#begin images/background-pattern.png
+/importer:TextureImporter
+/processor:TextureProcessor
+/processorParam:ColorKeyColor=255,0,255,255
+/processorParam:ColorKeyEnabled=True
+/processorParam:GenerateMipmaps=False
+/processorParam:PremultiplyAlpha=True
+/processorParam:ResizeToPowerOfTwo=False
+/processorParam:MakeSquare=False
+/processorParam:TextureFormat=Color
+/build:images/background-pattern.png
+
+#begin images/logo.png
+/importer:TextureImporter
+/processor:TextureProcessor
+/processorParam:ColorKeyColor=255,0,255,255
+/processorParam:ColorKeyEnabled=True
+/processorParam:GenerateMipmaps=False
+/processorParam:PremultiplyAlpha=True
+/processorParam:ResizeToPowerOfTwo=False
+/processorParam:MakeSquare=False
+/processorParam:TextureFormat=Color
+/build:images/logo.png
+
+#begin images/tilemap-definition.xml
+/copy:images/tilemap-definition.xml
+
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/Content/audio/bounce.wav b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/Content/audio/bounce.wav
new file mode 100644
index 00000000..baa7a47b
Binary files /dev/null and b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/Content/audio/bounce.wav differ
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/Content/audio/collect.wav b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/Content/audio/collect.wav
new file mode 100644
index 00000000..506220de
Binary files /dev/null and b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/Content/audio/collect.wav differ
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/Content/audio/theme.ogg b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/Content/audio/theme.ogg
new file mode 100644
index 00000000..72e1fd3b
Binary files /dev/null and b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/Content/audio/theme.ogg differ
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/Content/audio/ui.wav b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/Content/audio/ui.wav
new file mode 100644
index 00000000..63e8941e
Binary files /dev/null and b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/Content/audio/ui.wav differ
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/Content/effects/grayscaleEffect.fx b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/Content/effects/grayscaleEffect.fx
new file mode 100644
index 00000000..5dd0d8b6
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/Content/effects/grayscaleEffect.fx
@@ -0,0 +1,53 @@
+#if OPENGL
+ #define SV_POSITION POSITION
+ #define VS_SHADERMODEL vs_3_0
+ #define PS_SHADERMODEL ps_3_0
+#else
+ #define VS_SHADERMODEL vs_4_0_level_9_1
+ #define PS_SHADERMODEL ps_4_0_level_9_1
+#endif
+
+Texture2D SpriteTexture;
+
+// A value between 0 and 1 that controls the intensity of the grayscale effect.
+// 0 = full color, 1 = full grayscale.
+float Saturation = 1.0;
+
+sampler2D SpriteTextureSampler = sampler_state
+{
+ Texture = ;
+};
+
+struct VertexShaderOutput
+{
+ float4 Position : SV_POSITION;
+ float4 Color : COLOR0;
+ float2 TextureCoordinates : TEXCOORD0;
+};
+
+float4 MainPS(VertexShaderOutput input) : COLOR
+{
+ // Sample the texture
+ float4 color = tex2D(SpriteTextureSampler, input.TextureCoordinates) * input.Color;
+
+ // Calculate the grayscale value based on human perception of colors
+ float grayscale = dot(color.rgb, float3(0.3, 0.59, 0.11));
+
+ // create a grayscale color vector (same value for R, G, and B)
+ float3 grayscaleColor = float3(grayscale, grayscale, grayscale);
+
+ // Linear interpolation between he grayscale color and the original color's
+ // rgb values based on the saturation parameter.
+ float3 finalColor = lerp(grayscale, color.rgb, Saturation);
+
+ // Return the final color with the original alpha value
+ return float4(finalColor, color.a);
+}
+
+technique SpriteDrawing
+{
+ pass P0
+ {
+ PixelShader = compile PS_SHADERMODEL MainPS();
+ }
+};
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/Content/fonts/04B_30.spritefont b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/Content/fonts/04B_30.spritefont
new file mode 100644
index 00000000..63d4728c
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/Content/fonts/04B_30.spritefont
@@ -0,0 +1,16 @@
+
+
+
+ 04B_30.ttf
+ 17.5
+ 0
+ true
+
+
+
+
+ ~
+
+
+
+
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/Content/fonts/04B_30.ttf b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/Content/fonts/04B_30.ttf
new file mode 100644
index 00000000..4b93740c
Binary files /dev/null and b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/Content/fonts/04B_30.ttf differ
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/Content/fonts/04B_30_5x.spritefont b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/Content/fonts/04B_30_5x.spritefont
new file mode 100644
index 00000000..dd239a53
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/Content/fonts/04B_30_5x.spritefont
@@ -0,0 +1,16 @@
+
+
+
+ 04B_30.ttf
+ 87.5
+ 0
+ true
+
+
+
+
+ ~
+
+
+
+
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/Content/fonts/04b_30.fnt b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/Content/fonts/04b_30.fnt
new file mode 100644
index 00000000..772f8c54
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/Content/fonts/04b_30.fnt
@@ -0,0 +1,99 @@
+info face="04b30" size=35 bold=0 italic=0 charset="" unicode=1 stretchH=100 smooth=0 aa=1 padding=0,0,0,0 spacing=1,1 outline=0
+common lineHeight=35 base=31 scaleW=256 scaleH=512 pages=1 packed=0 alphaChnl=0 redChnl=4 greenChnl=4 blueChnl=4
+page id=0 file="../images/atlas.png"
+chars count=95
+char id=32 x=30 y=152 width=3 height=1 xoffset=-1 yoffset=34 xadvance=29 page=0 chnl=15
+char id=33 x=240 y=30 width=14 height=29 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=34 x=102 y=232 width=25 height=15 xoffset=1 yoffset=4 xadvance=29 page=0 chnl=15
+char id=35 x=184 y=0 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=36 x=250 y=150 width=3 height=1 xoffset=-1 yoffset=34 xadvance=29 page=0 chnl=15
+char id=37 x=0 y=34 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=38 x=30 y=150 width=3 height=1 xoffset=-1 yoffset=34 xadvance=29 page=0 chnl=15
+char id=39 x=245 y=202 width=10 height=15 xoffset=1 yoffset=4 xadvance=14 page=0 chnl=15
+char id=40 x=106 y=150 width=16 height=29 xoffset=1 yoffset=2 xadvance=21 page=0 chnl=15
+char id=41 x=123 y=150 width=16 height=29 xoffset=1 yoffset=2 xadvance=21 page=0 chnl=15
+char id=42 x=128 y=232 width=14 height=15 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=43 x=94 y=0 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=44 x=143 y=232 width=10 height=14 xoffset=1 yoffset=19 xadvance=14 page=0 chnl=15
+char id=45 x=154 y=232 width=25 height=11 xoffset=1 yoffset=12 xadvance=29 page=0 chnl=15
+char id=46 x=231 y=228 width=10 height=10 xoffset=1 yoffset=19 xadvance=14 page=0 chnl=15
+char id=47 x=60 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=48 x=90 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=49 x=46 y=150 width=19 height=29 xoffset=1 yoffset=2 xadvance=23 page=0 chnl=15
+char id=50 x=150 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=51 x=180 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=52 x=210 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=53 x=0 y=94 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=54 x=180 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=55 x=60 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=56 x=90 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=57 x=120 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=58 x=234 y=202 width=10 height=25 xoffset=1 yoffset=4 xadvance=14 page=0 chnl=15
+char id=59 x=244 y=0 width=10 height=29 xoffset=1 yoffset=4 xadvance=14 page=0 chnl=15
+char id=60 x=86 y=150 width=19 height=29 xoffset=1 yoffset=2 xadvance=23 page=0 chnl=15
+char id=61 x=182 y=176 width=25 height=25 xoffset=1 yoffset=4 xadvance=29 page=0 chnl=15
+char id=62 x=237 y=120 width=18 height=29 xoffset=1 yoffset=2 xadvance=23 page=0 chnl=15
+char id=63 x=180 y=120 width=28 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=64 x=34 y=150 width=3 height=1 xoffset=-1 yoffset=34 xadvance=29 page=0 chnl=15
+char id=65 x=120 y=120 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=66 x=150 y=120 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=67 x=124 y=0 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=68 x=154 y=0 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=69 x=214 y=0 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=70 x=30 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=71 x=60 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=72 x=90 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=73 x=240 y=90 width=14 height=29 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=74 x=120 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=75 x=150 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=76 x=209 y=120 width=27 height=29 xoffset=1 yoffset=2 xadvance=31 page=0 chnl=15
+char id=77 x=30 y=0 width=31 height=29 xoffset=1 yoffset=2 xadvance=35 page=0 chnl=15
+char id=78 x=210 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=79 x=0 y=64 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=80 x=30 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=81 x=0 y=0 width=29 height=33 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=82 x=120 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=83 x=30 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=84 x=150 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=85 x=180 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=86 x=210 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=87 x=62 y=0 width=31 height=29 xoffset=1 yoffset=2 xadvance=35 page=0 chnl=15
+char id=88 x=0 y=124 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=89 x=30 y=120 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=90 x=60 y=120 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=91 x=240 y=60 width=14 height=29 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=92 x=90 y=120 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=93 x=140 y=150 width=14 height=29 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=94 x=180 y=232 width=16 height=11 xoffset=1 yoffset=4 xadvance=21 page=0 chnl=15
+char id=95 x=0 y=262 width=29 height=10 xoffset=1 yoffset=21 xadvance=33 page=0 chnl=15
+char id=96 x=197 y=228 width=16 height=11 xoffset=1 yoffset=4 xadvance=21 page=0 chnl=15
+char id=97 x=208 y=176 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=98 x=0 y=210 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=99 x=26 y=210 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=100 x=52 y=206 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=101 x=78 y=206 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=102 x=104 y=206 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=103 x=130 y=206 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=104 x=156 y=206 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=105 x=234 y=176 width=12 height=25 xoffset=1 yoffset=6 xadvance=16 page=0 chnl=15
+char id=106 x=182 y=202 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=107 x=208 y=202 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=108 x=78 y=232 width=23 height=25 xoffset=1 yoffset=6 xadvance=27 page=0 chnl=15
+char id=109 x=197 y=150 width=26 height=25 xoffset=1 yoffset=6 xadvance=31 page=0 chnl=15
+char id=110 x=0 y=236 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=111 x=26 y=236 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=112 x=78 y=180 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=113 x=0 y=154 width=25 height=29 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=114 x=52 y=232 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=115 x=224 y=150 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=116 x=0 y=184 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=117 x=26 y=184 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=118 x=52 y=180 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=119 x=170 y=150 width=26 height=25 xoffset=1 yoffset=6 xadvance=31 page=0 chnl=15
+char id=120 x=104 y=180 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=121 x=130 y=180 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=122 x=156 y=180 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=123 x=26 y=154 width=19 height=29 xoffset=1 yoffset=2 xadvance=23 page=0 chnl=15
+char id=124 x=155 y=150 width=14 height=29 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=125 x=66 y=150 width=19 height=29 xoffset=1 yoffset=2 xadvance=23 page=0 chnl=15
+char id=126 x=214 y=228 width=16 height=11 xoffset=1 yoffset=4 xadvance=21 page=0 chnl=15
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/Content/images/atlas-definition.xml b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/Content/images/atlas-definition.xml
new file mode 100644
index 00000000..21772022
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/Content/images/atlas-definition.xml
@@ -0,0 +1,34 @@
+
+
+ images/atlas
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/Content/images/atlas.png b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/Content/images/atlas.png
new file mode 100644
index 00000000..f7def20f
Binary files /dev/null and b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/Content/images/atlas.png differ
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/Content/images/background-pattern.png b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/Content/images/background-pattern.png
new file mode 100644
index 00000000..2d8d878e
Binary files /dev/null and b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/Content/images/background-pattern.png differ
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/Content/images/logo.png b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/Content/images/logo.png
new file mode 100644
index 00000000..1509036c
Binary files /dev/null and b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/Content/images/logo.png differ
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/Content/images/tilemap-definition.xml b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/Content/images/tilemap-definition.xml
new file mode 100644
index 00000000..85658c60
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/Content/images/tilemap-definition.xml
@@ -0,0 +1,15 @@
+
+
+ images/atlas
+
+ 00 01 02 01 02 01 02 01 02 01 02 01 02 01 02 03
+ 04 05 05 06 05 05 06 05 05 06 05 05 06 05 05 07
+ 08 09 09 09 09 09 09 09 09 09 09 09 09 09 09 11
+ 04 09 09 09 09 09 09 09 10 09 09 09 09 10 09 07
+ 08 09 10 09 09 09 09 09 09 09 09 09 09 09 09 11
+ 04 09 09 09 09 09 09 09 09 09 09 09 09 09 09 07
+ 08 10 09 09 09 09 09 09 09 09 10 09 09 09 09 11
+ 04 09 09 09 09 09 10 09 09 09 09 09 09 09 09 07
+ 12 13 14 13 14 13 14 13 14 13 14 13 14 13 14 15
+
+
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/DungeonSlime.Android.csproj b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/DungeonSlime.Android.csproj
new file mode 100644
index 00000000..7daf3d23
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/DungeonSlime.Android.csproj
@@ -0,0 +1,26 @@
+
+
+ net9.0-android
+ 23
+ Exe
+ com.companyname.DungeonSlime.Android
+ 1
+ 1.0
+
+
+ False
+
+
+ False
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/Resources/Drawable/Icon.png b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/Resources/Drawable/Icon.png
new file mode 100644
index 00000000..25fe0444
Binary files /dev/null and b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/Resources/Drawable/Icon.png differ
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/Resources/Values/Strings.xml b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/Resources/Values/Strings.xml
new file mode 100644
index 00000000..7a59ecad
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Android/Resources/Values/Strings.xml
@@ -0,0 +1,4 @@
+
+
+ DungeonSlime.Android
+
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Common/.config/dotnet-tools.json b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Common/.config/dotnet-tools.json
new file mode 100644
index 00000000..b345335f
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Common/.config/dotnet-tools.json
@@ -0,0 +1,30 @@
+{
+ "version": 1,
+ "isRoot": true,
+ "tools": {
+ "dotnet-mgcb-editor": {
+ "version": "3.8.4",
+ "commands": [
+ "mgcb-editor"
+ ]
+ },
+ "dotnet-mgcb-editor-linux": {
+ "version": "3.8.4",
+ "commands": [
+ "mgcb-editor-linux"
+ ]
+ },
+ "dotnet-mgcb-editor-windows": {
+ "version": "3.8.4",
+ "commands": [
+ "mgcb-editor-windows"
+ ]
+ },
+ "dotnet-mgcb-editor-mac": {
+ "version": "3.8.4",
+ "commands": [
+ "mgcb-editor-mac"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Common/DungeonSlime.Common.csproj b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Common/DungeonSlime.Common.csproj
new file mode 100644
index 00000000..338dff36
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Common/DungeonSlime.Common.csproj
@@ -0,0 +1,20 @@
+
+
+ net9.0;net9.0-ios;net9.0-android
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Common/Game1.cs b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Common/Game1.cs
new file mode 100644
index 00000000..b216f41a
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Common/Game1.cs
@@ -0,0 +1,81 @@
+using System;
+using DungeonSlime.Scenes;
+using Microsoft.Xna.Framework.Media;
+using MonoGameLibrary;
+using MonoGameGum;
+using Gum.Forms.Controls;
+
+namespace DungeonSlime;
+
+public class Game1 : Core
+{
+ // The background theme song
+ private Song _themeSong;
+
+ public Game1() : base("Dungeon Slime", 1280, 720, false)
+ {
+ }
+
+ protected override void Initialize()
+ {
+ Window.AllowUserResizing = true;
+
+ try
+ {
+ base.Initialize();
+
+ // Start playing the background music
+ Audio.PlaySong(_themeSong);
+
+ // Initialize the Gum UI service
+ InitializeGum();
+
+ // Start the game with the title scene.
+ ChangeScene(new TitleScene());
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine(e);
+ throw;
+ }
+ }
+
+ private void InitializeGum()
+ {
+ // Initialize the Gum service
+ GumService.Default.Initialize(this);
+
+ // Tell the Gum service which content manager to use. We will tell it to
+ // use the global content manager from our Core.
+ GumService.Default.ContentLoader.XnaContentManager = Core.Content;
+
+ // Register keyboard input for UI control.
+ FrameworkElement.KeyboardsForUiControl.Add(GumService.Default.Keyboard);
+
+ // Register gamepad input for Ui control.
+ FrameworkElement.GamePadsForUiControl.AddRange(GumService.Default.Gamepads);
+
+ // Customize the tab reverse UI navigation to also trigger when the keyboard
+ // Up arrow key is pushed.
+ FrameworkElement.TabReverseKeyCombos.Add(
+ new KeyCombo() { PushedKey = Microsoft.Xna.Framework.Input.Keys.Up });
+
+ // Customize the tab UI navigation to also trigger when the keyboard
+ // Down arrow key is pushed.
+ FrameworkElement.TabKeyCombos.Add(
+ new KeyCombo() { PushedKey = Microsoft.Xna.Framework.Input.Keys.Down });
+
+ // The assets created for the UI were done so at 1/4th the size to keep the size of the
+ // texture atlas small. So we will set the default canvas size to be 1/4th the size of
+ // the game's resolution then tell gum to zoom in by a factor of 4.
+ GumService.Default.CanvasWidth = GraphicsDevice.PresentationParameters.BackBufferWidth / 4.0f;
+ GumService.Default.CanvasHeight = GraphicsDevice.PresentationParameters.BackBufferHeight / 4.0f;
+ GumService.Default.Renderer.Camera.Zoom = 4.0f;
+ }
+
+ protected override void LoadContent()
+ {
+ // Load the background theme music
+ _themeSong = Content.Load("audio/theme");
+ }
+}
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Common/GameController.cs b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Common/GameController.cs
new file mode 100644
index 00000000..a3b5841d
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Common/GameController.cs
@@ -0,0 +1,119 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Input;
+using MonoGameLibrary;
+using MonoGameLibrary.Input;
+
+namespace DungeonSlime;
+
+///
+/// Provides a game-specific input abstraction that maps physical inputs
+/// to game actions, bridging our input system with game-specific functionality.
+///
+public static class GameController
+{
+ private static KeyboardInfo s_keyboard => Core.Input.Keyboard;
+ private static GamePadInfo s_gamePad => Core.Input.GamePads[(int)PlayerIndex.One];
+ private static TouchInfo s_touch => Core.Input.Touch;
+ private const float TOUCH_MOVEMENT_THRESHOLD = 20.0f;
+
+ ///
+ /// Returns true if the player has triggered the "move up" action.
+ ///
+ public static bool MoveUp()
+ {
+ return s_keyboard.WasKeyJustPressed(Keys.Up) ||
+ s_keyboard.WasKeyJustPressed(Keys.W) ||
+ s_gamePad.WasButtonJustPressed(Buttons.DPadUp) ||
+ s_gamePad.WasButtonJustPressed(Buttons.LeftThumbstickUp) ||
+ IsTouchSwipeUp();
+ }
+
+ ///
+ /// Returns true if the player has triggered the "move down" action.
+ ///
+ public static bool MoveDown()
+ {
+ return s_keyboard.WasKeyJustPressed(Keys.Down) ||
+ s_keyboard.WasKeyJustPressed(Keys.S) ||
+ s_gamePad.WasButtonJustPressed(Buttons.DPadDown) ||
+ s_gamePad.WasButtonJustPressed(Buttons.LeftThumbstickDown) ||
+ IsTouchSwipeDown();
+ }
+
+ ///
+ /// Returns true if the player has triggered the "move left" action.
+ ///
+ public static bool MoveLeft()
+ {
+ return s_keyboard.WasKeyJustPressed(Keys.Left) ||
+ s_keyboard.WasKeyJustPressed(Keys.A) ||
+ s_gamePad.WasButtonJustPressed(Buttons.DPadLeft) ||
+ s_gamePad.WasButtonJustPressed(Buttons.LeftThumbstickLeft) ||
+ IsTouchSwipeLeft();
+ }
+
+ ///
+ /// Returns true if the player has triggered the "move right" action.
+ ///
+ public static bool MoveRight()
+ {
+ return s_keyboard.WasKeyJustPressed(Keys.Right) ||
+ s_keyboard.WasKeyJustPressed(Keys.D) ||
+ s_gamePad.WasButtonJustPressed(Buttons.DPadRight) ||
+ s_gamePad.WasButtonJustPressed(Buttons.LeftThumbstickRight) ||
+ IsTouchSwipeRight();
+ }
+
+ ///
+ /// Returns true if the player has triggered the "pause" action.
+ ///
+ public static bool Pause()
+ {
+ return s_keyboard.WasKeyJustPressed(Keys.Escape) ||
+ s_gamePad.WasButtonJustPressed(Buttons.Start);
+ }
+
+ ///
+ /// Returns true if the player has triggered the "action" button,
+ /// typically used for menu confirmation.
+ ///
+ public static bool Action()
+ {
+ return s_keyboard.WasKeyJustPressed(Keys.Enter) ||
+ s_gamePad.WasButtonJustPressed(Buttons.A) ||
+ s_touch.WasTouchJustPressed();
+ }
+
+ private static bool IsTouchSwipeUp()
+ {
+ if (!s_touch.HasTouches) return false;
+
+ var delta = s_touch.GetPrimaryTouchDelta();
+ return delta.Y < -TOUCH_MOVEMENT_THRESHOLD && Math.Abs(delta.X) < Math.Abs(delta.Y);
+ }
+
+ private static bool IsTouchSwipeDown()
+ {
+ if (!s_touch.HasTouches) return false;
+
+ var delta = s_touch.GetPrimaryTouchDelta();
+ return delta.Y > TOUCH_MOVEMENT_THRESHOLD && Math.Abs(delta.X) < Math.Abs(delta.Y);
+ }
+
+ private static bool IsTouchSwipeLeft()
+ {
+ if (!s_touch.HasTouches) return false;
+
+ var delta = s_touch.GetPrimaryTouchDelta();
+ return delta.X < -TOUCH_MOVEMENT_THRESHOLD && Math.Abs(delta.Y) < Math.Abs(delta.X);
+ }
+
+ private static bool IsTouchSwipeRight()
+ {
+ if (!s_touch.HasTouches) return false;
+
+ var delta = s_touch.GetPrimaryTouchDelta();
+ return delta.X > TOUCH_MOVEMENT_THRESHOLD && Math.Abs(delta.Y) < Math.Abs(delta.X);
+ }
+}
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Common/GameObjects/Bat.cs b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Common/GameObjects/Bat.cs
new file mode 100644
index 00000000..ddc855ed
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Common/GameObjects/Bat.cs
@@ -0,0 +1,123 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Audio;
+using MonoGameLibrary;
+using MonoGameLibrary.Graphics;
+
+namespace DungeonSlime.GameObjects;
+
+public class Bat
+{
+ private const float MOVEMENT_SPEED = 5.0f;
+
+ // The velocity of the bat that defines the direction and how much in that
+ // direction to update the bats position each update cycle.
+ private Vector2 _velocity;
+
+ // The AnimatedSprite used when drawing the bat.
+ private AnimatedSprite _sprite;
+
+ // The sound effect to play when the bat bounces off the edge of the room.
+ private SoundEffect _bounceSoundEffect;
+
+ ///
+ /// Gets or Sets the position of the bat.
+ ///
+ public Vector2 Position { get; set; }
+
+ ///
+ /// Creates a new Bat using the specified animated sprite and sound effect.
+ ///
+ /// The AnimatedSprite ot use when drawing the bat.
+ /// The sound effect to play when the bat bounces off a wall.
+ public Bat(AnimatedSprite sprite, SoundEffect bounceSoundEffect)
+ {
+ _sprite = sprite;
+ _bounceSoundEffect = bounceSoundEffect;
+ }
+
+ ///
+ /// Randomizes the velocity of the bat.
+ ///
+ public void RandomizeVelocity()
+ {
+ // Generate a random angle
+ float angle = (float)(Random.Shared.NextDouble() * MathHelper.TwoPi);
+
+ // Convert the angle to a direction vector
+ float x = (float)Math.Cos(angle);
+ float y = (float)Math.Sin(angle);
+ Vector2 direction = new Vector2(x, y);
+
+ // Multiply the direction vector by the movement speed to get the
+ // final velocity
+ _velocity = direction * MOVEMENT_SPEED;
+ }
+
+ ///
+ /// Handles a bounce event when the bat collides with a wall or boundary.
+ ///
+ /// The normal vector of the surface the bat is bouncing against.
+ public void Bounce(Vector2 normal)
+ {
+ Vector2 newPosition = Position;
+
+ // Adjust the position based on the normal to prevent sticking to walls.
+ if (normal.X != 0)
+ {
+ // We are bouncing off a vertical wall (left/right).
+ // Move slightly away from the wall in the direction of the normal.
+ newPosition.X += normal.X * (_sprite.Width * 0.1f);
+ }
+
+ if (normal.Y != 0)
+ {
+ // We are bouncing off a horizontal wall (top/bottom).
+ // Move slightly way from the wall in the direction of the normal.
+ newPosition.Y += normal.Y * (_sprite.Height * 0.1f);
+ }
+
+ // Apply the new position
+ Position = newPosition;
+
+ // Apply reflection based on the normal.
+ _velocity = Vector2.Reflect(_velocity, normal);
+
+ // Play the bounce sound effect.
+ Core.Audio.PlaySoundEffect(_bounceSoundEffect);
+ }
+
+ ///
+ /// Returns a Circle value that represents collision bounds of the bat.
+ ///
+ /// A Circle value.
+ public Circle GetBounds()
+ {
+ int x = (int)(Position.X + _sprite.Width * 0.5f);
+ int y = (int)(Position.Y + _sprite.Height * 0.5f);
+ int radius = (int)(_sprite.Width * 0.25f);
+
+ return new Circle(x, y, radius);
+ }
+
+ ///
+ /// Updates the bat.
+ ///
+ /// A snapshot of the timing values for the current update cycle.
+ public void Update(GameTime gameTime)
+ {
+ // Update the animated sprite
+ _sprite.Update(gameTime);
+
+ // Update the position of the bat based on the velocity.
+ Position += _velocity;
+ }
+
+ ///
+ /// Draws the bat.
+ ///
+ public void Draw()
+ {
+ _sprite.Draw(Core.SpriteBatch, Position);
+ }
+}
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Common/GameObjects/Slime.cs b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Common/GameObjects/Slime.cs
new file mode 100644
index 00000000..08b5a63d
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Common/GameObjects/Slime.cs
@@ -0,0 +1,265 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Xna.Framework;
+using MonoGameLibrary;
+using MonoGameLibrary.Graphics;
+
+namespace DungeonSlime.GameObjects;
+
+public class Slime
+{
+ // A constant value that represents the amount of time to wait between
+ // movement updates.
+ private static readonly TimeSpan s_movementTime = TimeSpan.FromMilliseconds(200);
+
+ // The amount of time that has elapsed since the last movement update.
+ private TimeSpan _movementTimer;
+
+ // Normalized value (0-1) representing progress between movement ticks for visual interpolation
+ private float _movementProgress;
+
+ // The next direction to apply to the head of the slime chain during the
+ // next movement update.
+ private Vector2 _nextDirection;
+
+ // The number of pixels to move the head segment during the movement cycle.
+ private float _stride;
+
+ // Tracks the segments of the slime chain.
+ private List _segments;
+
+ // The AnimatedSprite used when drawing each slime segment
+ private AnimatedSprite _sprite;
+
+ // Buffer to queue inputs input by player during input polling.
+ private Queue _inputBuffer;
+
+ // The maximum size of the buffer queue.
+ private const int MAX_BUFFER_SIZE = 2;
+
+ ///
+ /// Event that is raised if it is detected that the head segment of the slime
+ /// has collided with a body segment.
+ ///
+ public event EventHandler BodyCollision;
+
+ ///
+ /// Creates a new Slime using the specified animated sprite.
+ ///
+ /// The AnimatedSprite to use when drawing the slime.
+ public Slime(AnimatedSprite sprite)
+ {
+ _sprite = sprite;
+ }
+
+ ///
+ /// Initializes the slime, can be used to reset it back to an initial state.
+ ///
+ /// The position the slime should start at.
+ /// The total number of pixels to move the head segment during each movement cycle.
+ public void Initialize(Vector2 startingPosition, float stride)
+ {
+ // Initialize the segment collection.
+ _segments = new List();
+
+ // Set the stride
+ _stride = stride;
+
+ // Create the initial head of the slime chain.
+ SlimeSegment head = new SlimeSegment();
+ head.At = startingPosition;
+ head.To = startingPosition + new Vector2(_stride, 0);
+ head.Direction = Vector2.UnitX;
+
+ // Add it to the segment collection.
+ _segments.Add(head);
+
+ // Set the initial next direction as the same direction the head is
+ // moving.
+ _nextDirection = head.Direction;
+
+ // Zero out the movement timer.
+ _movementTimer = TimeSpan.Zero;
+
+ // initialize the input buffer.
+ _inputBuffer = new Queue(MAX_BUFFER_SIZE);
+ }
+
+ private void HandleInput()
+ {
+ Vector2 potentialNextDirection = Vector2.Zero;
+
+ if (GameController.MoveUp())
+ {
+ potentialNextDirection = -Vector2.UnitY;
+ }
+ else if (GameController.MoveDown())
+ {
+ potentialNextDirection = Vector2.UnitY;
+ }
+ else if (GameController.MoveLeft())
+ {
+ potentialNextDirection = -Vector2.UnitX;
+ }
+ else if (GameController.MoveRight())
+ {
+ potentialNextDirection = Vector2.UnitX;
+ }
+
+ // If a new direction was input, consider adding it to the buffer
+ if (potentialNextDirection != Vector2.Zero && _inputBuffer.Count < MAX_BUFFER_SIZE)
+ {
+ // If the buffer is empty, validate against the current direction;
+ // otherwise, validate against the last buffered direction
+ Vector2 validateAgainst = _inputBuffer.Count > 0 ?
+ _inputBuffer.Last() :
+ _segments[0].Direction;
+
+ // Only allow direction change if it is not reversing the current
+ // direction. This prevents th slime from backing into itself
+ float dot = Vector2.Dot(potentialNextDirection, validateAgainst);
+ if (dot >= 0)
+ {
+ _inputBuffer.Enqueue(potentialNextDirection);
+ }
+ }
+ }
+
+ private void Move()
+ {
+ // Get the next direction from the input buffer if one is available
+ if (_inputBuffer.Count > 0)
+ {
+ _nextDirection = _inputBuffer.Dequeue();
+ }
+
+ // Capture the value of the head segment
+ SlimeSegment head = _segments[0];
+
+ // Update the direction the head is supposed to move in to the
+ // next direction cached.
+ head.Direction = _nextDirection;
+
+ // Update the head's "at" position to be where it was moving "to"
+ head.At = head.To;
+
+ // Update the head's "to" position to the next tile in the direction
+ // it is moving.
+ head.To = head.At + head.Direction * _stride;
+
+ // Insert the new adjusted value for the head at the front of the
+ // segments and remove the tail segment. This effectively moves
+ // the entire chain forward without needing to loop through every
+ // segment and update its "at" and "to" positions.
+ _segments.Insert(0, head);
+ _segments.RemoveAt(_segments.Count - 1);
+
+ // Iterate through all of the segments except the head and check
+ // if they are at the same position as the head. If they are, then
+ // the head is colliding with a body segment and a body collision
+ // has occurred.
+ for (int i = 1; i < _segments.Count; i++)
+ {
+ SlimeSegment segment = _segments[i];
+
+ if (head.At == segment.At)
+ {
+ if (BodyCollision != null)
+ {
+ BodyCollision.Invoke(this, EventArgs.Empty);
+ }
+
+ return;
+ }
+ }
+ }
+
+ ///
+ /// Informs the slime to grow by one segment.
+ ///
+ public void Grow()
+ {
+ // Capture the value of the tail segment
+ SlimeSegment tail = _segments[_segments.Count - 1];
+
+ // Create a new tail segment that is positioned a grid cell in the
+ // reverse direction from the tail moving to the tail.
+ SlimeSegment newTail = new SlimeSegment();
+ newTail.At = tail.To + tail.ReverseDirection * _stride;
+ newTail.To = tail.At;
+ newTail.Direction = Vector2.Normalize(tail.At - newTail.At);
+
+ // Add the new tail segment
+ _segments.Add(newTail);
+ }
+
+ ///
+ /// Updates the slime.
+ ///
+ /// A snapshot of the timing values for the current update cycle.
+ public void Update(GameTime gameTime)
+ {
+ // Update the animated sprite.
+ _sprite.Update(gameTime);
+
+ // Handle any player input
+ HandleInput();
+
+ // Increment the movement timer by the frame elapsed time.
+ _movementTimer += gameTime.ElapsedGameTime;
+
+ // If the movement timer has accumulated enough time to be greater than
+ // the movement time threshold, then perform a full movement.
+ if (_movementTimer >= s_movementTime)
+ {
+ _movementTimer -= s_movementTime;
+ Move();
+ }
+
+ // Update the movement lerp offset amount
+ _movementProgress = (float)(_movementTimer.TotalSeconds / s_movementTime.TotalSeconds);
+ }
+
+ ///
+ /// Draws the slime.
+ ///
+ public void Draw()
+ {
+ // Iterate through each segment and draw it
+ foreach (SlimeSegment segment in _segments)
+ {
+ // Calculate the visual position of the segment at the moment by
+ // lerping between its "at" and "to" position by the movement
+ // offset lerp amount
+ Vector2 pos = Vector2.Lerp(segment.At, segment.To, _movementProgress);
+
+ // Draw the slime sprite at the calculated visual position of this
+ // segment
+ _sprite.Draw(Core.SpriteBatch, pos);
+ }
+ }
+
+ ///
+ /// Returns a Circle value that represents collision bounds of the slime.
+ ///
+ /// A Circle value.
+ public Circle GetBounds()
+ {
+ SlimeSegment head = _segments[0];
+
+ // Calculate the visual position of the head at the moment of this
+ // method call by lerping between the "at" and "to" position by the
+ // movement offset lerp amount
+ Vector2 pos = Vector2.Lerp(head.At, head.To, _movementProgress);
+
+ // Create the bounds using the calculated visual position of the head.
+ Circle bounds = new Circle(
+ (int)(pos.X + (_sprite.Width * 0.5f)),
+ (int)(pos.Y + (_sprite.Height * 0.5f)),
+ (int)(_sprite.Width * 0.5f)
+ );
+
+ return bounds;
+ }
+}
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Common/GameObjects/SlimeSegment.cs b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Common/GameObjects/SlimeSegment.cs
new file mode 100644
index 00000000..b00189eb
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Common/GameObjects/SlimeSegment.cs
@@ -0,0 +1,26 @@
+using Microsoft.Xna.Framework;
+
+namespace DungeonSlime.GameObjects;
+
+public struct SlimeSegment
+{
+ ///
+ /// The position this slime segment is at before the movement cycle occurs.
+ ///
+ public Vector2 At;
+
+ ///
+ /// The position this slime segment should move to during the next movement cycle.
+ ///
+ public Vector2 To;
+
+ ///
+ /// The direction this slime segment is moving.
+ ///
+ public Vector2 Direction;
+
+ ///
+ /// The opposite direction this slime segment is moving.
+ ///
+ public Vector2 ReverseDirection => new Vector2(-Direction.X, -Direction.Y);
+}
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Common/Scenes/GameScene.cs b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Common/Scenes/GameScene.cs
new file mode 100644
index 00000000..4004af7f
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Common/Scenes/GameScene.cs
@@ -0,0 +1,437 @@
+using DungeonSlime.GameObjects;
+using DungeonSlime.UI;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Audio;
+using Microsoft.Xna.Framework.Graphics;
+using MonoGame.Extended.ViewportAdapters;
+using MonoGameGum;
+using MonoGameLibrary;
+using MonoGameLibrary.Graphics;
+using MonoGameLibrary.Scenes;
+using System;
+
+namespace DungeonSlime.Scenes;
+
+public class GameScene : Scene
+{
+ private enum GameState
+ {
+ Playing,
+ Paused,
+ GameOver
+ }
+
+ // Reference to the slime.
+ private Slime _slime;
+
+ // Reference to the bat.
+ private Bat _bat;
+
+ // Defines the tilemap to draw.
+ private Tilemap _tilemap;
+
+ // Defines the bounds of the room that the slime and bat are contained within.
+ private Rectangle _roomBounds;
+
+ // The sound effect to play when the slime eats a bat.
+ private SoundEffect _collectSoundEffect;
+
+ // Tracks the players score.
+ private int _score;
+
+ private GameSceneUI _ui;
+
+ private GameState _state;
+
+ // The grayscale shader effect.
+ private Effect _grayscaleEffect;
+
+ // The amount of saturation to provide the grayscale shader effect
+ private float _saturation = 1.0f;
+
+ // The speed of the fade to grayscale effect.
+ private const float FADE_SPEED = 0.02f;
+
+ public override void Initialize()
+ {
+ // LoadContent is called during base.Initialize().
+ base.Initialize();
+
+ // During the game scene, we want to disable exit on escape. Instead,
+ // the escape key will be used to return back to the title screen
+ Core.ExitOnEscape = false;
+
+ // Create the room bounds using the virtual viewport size (1280x720) instead of actual window size
+ // This ensures characters stay within bounds regardless of window scaling/stretching
+ _roomBounds = new Rectangle(0, 0, 1280, 720);
+ _roomBounds.Inflate(-_tilemap.TileWidth, -_tilemap.TileHeight);
+
+ // Subscribe to the slime's BodyCollision event so that a game over
+ // can be triggered when this event is raised.
+ _slime.BodyCollision += OnSlimeBodyCollision;
+
+ // Create any UI elements from the root element created in previous
+ // scenes
+ GumService.Default.Root.Children.Clear();
+
+ _viewport = new(Core.GameWindow, Core.Graphics.GraphicsDevice, 1280, 720);
+
+ // Initialize the user interface for the game scene.
+ InitializeUI();
+
+ // Initialize a new game to be played.
+ InitializeNewGame();
+ }
+
+ BoxingViewportAdapter _viewport;
+
+ private void InitializeUI()
+ {
+ // Clear out any previous UI element incase we came here
+ // from a different scene.
+ GumService.Default.Root.Children.Clear();
+
+ // Create the game scene ui instance.
+ _ui = new GameSceneUI();
+
+ // Subscribe to the events from the game scene ui.
+ _ui.ResumeButtonClick += OnResumeButtonClicked;
+ _ui.RetryButtonClick += OnRetryButtonClicked;
+ _ui.QuitButtonClick += OnQuitButtonClicked;
+ }
+
+ private void OnResumeButtonClicked(object sender, EventArgs args)
+ {
+ // Change the game state back to playing
+ _state = GameState.Playing;
+ }
+
+ private void OnRetryButtonClicked(object sender, EventArgs args)
+ {
+ // Player has chosen to retry, so initialize a new game
+ InitializeNewGame();
+ }
+
+ private void OnQuitButtonClicked(object sender, EventArgs args)
+ {
+ // Player has chosen to quit, so return back to the title scene
+ Core.ChangeScene(new TitleScene());
+ }
+
+ private void InitializeNewGame()
+ {
+ // Calculate the position for the slime, which will be at the center
+ // tile of the tile map.
+ Vector2 slimePos = new Vector2();
+ slimePos.X = (_tilemap.Columns / 2) * _tilemap.TileWidth;
+ slimePos.Y = (_tilemap.Rows / 2) * _tilemap.TileHeight;
+
+ // Initialize the slime
+ _slime.Initialize(slimePos, _tilemap.TileWidth);
+
+ // Initialize the bat
+ _bat.RandomizeVelocity();
+ PositionBatAwayFromSlime();
+
+ // Reset the score
+ _score = 0;
+
+ // Set the game state to playing
+ _state = GameState.Playing;
+ }
+
+ public override void LoadContent()
+ {
+ // Create the texture atlas from the XML configuration file
+ TextureAtlas atlas = TextureAtlas.FromFile(Core.Content, "images/atlas-definition.xml");
+
+ // Create the tilemap from the XML configuration file.
+ _tilemap = Tilemap.FromFile(Content, "images/tilemap-definition.xml");
+ _tilemap.Scale = new Vector2(4.0f, 4.0f);
+
+ // Create the animated sprite for the slime from the atlas.
+ AnimatedSprite slimeAnimation = atlas.CreateAnimatedSprite("slime-animation");
+ slimeAnimation.Scale = new Vector2(4.0f, 4.0f);
+
+ // Create the slime
+ _slime = new Slime(slimeAnimation);
+
+ // Create the animated sprite for the bat from the atlas.
+ AnimatedSprite batAnimation = atlas.CreateAnimatedSprite("bat-animation");
+ batAnimation.Scale = new Vector2(4.0f, 4.0f);
+
+ // Load the bounce sound effect for the bat
+ SoundEffect bounceSoundEffect = Content.Load("audio/bounce");
+
+ // Create the bat
+ _bat = new Bat(batAnimation, bounceSoundEffect);
+
+ // Load the collect sound effect
+ _collectSoundEffect = Content.Load("audio/collect");
+
+ // Load the grayscale effect
+ _grayscaleEffect = Content.Load("effects/grayscaleEffect");
+ }
+
+ public override void Update(GameTime gameTime)
+ {
+ // Ensure the UI is always updated with proper cursor transform including viewport offsets
+ var scaleMatrix = _viewport.GetScaleMatrix();
+ var viewportBounds = _viewport.Viewport;
+ var offsetMatrix = Matrix.CreateTranslation(-viewportBounds.X, -viewportBounds.Y, 0);
+ var combinedMatrix = offsetMatrix * scaleMatrix;
+ GumService.Default.Cursor.TransformMatrix = Matrix.Invert(combinedMatrix);
+ _ui.Update(gameTime);
+
+ if (_state != GameState.Playing)
+ {
+ // The game is in either a paused or game over state, so
+ // gradually decrease the saturation to create the fading grayscale.
+ _saturation = Math.Max(0.0f, _saturation - FADE_SPEED);
+
+ // If its just a game over state, return back
+ if (_state == GameState.GameOver)
+ {
+ return;
+ }
+ }
+
+ // If the pause button is pressed, toggle the pause state
+ if (GameController.Pause())
+ {
+ TogglePause();
+ }
+
+ // At this point, if the game is paused, just return back early
+ if (_state == GameState.Paused)
+ {
+ return;
+ }
+
+ // Update the slime;
+ _slime.Update(gameTime);
+
+ // Update the bat;
+ _bat.Update(gameTime);
+
+ // Perform collision checks
+ CollisionChecks();
+ }
+
+ private void CollisionChecks()
+ {
+ // Capture the current bounds of the slime and bat
+ Circle slimeBounds = _slime.GetBounds();
+ Circle batBounds = _bat.GetBounds();
+
+ // FIrst perform a collision check to see if the slime is colliding with
+ // the bat, which means the slime eats the bat.
+ if (slimeBounds.Intersects(batBounds))
+ {
+ // Move the bat to a new position away from the slime.
+ PositionBatAwayFromSlime();
+
+ // Randomize the velocity of the bat.
+ _bat.RandomizeVelocity();
+
+ // Tell the slime to grow.
+ _slime.Grow();
+
+ // Increment the score.
+ _score += 100;
+
+ // Update the score display on the UI.
+ _ui.UpdateScoreText(_score);
+
+ // Play the collect sound effect
+ Core.Audio.PlaySoundEffect(_collectSoundEffect);
+ }
+
+ // Next check if the slime is colliding with the wall by validating if
+ // it is within the bounds of the room. If it is outside the room
+ // bounds, then it collided with a wall which triggers a game over.
+ if (slimeBounds.Top < _roomBounds.Top ||
+ slimeBounds.Bottom > _roomBounds.Bottom ||
+ slimeBounds.Left < _roomBounds.Left ||
+ slimeBounds.Right > _roomBounds.Right)
+ {
+ GameOver();
+ return;
+ }
+
+ // Finally, check if the bat is colliding with a wall by validating if
+ // it is within the bounds of the room. If it is outside the room
+ // bounds, then it collided with a wall, and the bat should bounce
+ // off of that wall.
+ if (batBounds.Top < _roomBounds.Top)
+ {
+ _bat.Bounce(Vector2.UnitY);
+ }
+ else if (batBounds.Bottom > _roomBounds.Bottom)
+ {
+ _bat.Bounce(-Vector2.UnitY);
+ }
+
+ if (batBounds.Left < _roomBounds.Left)
+ {
+ _bat.Bounce(Vector2.UnitX);
+ }
+ else if (batBounds.Right > _roomBounds.Right)
+ {
+ _bat.Bounce(-Vector2.UnitX);
+ }
+ }
+
+ private void PositionBatAwayFromSlime()
+ {
+ // Calculate the position that is in the center of the bounds
+ // of the room.
+ float roomCenterX = _roomBounds.X + _roomBounds.Width * 0.5f;
+ float roomCenterY = _roomBounds.Y + _roomBounds.Height * 0.5f;
+ Vector2 roomCenter = new Vector2(roomCenterX, roomCenterY);
+
+ // Get the bounds of the slime and calculate the center position
+ Circle slimeBounds = _slime.GetBounds();
+ Vector2 slimeCenter = new Vector2(slimeBounds.X, slimeBounds.Y);
+
+ // Calculate the distance vector from the center of the room to the
+ // center of the slime.
+ Vector2 centerToSlime = slimeCenter - roomCenter;
+
+ // Get the bounds of the bat
+ Circle batBounds = _bat.GetBounds();
+
+ // Calculate the amount of padding we will add to the new position of
+ // the bat to ensure it is not sticking to walls
+ int padding = batBounds.Radius * 2;
+
+ // Calculate the new position of the bat by finding which component of
+ // the center to slime vector (X or Y) is larger and in which direction.
+ Vector2 newBatPosition = Vector2.Zero;
+ if (Math.Abs(centerToSlime.X) > Math.Abs(centerToSlime.Y))
+ {
+ // The slime is closer to either the left or right wall, so the Y
+ // position will be a random position between the top and bottom
+ // walls.
+ newBatPosition.Y = Random.Shared.Next(
+ _roomBounds.Top + padding,
+ _roomBounds.Bottom - padding
+ );
+
+ if (centerToSlime.X > 0)
+ {
+ // The slime is closer to the right side wall, so place the
+ // bat on the left side wall
+ newBatPosition.X = _roomBounds.Left + padding;
+ }
+ else
+ {
+ // The slime is closer ot the left side wall, so place the
+ // bat on the right side wall.
+ newBatPosition.X = _roomBounds.Right - padding * 2;
+ }
+ }
+ else
+ {
+ // The slime is closer to either the top or bottom wall, so the X
+ // position will be a random position between the left and right
+ // walls.
+ newBatPosition.X = Random.Shared.Next(
+ _roomBounds.Left + padding,
+ _roomBounds.Right - padding
+ );
+
+ if (centerToSlime.Y > 0)
+ {
+ // The slime is closer to the top wall, so place the bat on the
+ // bottom wall
+ newBatPosition.Y = _roomBounds.Top + padding;
+ }
+ else
+ {
+ // The slime is closer to the bottom wall, so place the bat on
+ // the top wall.
+ newBatPosition.Y = _roomBounds.Bottom - padding * 2;
+ }
+ }
+
+ // Assign the new bat position
+ _bat.Position = newBatPosition;
+ }
+
+ private void OnSlimeBodyCollision(object sender, EventArgs args)
+ {
+ GameOver();
+ }
+
+ private void TogglePause()
+ {
+ if (_state == GameState.Paused)
+ {
+ // We're now unpausing the game, so hide the pause panel
+ _ui.HidePausePanel();
+
+ // And set the state back to playing
+ _state = GameState.Playing;
+ }
+ else
+ {
+ // We're now pausing the game, so show the pause panel
+ _ui.ShowPausePanel();
+
+ // And set the state to paused
+ _state = GameState.Paused;
+
+ // Set the grayscale effect saturation to 1.0f;
+ _saturation = 1.0f;
+ }
+ }
+
+ private void GameOver()
+ {
+ // Show the game over panel
+ _ui.ShowGameOverPanel();
+
+ // Set the game state to game over
+ _state = GameState.GameOver;
+
+ // Set the grayscale effect saturation to 1.0f;
+ _saturation = 1.0f;
+ }
+
+ public override void Draw(GameTime gameTime)
+ {
+ // Clear the back buffer.
+ Core.GraphicsDevice.Clear(Color.Black);
+
+ if (_state != GameState.Playing)
+ {
+ // We are in a game over state, so apply the saturation parameter.
+ _grayscaleEffect.Parameters["Saturation"].SetValue(_saturation);
+
+ // And begin the sprite batch using the grayscale effect.
+ Core.SpriteBatch.Begin(samplerState: SamplerState.PointClamp, effect: _grayscaleEffect, transformMatrix: _viewport.GetScaleMatrix());
+ }
+ else
+ {
+ // Otherwise, just begin the sprite batch as normal.
+ Core.SpriteBatch.Begin(samplerState: SamplerState.PointClamp, transformMatrix: _viewport.GetScaleMatrix());
+ }
+
+ // Draw the tilemap
+ _tilemap.Draw(Core.SpriteBatch);
+
+ // Draw the slime.
+ _slime.Draw();
+
+ // Draw the bat.
+ _bat.Draw();
+
+ // Always end the sprite batch when finished.
+ Core.SpriteBatch.End();
+
+ // Draw the UI
+ GumService.Default.Renderer.SpriteRenderer.ForcedMatrix = _viewport.GetScaleMatrix();
+ _ui.Draw();
+ }
+}
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Common/Scenes/TitleScene.cs b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Common/Scenes/TitleScene.cs
new file mode 100644
index 00000000..42e1b282
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Common/Scenes/TitleScene.cs
@@ -0,0 +1,382 @@
+using System;
+using DungeonSlime.UI;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Audio;
+using Microsoft.Xna.Framework.Graphics;
+using MonoGameGum;
+using Gum.Forms.Controls;
+using MonoGameGum.GueDeriving;
+using MonoGameLibrary;
+using MonoGameLibrary.Graphics;
+using MonoGameLibrary.Scenes;
+using MonoGame.Extended.ViewportAdapters;
+
+namespace DungeonSlime.Scenes;
+
+public class TitleScene : Scene
+{
+ private const string DUNGEON_TEXT = "Dungeon";
+ private const string SLIME_TEXT = "Slime";
+ private const string PRESS_ENTER_TEXT = "Press Enter To Start";
+
+ // The font to use to render normal text.
+ private SpriteFont _font;
+
+ // The font used to render the title text.
+ private SpriteFont _font5x;
+
+ // The position to draw the dungeon text at.
+ private Vector2 _dungeonTextPos;
+
+ // The origin to set for the dungeon text.
+ private Vector2 _dungeonTextOrigin;
+
+ // The position to draw the slime text at.
+ private Vector2 _slimeTextPos;
+
+ // The origin to set for the slime text.
+ private Vector2 _slimeTextOrigin;
+
+ // The position to draw the press enter text at.
+ private Vector2 _pressEnterPos;
+
+ // The origin to set for the press enter text when drawing it.
+ private Vector2 _pressEnterOrigin;
+
+ // The texture used for the background pattern.
+ private Texture2D _backgroundPattern;
+
+ // The destination rectangle for the background pattern to fill.
+ private Rectangle _backgroundDestination;
+
+ // The offset to apply when drawing the background pattern so it appears to
+ // be scrolling.
+ private Vector2 _backgroundOffset;
+
+ // The speed that the background pattern scrolls.
+ private float _scrollSpeed = 50.0f;
+
+ private SoundEffect _uiSoundEffect;
+ private Panel _titleScreenButtonsPanel;
+ private Panel _optionsPanel;
+
+ AnimatedButton _startButton;
+
+ // The options button used to open the options menu.
+ private AnimatedButton _optionsButton;
+
+ // The back button used to exit the options menu back to the title menu.
+ private AnimatedButton _optionsBackButton;
+
+ // Reference to the texture atlas that we can pass to UI elements when they
+ // are created.
+ private TextureAtlas _atlas;
+
+ public override void Initialize()
+ {
+ // LoadContent is called during base.Initialize().
+ base.Initialize();
+
+ // While on the title screen, we can enable exit on escape so the player
+ // can close the game by pressing the escape key.
+ Core.ExitOnEscape = true;
+
+ // Set the position and origin for the Dungeon text.
+ Vector2 size = _font5x.MeasureString(DUNGEON_TEXT);
+ _dungeonTextPos = new Vector2(640, 100);
+ _dungeonTextOrigin = size * 0.5f;
+
+ // Set the position and origin for the Slime text.
+ size = _font5x.MeasureString(SLIME_TEXT);
+ _slimeTextPos = new Vector2(757, 207);
+ _slimeTextOrigin = size * 0.5f;
+
+ // Set the position and origin for the press enter text.
+ size = _font.MeasureString(PRESS_ENTER_TEXT);
+ _pressEnterPos = new Vector2(640, 620);
+ _pressEnterOrigin = size * 0.5f;
+
+ // Initialize the offset of the background pattern at zero
+ _backgroundOffset = Vector2.Zero;
+
+ // Set the background pattern destination rectangle to fill the entire
+ // screen background
+ _backgroundDestination = new Rectangle(0,0, 1280, 720); // Core.GraphicsDevice.PresentationParameters.Bounds;
+
+ InitializeUI();
+ }
+
+ public override void LoadContent()
+ {
+ // Load the font for the standard text.
+ _font = Core.Content.Load("fonts/04B_30");
+
+ // Load the font for the title text
+ _font5x = Content.Load("fonts/04B_30_5x");
+
+ // Load the background pattern texture.
+ _backgroundPattern = Content.Load("images/background-pattern");
+
+ // Load the sound effect to play when ui actions occur.
+ _uiSoundEffect = Core.Content.Load("audio/ui");
+
+ // Load the texture atlas from the xml configuration file.
+ _atlas = TextureAtlas.FromFile(Core.Content, "images/atlas-definition.xml");
+ }
+
+ private void CreateTitlePanel()
+ {
+ // Create a container to hold all of our buttons
+ _titleScreenButtonsPanel = new Panel();
+ _titleScreenButtonsPanel.Dock(Gum.Wireframe.Dock.Fill);
+ _titleScreenButtonsPanel.AddToRoot();
+
+ _startButton = new AnimatedButton(_atlas);
+ _startButton.Anchor(Gum.Wireframe.Anchor.BottomLeft);
+ _startButton.Visual.X = 50;
+ _startButton.Visual.Y = -12;
+ _startButton.Text = "Start";
+ _startButton.Click += HandleStartClicked;
+ _titleScreenButtonsPanel.AddChild(_startButton);
+
+ _optionsButton = new AnimatedButton(_atlas);
+ _optionsButton.Anchor(Gum.Wireframe.Anchor.BottomRight);
+ _optionsButton.Visual.X = -50;
+ _optionsButton.Visual.Y = -12;
+ _optionsButton.Text = "Options";
+ _optionsButton.Click += HandleOptionsClicked;
+ _titleScreenButtonsPanel.AddChild(_optionsButton);
+
+ _startButton.IsFocused = true;
+ }
+
+ private void HandleStartClicked(object sender, EventArgs e)
+ {
+ // A UI interaction occurred, play the sound effect
+ Core.Audio.PlaySoundEffect(_uiSoundEffect);
+
+ // Change to the game scene to start the game.
+ Core.ChangeScene(new GameScene());
+ }
+
+ private void HandleOptionsClicked(object sender, EventArgs e)
+ {
+ // A UI interaction occurred, play the sound effect
+ Core.Audio.PlaySoundEffect(_uiSoundEffect);
+
+ // Set the title panel to be invisible.
+ _titleScreenButtonsPanel.IsVisible = false;
+
+ // Set the options panel to be visible.
+ _optionsPanel.IsVisible = true;
+
+ // Give the back button on the options panel focus.
+ _optionsBackButton.IsFocused = true;
+ }
+
+ private void CreateOptionsPanel()
+ {
+ _optionsPanel = new Panel();
+ _optionsPanel.Dock(Gum.Wireframe.Dock.Fill);
+ _optionsPanel.IsVisible = false;
+ _optionsPanel.AddToRoot();
+
+ TextRuntime optionsText = new TextRuntime();
+ optionsText.X = 10;
+ optionsText.Y = 10;
+ optionsText.Text = "OPTIONS";
+ optionsText.UseCustomFont = true;
+ optionsText.FontScale = 0.5f;
+ optionsText.CustomFontFile = @"fonts/04b_30.fnt";
+ _optionsPanel.AddChild(optionsText);
+
+ OptionsSlider musicSlider = new OptionsSlider(_atlas);
+ musicSlider.Name = "MusicSlider";
+ musicSlider.Text = "MUSIC";
+ musicSlider.Anchor(Gum.Wireframe.Anchor.Top);
+ musicSlider.Visual.Y = 30f;
+ musicSlider.Minimum = 0;
+ musicSlider.Maximum = 1;
+ musicSlider.Value = Core.Audio.SongVolume;
+ musicSlider.SmallChange = .1;
+ musicSlider.LargeChange = .2;
+ musicSlider.ValueChanged += HandleMusicSliderValueChanged;
+ musicSlider.ValueChangeCompleted += HandleMusicSliderValueChangeCompleted;
+ _optionsPanel.AddChild(musicSlider);
+
+ OptionsSlider sfxSlider = new OptionsSlider(_atlas);
+ sfxSlider.Name = "SfxSlider";
+ sfxSlider.Text = "SFX";
+ sfxSlider.Anchor(Gum.Wireframe.Anchor.Top);
+ sfxSlider.Visual.Y = 93;
+ sfxSlider.Minimum = 0;
+ sfxSlider.Maximum = 1;
+ sfxSlider.Value = Core.Audio.SoundEffectVolume;
+ sfxSlider.SmallChange = .1;
+ sfxSlider.LargeChange = .2;
+ sfxSlider.ValueChanged += HandleSfxSliderChanged;
+ sfxSlider.ValueChangeCompleted += HandleSfxSliderChangeCompleted;
+ _optionsPanel.AddChild(sfxSlider);
+
+ _optionsBackButton = new AnimatedButton(_atlas);
+ _optionsBackButton.Text = "BACK";
+ _optionsBackButton.Anchor(Gum.Wireframe.Anchor.BottomRight);
+ _optionsBackButton.X = -28f;
+ _optionsBackButton.Y = -10f;
+ _optionsBackButton.Click += HandleOptionsButtonBack;
+ _optionsPanel.AddChild(_optionsBackButton);
+ }
+
+ private void HandleSfxSliderChanged(object sender, EventArgs args)
+ {
+ // Intentionally not playing the UI sound effect here so that it is not
+ // constantly triggered as the user adjusts the slider's thumb on the
+ // track.
+
+ // Get a reference to the sender as a Slider.
+ Slider slider = (Slider)sender;
+
+ // Set the global sound effect volume to the value of the slider.;
+ Core.Audio.SoundEffectVolume = (float)slider.Value;
+ }
+
+ private void HandleSfxSliderChangeCompleted(object sender, EventArgs e)
+ {
+ // Play the UI Sound effect so the player can hear the difference in audio.
+ Core.Audio.PlaySoundEffect(_uiSoundEffect);
+ }
+
+ private void HandleMusicSliderValueChanged(object sender, EventArgs args)
+ {
+ // Intentionally not playing the UI sound effect here so that it is not
+ // constantly triggered as the user adjusts the slider's thumb on the
+ // track.
+
+ // Get a reference to the sender as a Slider.
+ Slider slider = (Slider)sender;
+
+ // Set the global song volume to the value of the slider.
+ Core.Audio.SongVolume = (float)slider.Value;
+ }
+
+ private void HandleMusicSliderValueChangeCompleted(object sender, EventArgs args)
+ {
+ // A UI interaction occurred, play the sound effect
+ Core.Audio.PlaySoundEffect(_uiSoundEffect);
+ }
+
+ private void HandleOptionsButtonBack(object sender, EventArgs e)
+ {
+ // A UI interaction occurred, play the sound effect
+ Core.Audio.PlaySoundEffect(_uiSoundEffect);
+
+ // Set the title panel to be visible.
+ _titleScreenButtonsPanel.IsVisible = true;
+
+ // Set the options panel to be invisible.
+ _optionsPanel.IsVisible = false;
+
+ // Give the options button on the title panel focus since we are coming
+ // back from the options screen.
+ _optionsButton.IsFocused = true;
+ }
+
+ private void InitializeUI()
+ {
+ // Clear out any previous UI in case we came here from
+ // a different screen:
+ GumService.Default.Root.Children.Clear();
+
+ // this game was built for 1280x720 scaled up by 4.
+ GumService.Default.CanvasWidth = 1280 / 4f;
+ GumService.Default.CanvasHeight = 720 / 4f;
+
+ CreateTitlePanel();
+ CreateOptionsPanel();
+
+ // the game will render at 1280x720.
+ _viewport = new(Core.GameWindow, Core.Graphics.GraphicsDevice, 1280, 720);
+ }
+
+ public override void Update(GameTime gameTime)
+ {
+ // Update the offsets for the background pattern wrapping so that it
+ // scrolls down and to the right.
+ float offset = _scrollSpeed * (float)gameTime.ElapsedGameTime.TotalSeconds;
+ _backgroundOffset.X -= offset;
+ _backgroundOffset.Y -= offset;
+
+ // Ensure that the offsets do not go beyond the texture bounds so it is
+ // a seamless wrap
+ _backgroundOffset.X %= _backgroundPattern.Width;
+ _backgroundOffset.Y %= _backgroundPattern.Height;
+
+ // Set cursor transform to account for viewport scaling AND offset from letterboxing
+ // This properly maps mouse coordinates considering viewport bounds and scaling
+ Matrix scaleMatrix = _viewport.GetScaleMatrix();
+ Viewport viewportBounds = _viewport.Viewport;
+ Matrix offsetMatrix = Matrix.CreateTranslation(-viewportBounds.X, -viewportBounds.Y, 0);
+ Matrix combinedMatrix = offsetMatrix * scaleMatrix;
+
+ // Detailed debug output for coordinate mapping
+ Vector2 rawMouse = new Vector2(Core.Input.Mouse.X, Core.Input.Mouse.Y);
+ Vector2 gumCursor = new Vector2(GumService.Default.Cursor.X, GumService.Default.Cursor.Y);
+ Vector2 buttonPos = new Vector2(_startButton.Visual.AbsoluteX, _startButton.Visual.AbsoluteY);
+ Vector2 buttonSize = new Vector2(_startButton.Visual.Width, _startButton.Visual.Height);
+
+ // Test if mouse should be hitting button
+ /*Rectangle buttonBounds = new Rectangle((int)buttonPos.X, (int)buttonPos.Y, (int)buttonSize.X, (int)buttonSize.Y);
+ if (buttonBounds.Contains(gumCursor))
+ {
+ Debug.WriteLine("*** GUM CURSOR IS INSIDE BUTTON BOUNDS ***");
+ }*/
+
+ GumService.Default.Cursor.TransformMatrix = Matrix.Invert(combinedMatrix);
+ GumService.Default.Update(gameTime);
+
+ GumService.Default.Renderer.Camera.ClientWidth = _viewport.ViewportWidth / 4;
+ GumService.Default.Renderer.Camera.ClientHeight = _viewport.ViewportHeight / 4;
+
+ }
+
+ BoxingViewportAdapter _viewport;
+
+ public override void Draw(GameTime gameTime)
+ {
+ Core.GraphicsDevice.Clear(new Color(32, 40, 78, 255));
+
+ // Draw the background pattern first using the PointWrap sampler state.
+ Core.SpriteBatch.Begin(samplerState: SamplerState.PointWrap, transformMatrix: _viewport.GetScaleMatrix());
+ Core.SpriteBatch.Draw(_backgroundPattern, _backgroundDestination, new Rectangle(_backgroundOffset.ToPoint(), _backgroundDestination.Size), Color.White * 0.5f);
+ Core.SpriteBatch.End();
+
+ if (_titleScreenButtonsPanel.IsVisible)
+ {
+ // Begin the sprite batch to prepare for rendering.
+ Core.SpriteBatch.Begin(samplerState: SamplerState.PointClamp, transformMatrix: _viewport.GetScaleMatrix());
+
+ // The color to use for the drop shadow text.
+ Color dropShadowColor = Color.Black * 0.5f;
+
+ // Draw the Dungeon text slightly offset from it is original position and
+ // with a transparent color to give it a drop shadow
+ Core.SpriteBatch.DrawString(_font5x, DUNGEON_TEXT, _dungeonTextPos + new Vector2(10, 10), dropShadowColor, 0.0f, _dungeonTextOrigin, 1.0f, SpriteEffects.None, 1.0f);
+
+ // Draw the Dungeon text on top of that at its original position
+ Core.SpriteBatch.DrawString(_font5x, DUNGEON_TEXT, _dungeonTextPos, Color.White, 0.0f, _dungeonTextOrigin, 1.0f, SpriteEffects.None, 1.0f);
+
+ // Draw the Slime text slightly offset from it is original position and
+ // with a transparent color to give it a drop shadow
+ Core.SpriteBatch.DrawString(_font5x, SLIME_TEXT, _slimeTextPos + new Vector2(10, 10), dropShadowColor, 0.0f, _slimeTextOrigin, 1.0f, SpriteEffects.None, 1.0f);
+
+ // Draw the Slime text on top of that at its original position
+ Core.SpriteBatch.DrawString(_font5x, SLIME_TEXT, _slimeTextPos, Color.White, 0.0f, _slimeTextOrigin, 1.0f, SpriteEffects.None, 1.0f);
+
+ // Always end the sprite batch when finished.
+ Core.SpriteBatch.End();
+ }
+
+ GumService.Default.Renderer.SpriteRenderer.ForcedMatrix = _viewport.GetScaleMatrix();
+ GumService.Default.Draw();
+ }
+}
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Common/UI/AnimatedButton.cs b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Common/UI/AnimatedButton.cs
new file mode 100644
index 00000000..56ba8cfc
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Common/UI/AnimatedButton.cs
@@ -0,0 +1,163 @@
+using System;
+using Gum.DataTypes;
+using Gum.DataTypes.Variables;
+using Gum.Forms.Controls;
+using Gum.Forms.DefaultVisuals;
+using Gum.Graphics.Animation;
+using Gum.Managers;
+using Microsoft.Xna.Framework.Input;
+using MonoGameGum.GueDeriving;
+using MonoGameLibrary.Graphics;
+
+namespace DungeonSlime.UI;
+
+///
+/// A custom button implementation that inherits from Gum's Button class to provide
+/// animated visual feedback when focused.
+///
+internal class AnimatedButton : Button
+{
+ ///
+ /// Creates a new AnimatedButton instance using graphics from the specified texture atlas.
+ ///
+ /// The texture atlas containing button graphics and animations
+ public AnimatedButton(TextureAtlas atlas)
+ {
+ // Each Forms conrol has a general Visual property that
+ // has properties shared by all control types. This Visual
+ // type matches the Forms type. It can be casted to access
+ // controls-specific properties.
+ ButtonVisual buttonVisual = (ButtonVisual)Visual;
+ // Width is relative to children with extra padding, height is fixed
+ buttonVisual.Height = 14f;
+ buttonVisual.HeightUnits = DimensionUnitType.Absolute;
+ buttonVisual.Width = 21f;
+ buttonVisual.WidthUnits = DimensionUnitType.RelativeToChildren;
+
+ // Get a reference to the nine-slice background to display the button graphics
+ // A nine-slice allows the button to stretch while preserving corner appearance
+ NineSliceRuntime background = buttonVisual.Background;
+ background.Texture = atlas.Texture;
+ background.TextureAddress = TextureAddress.Custom;
+ background.Color = Microsoft.Xna.Framework.Color.White;
+ // texture coordinates for the background are set down below
+
+ TextRuntime textInstance = buttonVisual.TextInstance;
+ textInstance.Text = "START";
+ textInstance.Blue = 130;
+ textInstance.Green = 86;
+ textInstance.Red = 70;
+ textInstance.UseCustomFont = true;
+ textInstance.CustomFontFile = "fonts/04b_30.fnt";
+ textInstance.FontScale = 0.25f;
+ textInstance.Anchor(Gum.Wireframe.Anchor.Center);
+ textInstance.Width = 0;
+ textInstance.WidthUnits = DimensionUnitType.RelativeToChildren;
+
+ // Get the texture region for the unfocused button state from the atlas
+ TextureRegion unfocusedTextureRegion = atlas.GetRegion("unfocused-button");
+
+ // Create an animation chain for the unfocused state with a single frame
+ AnimationChain unfocusedAnimation = new AnimationChain();
+ unfocusedAnimation.Name = nameof(unfocusedAnimation);
+ AnimationFrame unfocusedFrame = new AnimationFrame
+ {
+ TopCoordinate = unfocusedTextureRegion.TopTextureCoordinate,
+ BottomCoordinate = unfocusedTextureRegion.BottomTextureCoordinate,
+ LeftCoordinate = unfocusedTextureRegion.LeftTextureCoordinate,
+ RightCoordinate = unfocusedTextureRegion.RightTextureCoordinate,
+ FrameLength = 0.3f,
+ Texture = unfocusedTextureRegion.Texture
+ };
+ unfocusedAnimation.Add(unfocusedFrame);
+
+ // Get the multi-frame animation for the focused button state from the atlas
+ Animation focusedAtlasAnimation = atlas.GetAnimation("focused-button-animation");
+
+ // Create an animation chain for the focused state using all frames from the atlas animation
+ AnimationChain focusedAnimation = new AnimationChain();
+ focusedAnimation.Name = nameof(focusedAnimation);
+ foreach (TextureRegion region in focusedAtlasAnimation.Frames)
+ {
+ AnimationFrame frame = new AnimationFrame
+ {
+ TopCoordinate = region.TopTextureCoordinate,
+ BottomCoordinate = region.BottomTextureCoordinate,
+ LeftCoordinate = region.LeftTextureCoordinate,
+ RightCoordinate = region.RightTextureCoordinate,
+ FrameLength = (float)focusedAtlasAnimation.Delay.TotalSeconds,
+ Texture = region.Texture
+ };
+
+ focusedAnimation.Add(frame);
+ }
+
+ // Assign both animation chains to the nine-slice background
+ background.AnimationChains = new AnimationChainList
+ {
+ unfocusedAnimation,
+ focusedAnimation
+ };
+
+
+ // Reset all state to default so we don't have unexpected variable assignments:
+ buttonVisual.ButtonCategory.ResetAllStates();
+
+ // Get the enabled (default/unfocused) state
+ StateSave enabledState = buttonVisual.States.Enabled;
+ enabledState.Apply = () =>
+ {
+ // When enabled but not focused, use the unfocused animation
+ background.CurrentChainName = unfocusedAnimation.Name;
+ };
+
+ // Create the focused state
+ StateSave focusedState = buttonVisual.States.Focused;
+ focusedState.Apply = () =>
+ {
+ // When focused, use the focused animation and enable animation playback
+ background.CurrentChainName = focusedAnimation.Name;
+ background.Animate = true;
+ };
+
+ // Create the highlighted+focused state (for mouse hover while focused)
+ StateSave highlightedFocused = buttonVisual.States.HighlightedFocused;
+ highlightedFocused.Apply = focusedState.Apply;
+
+ // Create the highlighted state (for mouse hover)
+ // by cloning the enabled state since they appear the same
+ StateSave highlighted = buttonVisual.States.Highlighted;
+ highlighted.Apply = enabledState.Apply;
+
+ // Add event handlers for keyboard input.
+ KeyDown += HandleKeyDown;
+
+ // Add event handler for mouse hover focus.
+ buttonVisual.RollOn += HandleRollOn;
+ }
+
+ ///
+ /// Handles keyboard input for navigation between buttons using left/right keys.
+ ///
+ private void HandleKeyDown(object sender, KeyEventArgs e)
+ {
+ if (e.Key == Keys.Left)
+ {
+ // Left arrow navigates to previous control
+ HandleTab(TabDirection.Up, loop: true);
+ }
+ if (e.Key == Keys.Right)
+ {
+ // Right arrow navigates to next control
+ HandleTab(TabDirection.Down, loop: true);
+ }
+ }
+
+ ///
+ /// Automatically focuses the button when the mouse hovers over it.
+ ///
+ private void HandleRollOn(object sender, EventArgs e)
+ {
+ IsFocused = true;
+ }
+}
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Common/UI/GameSceneUI.cs b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Common/UI/GameSceneUI.cs
new file mode 100644
index 00000000..84be9293
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Common/UI/GameSceneUI.cs
@@ -0,0 +1,340 @@
+using System;
+using Gum.DataTypes;
+using Gum.Managers;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Audio;
+using Microsoft.Xna.Framework.Content;
+using MonoGameGum;
+using Gum.Forms.Controls;
+using MonoGameGum.GueDeriving;
+using MonoGameLibrary;
+using MonoGameLibrary.Graphics;
+
+namespace DungeonSlime.UI;
+
+public class GameSceneUI : ContainerRuntime
+{
+ // The string format to use when updating the text for the score display.
+ private static readonly string s_scoreFormat = "SCORE: {0:D6}";
+
+ // The sound effect to play for auditory feedback of the user interface.
+ private SoundEffect _uiSoundEffect;
+
+ // The pause panel
+ private Panel _pausePanel;
+
+ // The resume button on the pause panel. Field is used to track reference so
+ // focus can be set when the pause panel is shown.
+ private AnimatedButton _resumeButton;
+
+ // The game over panel.
+ private Panel _gameOverPanel;
+
+ // The retry button on the game over panel. Field is used to track reference
+ // so focus can be set when the game over panel is shown.
+ private AnimatedButton _retryButton;
+
+ // The text runtime used to display the players score on the game screen.
+ private TextRuntime _scoreText;
+
+ ///
+ /// Event invoked when the Resume button on the Pause panel is clicked.
+ ///
+ public event EventHandler ResumeButtonClick;
+
+ ///
+ /// Event invoked when the Quit button on either the Pause panel or the
+ /// Game Over panel is clicked.
+ ///
+ public event EventHandler QuitButtonClick;
+
+ ///
+ /// Event invoked when the Retry button on the Game Over panel is clicked.
+ ///
+ public event EventHandler RetryButtonClick;
+
+ public GameSceneUI()
+ {
+ // The game scene UI inherits from ContainerRuntime, so we set its
+ // doc to fill so it fills the entire screen.
+ Dock(Gum.Wireframe.Dock.Fill);
+
+ // Add it to the root element.
+ this.AddToRoot();
+
+ // Get a reference to the content manager that was registered with the
+ // GumService when it was original initialized.
+ ContentManager content = GumService.Default.ContentLoader.XnaContentManager;
+
+ // Use that content manager to load the sound effect and atlas for the
+ // user interface elements
+ _uiSoundEffect = content.Load("audio/ui");
+ TextureAtlas atlas = TextureAtlas.FromFile(content, "images/atlas-definition.xml");
+
+ // Create the text that will display the players score and add it as
+ // a child to this container.
+ _scoreText = CreateScoreText();
+ AddChild(_scoreText);
+
+ // Create the Pause panel that is displayed when the game is paused and
+ // add it as a child to this container
+ _pausePanel = CreatePausePanel(atlas);
+ AddChild(_pausePanel.Visual);
+
+ // Create the Game Over panel that is displayed when a game over occurs
+ // and add it as a child to this container
+ _gameOverPanel = CreateGameOverPanel(atlas);
+ AddChild(_gameOverPanel.Visual);
+ }
+
+ private TextRuntime CreateScoreText()
+ {
+ TextRuntime text = new TextRuntime();
+ text.Anchor(Gum.Wireframe.Anchor.TopLeft);
+ text.WidthUnits = DimensionUnitType.RelativeToChildren;
+ text.X = 20.0f;
+ text.Y = 5.0f;
+ text.UseCustomFont = true;
+ text.CustomFontFile = @"fonts/04b_30.fnt";
+ text.FontScale = 0.25f;
+ text.Text = string.Format(s_scoreFormat, 0);
+
+ return text;
+ }
+
+ private Panel CreatePausePanel(TextureAtlas atlas)
+ {
+ Panel panel = new Panel();
+ panel.Anchor(Gum.Wireframe.Anchor.Center);
+ panel.Visual.WidthUnits = DimensionUnitType.Absolute;
+ panel.Visual.HeightUnits = DimensionUnitType.Absolute;
+ panel.Visual.Width = 264.0f;
+ panel.Visual.Height = 70.0f;
+ panel.IsVisible = false;
+
+ TextureRegion backgroundRegion = atlas.GetRegion("panel-background");
+
+ NineSliceRuntime background = new NineSliceRuntime();
+ background.Dock(Gum.Wireframe.Dock.Fill);
+ background.Texture = backgroundRegion.Texture;
+ background.TextureAddress = TextureAddress.Custom;
+ background.TextureHeight = backgroundRegion.Height;
+ background.TextureWidth = backgroundRegion.Width;
+ background.TextureTop = backgroundRegion.SourceRectangle.Top;
+ background.TextureLeft = backgroundRegion.SourceRectangle.Left;
+ panel.AddChild(background);
+
+ TextRuntime text = new TextRuntime();
+ text.Text = "PAUSED";
+ text.UseCustomFont = true;
+ text.CustomFontFile = "fonts/04b_30.fnt";
+ text.FontScale = 0.5f;
+ text.X = 10.0f;
+ text.Y = 10.0f;
+ panel.AddChild(text);
+
+ _resumeButton = new AnimatedButton(atlas);
+ _resumeButton.Text = "RESUME";
+ _resumeButton.Anchor(Gum.Wireframe.Anchor.BottomLeft);
+ _resumeButton.Visual.X = 9.0f;
+ _resumeButton.Visual.Y = -9.0f;
+
+ _resumeButton.Click += OnResumeButtonClicked;
+ _resumeButton.GotFocus += OnElementGotFocus;
+
+ panel.AddChild(_resumeButton);
+
+ AnimatedButton quitButton = new AnimatedButton(atlas);
+ quitButton.Text = "QUIT";
+ quitButton.Anchor(Gum.Wireframe.Anchor.BottomRight);
+ quitButton.Visual.X = -9.0f;
+ quitButton.Visual.Y = -9.0f;
+
+ quitButton.Click += OnQuitButtonClicked;
+ quitButton.GotFocus += OnElementGotFocus;
+
+ panel.AddChild(quitButton);
+
+ return panel;
+ }
+
+ private Panel CreateGameOverPanel(TextureAtlas atlas)
+ {
+ Panel panel = new Panel();
+ panel.Anchor(Gum.Wireframe.Anchor.Center);
+ panel.Visual.WidthUnits = DimensionUnitType.Absolute;
+ panel.Visual.HeightUnits = DimensionUnitType.Absolute;
+ panel.Visual.Width = 264.0f;
+ panel.Visual.Height = 70.0f;
+ panel.IsVisible = false;
+
+ TextureRegion backgroundRegion = atlas.GetRegion("panel-background");
+
+ NineSliceRuntime background = new NineSliceRuntime();
+ background.Dock(Gum.Wireframe.Dock.Fill);
+ background.Texture = backgroundRegion.Texture;
+ background.TextureAddress = TextureAddress.Custom;
+ background.TextureHeight = backgroundRegion.Height;
+ background.TextureWidth = backgroundRegion.Width;
+ background.TextureTop = backgroundRegion.SourceRectangle.Top;
+ background.TextureLeft = backgroundRegion.SourceRectangle.Left;
+ panel.AddChild(background);
+
+ TextRuntime text = new TextRuntime();
+ text.Text = "GAME OVER";
+ text.WidthUnits = DimensionUnitType.RelativeToChildren;
+ text.UseCustomFont = true;
+ text.CustomFontFile = "fonts/04b_30.fnt";
+ text.FontScale = 0.5f;
+ text.X = 10.0f;
+ text.Y = 10.0f;
+ panel.AddChild(text);
+
+ _retryButton = new AnimatedButton(atlas);
+ _retryButton.Text = "RETRY";
+ _retryButton.Anchor(Gum.Wireframe.Anchor.BottomLeft);
+ _retryButton.Visual.X = 9.0f;
+ _retryButton.Visual.Y = -9.0f;
+
+ _retryButton.Click += OnRetryButtonClicked;
+ _retryButton.GotFocus += OnElementGotFocus;
+
+ panel.AddChild(_retryButton);
+
+ AnimatedButton quitButton = new AnimatedButton(atlas);
+ quitButton.Text = "QUIT";
+ quitButton.Anchor(Gum.Wireframe.Anchor.BottomRight);
+ quitButton.Visual.X = -9.0f;
+ quitButton.Visual.Y = -9.0f;
+
+ quitButton.Click += OnQuitButtonClicked;
+ quitButton.GotFocus += OnElementGotFocus;
+
+ panel.AddChild(quitButton);
+
+ return panel;
+ }
+
+ private void OnResumeButtonClicked(object sender, EventArgs args)
+ {
+ // Button was clicked, play the ui sound effect for auditory feedback.
+ Core.Audio.PlaySoundEffect(_uiSoundEffect);
+
+ // Since the resume button was clicked, we need to hide the pause panel.
+ HidePausePanel();
+
+ // Invoke the ResumeButtonClick event
+ if (ResumeButtonClick != null)
+ {
+ ResumeButtonClick(sender, args);
+ }
+ }
+
+ private void OnRetryButtonClicked(object sender, EventArgs args)
+ {
+ // Button was clicked, play the ui sound effect for auditory feedback.
+ Core.Audio.PlaySoundEffect(_uiSoundEffect);
+
+ // Since the retry button was clicked, we need to hide the game over panel.
+ HideGameOverPanel();
+
+ // Invoke the RetryButtonClick event.
+ if (RetryButtonClick != null)
+ {
+ RetryButtonClick(sender, args);
+ }
+ }
+
+ private void OnQuitButtonClicked(object sender, EventArgs args)
+ {
+ // Button was clicked, play the ui sound effect for auditory feedback.
+ Core.Audio.PlaySoundEffect(_uiSoundEffect);
+
+ // Both panels have a quit button, so hide both panels
+ HidePausePanel();
+ HideGameOverPanel();
+
+ // Invoke the QuitButtonClick event.
+ if (QuitButtonClick != null)
+ {
+ QuitButtonClick(sender, args);
+ }
+ }
+
+ private void OnElementGotFocus(object sender, EventArgs args)
+ {
+ // A ui element that can receive focus has received focus, play the
+ // ui sound effect for auditory feedback.
+ Core.Audio.PlaySoundEffect(_uiSoundEffect);
+ }
+
+ ///
+ /// Updates the text on the score display.
+ ///
+ /// The score to display.
+ public void UpdateScoreText(int score)
+ {
+ _scoreText.Text = string.Format(s_scoreFormat, score);
+ }
+
+ ///
+ /// Tells the game scene ui to show the pause panel.
+ ///
+ public void ShowPausePanel()
+ {
+ _pausePanel.IsVisible = true;
+
+ // Give the resume button focus for keyboard/gamepad input.
+ _resumeButton.IsFocused = true;
+
+ // Ensure the game over panel isn't visible.
+ _gameOverPanel.IsVisible = false;
+ }
+
+ ///
+ /// Tells the game scene ui to hide the pause panel.
+ ///
+ public void HidePausePanel()
+ {
+ _pausePanel.IsVisible = false;
+ }
+
+ ///
+ /// Tells the game scene ui to show the game over panel.
+ ///
+ public void ShowGameOverPanel()
+ {
+ _gameOverPanel.IsVisible = true;
+
+ // Give the retry button focus for keyboard/gamepad input.
+ _retryButton.IsFocused = true;
+
+ // Ensure the pause panel isn't visible.
+ _pausePanel.IsVisible = false;
+ }
+
+ ///
+ /// Tells the game scene ui to hide the game over panel.
+ ///
+ public void HideGameOverPanel()
+ {
+ _gameOverPanel.IsVisible = false;
+ }
+
+ ///
+ /// Updates the game scene ui.
+ ///
+ /// A snapshot of the timing values for the current update cycle.
+ public void Update(GameTime gameTime)
+ {
+ GumService.Default.Update(gameTime);
+ }
+
+ ///
+ /// Draws the game scene ui.
+ ///
+ public void Draw()
+ {
+ GumService.Default.Draw();
+ }
+}
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Common/UI/OptionsSlider.cs b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Common/UI/OptionsSlider.cs
new file mode 100644
index 00000000..86d03281
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.Common/UI/OptionsSlider.cs
@@ -0,0 +1,253 @@
+using System;
+using Gum.DataTypes;
+using Gum.DataTypes.Variables;
+using Gum.Managers;
+using Microsoft.Xna.Framework;
+using Gum.Forms.Controls;
+using MonoGameGum.GueDeriving;
+using MonoGameLibrary.Graphics;
+
+namespace DungeonSlime.UI;
+
+///
+/// A custom slider control that inherits from Gum's Slider class.
+///
+public class OptionsSlider : Slider
+{
+ // Reference to the text label that displays the slider's title
+ private TextRuntime _textInstance;
+
+ // Reference to the rectangle that visually represents the current value
+ private ColoredRectangleRuntime _fillRectangle;
+
+ ///
+ /// Gets or sets the text label for this slider.
+ ///
+ public string Text
+ {
+ get => _textInstance.Text;
+ set => _textInstance.Text = value;
+ }
+
+ ///
+ /// Creates a new OptionsSlider instance using graphics from the specified texture atlas.
+ ///
+ /// The texture atlas containing slider graphics.
+ public OptionsSlider(TextureAtlas atlas)
+ {
+ // Create the top-level container for all visual elements
+ ContainerRuntime topLevelContainer = new ContainerRuntime();
+ topLevelContainer.Height = 55f;
+ topLevelContainer.Width = 264f;
+
+ TextureRegion backgroundRegion = atlas.GetRegion("panel-background");
+
+ // Create the background panel that contains everything
+ NineSliceRuntime background = new NineSliceRuntime();
+ background.Texture = atlas.Texture;
+ background.TextureAddress = TextureAddress.Custom;
+ background.TextureHeight = backgroundRegion.Height;
+ background.TextureLeft = backgroundRegion.SourceRectangle.Left;
+ background.TextureTop = backgroundRegion.SourceRectangle.Top;
+ background.TextureWidth = backgroundRegion.Width;
+ background.Dock(Gum.Wireframe.Dock.Fill);
+ topLevelContainer.AddChild(background);
+
+ // Create the title text element
+ _textInstance = new TextRuntime();
+ _textInstance.CustomFontFile = @"fonts/04b_30.fnt";
+ _textInstance.UseCustomFont = true;
+ _textInstance.FontScale = 0.5f;
+ _textInstance.Text = "Replace Me";
+ _textInstance.X = 10f;
+ _textInstance.Y = 10f;
+ _textInstance.WidthUnits = DimensionUnitType.RelativeToChildren;
+ topLevelContainer.AddChild(_textInstance);
+
+ // Create the container for the slider track and decorative elements
+ ContainerRuntime innerContainer = new ContainerRuntime();
+ innerContainer.Height = 13f;
+ innerContainer.Width = 241f;
+ innerContainer.X = 10f;
+ innerContainer.Y = 33f;
+ topLevelContainer.AddChild(innerContainer);
+
+ TextureRegion offBackgroundRegion = atlas.GetRegion("slider-off-background");
+
+ // Create the "OFF" side of the slider (left end)
+ NineSliceRuntime offBackground = new NineSliceRuntime();
+ offBackground.Dock(Gum.Wireframe.Dock.Left);
+ offBackground.Texture = atlas.Texture;
+ offBackground.TextureAddress = TextureAddress.Custom;
+ offBackground.TextureHeight = offBackgroundRegion.Height;
+ offBackground.TextureLeft = offBackgroundRegion.SourceRectangle.Left;
+ offBackground.TextureTop = offBackgroundRegion.SourceRectangle.Top;
+ offBackground.TextureWidth = offBackgroundRegion.Width;
+ offBackground.Width = 28f;
+ offBackground.WidthUnits = DimensionUnitType.Absolute;
+ offBackground.Dock(Gum.Wireframe.Dock.Left);
+ innerContainer.AddChild(offBackground);
+
+ TextureRegion middleBackgroundRegion = atlas.GetRegion("slider-middle-background");
+
+ // Create the middle track portion of the slider
+ NineSliceRuntime middleBackground = new NineSliceRuntime();
+ middleBackground.Dock(Gum.Wireframe.Dock.FillVertically);
+ middleBackground.Texture = middleBackgroundRegion.Texture;
+ middleBackground.TextureAddress = TextureAddress.Custom;
+ middleBackground.TextureHeight = middleBackgroundRegion.Height;
+ middleBackground.TextureLeft = middleBackgroundRegion.SourceRectangle.Left;
+ middleBackground.TextureTop = middleBackgroundRegion.SourceRectangle.Top;
+ middleBackground.TextureWidth = middleBackgroundRegion.Width;
+ middleBackground.Width = 179f;
+ middleBackground.WidthUnits = DimensionUnitType.Absolute;
+ middleBackground.Dock(Gum.Wireframe.Dock.Left);
+ middleBackground.X = 27f;
+ innerContainer.AddChild(middleBackground);
+
+ TextureRegion maxBackgroundRegion = atlas.GetRegion("slider-max-background");
+
+ // Create the "MAX" side of the slider (right end)
+ NineSliceRuntime maxBackground = new NineSliceRuntime();
+ maxBackground.Texture = maxBackgroundRegion.Texture;
+ maxBackground.TextureAddress = TextureAddress.Custom;
+ maxBackground.TextureHeight = maxBackgroundRegion.Height;
+ maxBackground.TextureLeft = maxBackgroundRegion.SourceRectangle.Left;
+ maxBackground.TextureTop = maxBackgroundRegion.SourceRectangle.Top;
+ maxBackground.TextureWidth = maxBackgroundRegion.Width;
+ maxBackground.Width = 36f;
+ maxBackground.WidthUnits = DimensionUnitType.Absolute;
+ maxBackground.Dock(Gum.Wireframe.Dock.Right);
+ innerContainer.AddChild(maxBackground);
+
+ // Create the interactive track that responds to clicks
+ // The special name "TrackInstance" is required for Slider functionality
+ ContainerRuntime trackInstance = new ContainerRuntime();
+ trackInstance.Name = "TrackInstance";
+ trackInstance.Dock(Gum.Wireframe.Dock.Fill);
+ trackInstance.Height = -2f;
+ trackInstance.Width = -2f;
+ middleBackground.AddChild(trackInstance);
+
+ // Create the fill rectangle that visually displays the current value
+ _fillRectangle = new ColoredRectangleRuntime();
+ _fillRectangle.Dock(Gum.Wireframe.Dock.Left);
+ _fillRectangle.Width = 90f; // Default to 90% - will be updated by value changes
+ _fillRectangle.WidthUnits = DimensionUnitType.PercentageOfParent;
+ trackInstance.AddChild(_fillRectangle);
+
+ // Add "OFF" text to the left end
+ TextRuntime offText = new TextRuntime();
+ offText.Red = 70;
+ offText.Green = 86;
+ offText.Blue = 130;
+ offText.CustomFontFile = @"fonts/04b_30.fnt";
+ offText.FontScale = 0.25f;
+ offText.UseCustomFont = true;
+ offText.Text = "OFF";
+ offText.Anchor(Gum.Wireframe.Anchor.Center);
+ offBackground.AddChild(offText);
+
+ // Add "MAX" text to the right end
+ TextRuntime maxText = new TextRuntime();
+ maxText.Red = 70;
+ maxText.Green = 86;
+ maxText.Blue = 130;
+ maxText.CustomFontFile = @"fonts/04b_30.fnt";
+ maxText.FontScale = 0.25f;
+ maxText.UseCustomFont = true;
+ maxText.Text = "MAX";
+ maxText.Anchor(Gum.Wireframe.Anchor.Center);
+ maxBackground.AddChild(maxText);
+
+ // Define colors for focused and unfocused states
+ Color focusedColor = Color.White;
+ Color unfocusedColor = Color.Gray;
+
+ // Create slider state category - Slider.SliderCategoryName is the required name
+ StateSaveCategory sliderCategory = new StateSaveCategory();
+ sliderCategory.Name = Slider.SliderCategoryName;
+ topLevelContainer.AddCategory(sliderCategory);
+
+ // Create the enabled (default/unfocused) state
+ StateSave enabled = new StateSave();
+ enabled.Name = FrameworkElement.EnabledStateName;
+ enabled.Apply = () =>
+ {
+ // When enabled but not focused, use gray coloring for all elements
+ background.Color = unfocusedColor;
+ _textInstance.Color = unfocusedColor;
+ offBackground.Color = unfocusedColor;
+ middleBackground.Color = unfocusedColor;
+ maxBackground.Color = unfocusedColor;
+ _fillRectangle.Color = unfocusedColor;
+ };
+ sliderCategory.States.Add(enabled);
+
+ // Create the focused state
+ StateSave focused = new StateSave();
+ focused.Name = FrameworkElement.FocusedStateName;
+ focused.Apply = () =>
+ {
+ // When focused, use white coloring for all elements
+ background.Color = focusedColor;
+ _textInstance.Color = focusedColor;
+ offBackground.Color = focusedColor;
+ middleBackground.Color = focusedColor;
+ maxBackground.Color = focusedColor;
+ _fillRectangle.Color = focusedColor;
+ };
+ sliderCategory.States.Add(focused);
+
+ // Create the highlighted+focused state by cloning the focused state
+ StateSave highlightedFocused = focused.Clone();
+ highlightedFocused.Name = FrameworkElement.HighlightedFocusedStateName;
+ sliderCategory.States.Add(highlightedFocused);
+
+ // Create the highlighted state by cloning the enabled state
+ StateSave highlighted = enabled.Clone();
+ highlighted.Name = FrameworkElement.HighlightedStateName;
+ sliderCategory.States.Add(highlighted);
+
+ // Assign the configured container as this slider's visual
+ Visual = topLevelContainer;
+
+ // Enable click-to-point functionality for the slider
+ // This allows users to click anywhere on the track to jump to that value
+ IsMoveToPointEnabled = true;
+
+ // Add event handlers
+ Visual.RollOn += HandleRollOn;
+ ValueChanged += HandleValueChanged;
+ ValueChangedByUi += HandleValueChangedByUi;
+ }
+
+ ///
+ /// Automatically focuses the slider when the user interacts with it
+ ///
+ private void HandleValueChangedByUi(object sender, EventArgs e)
+ {
+ IsFocused = true;
+ }
+
+ ///
+ /// Automatically focuses the slider when the mouse hovers over it
+ ///
+ private void HandleRollOn(object sender, EventArgs e)
+ {
+ IsFocused = true;
+ }
+
+ ///
+ /// Updates the fill rectangle width to visually represent the current value
+ ///
+ private void HandleValueChanged(object sender, EventArgs e)
+ {
+ // Calculate the ratio of the current value within its range
+ double ratio = (Value - Minimum) / (Maximum - Minimum);
+
+ // Update the fill rectangle width as a percentage
+ // _fillRectangle uses percentage width units, so we multiply by 100
+ _fillRectangle.Width = 100 * (float)ratio;
+ }
+}
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/.config/dotnet-tools.json b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/.config/dotnet-tools.json
new file mode 100644
index 00000000..fbedee15
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/.config/dotnet-tools.json
@@ -0,0 +1,36 @@
+{
+ "version": 1,
+ "isRoot": true,
+ "tools": {
+ "dotnet-mgcb": {
+ "version": "3.8.4",
+ "commands": [
+ "mgcb"
+ ]
+ },
+ "dotnet-mgcb-editor": {
+ "version": "3.8.4",
+ "commands": [
+ "mgcb-editor"
+ ]
+ },
+ "dotnet-mgcb-editor-linux": {
+ "version": "3.8.4",
+ "commands": [
+ "mgcb-editor-linux"
+ ]
+ },
+ "dotnet-mgcb-editor-windows": {
+ "version": "3.8.4",
+ "commands": [
+ "mgcb-editor-windows"
+ ]
+ },
+ "dotnet-mgcb-editor-mac": {
+ "version": "3.8.4",
+ "commands": [
+ "mgcb-editor-mac"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/Contents.json b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 00000000..0763ce12
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,93 @@
+{
+ "images": [
+ {
+ "idiom": "iphone",
+ "size": "20x20",
+ "scale": "2x",
+ "filename": "appicon20x20@2x.png"
+ },
+ {
+ "idiom": "ipad",
+ "size": "20x20",
+ "scale": "2x",
+ "filename": "appicon20x20@2x.png"
+ },
+ {
+ "idiom": "iphone",
+ "size": "20x20",
+ "scale": "3x",
+ "filename": "appicon20x20@3x.png"
+ },
+ {
+ "idiom": "iphone",
+ "size": "29x29",
+ "scale": "2x",
+ "filename": "appicon29x29@2x.png"
+ },
+ {
+ "idiom": "ipad",
+ "size": "29x29",
+ "scale": "2x",
+ "filename": "appicon29x29@2x.png"
+ },
+ {
+ "idiom": "iphone",
+ "size": "29x29",
+ "scale": "3x",
+ "filename": "appicon29x29@3x.png"
+ },
+ {
+ "idiom": "iphone",
+ "size": "40x40",
+ "scale": "2x",
+ "filename": "appicon40x40@2x.png"
+ },
+ {
+ "idiom": "ipad",
+ "size": "40x40",
+ "scale": "2x",
+ "filename": "appicon40x40@2x.png"
+ },
+ {
+ "idiom": "iphone",
+ "size": "40x40",
+ "scale": "3x",
+ "filename": "appicon40x40@3x.png"
+ },
+ {
+ "idiom": "iphone",
+ "size": "60x60",
+ "scale": "2x",
+ "filename": "appicon60x60@2x.png"
+ },
+ {
+ "idiom": "iphone",
+ "size": "60x60",
+ "scale": "3x",
+ "filename": "appicon60x60@3x.png"
+ },
+ {
+ "idiom": "ipad",
+ "size": "76x76",
+ "scale": "2x",
+ "filename": "appicon76x76@2x.png"
+ },
+ {
+ "idiom": "ipad",
+ "size": "83.5x83.5",
+ "scale": "2x",
+ "filename": "appicon83.5x83.5@2x.png"
+ },
+ {
+ "idiom": "ios-marketing",
+ "size": "1024x1024",
+ "scale": "1x",
+ "filename": "appiconItunesArtwork.png"
+ }
+ ],
+ "properties": {},
+ "info": {
+ "version": 1,
+ "author": "xcode"
+ }
+}
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon20x20@2x.png b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon20x20@2x.png
new file mode 100644
index 00000000..0d36c4be
Binary files /dev/null and b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon20x20@2x.png differ
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon20x20@3x.png b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon20x20@3x.png
new file mode 100644
index 00000000..13e81e5b
Binary files /dev/null and b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon20x20@3x.png differ
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon29x29@2x.png b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon29x29@2x.png
new file mode 100644
index 00000000..826942c9
Binary files /dev/null and b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon29x29@2x.png differ
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon29x29@3x.png b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon29x29@3x.png
new file mode 100644
index 00000000..eef69b7e
Binary files /dev/null and b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon29x29@3x.png differ
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon40x40@2x.png b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon40x40@2x.png
new file mode 100644
index 00000000..76bf8b6c
Binary files /dev/null and b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon40x40@2x.png differ
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon40x40@3x.png b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon40x40@3x.png
new file mode 100644
index 00000000..09da383e
Binary files /dev/null and b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon40x40@3x.png differ
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon60x60@2x.png b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon60x60@2x.png
new file mode 100644
index 00000000..2a6eba37
Binary files /dev/null and b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon60x60@2x.png differ
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon60x60@3x.png b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon60x60@3x.png
new file mode 100644
index 00000000..fd8c4a04
Binary files /dev/null and b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon60x60@3x.png differ
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon76x76@2x.png b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon76x76@2x.png
new file mode 100644
index 00000000..b3ef476c
Binary files /dev/null and b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon76x76@2x.png differ
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon83.5x83.5@2x.png b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon83.5x83.5@2x.png
new file mode 100644
index 00000000..cb03f365
Binary files /dev/null and b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appicon83.5x83.5@2x.png differ
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appiconItunesArtwork.png b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appiconItunesArtwork.png
new file mode 100644
index 00000000..3f789e7e
Binary files /dev/null and b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Assets.xcassets/AppIcon.appiconset/appiconItunesArtwork.png differ
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Assets.xcassets/Contents.json b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Assets.xcassets/Contents.json
new file mode 100644
index 00000000..121dee67
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info": {
+ "version": 1,
+ "author": "xcode"
+ }
+}
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Content/Content.mgcb b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Content/Content.mgcb
new file mode 100644
index 00000000..4c978bab
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Content/Content.mgcb
@@ -0,0 +1,102 @@
+#----------------------------- Global Properties ----------------------------#
+
+/outputDir:bin/$(Platform)
+/intermediateDir:obj/$(Platform)
+/platform:iOS
+/config:
+/profile:Reach
+/compress:False
+
+#-------------------------------- References --------------------------------#
+
+
+#---------------------------------- Content ---------------------------------#
+
+#begin audio/bounce.wav
+/importer:WavImporter
+/processor:SoundEffectProcessor
+/processorParam:Quality=Best
+/build:audio/bounce.wav
+
+#begin audio/collect.wav
+/importer:WavImporter
+/processor:SoundEffectProcessor
+/processorParam:Quality=Best
+/build:audio/collect.wav
+
+#begin audio/theme.ogg
+/importer:OggImporter
+/processor:SongProcessor
+/processorParam:Quality=Best
+/build:audio/theme.ogg
+
+#begin audio/ui.wav
+/importer:WavImporter
+/processor:SoundEffectProcessor
+/processorParam:Quality=Best
+/build:audio/ui.wav
+
+#begin effects/grayscaleEffect.fx
+/importer:EffectImporter
+/processor:EffectProcessor
+/processorParam:DebugMode=Auto
+/build:effects/grayscaleEffect.fx
+
+#begin fonts/04B_30_5x.spritefont
+/importer:FontDescriptionImporter
+/processor:FontDescriptionProcessor
+/processorParam:PremultiplyAlpha=True
+/processorParam:TextureFormat=Compressed
+/build:fonts/04B_30_5x.spritefont
+
+#begin fonts/04b_30.fnt
+/copy:fonts/04b_30.fnt
+
+#begin fonts/04B_30.spritefont
+/importer:FontDescriptionImporter
+/processor:FontDescriptionProcessor
+/processorParam:PremultiplyAlpha=True
+/processorParam:TextureFormat=Compressed
+/build:fonts/04B_30.spritefont
+
+#begin images/atlas-definition.xml
+/copy:images/atlas-definition.xml
+
+#begin images/atlas.png
+/importer:TextureImporter
+/processor:TextureProcessor
+/processorParam:ColorKeyColor=255,0,255,255
+/processorParam:ColorKeyEnabled=True
+/processorParam:GenerateMipmaps=False
+/processorParam:PremultiplyAlpha=True
+/processorParam:ResizeToPowerOfTwo=False
+/processorParam:MakeSquare=False
+/processorParam:TextureFormat=Color
+/build:images/atlas.png
+
+#begin images/background-pattern.png
+/importer:TextureImporter
+/processor:TextureProcessor
+/processorParam:ColorKeyColor=255,0,255,255
+/processorParam:ColorKeyEnabled=True
+/processorParam:GenerateMipmaps=False
+/processorParam:PremultiplyAlpha=True
+/processorParam:ResizeToPowerOfTwo=False
+/processorParam:MakeSquare=False
+/processorParam:TextureFormat=Color
+/build:images/background-pattern.png
+
+#begin images/logo.png
+/importer:TextureImporter
+/processor:TextureProcessor
+/processorParam:ColorKeyColor=255,0,255,255
+/processorParam:ColorKeyEnabled=True
+/processorParam:GenerateMipmaps=False
+/processorParam:PremultiplyAlpha=True
+/processorParam:ResizeToPowerOfTwo=False
+/processorParam:MakeSquare=False
+/processorParam:TextureFormat=Color
+/build:images/logo.png
+
+#begin images/tilemap-definition.xml
+/copy:images/tilemap-definition.xml
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Content/audio/bounce.wav b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Content/audio/bounce.wav
new file mode 100644
index 00000000..baa7a47b
Binary files /dev/null and b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Content/audio/bounce.wav differ
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Content/audio/collect.wav b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Content/audio/collect.wav
new file mode 100644
index 00000000..506220de
Binary files /dev/null and b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Content/audio/collect.wav differ
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Content/audio/theme.ogg b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Content/audio/theme.ogg
new file mode 100644
index 00000000..72e1fd3b
Binary files /dev/null and b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Content/audio/theme.ogg differ
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Content/audio/ui.wav b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Content/audio/ui.wav
new file mode 100644
index 00000000..63e8941e
Binary files /dev/null and b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Content/audio/ui.wav differ
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Content/effects/grayscaleEffect.fx b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Content/effects/grayscaleEffect.fx
new file mode 100644
index 00000000..5dd0d8b6
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Content/effects/grayscaleEffect.fx
@@ -0,0 +1,53 @@
+#if OPENGL
+ #define SV_POSITION POSITION
+ #define VS_SHADERMODEL vs_3_0
+ #define PS_SHADERMODEL ps_3_0
+#else
+ #define VS_SHADERMODEL vs_4_0_level_9_1
+ #define PS_SHADERMODEL ps_4_0_level_9_1
+#endif
+
+Texture2D SpriteTexture;
+
+// A value between 0 and 1 that controls the intensity of the grayscale effect.
+// 0 = full color, 1 = full grayscale.
+float Saturation = 1.0;
+
+sampler2D SpriteTextureSampler = sampler_state
+{
+ Texture = ;
+};
+
+struct VertexShaderOutput
+{
+ float4 Position : SV_POSITION;
+ float4 Color : COLOR0;
+ float2 TextureCoordinates : TEXCOORD0;
+};
+
+float4 MainPS(VertexShaderOutput input) : COLOR
+{
+ // Sample the texture
+ float4 color = tex2D(SpriteTextureSampler, input.TextureCoordinates) * input.Color;
+
+ // Calculate the grayscale value based on human perception of colors
+ float grayscale = dot(color.rgb, float3(0.3, 0.59, 0.11));
+
+ // create a grayscale color vector (same value for R, G, and B)
+ float3 grayscaleColor = float3(grayscale, grayscale, grayscale);
+
+ // Linear interpolation between he grayscale color and the original color's
+ // rgb values based on the saturation parameter.
+ float3 finalColor = lerp(grayscale, color.rgb, Saturation);
+
+ // Return the final color with the original alpha value
+ return float4(finalColor, color.a);
+}
+
+technique SpriteDrawing
+{
+ pass P0
+ {
+ PixelShader = compile PS_SHADERMODEL MainPS();
+ }
+};
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Content/fonts/04B_30.spritefont b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Content/fonts/04B_30.spritefont
new file mode 100644
index 00000000..63d4728c
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Content/fonts/04B_30.spritefont
@@ -0,0 +1,16 @@
+
+
+
+ 04B_30.ttf
+ 17.5
+ 0
+ true
+
+
+
+
+ ~
+
+
+
+
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Content/fonts/04B_30.ttf b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Content/fonts/04B_30.ttf
new file mode 100644
index 00000000..4b93740c
Binary files /dev/null and b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Content/fonts/04B_30.ttf differ
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Content/fonts/04B_30_5x.spritefont b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Content/fonts/04B_30_5x.spritefont
new file mode 100644
index 00000000..dd239a53
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Content/fonts/04B_30_5x.spritefont
@@ -0,0 +1,16 @@
+
+
+
+ 04B_30.ttf
+ 87.5
+ 0
+ true
+
+
+
+
+ ~
+
+
+
+
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Content/fonts/04b_30.fnt b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Content/fonts/04b_30.fnt
new file mode 100644
index 00000000..772f8c54
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Content/fonts/04b_30.fnt
@@ -0,0 +1,99 @@
+info face="04b30" size=35 bold=0 italic=0 charset="" unicode=1 stretchH=100 smooth=0 aa=1 padding=0,0,0,0 spacing=1,1 outline=0
+common lineHeight=35 base=31 scaleW=256 scaleH=512 pages=1 packed=0 alphaChnl=0 redChnl=4 greenChnl=4 blueChnl=4
+page id=0 file="../images/atlas.png"
+chars count=95
+char id=32 x=30 y=152 width=3 height=1 xoffset=-1 yoffset=34 xadvance=29 page=0 chnl=15
+char id=33 x=240 y=30 width=14 height=29 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=34 x=102 y=232 width=25 height=15 xoffset=1 yoffset=4 xadvance=29 page=0 chnl=15
+char id=35 x=184 y=0 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=36 x=250 y=150 width=3 height=1 xoffset=-1 yoffset=34 xadvance=29 page=0 chnl=15
+char id=37 x=0 y=34 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=38 x=30 y=150 width=3 height=1 xoffset=-1 yoffset=34 xadvance=29 page=0 chnl=15
+char id=39 x=245 y=202 width=10 height=15 xoffset=1 yoffset=4 xadvance=14 page=0 chnl=15
+char id=40 x=106 y=150 width=16 height=29 xoffset=1 yoffset=2 xadvance=21 page=0 chnl=15
+char id=41 x=123 y=150 width=16 height=29 xoffset=1 yoffset=2 xadvance=21 page=0 chnl=15
+char id=42 x=128 y=232 width=14 height=15 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=43 x=94 y=0 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=44 x=143 y=232 width=10 height=14 xoffset=1 yoffset=19 xadvance=14 page=0 chnl=15
+char id=45 x=154 y=232 width=25 height=11 xoffset=1 yoffset=12 xadvance=29 page=0 chnl=15
+char id=46 x=231 y=228 width=10 height=10 xoffset=1 yoffset=19 xadvance=14 page=0 chnl=15
+char id=47 x=60 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=48 x=90 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=49 x=46 y=150 width=19 height=29 xoffset=1 yoffset=2 xadvance=23 page=0 chnl=15
+char id=50 x=150 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=51 x=180 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=52 x=210 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=53 x=0 y=94 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=54 x=180 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=55 x=60 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=56 x=90 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=57 x=120 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=58 x=234 y=202 width=10 height=25 xoffset=1 yoffset=4 xadvance=14 page=0 chnl=15
+char id=59 x=244 y=0 width=10 height=29 xoffset=1 yoffset=4 xadvance=14 page=0 chnl=15
+char id=60 x=86 y=150 width=19 height=29 xoffset=1 yoffset=2 xadvance=23 page=0 chnl=15
+char id=61 x=182 y=176 width=25 height=25 xoffset=1 yoffset=4 xadvance=29 page=0 chnl=15
+char id=62 x=237 y=120 width=18 height=29 xoffset=1 yoffset=2 xadvance=23 page=0 chnl=15
+char id=63 x=180 y=120 width=28 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=64 x=34 y=150 width=3 height=1 xoffset=-1 yoffset=34 xadvance=29 page=0 chnl=15
+char id=65 x=120 y=120 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=66 x=150 y=120 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=67 x=124 y=0 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=68 x=154 y=0 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=69 x=214 y=0 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=70 x=30 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=71 x=60 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=72 x=90 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=73 x=240 y=90 width=14 height=29 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=74 x=120 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=75 x=150 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=76 x=209 y=120 width=27 height=29 xoffset=1 yoffset=2 xadvance=31 page=0 chnl=15
+char id=77 x=30 y=0 width=31 height=29 xoffset=1 yoffset=2 xadvance=35 page=0 chnl=15
+char id=78 x=210 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=79 x=0 y=64 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=80 x=30 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=81 x=0 y=0 width=29 height=33 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=82 x=120 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=83 x=30 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=84 x=150 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=85 x=180 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=86 x=210 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=87 x=62 y=0 width=31 height=29 xoffset=1 yoffset=2 xadvance=35 page=0 chnl=15
+char id=88 x=0 y=124 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=89 x=30 y=120 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=90 x=60 y=120 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=91 x=240 y=60 width=14 height=29 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=92 x=90 y=120 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=93 x=140 y=150 width=14 height=29 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=94 x=180 y=232 width=16 height=11 xoffset=1 yoffset=4 xadvance=21 page=0 chnl=15
+char id=95 x=0 y=262 width=29 height=10 xoffset=1 yoffset=21 xadvance=33 page=0 chnl=15
+char id=96 x=197 y=228 width=16 height=11 xoffset=1 yoffset=4 xadvance=21 page=0 chnl=15
+char id=97 x=208 y=176 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=98 x=0 y=210 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=99 x=26 y=210 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=100 x=52 y=206 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=101 x=78 y=206 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=102 x=104 y=206 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=103 x=130 y=206 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=104 x=156 y=206 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=105 x=234 y=176 width=12 height=25 xoffset=1 yoffset=6 xadvance=16 page=0 chnl=15
+char id=106 x=182 y=202 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=107 x=208 y=202 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=108 x=78 y=232 width=23 height=25 xoffset=1 yoffset=6 xadvance=27 page=0 chnl=15
+char id=109 x=197 y=150 width=26 height=25 xoffset=1 yoffset=6 xadvance=31 page=0 chnl=15
+char id=110 x=0 y=236 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=111 x=26 y=236 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=112 x=78 y=180 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=113 x=0 y=154 width=25 height=29 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=114 x=52 y=232 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=115 x=224 y=150 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=116 x=0 y=184 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=117 x=26 y=184 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=118 x=52 y=180 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=119 x=170 y=150 width=26 height=25 xoffset=1 yoffset=6 xadvance=31 page=0 chnl=15
+char id=120 x=104 y=180 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=121 x=130 y=180 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=122 x=156 y=180 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=123 x=26 y=154 width=19 height=29 xoffset=1 yoffset=2 xadvance=23 page=0 chnl=15
+char id=124 x=155 y=150 width=14 height=29 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=125 x=66 y=150 width=19 height=29 xoffset=1 yoffset=2 xadvance=23 page=0 chnl=15
+char id=126 x=214 y=228 width=16 height=11 xoffset=1 yoffset=4 xadvance=21 page=0 chnl=15
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Content/images/atlas-definition.xml b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Content/images/atlas-definition.xml
new file mode 100644
index 00000000..21772022
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Content/images/atlas-definition.xml
@@ -0,0 +1,34 @@
+
+
+ images/atlas
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Content/images/atlas.png b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Content/images/atlas.png
new file mode 100644
index 00000000..f7def20f
Binary files /dev/null and b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Content/images/atlas.png differ
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Content/images/background-pattern.png b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Content/images/background-pattern.png
new file mode 100644
index 00000000..2d8d878e
Binary files /dev/null and b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Content/images/background-pattern.png differ
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Content/images/logo.png b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Content/images/logo.png
new file mode 100644
index 00000000..1509036c
Binary files /dev/null and b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Content/images/logo.png differ
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Content/images/tilemap-definition.xml b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Content/images/tilemap-definition.xml
new file mode 100644
index 00000000..85658c60
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Content/images/tilemap-definition.xml
@@ -0,0 +1,15 @@
+
+
+ images/atlas
+
+ 00 01 02 01 02 01 02 01 02 01 02 01 02 01 02 03
+ 04 05 05 06 05 05 06 05 05 06 05 05 06 05 05 07
+ 08 09 09 09 09 09 09 09 09 09 09 09 09 09 09 11
+ 04 09 09 09 09 09 09 09 10 09 09 09 09 10 09 07
+ 08 09 10 09 09 09 09 09 09 09 09 09 09 09 09 11
+ 04 09 09 09 09 09 09 09 09 09 09 09 09 09 09 07
+ 08 10 09 09 09 09 09 09 09 09 10 09 09 09 09 11
+ 04 09 09 09 09 09 10 09 09 09 09 09 09 09 09 07
+ 12 13 14 13 14 13 14 13 14 13 14 13 14 13 14 15
+
+
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Default.png b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Default.png
new file mode 100644
index 00000000..1f9b909f
Binary files /dev/null and b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Default.png differ
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/DungeonSlime.iOS.csproj b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/DungeonSlime.iOS.csproj
new file mode 100644
index 00000000..bf5bebac
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/DungeonSlime.iOS.csproj
@@ -0,0 +1,42 @@
+
+
+
+ Exe
+ net9.0-ios
+ 12.2
+ com.monogame.dungeonslime
+ AppIcon
+
+
+
+ iPhone Distribution
+ EntitlementsProduction.plist
+
+
+
+ iPhone Developer
+ Entitlements.plist
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Assets.car
+
+
+
+
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Entitlements.plist b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Entitlements.plist
new file mode 100644
index 00000000..4eac5f62
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Entitlements.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ get-task-allow
+
+
+
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/EntitlementsProduction.plist b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/EntitlementsProduction.plist
new file mode 100644
index 00000000..efc29758
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/EntitlementsProduction.plist
@@ -0,0 +1,9 @@
+
+
+
+
+
+ get-task-allow
+
+
+
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/GameThumbnail.png b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/GameThumbnail.png
new file mode 100644
index 00000000..99814c32
Binary files /dev/null and b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/GameThumbnail.png differ
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Info.plist b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Info.plist
new file mode 100644
index 00000000..a53431ce
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Info.plist
@@ -0,0 +1,52 @@
+
+
+
+
+ CFBundleIconName
+ AppIcon
+ CFBundleIcons
+
+ CFBundlePrimaryIcon
+
+ CFBundleIconName
+ AppIcon
+
+
+ ITSAppUsesNonExemptEncryption
+
+ CFBundleDisplayName
+ DungeonSlime
+ CFBundleIdentifier
+ com.monogame.dungeonslime
+ MinimumOSVersion
+ 12.2
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ CFBundleName
+ DungeonSlime
+ CFBundleVersion
+ 11
+ CFBundleShortVersionString
+ 1.10
+ UIRequiresFullScreen
+
+ UIStatusBarHidden
+
+ UIRequiredDeviceCapabilities
+
+ opengles-2
+
+ UILaunchStoryboardName
+ LaunchScreen
+ UIDeviceFamily
+
+ 1
+ 2
+
+ CFBundleExecutable
+
+
+
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/LaunchScreen.storyboard b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/LaunchScreen.storyboard
new file mode 100644
index 00000000..5d2e905a
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/LaunchScreen.storyboard
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Program.cs b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Program.cs
new file mode 100644
index 00000000..1b312ad9
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/Program.cs
@@ -0,0 +1,39 @@
+using System;
+using Foundation;
+using UIKit;
+
+namespace DungeonSlime.iOS
+{
+ [Register("AppDelegate")]
+ class Program : UIApplicationDelegate
+ {
+ private static Game1 game;
+
+ internal static void RunGame()
+ {
+ try
+ {
+ game = new Game1();
+ game.Run();
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Game failed to start: {ex}");
+ throw;
+ }
+ }
+
+ ///
+ /// The main entry point for the application.
+ ///
+ static void Main(string[] args)
+ {
+ UIApplication.Main(args, null, typeof(Program));
+ }
+
+ public override void FinishedLaunching(UIApplication app)
+ {
+ RunGame();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/build/AppIcon60x60@2x.png b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/build/AppIcon60x60@2x.png
new file mode 100644
index 00000000..dfc2bd20
Binary files /dev/null and b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/build/AppIcon60x60@2x.png differ
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/build/AppIcon76x76@2x~ipad.png b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/build/AppIcon76x76@2x~ipad.png
new file mode 100644
index 00000000..4af7ad2b
Binary files /dev/null and b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/build/AppIcon76x76@2x~ipad.png differ
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/build/Assets.car b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/build/Assets.car
new file mode 100644
index 00000000..468c84e0
Binary files /dev/null and b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/build/Assets.car differ
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/build/AssetsCompiled/AppIcon60x60@2x.png b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/build/AssetsCompiled/AppIcon60x60@2x.png
new file mode 100644
index 00000000..dfc2bd20
Binary files /dev/null and b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/build/AssetsCompiled/AppIcon60x60@2x.png differ
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/build/AssetsCompiled/AppIcon76x76@2x~ipad.png b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/build/AssetsCompiled/AppIcon76x76@2x~ipad.png
new file mode 100644
index 00000000..4af7ad2b
Binary files /dev/null and b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/build/AssetsCompiled/AppIcon76x76@2x~ipad.png differ
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/build/AssetsCompiled/Assets.car b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/build/AssetsCompiled/Assets.car
new file mode 100644
index 00000000..468c84e0
Binary files /dev/null and b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/build/AssetsCompiled/Assets.car differ
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/partial-info.plist b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/partial-info.plist
new file mode 100644
index 00000000..a19ab726
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.iOS/partial-info.plist
@@ -0,0 +1,31 @@
+
+
+
+
+ CFBundleIcons
+
+ CFBundlePrimaryIcon
+
+ CFBundleIconFiles
+
+ AppIcon60x60
+
+ CFBundleIconName
+ AppIcon
+
+
+ CFBundleIcons~ipad
+
+ CFBundlePrimaryIcon
+
+ CFBundleIconFiles
+
+ AppIcon60x60
+ AppIcon76x76
+
+ CFBundleIconName
+ AppIcon
+
+
+
+
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.slnx b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.slnx
new file mode 100644
index 00000000..55325e91
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime.slnx
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/.config/dotnet-tools.json b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/.config/dotnet-tools.json
new file mode 100644
index 00000000..afd4e2c4
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/.config/dotnet-tools.json
@@ -0,0 +1,36 @@
+{
+ "version": 1,
+ "isRoot": true,
+ "tools": {
+ "dotnet-mgcb": {
+ "version": "3.8.3",
+ "commands": [
+ "mgcb"
+ ]
+ },
+ "dotnet-mgcb-editor": {
+ "version": "3.8.3",
+ "commands": [
+ "mgcb-editor"
+ ]
+ },
+ "dotnet-mgcb-editor-linux": {
+ "version": "3.8.3",
+ "commands": [
+ "mgcb-editor-linux"
+ ]
+ },
+ "dotnet-mgcb-editor-windows": {
+ "version": "3.8.3",
+ "commands": [
+ "mgcb-editor-windows"
+ ]
+ },
+ "dotnet-mgcb-editor-mac": {
+ "version": "3.8.3",
+ "commands": [
+ "mgcb-editor-mac"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/Content/Content.mgcb b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/Content/Content.mgcb
new file mode 100644
index 00000000..d26ea4f1
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/Content/Content.mgcb
@@ -0,0 +1,104 @@
+
+#----------------------------- Global Properties ----------------------------#
+
+/outputDir:bin/$(Platform)
+/intermediateDir:obj/$(Platform)
+/platform:DesktopGL
+/config:
+/profile:Reach
+/compress:False
+
+#-------------------------------- References --------------------------------#
+
+
+#---------------------------------- Content ---------------------------------#
+
+#begin audio/bounce.wav
+/importer:WavImporter
+/processor:SoundEffectProcessor
+/processorParam:Quality=Best
+/build:audio/bounce.wav
+
+#begin audio/collect.wav
+/importer:WavImporter
+/processor:SoundEffectProcessor
+/processorParam:Quality=Best
+/build:audio/collect.wav
+
+#begin audio/theme.ogg
+/importer:OggImporter
+/processor:SongProcessor
+/processorParam:Quality=Best
+/build:audio/theme.ogg
+
+#begin audio/ui.wav
+/importer:WavImporter
+/processor:SoundEffectProcessor
+/processorParam:Quality=Best
+/build:audio/ui.wav
+
+#begin effects/grayscaleEffect.fx
+/importer:EffectImporter
+/processor:EffectProcessor
+/processorParam:DebugMode=Auto
+/build:effects/grayscaleEffect.fx
+
+#begin fonts/04B_30_5x.spritefont
+/importer:FontDescriptionImporter
+/processor:FontDescriptionProcessor
+/processorParam:PremultiplyAlpha=True
+/processorParam:TextureFormat=Compressed
+/build:fonts/04B_30_5x.spritefont
+
+#begin fonts/04b_30.fnt
+/copy:fonts/04b_30.fnt
+
+#begin fonts/04B_30.spritefont
+/importer:FontDescriptionImporter
+/processor:FontDescriptionProcessor
+/processorParam:PremultiplyAlpha=True
+/processorParam:TextureFormat=Compressed
+/build:fonts/04B_30.spritefont
+
+#begin images/atlas-definition.xml
+/copy:images/atlas-definition.xml
+
+#begin images/atlas.png
+/importer:TextureImporter
+/processor:TextureProcessor
+/processorParam:ColorKeyColor=255,0,255,255
+/processorParam:ColorKeyEnabled=True
+/processorParam:GenerateMipmaps=False
+/processorParam:PremultiplyAlpha=True
+/processorParam:ResizeToPowerOfTwo=False
+/processorParam:MakeSquare=False
+/processorParam:TextureFormat=Color
+/build:images/atlas.png
+
+#begin images/background-pattern.png
+/importer:TextureImporter
+/processor:TextureProcessor
+/processorParam:ColorKeyColor=255,0,255,255
+/processorParam:ColorKeyEnabled=True
+/processorParam:GenerateMipmaps=False
+/processorParam:PremultiplyAlpha=True
+/processorParam:ResizeToPowerOfTwo=False
+/processorParam:MakeSquare=False
+/processorParam:TextureFormat=Color
+/build:images/background-pattern.png
+
+#begin images/logo.png
+/importer:TextureImporter
+/processor:TextureProcessor
+/processorParam:ColorKeyColor=255,0,255,255
+/processorParam:ColorKeyEnabled=True
+/processorParam:GenerateMipmaps=False
+/processorParam:PremultiplyAlpha=True
+/processorParam:ResizeToPowerOfTwo=False
+/processorParam:MakeSquare=False
+/processorParam:TextureFormat=Color
+/build:images/logo.png
+
+#begin images/tilemap-definition.xml
+/copy:images/tilemap-definition.xml
+
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/Content/audio/bounce.wav b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/Content/audio/bounce.wav
new file mode 100644
index 00000000..baa7a47b
Binary files /dev/null and b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/Content/audio/bounce.wav differ
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/Content/audio/collect.wav b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/Content/audio/collect.wav
new file mode 100644
index 00000000..506220de
Binary files /dev/null and b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/Content/audio/collect.wav differ
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/Content/audio/theme.ogg b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/Content/audio/theme.ogg
new file mode 100644
index 00000000..72e1fd3b
Binary files /dev/null and b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/Content/audio/theme.ogg differ
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/Content/audio/ui.wav b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/Content/audio/ui.wav
new file mode 100644
index 00000000..63e8941e
Binary files /dev/null and b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/Content/audio/ui.wav differ
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/Content/effects/grayscaleEffect.fx b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/Content/effects/grayscaleEffect.fx
new file mode 100644
index 00000000..5dd0d8b6
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/Content/effects/grayscaleEffect.fx
@@ -0,0 +1,53 @@
+#if OPENGL
+ #define SV_POSITION POSITION
+ #define VS_SHADERMODEL vs_3_0
+ #define PS_SHADERMODEL ps_3_0
+#else
+ #define VS_SHADERMODEL vs_4_0_level_9_1
+ #define PS_SHADERMODEL ps_4_0_level_9_1
+#endif
+
+Texture2D SpriteTexture;
+
+// A value between 0 and 1 that controls the intensity of the grayscale effect.
+// 0 = full color, 1 = full grayscale.
+float Saturation = 1.0;
+
+sampler2D SpriteTextureSampler = sampler_state
+{
+ Texture = ;
+};
+
+struct VertexShaderOutput
+{
+ float4 Position : SV_POSITION;
+ float4 Color : COLOR0;
+ float2 TextureCoordinates : TEXCOORD0;
+};
+
+float4 MainPS(VertexShaderOutput input) : COLOR
+{
+ // Sample the texture
+ float4 color = tex2D(SpriteTextureSampler, input.TextureCoordinates) * input.Color;
+
+ // Calculate the grayscale value based on human perception of colors
+ float grayscale = dot(color.rgb, float3(0.3, 0.59, 0.11));
+
+ // create a grayscale color vector (same value for R, G, and B)
+ float3 grayscaleColor = float3(grayscale, grayscale, grayscale);
+
+ // Linear interpolation between he grayscale color and the original color's
+ // rgb values based on the saturation parameter.
+ float3 finalColor = lerp(grayscale, color.rgb, Saturation);
+
+ // Return the final color with the original alpha value
+ return float4(finalColor, color.a);
+}
+
+technique SpriteDrawing
+{
+ pass P0
+ {
+ PixelShader = compile PS_SHADERMODEL MainPS();
+ }
+};
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/Content/fonts/04B_30.spritefont b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/Content/fonts/04B_30.spritefont
new file mode 100644
index 00000000..63d4728c
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/Content/fonts/04B_30.spritefont
@@ -0,0 +1,16 @@
+
+
+
+ 04B_30.ttf
+ 17.5
+ 0
+ true
+
+
+
+
+ ~
+
+
+
+
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/Content/fonts/04B_30.ttf b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/Content/fonts/04B_30.ttf
new file mode 100644
index 00000000..4b93740c
Binary files /dev/null and b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/Content/fonts/04B_30.ttf differ
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/Content/fonts/04B_30_5x.spritefont b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/Content/fonts/04B_30_5x.spritefont
new file mode 100644
index 00000000..dd239a53
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/Content/fonts/04B_30_5x.spritefont
@@ -0,0 +1,16 @@
+
+
+
+ 04B_30.ttf
+ 87.5
+ 0
+ true
+
+
+
+
+ ~
+
+
+
+
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/Content/fonts/04b_30.fnt b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/Content/fonts/04b_30.fnt
new file mode 100644
index 00000000..772f8c54
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/Content/fonts/04b_30.fnt
@@ -0,0 +1,99 @@
+info face="04b30" size=35 bold=0 italic=0 charset="" unicode=1 stretchH=100 smooth=0 aa=1 padding=0,0,0,0 spacing=1,1 outline=0
+common lineHeight=35 base=31 scaleW=256 scaleH=512 pages=1 packed=0 alphaChnl=0 redChnl=4 greenChnl=4 blueChnl=4
+page id=0 file="../images/atlas.png"
+chars count=95
+char id=32 x=30 y=152 width=3 height=1 xoffset=-1 yoffset=34 xadvance=29 page=0 chnl=15
+char id=33 x=240 y=30 width=14 height=29 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=34 x=102 y=232 width=25 height=15 xoffset=1 yoffset=4 xadvance=29 page=0 chnl=15
+char id=35 x=184 y=0 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=36 x=250 y=150 width=3 height=1 xoffset=-1 yoffset=34 xadvance=29 page=0 chnl=15
+char id=37 x=0 y=34 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=38 x=30 y=150 width=3 height=1 xoffset=-1 yoffset=34 xadvance=29 page=0 chnl=15
+char id=39 x=245 y=202 width=10 height=15 xoffset=1 yoffset=4 xadvance=14 page=0 chnl=15
+char id=40 x=106 y=150 width=16 height=29 xoffset=1 yoffset=2 xadvance=21 page=0 chnl=15
+char id=41 x=123 y=150 width=16 height=29 xoffset=1 yoffset=2 xadvance=21 page=0 chnl=15
+char id=42 x=128 y=232 width=14 height=15 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=43 x=94 y=0 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=44 x=143 y=232 width=10 height=14 xoffset=1 yoffset=19 xadvance=14 page=0 chnl=15
+char id=45 x=154 y=232 width=25 height=11 xoffset=1 yoffset=12 xadvance=29 page=0 chnl=15
+char id=46 x=231 y=228 width=10 height=10 xoffset=1 yoffset=19 xadvance=14 page=0 chnl=15
+char id=47 x=60 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=48 x=90 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=49 x=46 y=150 width=19 height=29 xoffset=1 yoffset=2 xadvance=23 page=0 chnl=15
+char id=50 x=150 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=51 x=180 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=52 x=210 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=53 x=0 y=94 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=54 x=180 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=55 x=60 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=56 x=90 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=57 x=120 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=58 x=234 y=202 width=10 height=25 xoffset=1 yoffset=4 xadvance=14 page=0 chnl=15
+char id=59 x=244 y=0 width=10 height=29 xoffset=1 yoffset=4 xadvance=14 page=0 chnl=15
+char id=60 x=86 y=150 width=19 height=29 xoffset=1 yoffset=2 xadvance=23 page=0 chnl=15
+char id=61 x=182 y=176 width=25 height=25 xoffset=1 yoffset=4 xadvance=29 page=0 chnl=15
+char id=62 x=237 y=120 width=18 height=29 xoffset=1 yoffset=2 xadvance=23 page=0 chnl=15
+char id=63 x=180 y=120 width=28 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=64 x=34 y=150 width=3 height=1 xoffset=-1 yoffset=34 xadvance=29 page=0 chnl=15
+char id=65 x=120 y=120 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=66 x=150 y=120 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=67 x=124 y=0 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=68 x=154 y=0 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=69 x=214 y=0 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=70 x=30 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=71 x=60 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=72 x=90 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=73 x=240 y=90 width=14 height=29 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=74 x=120 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=75 x=150 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=76 x=209 y=120 width=27 height=29 xoffset=1 yoffset=2 xadvance=31 page=0 chnl=15
+char id=77 x=30 y=0 width=31 height=29 xoffset=1 yoffset=2 xadvance=35 page=0 chnl=15
+char id=78 x=210 y=30 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=79 x=0 y=64 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=80 x=30 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=81 x=0 y=0 width=29 height=33 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=82 x=120 y=60 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=83 x=30 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=84 x=150 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=85 x=180 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=86 x=210 y=90 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=87 x=62 y=0 width=31 height=29 xoffset=1 yoffset=2 xadvance=35 page=0 chnl=15
+char id=88 x=0 y=124 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=89 x=30 y=120 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=90 x=60 y=120 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=91 x=240 y=60 width=14 height=29 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=92 x=90 y=120 width=29 height=29 xoffset=1 yoffset=2 xadvance=33 page=0 chnl=15
+char id=93 x=140 y=150 width=14 height=29 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=94 x=180 y=232 width=16 height=11 xoffset=1 yoffset=4 xadvance=21 page=0 chnl=15
+char id=95 x=0 y=262 width=29 height=10 xoffset=1 yoffset=21 xadvance=33 page=0 chnl=15
+char id=96 x=197 y=228 width=16 height=11 xoffset=1 yoffset=4 xadvance=21 page=0 chnl=15
+char id=97 x=208 y=176 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=98 x=0 y=210 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=99 x=26 y=210 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=100 x=52 y=206 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=101 x=78 y=206 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=102 x=104 y=206 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=103 x=130 y=206 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=104 x=156 y=206 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=105 x=234 y=176 width=12 height=25 xoffset=1 yoffset=6 xadvance=16 page=0 chnl=15
+char id=106 x=182 y=202 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=107 x=208 y=202 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=108 x=78 y=232 width=23 height=25 xoffset=1 yoffset=6 xadvance=27 page=0 chnl=15
+char id=109 x=197 y=150 width=26 height=25 xoffset=1 yoffset=6 xadvance=31 page=0 chnl=15
+char id=110 x=0 y=236 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=111 x=26 y=236 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=112 x=78 y=180 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=113 x=0 y=154 width=25 height=29 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=114 x=52 y=232 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=115 x=224 y=150 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=116 x=0 y=184 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=117 x=26 y=184 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=118 x=52 y=180 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=119 x=170 y=150 width=26 height=25 xoffset=1 yoffset=6 xadvance=31 page=0 chnl=15
+char id=120 x=104 y=180 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=121 x=130 y=180 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=122 x=156 y=180 width=25 height=25 xoffset=1 yoffset=6 xadvance=29 page=0 chnl=15
+char id=123 x=26 y=154 width=19 height=29 xoffset=1 yoffset=2 xadvance=23 page=0 chnl=15
+char id=124 x=155 y=150 width=14 height=29 xoffset=1 yoffset=2 xadvance=19 page=0 chnl=15
+char id=125 x=66 y=150 width=19 height=29 xoffset=1 yoffset=2 xadvance=23 page=0 chnl=15
+char id=126 x=214 y=228 width=16 height=11 xoffset=1 yoffset=4 xadvance=21 page=0 chnl=15
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/Content/images/atlas-definition.xml b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/Content/images/atlas-definition.xml
new file mode 100644
index 00000000..21772022
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/Content/images/atlas-definition.xml
@@ -0,0 +1,34 @@
+
+
+ images/atlas
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/Content/images/atlas.png b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/Content/images/atlas.png
new file mode 100644
index 00000000..f7def20f
Binary files /dev/null and b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/Content/images/atlas.png differ
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/Content/images/background-pattern.png b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/Content/images/background-pattern.png
new file mode 100644
index 00000000..2d8d878e
Binary files /dev/null and b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/Content/images/background-pattern.png differ
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/Content/images/logo.png b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/Content/images/logo.png
new file mode 100644
index 00000000..1509036c
Binary files /dev/null and b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/Content/images/logo.png differ
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/Content/images/tilemap-definition.xml b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/Content/images/tilemap-definition.xml
new file mode 100644
index 00000000..85658c60
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/Content/images/tilemap-definition.xml
@@ -0,0 +1,15 @@
+
+
+ images/atlas
+
+ 00 01 02 01 02 01 02 01 02 01 02 01 02 01 02 03
+ 04 05 05 06 05 05 06 05 05 06 05 05 06 05 05 07
+ 08 09 09 09 09 09 09 09 09 09 09 09 09 09 09 11
+ 04 09 09 09 09 09 09 09 10 09 09 09 09 10 09 07
+ 08 09 10 09 09 09 09 09 09 09 09 09 09 09 09 11
+ 04 09 09 09 09 09 09 09 09 09 09 09 09 09 09 07
+ 08 10 09 09 09 09 09 09 09 09 10 09 09 09 09 11
+ 04 09 09 09 09 09 10 09 09 09 09 09 09 09 09 07
+ 12 13 14 13 14 13 14 13 14 13 14 13 14 13 14 15
+
+
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/DungeonSlime.Windows.csproj b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/DungeonSlime.Windows.csproj
new file mode 100644
index 00000000..a5e61145
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/DungeonSlime.Windows.csproj
@@ -0,0 +1,34 @@
+
+
+ WinExe
+ net9.0
+ Major
+ false
+ false
+
+
+ app.manifest
+ Icon.ico
+
+
+
+
+
+
+
+ Icon.ico
+
+
+ Icon.bmp
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/Icon.bmp b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/Icon.bmp
new file mode 100644
index 00000000..2b481653
Binary files /dev/null and b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/Icon.bmp differ
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/Icon.ico b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/Icon.ico
new file mode 100644
index 00000000..7d9dec18
Binary files /dev/null and b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/Icon.ico differ
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/Program.cs b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/Program.cs
new file mode 100644
index 00000000..d491c406
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/Program.cs
@@ -0,0 +1,2 @@
+using var game = new DungeonSlime.Game1();
+game.Run();
diff --git a/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/app.manifest b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/app.manifest
new file mode 100644
index 00000000..caf45166
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/DungeonSlime/app.manifest
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true/pm
+ permonitorv2,permonitor
+
+
+
+
diff --git a/Tutorials/MobileDeployment/06-Publishing/MonoGameLibrary/Audio/AudioController.cs b/Tutorials/MobileDeployment/06-Publishing/MonoGameLibrary/Audio/AudioController.cs
new file mode 100644
index 00000000..2c31c166
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/MonoGameLibrary/Audio/AudioController.cs
@@ -0,0 +1,282 @@
+using System;
+using System.Collections.Generic;
+using Microsoft.Xna.Framework.Audio;
+using Microsoft.Xna.Framework.Media;
+
+namespace MonoGameLibrary.Audio;
+
+using MediaPlayer = Microsoft.Xna.Framework.Media.MediaPlayer;
+
+public class AudioController : IDisposable
+{
+ // Tracks sound effect instances created so they can be paused, unpaused, and/or disposed.
+ private readonly List _activeSoundEffectInstances;
+
+ // Tracks the volume for song playback when muting and unmuting.
+ private float _previousSongVolume;
+
+ // Tracks the volume for sound effect playback when muting and unmuting.
+ private float _previousSoundEffectVolume;
+
+ ///
+ /// Gets a value that indicates if audio is muted.
+ ///
+ public bool IsMuted { get; private set; }
+
+ ///
+ /// Gets or Sets the global volume of songs.
+ ///
+ ///
+ /// If IsMuted is true, the getter will always return back 0.0f and the
+ /// setter will ignore setting the volume.
+ ///
+ public float SongVolume
+ {
+ get
+ {
+ if (IsMuted)
+ {
+ return 0.0f;
+ }
+
+ return MediaPlayer.Volume;
+ }
+ set
+ {
+ if (IsMuted)
+ {
+ return;
+ }
+
+ MediaPlayer.Volume = Math.Clamp(value, 0.0f, 1.0f);
+ }
+ }
+
+ ///
+ /// Gets or Sets the global volume of sound effects.
+ ///
+ ///
+ /// If IsMuted is true, the getter will always return back 0.0f and the
+ /// setter will ignore setting the volume.
+ ///
+ public float SoundEffectVolume
+ {
+ get
+ {
+ if (IsMuted)
+ {
+ return 0.0f;
+ }
+
+ return SoundEffect.MasterVolume;
+ }
+ set
+ {
+ if (IsMuted)
+ {
+ return;
+ }
+
+ SoundEffect.MasterVolume = Math.Clamp(value, 0.0f, 1.0f);
+ }
+ }
+
+ ///
+ /// Gets a value that indicates if this audio controller has been disposed.
+ ///
+ public bool IsDisposed { get; private set; }
+
+ ///
+ /// Creates a new audio controller instance.
+ ///
+ public AudioController()
+ {
+ _activeSoundEffectInstances = new List();
+ }
+
+ // Finalizer called when object is collected by the garbage collector
+ ~AudioController() => Dispose(false);
+
+ ///
+ /// Updates this audio controller
+ ///
+ public void Update()
+ {
+ int index = 0;
+
+ while (index < _activeSoundEffectInstances.Count)
+ {
+ SoundEffectInstance instance = _activeSoundEffectInstances[index];
+
+ if (instance.State == SoundState.Stopped && !instance.IsDisposed)
+ {
+ instance.Dispose();
+ }
+
+ _activeSoundEffectInstances.RemoveAt(index);
+ }
+ }
+
+ ///
+ /// Plays the given sound effect.
+ ///
+ /// The sound effect to play.
+ /// The sound effect instance created by this method.
+ public SoundEffectInstance PlaySoundEffect(SoundEffect soundEffect)
+ {
+ return PlaySoundEffect(soundEffect, 1.0f, 1.0f, 0.0f, false);
+ }
+
+ ///
+ /// Plays the given sound effect with the specified properties.
+ ///
+ /// The sound effect to play.
+ /// The volume, ranging from 0.0 (silence) to 1.0 (full volume).
+ /// The pitch adjustment, ranging from -1.0 (down an octave) to 0.0 (no change) to 1.0 (up an octave).
+ /// The panning, ranging from -1.0 (left speaker) to 0.0 (centered), 1.0 (right speaker).
+ /// Whether the the sound effect should loop after playback.
+ /// The sound effect instance created by playing the sound effect.
+ /// The sound effect instance created by this method.
+ public SoundEffectInstance PlaySoundEffect(SoundEffect soundEffect, float volume, float pitch, float pan, bool isLooped)
+ {
+ // Create an instance from the sound effect given.
+ SoundEffectInstance soundEffectInstance = soundEffect.CreateInstance();
+
+ // Apply the volume, pitch, pan, and loop values specified.
+ soundEffectInstance.Volume = volume;
+ soundEffectInstance.Pitch = pitch;
+ soundEffectInstance.Pan = pan;
+ soundEffectInstance.IsLooped = isLooped;
+
+ // Tell the instance to play
+ soundEffectInstance.Play();
+
+ // Add it to the active instances for tracking
+ _activeSoundEffectInstances.Add(soundEffectInstance);
+
+ return soundEffectInstance;
+ }
+
+ ///
+ /// Plays the given song.
+ ///
+ /// The song to play.
+ /// Optionally specify if the song should repeat. Default is true.
+ public void PlaySong(Song song, bool isRepeating = true)
+ {
+ // Check if the media player is already playing, if so, stop it.
+ // If we do not stop it, this could cause issues on some platforms
+ if (MediaPlayer.State == MediaState.Playing)
+ {
+ MediaPlayer.Stop();
+ }
+
+ MediaPlayer.Play(song);
+ MediaPlayer.IsRepeating = isRepeating;
+ }
+
+ ///
+ /// Pauses all audio.
+ ///
+ public void PauseAudio()
+ {
+ // Pause any active songs playing
+ MediaPlayer.Pause();
+
+ // Pause any active sound effects
+ foreach (SoundEffectInstance soundEffectInstance in _activeSoundEffectInstances)
+ {
+ soundEffectInstance.Pause();
+ }
+ }
+
+ ///
+ /// Resumes play of all previous paused audio.
+ ///
+ public void ResumeAudio()
+ {
+ // Resume paused music
+ MediaPlayer.Resume();
+
+ // Resume any active sound effects
+ foreach (SoundEffectInstance soundEffectInstance in _activeSoundEffectInstances)
+ {
+ soundEffectInstance.Resume();
+ }
+ }
+
+ ///
+ /// Mutes all audio.
+ ///
+ public void MuteAudio()
+ {
+ // Store the volume so they can be restored during ResumeAudio
+ _previousSongVolume = MediaPlayer.Volume;
+ _previousSoundEffectVolume = SoundEffect.MasterVolume;
+
+ // Set all volumes to 0
+ MediaPlayer.Volume = 0.0f;
+ SoundEffect.MasterVolume = 0.0f;
+
+ IsMuted = true;
+ }
+
+ ///
+ /// Unmutes all audio to the volume level prior to muting.
+ ///
+ public void UnmuteAudio()
+ {
+ // Restore the previous volume values
+ MediaPlayer.Volume = _previousSongVolume;
+ SoundEffect.MasterVolume = _previousSoundEffectVolume;
+
+ IsMuted = false;
+ }
+
+ ///
+ /// Toggles the current audio mute state.
+ ///
+ public void ToggleMute()
+ {
+ if (IsMuted)
+ {
+ UnmuteAudio();
+ }
+ else
+ {
+ MuteAudio();
+ }
+ }
+
+ ///
+ /// Disposes of this audio controller and cleans up resources.
+ ///
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ /// Disposes this audio controller and cleans up resources.
+ ///
+ /// Indicates whether managed resources should be disposed.
+ protected void Dispose(bool disposing)
+ {
+ if (IsDisposed)
+ {
+ return;
+ }
+
+ if (disposing)
+ {
+ foreach (SoundEffectInstance soundEffectInstance in _activeSoundEffectInstances)
+ {
+ soundEffectInstance.Dispose();
+ }
+ _activeSoundEffectInstances.Clear();
+ }
+
+ IsDisposed = true;
+ }
+}
diff --git a/Tutorials/MobileDeployment/06-Publishing/MonoGameLibrary/Circle.cs b/Tutorials/MobileDeployment/06-Publishing/MonoGameLibrary/Circle.cs
new file mode 100644
index 00000000..0bb691bc
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/MonoGameLibrary/Circle.cs
@@ -0,0 +1,136 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGameLibrary;
+
+public readonly struct Circle : IEquatable
+{
+ private static readonly Circle s_empty = new Circle();
+
+ ///
+ /// The x-coordinate of the center of this circle.
+ ///
+ public readonly int X;
+
+ ///
+ /// The y-coordinate of the center of this circle.
+ ///
+ public readonly int Y;
+
+ ///
+ /// The length, in pixels, from the center of this circle to the edge.
+ ///
+ public readonly int Radius;
+
+ ///
+ /// Gets the location of the center of this circle.
+ ///
+ public readonly Point Location => new Point(X, Y);
+
+ ///
+ /// Gets a circle with X=0, Y=0, and Radius=0.
+ ///
+ public static Circle Empty => s_empty;
+
+ ///
+ /// Gets a value that indicates whether this circle has a radius of 0 and a location of (0, 0).
+ ///
+ public readonly bool IsEmpty => X == 0 && Y == 0 && Radius == 0;
+
+ ///
+ /// Gets the y-coordinate of the highest point on this circle.
+ ///
+ public readonly int Top => Y - Radius;
+
+ ///
+ /// Gets the y-coordinate of the lowest point on this circle.
+ ///
+ public readonly int Bottom => Y + Radius;
+
+ ///
+ /// Gets the x-coordinate of the leftmost point on this circle.
+ ///
+ public readonly int Left => X - Radius;
+
+ ///
+ /// Gets the x-coordinate of the rightmost point on this circle.
+ ///
+ public readonly int Right => X + Radius;
+
+ ///
+ /// Creates a new circle with the specified position and radius.
+ ///
+ /// The x-coordinate of the center of the circle.
+ /// The y-coordinate of the center of the circle..
+ /// The length from the center of the circle to an edge.
+ public Circle(int x, int y, int radius)
+ {
+ X = x;
+ Y = y;
+ Radius = radius;
+ }
+
+ ///
+ /// Creates a new circle with the specified position and radius.
+ ///
+ /// The center of the circle.
+ /// The length from the center of the circle to an edge.
+ public Circle(Point location, int radius)
+ {
+ X = location.X;
+ Y = location.Y;
+ Radius = radius;
+ }
+
+ ///
+ /// Returns a value that indicates whether the specified circle intersects with this circle.
+ ///
+ /// The other circle to check.
+ /// true if the other circle intersects with this circle; otherwise, false.
+ public bool Intersects(Circle other)
+ {
+ int radiiSquared = (this.Radius + other.Radius) * (this.Radius + other.Radius);
+ float distanceSquared = Vector2.DistanceSquared(this.Location.ToVector2(), other.Location.ToVector2());
+ return distanceSquared < radiiSquared;
+ }
+
+ ///
+ /// Returns a value that indicates whether this circle and the specified object are equal
+ ///
+ /// The object to compare with this circle.
+ /// true if this circle and the specified object are equal; otherwise, false.
+ public override readonly bool Equals(object obj) => obj is Circle other && Equals(other);
+
+ ///
+ /// Returns a value that indicates whether this circle and the specified circle are equal.
+ ///
+ /// The circle to compare with this circle.
+ /// true if this circle and the specified circle are equal; otherwise, false.
+ public readonly bool Equals(Circle other) => this.X == other.X &&
+ this.Y == other.Y &&
+ this.Radius == other.Radius;
+
+ ///
+ /// Returns the hash code for this circle.
+ ///
+ /// The hash code for this circle as a 32-bit signed integer.
+ public override readonly int GetHashCode() => HashCode.Combine(X, Y, Radius);
+
+ ///
+ /// Returns a value that indicates if the circle on the left hand side of the equality operator is equal to the
+ /// circle on the right hand side of the equality operator.
+ ///
+ /// The circle on the left hand side of the equality operator.
+ /// The circle on the right hand side of the equality operator.
+ /// true if the two circles are equal; otherwise, false.
+ public static bool operator ==(Circle lhs, Circle rhs) => lhs.Equals(rhs);
+
+ ///
+ /// Returns a value that indicates if the circle on the left hand side of the inequality operator is not equal to the
+ /// circle on the right hand side of the inequality operator.
+ ///
+ /// The circle on the left hand side of the inequality operator.
+ /// The circle on the right hand side fo the inequality operator.
+ /// true if the two circle are not equal; otherwise, false.
+ public static bool operator !=(Circle lhs, Circle rhs) => !lhs.Equals(rhs);
+}
diff --git a/Tutorials/MobileDeployment/06-Publishing/MonoGameLibrary/Core.cs b/Tutorials/MobileDeployment/06-Publishing/MonoGameLibrary/Core.cs
new file mode 100644
index 00000000..5698fc28
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/MonoGameLibrary/Core.cs
@@ -0,0 +1,214 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content;
+using Microsoft.Xna.Framework.Graphics;
+using Microsoft.Xna.Framework.Input;
+using MonoGameLibrary.Audio;
+using MonoGameLibrary.Input;
+using MonoGameLibrary.Scenes;
+
+namespace MonoGameLibrary;
+
+public class Core : Game
+{
+ internal static Core s_instance;
+
+ ///
+ /// Gets a reference to the Core instance.
+ ///
+ public static Core Instance => s_instance;
+
+ // The scene that is currently active.
+ private static Scene s_activeScene;
+
+ // The next scene to switch to, if there is one.
+ private static Scene s_nextScene;
+
+ ///
+ /// Gets the graphics device manager to control the presentation of graphics.
+ ///
+ public static GraphicsDeviceManager Graphics { get; private set; }
+
+ public static GameWindow GameWindow { get; private set; }
+
+ ///
+ /// Gets the graphics device used to create graphical resources and perform primitive rendering.
+ ///
+ public static new GraphicsDevice GraphicsDevice { get; private set; }
+
+ ///
+ /// Gets the sprite batch used for all 2D rendering.
+ ///
+ public static SpriteBatch SpriteBatch { get; private set; }
+
+ ///
+ /// Gets the content manager used to load global assets.
+ ///
+ public static new ContentManager Content { get; private set; }
+
+ ///
+ /// Gets a reference to to the input management system.
+ ///
+ public static InputManager Input { get; private set; }
+
+ ///
+ /// Gets or Sets a value that indicates if the game should exit when the esc key on the keyboard is pressed.
+ ///
+ public static bool ExitOnEscape { get; set; }
+
+ ///
+ /// Gets a reference to the audio control system.
+ ///
+ public static AudioController Audio { get; private set; }
+
+ ///
+ /// Creates a new Core instance.
+ ///
+ /// The title to display in the title bar of the game window.
+ /// The initial width, in pixels, of the game window.
+ /// The initial height, in pixels, of the game window.
+ /// Indicates if the game should start in fullscreen mode.
+ public Core(string title, int width, int height, bool fullScreen)
+ {
+ // Ensure that multiple cores are not created.
+ if (s_instance != null)
+ {
+ throw new InvalidOperationException($"Only a single Core instance can be created");
+ }
+
+ // Store reference to engine for global member access.
+ s_instance = this;
+
+ // Create a new graphics device manager.
+ Graphics = new GraphicsDeviceManager(this);
+
+ // Set the graphics defaults
+#if WINDOWS
+ Graphics.PreferredBackBufferWidth = width;
+ Graphics.PreferredBackBufferHeight = height;
+ Graphics.IsFullScreen = fullScreen;
+#endif
+
+ // Apply the graphic presentation changes
+ Graphics.ApplyChanges();
+
+ // Set the window title
+ Window.Title = title;
+
+ // Set the core's content manager to a reference of hte base Game's
+ // content manager.
+ Content = base.Content;
+
+ // Set the root directory for content
+ Content.RootDirectory = "Content";
+
+ // Mouse is visible by default
+ IsMouseVisible = true;
+ }
+
+ protected override void Initialize()
+ {
+ base.Initialize();
+
+ // Set the core's graphics device to a reference of the base Game's
+ // graphics device.
+ GraphicsDevice = base.GraphicsDevice;
+
+ GameWindow = base.Window;
+
+ // Create the sprite batch instance.
+ SpriteBatch = new SpriteBatch(GraphicsDevice);
+
+ // Create a new input manager
+ Input = new InputManager();
+
+ // Create a new audio controller.
+ Audio = new AudioController();
+ }
+
+ protected override void UnloadContent()
+ {
+ // Dispose of the audio controller.
+ Audio.Dispose();
+
+ base.UnloadContent();
+ }
+
+ protected override void Update(GameTime gameTime)
+ {
+ // Update the input manager.
+ Input.Update(gameTime);
+
+ // Update the audio controller.
+ Audio.Update();
+
+#if !IOS
+ if (ExitOnEscape && Input.Keyboard.WasKeyJustPressed(Keys.Escape))
+ {
+ Exit();
+ }
+#endif
+
+ // if there is a next scene waiting to be switch to, then transition
+ // to that scene
+ if (s_nextScene != null)
+ {
+ TransitionScene();
+ }
+
+ // If there is an active scene, update it.
+ if (s_activeScene != null)
+ {
+ s_activeScene.Update(gameTime);
+ }
+
+ base.Update(gameTime);
+ }
+
+ protected override void Draw(GameTime gameTime)
+ {
+ // If there is an active scene, draw it.
+ if (s_activeScene != null)
+ {
+ s_activeScene.Draw(gameTime);
+ }
+
+ base.Draw(gameTime);
+ }
+
+ public static void ChangeScene(Scene next)
+ {
+ // Only set the next scene value if it is not the same
+ // instance as the currently active scene.
+ if (s_activeScene != next)
+ {
+ s_nextScene = next;
+ }
+ }
+
+ private static void TransitionScene()
+ {
+ // If there is an active scene, dispose of it
+ if (s_activeScene != null)
+ {
+ s_activeScene.Dispose();
+ }
+
+ // Force the garbage collector to collect to ensure memory is cleared
+ GC.Collect();
+
+ // Change the currently active scene to the new scene
+ s_activeScene = s_nextScene;
+
+ // Null out the next scene value so it does not trigger a change over and over.
+ s_nextScene = null;
+
+ // If the active scene now is not null, initialize it.
+ // Remember, just like with Game, the Initialize call also calls the
+ // Scene.LoadContent
+ if (s_activeScene != null)
+ {
+ s_activeScene.Initialize();
+ }
+ }
+}
diff --git a/Tutorials/MobileDeployment/06-Publishing/MonoGameLibrary/Graphics/AnimatedSprite.cs b/Tutorials/MobileDeployment/06-Publishing/MonoGameLibrary/Graphics/AnimatedSprite.cs
new file mode 100644
index 00000000..a1a3594e
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/MonoGameLibrary/Graphics/AnimatedSprite.cs
@@ -0,0 +1,60 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace MonoGameLibrary.Graphics;
+
+public class AnimatedSprite : Sprite
+{
+ private int _currentFrame;
+ private TimeSpan _elapsed;
+ private Animation _animation;
+
+ ///
+ /// Gets or Sets the animation for this animated sprite.
+ ///
+ public Animation Animation
+ {
+ get => _animation;
+ set
+ {
+ _animation = value;
+ Region = _animation.Frames[0];
+ }
+ }
+
+ ///
+ /// Creates a new animated sprite.
+ ///
+ public AnimatedSprite() { }
+
+ ///
+ /// Creates a new animated sprite with the specified frames and delay.
+ ///
+ /// The animation for this animated sprite.
+ public AnimatedSprite(Animation animation)
+ {
+ Animation = animation;
+ }
+
+ ///
+ /// Updates this animated sprite.
+ ///
+ /// A snapshot of the game timing values provided by the framework.
+ public void Update(GameTime gameTime)
+ {
+ _elapsed += gameTime.ElapsedGameTime;
+
+ if (_elapsed >= _animation.Delay)
+ {
+ _elapsed -= _animation.Delay;
+ _currentFrame++;
+
+ if (_currentFrame >= _animation.Frames.Count)
+ {
+ _currentFrame = 0;
+ }
+
+ Region = _animation.Frames[_currentFrame];
+ }
+ }
+}
diff --git a/Tutorials/MobileDeployment/06-Publishing/MonoGameLibrary/Graphics/Animation.cs b/Tutorials/MobileDeployment/06-Publishing/MonoGameLibrary/Graphics/Animation.cs
new file mode 100644
index 00000000..44d61b65
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/MonoGameLibrary/Graphics/Animation.cs
@@ -0,0 +1,38 @@
+using System;
+using System.Collections.Generic;
+
+namespace MonoGameLibrary.Graphics;
+
+public class Animation
+{
+ ///
+ /// The texture regions that make up the frames of this animation. The order of the regions within the collection
+ /// are the order that the frames should be displayed in.
+ ///
+ public List Frames { get; set; }
+
+ ///
+ /// The amount of time to delay between each frame before moving to the next frame for this animation.
+ ///
+ public TimeSpan Delay { get; set; }
+
+ ///
+ /// Creates a new animation.
+ ///
+ public Animation()
+ {
+ Frames = new List();
+ Delay = TimeSpan.FromMilliseconds(100);
+ }
+
+ ///
+ /// Creates a new animation with the specified frames and delay.
+ ///
+ /// An ordered collection of the frames for this animation.
+ /// The amount of time to delay between each frame of this animation.
+ public Animation(List frames, TimeSpan delay)
+ {
+ Frames = frames;
+ Delay = delay;
+ }
+}
diff --git a/Tutorials/MobileDeployment/06-Publishing/MonoGameLibrary/Graphics/Sprite.cs b/Tutorials/MobileDeployment/06-Publishing/MonoGameLibrary/Graphics/Sprite.cs
new file mode 100644
index 00000000..20c44f0b
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/MonoGameLibrary/Graphics/Sprite.cs
@@ -0,0 +1,108 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGameLibrary.Graphics;
+
+public class Sprite
+{
+ ///
+ /// Gets or Sets the source texture region represented by this sprite.
+ ///
+ public TextureRegion Region { get; set; }
+
+ ///
+ /// Gets or Sets the color mask to apply when rendering this sprite.
+ ///
+ ///
+ /// Default value is Color.White
+ ///
+ public Color Color { get; set; } = Color.White;
+
+ ///
+ /// Gets or Sets the amount of rotation, in radians, to apply when rendering this sprite.
+ ///
+ ///
+ /// Default value is 0.0f
+ ///
+ public float Rotation { get; set; } = 0.0f;
+
+ ///
+ /// Gets or Sets the scale factor to apply to the x- and y-axes when rendering this sprite.
+ ///
+ ///
+ /// Default value is Vector2.One
+ ///
+ public Vector2 Scale { get; set; } = Vector2.One;
+
+ ///
+ /// Gets or Sets the xy-coordinate origin point, relative to the top-left corner, of this sprite.
+ ///
+ ///
+ /// Default value is Vector2.Zero
+ ///
+ public Vector2 Origin { get; set; } = Vector2.Zero;
+
+ ///
+ /// Gets or Sets the sprite effects to apply when rendering this sprite.
+ ///
+ ///
+ /// Default value is SpriteEffects.None
+ ///
+ public SpriteEffects Effects { get; set; } = SpriteEffects.None;
+
+ ///
+ /// Gets or Sets the layer depth to apply when rendering this sprite.
+ ///
+ ///
+ /// Default value is 0.0f
+ ///
+ public float LayerDepth { get; set; } = 0.0f;
+
+ ///
+ /// Gets the width, in pixels, of this sprite.
+ ///
+ ///
+ /// Width is calculated by multiplying the width of the source texture region by the x-axis scale factor.
+ ///
+ public float Width => Region.Width * Scale.X;
+
+ ///
+ /// Gets the height, in pixels, of this sprite.
+ ///
+ ///
+ /// Height is calculated by multiplying the height of the source texture region by the y-axis scale factor.
+ ///
+ public float Height => Region.Height * Scale.Y;
+
+ ///
+ /// Creates a new sprite.
+ ///
+ public Sprite() { }
+
+ ///
+ /// Creates a new sprite using the specified source texture region.
+ ///
+ /// The texture region to use as the source texture region for this sprite.
+ public Sprite(TextureRegion region)
+ {
+ Region = region;
+ }
+
+ ///
+ /// Sets the origin of this sprite to the center
+ ///
+ public void CenterOrigin()
+ {
+ Origin = new Vector2(Region.Width, Region.Height) * 0.5f;
+ }
+
+ ///
+ /// Submit this sprite for drawing to the current batch.
+ ///
+ /// The SpriteBatch instance used for batching draw calls.
+ /// The xy-coordinate position to render this sprite at.
+ public void Draw(SpriteBatch spriteBatch, Vector2 position)
+ {
+ Region.Draw(spriteBatch, position, Color, Rotation, Origin, Scale, Effects, LayerDepth);
+ }
+}
diff --git a/Tutorials/MobileDeployment/06-Publishing/MonoGameLibrary/Graphics/TextureAtlas.cs b/Tutorials/MobileDeployment/06-Publishing/MonoGameLibrary/Graphics/TextureAtlas.cs
new file mode 100644
index 00000000..e48c9abd
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/MonoGameLibrary/Graphics/TextureAtlas.cs
@@ -0,0 +1,239 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Xml;
+using System.Xml.Linq;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content;
+using Microsoft.Xna.Framework.Graphics;
+
+
+namespace MonoGameLibrary.Graphics;
+
+public class TextureAtlas
+{
+ private Dictionary _regions;
+
+ // Stores animations added to this atlas.
+ private Dictionary _animations;
+
+ ///
+ /// Gets or Sets the source texture represented by this texture atlas.
+ ///
+ public Texture2D Texture { get; set; }
+
+ ///
+ /// Creates a new texture atlas.
+ ///
+ public TextureAtlas()
+ {
+ _regions = new Dictionary();
+ _animations = new Dictionary();
+ }
+
+ ///
+ /// Creates a new texture atlas instance using the given texture.
+ ///
+ /// The source texture represented by the texture atlas.
+ public TextureAtlas(Texture2D texture)
+ {
+ Texture = texture;
+ _regions = new Dictionary();
+ _animations = new Dictionary();
+ }
+
+ ///
+ /// Creates a new region and adds it to this texture atlas.
+ ///
+ /// The name to give the texture region.
+ /// The top-left x-coordinate position of the region boundary relative to the top-left corner of the source texture boundary.
+ /// The top-left y-coordinate position of the region boundary relative to the top-left corner of the source texture boundary.
+ /// The width, in pixels, of the region.
+ /// The height, in pixels, of the region.
+ public void AddRegion(string name, int x, int y, int width, int height)
+ {
+ TextureRegion region = new TextureRegion(Texture, x, y, width, height);
+ _regions.Add(name, region);
+ }
+
+ ///
+ /// Gets the region from this texture atlas with the specified name.
+ ///
+ /// The name of the region to retrieve.
+ /// The TextureRegion with the specified name.
+ public TextureRegion GetRegion(string name)
+ {
+ return _regions[name];
+ }
+
+ ///
+ /// Removes the region from this texture atlas with the specified name.
+ ///
+ /// The name of the region to remove.
+ ///
+ public bool RemoveRegion(string name)
+ {
+ return _regions.Remove(name);
+ }
+
+ ///
+ /// Removes all regions from this texture atlas.
+ ///
+ public void Clear()
+ {
+ _regions.Clear();
+ }
+
+ ///
+ /// Creates a new sprite using the region from this texture atlas with the specified name.
+ ///
+ /// The name of the region to create the sprite with.
+ /// A new Sprite using the texture region with the specified name.
+ public Sprite CreateSprite(string regionName)
+ {
+ TextureRegion region = GetRegion(regionName);
+ return new Sprite(region);
+ }
+
+ ///
+ /// Adds the given animation to this texture atlas with the specified name.
+ ///
+ /// The name of the animation to add.
+ /// The animation to add.
+ public void AddAnimation(string animationName, Animation animation)
+ {
+ _animations.Add(animationName, animation);
+ }
+
+ ///
+ /// Gets the animation from this texture atlas with the specified name.
+ ///
+ /// The name of the animation to retrieve.
+ /// The animation with the specified name.
+ public Animation GetAnimation(string animationName)
+ {
+ return _animations[animationName];
+ }
+
+ ///
+ /// Removes the animation with the specified name from this texture atlas.
+ ///
+ /// The name of the animation to remove.
+ /// true if the animation is removed successfully; otherwise, false.
+ public bool RemoveAnimation(string animationName)
+ {
+ return _animations.Remove(animationName);
+ }
+
+ ///
+ /// Creates a new animated sprite using the animation from this texture atlas with the specified name.
+ ///
+ /// The name of the animation to use.
+ /// A new AnimatedSprite using the animation with the specified name.
+ public AnimatedSprite CreateAnimatedSprite(string animationName)
+ {
+ Animation animation = GetAnimation(animationName);
+ return new AnimatedSprite(animation);
+ }
+
+ ///
+ /// Creates a new texture atlas based a texture atlas xml configuration file.
+ ///
+ /// The content manager used to load the texture for the atlas.
+ /// The path to the xml file, relative to the content root directory..
+ /// The texture atlas created by this method.
+ public static TextureAtlas FromFile(ContentManager content, string fileName)
+ {
+ TextureAtlas atlas = new TextureAtlas();
+
+ string filePath = Path.Combine(content.RootDirectory, fileName);
+
+ using (Stream stream = TitleContainer.OpenStream(filePath))
+ {
+ using (XmlReader reader = XmlReader.Create(stream))
+ {
+ XDocument doc = XDocument.Load(reader);
+ XElement root = doc.Root;
+
+ // The element contains the content path for the Texture2D to load.
+ // So we will retrieve that value then use the content manager to load the texture.
+ string texturePath = root.Element("Texture").Value;
+ atlas.Texture = content.Load(texturePath);
+
+ // The element contains individual elements, each one describing
+ // a different texture region within the atlas.
+ //
+ // Example:
+ //
+ //
+ //
+ //
+ //
+ // So we retrieve all of the elements then loop through each one
+ // and generate a new TextureRegion instance from it and add it to this atlas.
+ var regions = root.Element("Regions")?.Elements("Region");
+
+ if (regions != null)
+ {
+ foreach (var 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);
+ }
+ }
+ }
+
+ // The element contains individual elements, each one describing
+ // a different animation within the atlas.
+ //
+ // Example:
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ // So we retrieve all of the elements then loop through each one
+ // and generate a new Animation instance from it and add it to this atlas.
+ var animationElements = root.Element("Animations").Elements("Animation");
+
+ if (animationElements != null)
+ {
+ foreach (var animationElement in animationElements)
+ {
+ string name = animationElement.Attribute("name")?.Value;
+ float delayInMilliseconds = float.Parse(animationElement.Attribute("delay")?.Value ?? "0");
+ TimeSpan delay = TimeSpan.FromMilliseconds(delayInMilliseconds);
+
+ List frames = new List();
+
+ var frameElements = animationElement.Elements("Frame");
+
+ if (frameElements != null)
+ {
+ foreach (var frameElement in frameElements)
+ {
+ string regionName = frameElement.Attribute("region").Value;
+ TextureRegion region = atlas.GetRegion(regionName);
+ frames.Add(region);
+ }
+ }
+
+ Animation animation = new Animation(frames, delay);
+ atlas.AddAnimation(name, animation);
+ }
+ }
+
+ return atlas;
+ }
+ }
+ }
+}
diff --git a/Tutorials/MobileDeployment/06-Publishing/MonoGameLibrary/Graphics/TextureRegion.cs b/Tutorials/MobileDeployment/06-Publishing/MonoGameLibrary/Graphics/TextureRegion.cs
new file mode 100644
index 00000000..98edc2da
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/MonoGameLibrary/Graphics/TextureRegion.cs
@@ -0,0 +1,131 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGameLibrary.Graphics;
+
+///
+/// Represents a rectangular region within a texture.
+///
+public class TextureRegion
+{
+ ///
+ /// Gets or Sets the source texture this texture region is part of.
+ ///
+ public Texture2D Texture { get; set; }
+
+ ///
+ /// Gets or Sets the source rectangle boundary of this texture region within the source texture.
+ ///
+ public Rectangle SourceRectangle { get; set; }
+
+ ///
+ /// Gets the width, in pixels, of this texture region.
+ ///
+ public int Width => SourceRectangle.Width;
+
+ ///
+ /// Gets the height, in pixels, of this texture region.
+ ///
+ public int Height => SourceRectangle.Height;
+
+ ///
+ /// Gets the top normalized texture coordinate of this region.
+ ///
+ public float TopTextureCoordinate => SourceRectangle.Top / (float)Texture.Height;
+
+ ///
+ /// Gets the bottom normalized texture coordinate of this region.
+ ///
+ public float BottomTextureCoordinate => SourceRectangle.Bottom / (float)Texture.Height;
+
+ ///
+ /// Gets the left normalized texture coordinate of this region.
+ ///
+ public float LeftTextureCoordinate => SourceRectangle.Left / (float)Texture.Width;
+
+ ///
+ /// Gets the right normalized texture coordinate of this region.
+ ///
+ public float RightTextureCoordinate => SourceRectangle.Right / (float)Texture.Width;
+
+ ///
+ /// Creates a new texture region.
+ ///
+ public TextureRegion() { }
+
+ ///
+ /// Creates a new texture region using the specified source texture.
+ ///
+ /// The texture to use as the source texture for this texture region.
+ /// The x-coordinate position of the upper-left corner of this texture region relative to the upper-left corner of the source texture.
+ /// The y-coordinate position of the upper-left corner of this texture region relative to the upper-left corner of the source texture.
+ /// The width, in pixels, of this texture region.
+ /// The height, in pixels, of this texture region.
+ public TextureRegion(Texture2D texture, int x, int y, int width, int height)
+ {
+ Texture = texture;
+ SourceRectangle = new Rectangle(x, y, width, height);
+ }
+
+ ///
+ /// Submit this texture region for drawing in the current batch.
+ ///
+ /// The spritebatch instance used for batching draw calls.
+ /// The xy-coordinate location to draw this texture region on the screen.
+ /// The color mask to apply when drawing this texture region on screen.
+ public void Draw(SpriteBatch spriteBatch, Vector2 position, Color color)
+ {
+ Draw(spriteBatch, position, color, 0.0f, Vector2.Zero, Vector2.One, SpriteEffects.None, 0.0f);
+ }
+
+ ///
+ /// Submit this texture region for drawing in the current batch.
+ ///
+ /// The spritebatch instance used for batching draw calls.
+ /// The xy-coordinate location to draw this texture region on the screen.
+ /// The color mask to apply when drawing this texture region on screen.
+ /// The amount of rotation, in radians, to apply when drawing this texture region on screen.
+ /// The center of rotation, scaling, and position when drawing this texture region on screen.
+ /// The scale factor to apply when drawing this texture region on screen.
+ /// Specifies if this texture region should be flipped horizontally, vertically, or both when drawing on screen.
+ /// The depth of the layer to use when drawing this texture region on screen.
+ public void Draw(SpriteBatch spriteBatch, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects effects, float layerDepth)
+ {
+ Draw(
+ spriteBatch,
+ position,
+ color,
+ rotation,
+ origin,
+ new Vector2(scale, scale),
+ effects,
+ layerDepth
+ );
+ }
+
+ ///
+ /// Submit this texture region for drawing in the current batch.
+ ///
+ /// The spritebatch instance used for batching draw calls.
+ /// The xy-coordinate location to draw this texture region on the screen.
+ /// The color mask to apply when drawing this texture region on screen.
+ /// The amount of rotation, in radians, to apply when drawing this texture region on screen.
+ /// The center of rotation, scaling, and position when drawing this texture region on screen.
+ /// The amount of scaling to apply to the x- and y-axes when drawing this texture region on screen.
+ /// Specifies if this texture region should be flipped horizontally, vertically, or both when drawing on screen.
+ /// The depth of the layer to use when drawing this texture region on screen.
+ public void Draw(SpriteBatch spriteBatch, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth)
+ {
+ spriteBatch.Draw(
+ Texture,
+ position,
+ SourceRectangle,
+ color,
+ rotation,
+ origin,
+ scale,
+ effects,
+ layerDepth
+ );
+ }
+}
diff --git a/Tutorials/MobileDeployment/06-Publishing/MonoGameLibrary/Graphics/Tilemap.cs b/Tutorials/MobileDeployment/06-Publishing/MonoGameLibrary/Graphics/Tilemap.cs
new file mode 100644
index 00000000..96e1ee5e
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/MonoGameLibrary/Graphics/Tilemap.cs
@@ -0,0 +1,231 @@
+using System;
+using System.IO;
+using System.Xml;
+using System.Xml.Linq;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace MonoGameLibrary.Graphics;
+
+public class Tilemap
+{
+ private readonly Tileset _tileset;
+ private readonly int[] _tiles;
+
+ ///
+ /// Gets the total number of rows in this tilemap.
+ ///
+ public int Rows { get; }
+
+ ///
+ /// Gets the total number of columns in this tilemap.
+ ///
+ public int Columns { get; }
+
+ ///
+ /// Gets the total number of tiles in this tilemap.
+ ///
+ public int Count { get; }
+
+ ///
+ /// Gets or Sets the scale factor to draw each tile at.
+ ///
+ public Vector2 Scale { get; set; }
+
+ ///
+ /// Gets the width, in pixels, each tile is drawn at.
+ ///
+ public float TileWidth => _tileset.TileWidth * Scale.X;
+
+ ///
+ /// Gets the height, in pixels, each tile is drawn at.
+ ///
+ public float TileHeight => _tileset.TileHeight * Scale.Y;
+
+ ///
+ /// Creates a new tilemap.
+ ///
+ /// The tileset used by this tilemap.
+ /// The total number of columns in this tilemap.
+ /// The total number of rows in this tilemap.
+ public Tilemap(Tileset tileset, int columns, int rows)
+ {
+ _tileset = tileset;
+ Rows = rows;
+ Columns = columns;
+ Count = Columns * Rows;
+ Scale = Vector2.One;
+ _tiles = new int[Count];
+ }
+
+ ///
+ /// Sets the tile at the given index in this tilemap to use the tile from
+ /// the tileset at the specified tileset id.
+ ///
+ /// The index of the tile in this tilemap.
+ /// The tileset id of the tile from the tileset to use.
+ public void SetTile(int index, int tilesetID)
+ {
+ _tiles[index] = tilesetID;
+ }
+
+ ///
+ /// Sets the tile at the given column and row in this tilemap to use the tile
+ /// from the tileset at the specified tileset id.
+ ///
+ /// The column of the tile in this tilemap.
+ /// The row of the tile in this tilemap.
+ /// The tileset id of the tile from the tileset to use.
+ public void SetTile(int column, int row, int tilesetID)
+ {
+ int index = row * Columns + column;
+ SetTile(index, tilesetID);
+ }
+
+ ///
+ /// Gets the texture region of the tile from this tilemap at the specified index.
+ ///
+ /// The index of the tile in this tilemap.
+ /// The texture region of the tile from this tilemap at the specified index.
+ public TextureRegion GetTile(int index)
+ {
+ return _tileset.GetTile(_tiles[index]);
+ }
+
+ ///
+ /// Gets the texture region of the tile frm this tilemap at the specified
+ /// column and row.
+ ///
+ /// The column of the tile in this tilemap.
+ /// The row of hte tile in this tilemap.
+ /// The texture region of the tile from this tilemap at the specified column and row.
+ public TextureRegion GetTile(int column, int row)
+ {
+ int index = row * Columns + column;
+ return GetTile(index);
+ }
+
+ ///
+ /// Draws this tilemap using the given sprite batch.
+ ///
+ /// The sprite batch used to draw this tilemap.
+ public void Draw(SpriteBatch spriteBatch)
+ {
+ for (int i = 0; i < Count; i++)
+ {
+ int tileSetIndex = _tiles[i];
+ TextureRegion tile = _tileset.GetTile(tileSetIndex);
+
+ int x = i % Columns;
+ int y = i / Columns;
+
+ Vector2 position = new Vector2(x * TileWidth, y * TileHeight);
+ tile.Draw(spriteBatch, position, Color.White, 0.0f, Vector2.Zero, Scale, SpriteEffects.None, 1.0f);
+ }
+ }
+
+ ///
+ /// Creates a new tilemap based on a tilemap xml configuration file.
+ ///
+ /// The content manager used to load the texture for the tileset.
+ /// The path to the xml file, relative to the content root directory.
+ /// The tilemap created by this method.
+ public static Tilemap FromFile(ContentManager content, string filename)
+ {
+ string filePath = Path.Combine(content.RootDirectory, filename);
+
+ using (Stream stream = TitleContainer.OpenStream(filePath))
+ {
+ using (XmlReader reader = XmlReader.Create(stream))
+ {
+ XDocument doc = XDocument.Load(reader);
+ XElement root = doc.Root;
+
+ // The element contains the information about the tileset
+ // used by the tilemap.
+ //
+ // Example
+ // contentPath
+ //
+ // The region attribute represents the x, y, width, and height
+ // components of the boundary for the texture region within the
+ // texture at the contentPath specified.
+ //
+ // the tileWidth and tileHeight attributes specify the width and
+ // height of each tile in the tileset.
+ //
+ // the contentPath value is the contentPath to the texture to
+ // load that contains the tileset
+ XElement tilesetElement = root.Element("Tileset");
+
+ string regionAttribute = tilesetElement.Attribute("region").Value;
+ string[] split = regionAttribute.Split(" ", StringSplitOptions.RemoveEmptyEntries);
+ int x = int.Parse(split[0]);
+ int y = int.Parse(split[1]);
+ int width = int.Parse(split[2]);
+ int height = int.Parse(split[3]);
+
+ int tileWidth = int.Parse(tilesetElement.Attribute("tileWidth").Value);
+ int tileHeight = int.Parse(tilesetElement.Attribute("tileHeight").Value);
+ string contentPath = tilesetElement.Value;
+
+ // Load the texture 2d at the content path
+ Texture2D texture = content.Load(contentPath);
+
+ // Create the texture region from the texture
+ TextureRegion textureRegion = new TextureRegion(texture, x, y, width, height);
+
+ // Create the tileset using the texture region
+ Tileset tileset = new Tileset(textureRegion, tileWidth, tileHeight);
+
+ // The element contains lines of strings where each line
+ // represents a row in the tilemap. Each line is a space
+ // separated string where each element represents a column in that
+ // row. The value of the column is the id of the tile in the
+ // tileset to draw for that location.
+ //
+ // Example:
+ //
+ // 00 01 01 02
+ // 03 04 04 05
+ // 03 04 04 05
+ // 06 07 07 08
+ //
+ XElement tilesElement = root.Element("Tiles");
+
+ // Split the value of the tiles data into rows by splitting on
+ // the new line character
+ string[] rows = tilesElement.Value.Trim().Split('\n', StringSplitOptions.RemoveEmptyEntries);
+
+ // Split the value of the first row to determine the total number of columns
+ int columnCount = rows[0].Split(" ", StringSplitOptions.RemoveEmptyEntries).Length;
+
+ // Create the tilemap
+ Tilemap tilemap = new Tilemap(tileset, columnCount, rows.Length);
+
+ // Process each row
+ for (int row = 0; row < rows.Length; row++)
+ {
+ // Split the row into individual columns
+ string[] columns = rows[row].Trim().Split(" ", StringSplitOptions.RemoveEmptyEntries);
+
+ // Process each column of the current row
+ for (int column = 0; column < columnCount; column++)
+ {
+ // Get the tileset index for this location
+ int tilesetIndex = int.Parse(columns[column]);
+
+ // Get the texture region of that tile from the tileset
+ TextureRegion region = tileset.GetTile(tilesetIndex);
+
+ // Add that region to the tilemap at the row and column location
+ tilemap.SetTile(column, row, tilesetIndex);
+ }
+ }
+
+ return tilemap;
+ }
+ }
+ }
+}
diff --git a/Tutorials/MobileDeployment/06-Publishing/MonoGameLibrary/Graphics/Tileset.cs b/Tutorials/MobileDeployment/06-Publishing/MonoGameLibrary/Graphics/Tileset.cs
new file mode 100644
index 00000000..80c2e65a
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/MonoGameLibrary/Graphics/Tileset.cs
@@ -0,0 +1,76 @@
+namespace MonoGameLibrary.Graphics;
+
+public class Tileset
+{
+ private readonly TextureRegion[] _tiles;
+
+ ///
+ /// Gets the width, in pixels, of each tile in this tileset.
+ ///
+ public int TileWidth { get; }
+
+ ///
+ /// Gets the height, in pixels, of each tile in this tileset.
+ ///
+ public int TileHeight { get; }
+
+ ///
+ /// Gets the total number of columns in this tileset.
+ ///
+ public int Columns { get; }
+
+ ///
+ /// Gets the total number of rows in this tileset.
+ ///
+ public int Rows { get; }
+
+ ///
+ /// Gets the total number of tiles in this tileset.
+ ///
+ public int Count { get; }
+
+ ///
+ /// Creates a new tileset based on the given texture region with the specified
+ /// tile width and height.
+ ///
+ /// The texture region that contains the tiles for the tileset.
+ /// The width of each tile in the tileset.
+ /// The height of each tile in the tileset.
+ public Tileset(TextureRegion textureRegion, int tileWidth, int tileHeight)
+ {
+ TileWidth = tileWidth;
+ TileHeight = tileHeight;
+ Columns = textureRegion.Width / tileWidth;
+ Rows = textureRegion.Height / tileHeight;
+ Count = Columns * Rows;
+
+ // Create the texture regions that make up each individual tile
+ _tiles = new TextureRegion[Count];
+
+ for (int i = 0; i < Count; i++)
+ {
+ int x = i % Columns * tileWidth;
+ int y = i / Columns * tileHeight;
+ _tiles[i] = new TextureRegion(textureRegion.Texture, textureRegion.SourceRectangle.X + x, textureRegion.SourceRectangle.Y + y, tileWidth, tileHeight);
+ }
+ }
+
+ ///
+ /// Gets the texture region for the tile from this tileset at the given index.
+ ///
+ /// The index of the texture region in this tile set.
+ /// The texture region for the tile form this tileset at the given index.
+ public TextureRegion GetTile(int index) => _tiles[index];
+
+ ///
+ /// Gets the texture region for the tile from this tileset at the given location.
+ ///
+ /// The column in this tileset of the texture region.
+ /// The row in this tileset of the texture region.
+ /// The texture region for the tile from this tileset at given location.
+ public TextureRegion GetTile(int column, int row)
+ {
+ int index = row * Columns + column;
+ return GetTile(index);
+ }
+}
diff --git a/Tutorials/MobileDeployment/06-Publishing/MonoGameLibrary/Input/GamePadInfo.cs b/Tutorials/MobileDeployment/06-Publishing/MonoGameLibrary/Input/GamePadInfo.cs
new file mode 100644
index 00000000..5e3d0a00
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/MonoGameLibrary/Input/GamePadInfo.cs
@@ -0,0 +1,140 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Input;
+
+namespace MonoGameLibrary.Input;
+
+public class GamePadInfo
+{
+ private TimeSpan _vibrationTimeRemaining = TimeSpan.Zero;
+
+ ///
+ /// Gets the index of the player this gamepad is for.
+ ///
+ public PlayerIndex PlayerIndex { get; }
+
+ ///
+ /// Gets the state of input for this gamepad during the previous update cycle.
+ ///
+ public GamePadState PreviousState { get; private set; }
+
+ ///
+ /// Gets the state of input for this gamepad during the current update cycle.
+ ///
+ public GamePadState CurrentState { get; private set; }
+
+ ///
+ /// Gets a value that indicates if this gamepad is currently connected.
+ ///
+ public bool IsConnected => CurrentState.IsConnected;
+
+ ///
+ /// Gets the value of the left thumbstick of this gamepad.
+ ///
+ public Vector2 LeftThumbStick => CurrentState.ThumbSticks.Left;
+
+ ///
+ /// Gets the value of the right thumbstick of this gamepad.
+ ///
+ public Vector2 RightThumbStick => CurrentState.ThumbSticks.Right;
+
+ ///
+ /// Gets the value of the left trigger of this gamepad.
+ ///
+ public float LeftTrigger => CurrentState.Triggers.Left;
+
+ ///
+ /// Gets the value of the right trigger of this gamepad.
+ ///
+ public float RightTrigger => CurrentState.Triggers.Right;
+
+ ///
+ /// Creates a new GamePadInfo for the gamepad connected at the specified player index.
+ ///
+ /// The index of the player for this gamepad.
+ public GamePadInfo(PlayerIndex playerIndex)
+ {
+ PlayerIndex = playerIndex;
+ PreviousState = new GamePadState();
+ CurrentState = GamePad.GetState(playerIndex);
+ }
+
+ ///
+ /// Updates the state information for this gamepad input.
+ ///
+ ///
+ public void Update(GameTime gameTime)
+ {
+ PreviousState = CurrentState;
+ CurrentState = GamePad.GetState(PlayerIndex);
+
+ if (_vibrationTimeRemaining > TimeSpan.Zero)
+ {
+ _vibrationTimeRemaining -= gameTime.ElapsedGameTime;
+
+ if (_vibrationTimeRemaining <= TimeSpan.Zero)
+ {
+ StopVibration();
+ }
+ }
+ }
+
+ ///
+ /// Returns a value that indicates whether the specified gamepad button is current down.
+ ///
+ /// The gamepad button to check.
+ /// true if the specified gamepad button is currently down; otherwise, false.
+ public bool IsButtonDown(Buttons button)
+ {
+ return CurrentState.IsButtonDown(button);
+ }
+
+ ///
+ /// Returns a value that indicates whether the specified gamepad button is currently up.
+ ///
+ /// The gamepad button to check.
+ /// true if the specified gamepad button is currently up; otherwise, false.
+ public bool IsButtonUp(Buttons button)
+ {
+ return CurrentState.IsButtonUp(button);
+ }
+
+ ///
+ /// Returns a value that indicates whether the specified gamepad button was just pressed on the current frame.
+ ///
+ /// The gamepad button to check.
+ /// true if the specified gamepad button was just pressed on the current frame; otherwise, false.
+ public bool WasButtonJustPressed(Buttons button)
+ {
+ return CurrentState.IsButtonDown(button) && PreviousState.IsButtonUp(button);
+ }
+
+ ///
+ /// Returns a value that indicates whether the specified gamepad button was just released on the current frame.
+ ///
+ /// The gamepad button to check.
+ /// true if the specified gamepad button was just released on the current frame; otherwise, false.
+ public bool WasButtonJustReleased(Buttons button)
+ {
+ return CurrentState.IsButtonUp(button) && PreviousState.IsButtonDown(button);
+ }
+
+ ///
+ /// Sets the vibration for all motors of this gamepad.
+ ///
+ /// The strength of the vibration from 0.0f (none) to 1.0f (full).
+ /// The amount of time the vibration should occur.
+ public void SetVibration(float strength, TimeSpan time)
+ {
+ _vibrationTimeRemaining = time;
+ GamePad.SetVibration(PlayerIndex, strength, strength);
+ }
+
+ ///
+ /// Stops the vibration of all motors for this gamepad.
+ ///
+ public void StopVibration()
+ {
+ GamePad.SetVibration(PlayerIndex, 0.0f, 0.0f);
+ }
+}
diff --git a/Tutorials/MobileDeployment/06-Publishing/MonoGameLibrary/Input/InputManager.cs b/Tutorials/MobileDeployment/06-Publishing/MonoGameLibrary/Input/InputManager.cs
new file mode 100644
index 00000000..cddb200d
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/MonoGameLibrary/Input/InputManager.cs
@@ -0,0 +1,58 @@
+using Microsoft.Xna.Framework;
+
+namespace MonoGameLibrary.Input;
+
+public class InputManager
+{
+ ///
+ /// Gets the state information of keyboard input.
+ ///
+ public KeyboardInfo Keyboard { get; private set; }
+
+ ///
+ /// Gets the state information of mouse input.
+ ///
+ public MouseInfo Mouse { get; private set; }
+
+ ///
+ /// Gets the state information of a gamepad.
+ ///
+ public GamePadInfo[] GamePads { get; private set; }
+
+ ///
+ /// Gets the state information of touch input.
+ ///
+ public TouchInfo Touch { get; private set; }
+
+ ///
+ /// Creates a new InputManager.
+ ///
+ public InputManager()
+ {
+ Keyboard = new KeyboardInfo();
+ Mouse = new MouseInfo();
+ Touch = new TouchInfo();
+
+ GamePads = new GamePadInfo[4];
+ for (int i = 0; i < 4; i++)
+ {
+ GamePads[i] = new GamePadInfo((PlayerIndex)i);
+ }
+ }
+
+ ///
+ /// Updates the state information for the keyboard, mouse, touch, and gamepad inputs.
+ ///
+ /// A snapshot of the timing values for the current frame.
+ public void Update(GameTime gameTime)
+ {
+ Keyboard.Update();
+ Mouse.Update();
+ Touch.Update();
+
+ for (int i = 0; i < 4; i++)
+ {
+ GamePads[i].Update(gameTime);
+ }
+ }
+}
diff --git a/Tutorials/MobileDeployment/06-Publishing/MonoGameLibrary/Input/KeyboardInfo.cs b/Tutorials/MobileDeployment/06-Publishing/MonoGameLibrary/Input/KeyboardInfo.cs
new file mode 100644
index 00000000..c6770cb0
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/MonoGameLibrary/Input/KeyboardInfo.cs
@@ -0,0 +1,74 @@
+using Microsoft.Xna.Framework.Input;
+
+namespace MonoGameLibrary.Input;
+
+public class KeyboardInfo
+{
+ ///
+ /// Gets the state of keyboard input during the previous update cycle.
+ ///
+ public KeyboardState PreviousState { get; private set; }
+
+ ///
+ /// Gets the state of keyboard input during the current input cycle.
+ ///
+ public KeyboardState CurrentState { get; private set; }
+
+ ///
+ /// Creates a new KeyboardInfo
+ ///
+ public KeyboardInfo()
+ {
+ PreviousState = new KeyboardState();
+ CurrentState = Keyboard.GetState();
+ }
+
+ ///
+ /// Updates the state information about keyboard input.
+ ///
+ public void Update()
+ {
+ PreviousState = CurrentState;
+ CurrentState = Keyboard.GetState();
+ }
+
+ ///
+ /// Returns a value that indicates if the specified key is currently down.
+ ///
+ /// The key to check.
+ /// true if the specified key is currently down; otherwise, false.
+ public bool IsKeyDown(Keys key)
+ {
+ return CurrentState.IsKeyDown(key);
+ }
+
+ ///
+ /// Returns a value that indicates whether the specified key is currently up.
+ ///
+ /// The key to check.
+ /// true if the specified key is currently up; otherwise, false.
+ public bool IsKeyUp(Keys key)
+ {
+ return CurrentState.IsKeyUp(key);
+ }
+
+ ///
+ /// Returns a value that indicates if the specified key was just pressed on the current frame.
+ ///
+ /// The key to check.
+ /// true if the specified key was just pressed on the current frame; otherwise, false.
+ public bool WasKeyJustPressed(Keys key)
+ {
+ return CurrentState.IsKeyDown(key) && PreviousState.IsKeyUp(key);
+ }
+
+ ///
+ /// Returns a value that indicates if the specified key was just released on the current frame.
+ ///
+ /// The key to check.
+ /// true if the specified key was just released on the current frame; otherwise, false.
+ public bool WasKeyJustReleased(Keys key)
+ {
+ return CurrentState.IsKeyUp(key) && PreviousState.IsKeyDown(key);
+ }
+}
diff --git a/Tutorials/MobileDeployment/06-Publishing/MonoGameLibrary/Input/MouseButton.cs b/Tutorials/MobileDeployment/06-Publishing/MonoGameLibrary/Input/MouseButton.cs
new file mode 100644
index 00000000..5b041f80
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/MonoGameLibrary/Input/MouseButton.cs
@@ -0,0 +1,10 @@
+namespace MonoGameLibrary.Input;
+
+public enum MouseButton
+{
+ Left,
+ Middle,
+ Right,
+ XButton1,
+ XButton2
+}
diff --git a/Tutorials/MobileDeployment/06-Publishing/MonoGameLibrary/Input/MouseInfo.cs b/Tutorials/MobileDeployment/06-Publishing/MonoGameLibrary/Input/MouseInfo.cs
new file mode 100644
index 00000000..09d6207c
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/MonoGameLibrary/Input/MouseInfo.cs
@@ -0,0 +1,208 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Input;
+
+namespace MonoGameLibrary.Input;
+
+public class MouseInfo
+{
+ ///
+ /// The state of mouse input during the previous update cycle.
+ ///
+ public MouseState PreviousState { get; private set; }
+
+ ///
+ /// The state of mouse input during the current update cycle.
+ ///
+ public MouseState CurrentState { get; private set; }
+
+ ///
+ /// Gets or Sets the current position of the mouse cursor in screen space.
+ ///
+ public Point Position
+ {
+ get => CurrentState.Position;
+ set => SetPosition(value.X, value.Y);
+ }
+
+ ///
+ /// Gets or Sets the current x-coordinate position of the mouse cursor in screen space.
+ ///
+ public int X
+ {
+ get => CurrentState.X;
+ set => SetPosition(value, CurrentState.Y);
+ }
+
+ ///
+ /// Gets or Sets the current y-coordinate position of the mouse cursor in screen space.
+ ///
+ public int Y
+ {
+ get => CurrentState.Y;
+ set => SetPosition(CurrentState.X, value);
+ }
+
+ ///
+ /// Gets the difference in the mouse cursor position between the previous and current frame.
+ ///
+ public Point PositionDelta => CurrentState.Position - PreviousState.Position;
+
+ ///
+ /// Gets the difference in the mouse cursor x-position between the previous and current frame.
+ ///
+ public int XDelta => CurrentState.X - PreviousState.X;
+
+ ///
+ /// Gets the difference in the mouse cursor y-position between the previous and current frame.
+ ///
+ public int YDelta => CurrentState.Y - PreviousState.Y;
+
+ ///
+ /// Gets a value that indicates if the mouse cursor moved between the previous and current frames.
+ ///
+ public bool WasMoved => PositionDelta != Point.Zero;
+
+ ///
+ /// Gets the cumulative value of the mouse scroll wheel since the start of the game.
+ ///
+ public int ScrollWheel => CurrentState.ScrollWheelValue;
+
+ ///
+ /// Gets the value of the scroll wheel between the previous and current frame.
+ ///
+ public int ScrollWheelDelta => CurrentState.ScrollWheelValue - PreviousState.ScrollWheelValue;
+
+ ///
+ /// Creates a new MouseInfo.
+ ///
+ public MouseInfo()
+ {
+ PreviousState = new MouseState();
+ CurrentState = Mouse.GetState();
+ }
+
+ ///
+ /// Updates the state information about mouse input.
+ ///
+ public void Update()
+ {
+ PreviousState = CurrentState;
+ CurrentState = Mouse.GetState();
+ }
+
+ ///
+ /// Returns a value that indicates whether the specified mouse button is currently down.
+ ///
+ /// The mouse button to check.
+ /// true if the specified mouse button is currently down; otherwise, false.
+ public bool IsButtonDown(MouseButton button)
+ {
+ switch (button)
+ {
+ case MouseButton.Left:
+ return CurrentState.LeftButton == ButtonState.Pressed;
+ case MouseButton.Middle:
+ return CurrentState.MiddleButton == ButtonState.Pressed;
+ case MouseButton.Right:
+ return CurrentState.RightButton == ButtonState.Pressed;
+ case MouseButton.XButton1:
+ return CurrentState.XButton1 == ButtonState.Pressed;
+ case MouseButton.XButton2:
+ return CurrentState.XButton2 == ButtonState.Pressed;
+ default:
+ return false;
+ }
+ }
+
+ ///
+ /// Returns a value that indicates whether the specified mouse button is current up.
+ ///
+ /// The mouse button to check.
+ /// true if the specified mouse button is currently up; otherwise, false.
+ public bool IsButtonUp(MouseButton button)
+ {
+ switch (button)
+ {
+ case MouseButton.Left:
+ return CurrentState.LeftButton == ButtonState.Released;
+ case MouseButton.Middle:
+ return CurrentState.MiddleButton == ButtonState.Released;
+ case MouseButton.Right:
+ return CurrentState.RightButton == ButtonState.Released;
+ case MouseButton.XButton1:
+ return CurrentState.XButton1 == ButtonState.Released;
+ case MouseButton.XButton2:
+ return CurrentState.XButton2 == ButtonState.Released;
+ default:
+ return false;
+ }
+ }
+
+ ///
+ /// Returns a value that indicates whether the specified mouse button was just pressed on the current frame.
+ ///
+ /// The mouse button to check.
+ /// true if the specified mouse button was just pressed on the current frame; otherwise, false.
+ public bool WasButtonJustPressed(MouseButton button)
+ {
+ switch (button)
+ {
+ case MouseButton.Left:
+ return CurrentState.LeftButton == ButtonState.Pressed && PreviousState.LeftButton == ButtonState.Released;
+ case MouseButton.Middle:
+ return CurrentState.MiddleButton == ButtonState.Pressed && PreviousState.MiddleButton == ButtonState.Released;
+ case MouseButton.Right:
+ return CurrentState.RightButton == ButtonState.Pressed && PreviousState.RightButton == ButtonState.Released;
+ case MouseButton.XButton1:
+ return CurrentState.XButton1 == ButtonState.Pressed && PreviousState.XButton1 == ButtonState.Released;
+ case MouseButton.XButton2:
+ return CurrentState.XButton2 == ButtonState.Pressed && PreviousState.XButton2 == ButtonState.Released;
+ default:
+ return false;
+ }
+ }
+
+ ///
+ /// Returns a value that indicates whether the specified mouse button was just released on the current frame.
+ ///
+ /// The mouse button to check.
+ /// true if the specified mouse button was just released on the current frame; otherwise, false.F
+ public bool WasButtonJustReleased(MouseButton button)
+ {
+ switch (button)
+ {
+ case MouseButton.Left:
+ return CurrentState.LeftButton == ButtonState.Released && PreviousState.LeftButton == ButtonState.Pressed;
+ case MouseButton.Middle:
+ return CurrentState.MiddleButton == ButtonState.Released && PreviousState.MiddleButton == ButtonState.Pressed;
+ case MouseButton.Right:
+ return CurrentState.RightButton == ButtonState.Released && PreviousState.RightButton == ButtonState.Pressed;
+ case MouseButton.XButton1:
+ return CurrentState.XButton1 == ButtonState.Released && PreviousState.XButton1 == ButtonState.Pressed;
+ case MouseButton.XButton2:
+ return CurrentState.XButton2 == ButtonState.Released && PreviousState.XButton2 == ButtonState.Pressed;
+ default:
+ return false;
+ }
+ }
+
+ ///
+ /// Sets the current position of the mouse cursor in screen space and updates the CurrentState with the new position.
+ ///
+ /// The x-coordinate location of the mouse cursor in screen space.
+ /// The y-coordinate location of the mouse cursor in screen space.
+ public void SetPosition(int x, int y)
+ {
+ Mouse.SetPosition(x, y);
+ CurrentState = new MouseState(
+ x,
+ y,
+ CurrentState.ScrollWheelValue,
+ CurrentState.LeftButton,
+ CurrentState.MiddleButton,
+ CurrentState.RightButton,
+ CurrentState.XButton1,
+ CurrentState.XButton2
+ );
+ }
+}
diff --git a/Tutorials/MobileDeployment/06-Publishing/MonoGameLibrary/Input/TouchInfo.cs b/Tutorials/MobileDeployment/06-Publishing/MonoGameLibrary/Input/TouchInfo.cs
new file mode 100644
index 00000000..80465b37
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/MonoGameLibrary/Input/TouchInfo.cs
@@ -0,0 +1,131 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Input.Touch;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace MonoGameLibrary.Input;
+
+public class TouchInfo
+{
+ ///
+ /// Gets the state of touch input during the previous update cycle.
+ ///
+ public TouchCollection PreviousState { get; private set; }
+
+ ///
+ /// Gets the state of touch input during the current update cycle.
+ ///
+ public TouchCollection CurrentState { get; private set; }
+
+ ///
+ /// Gets a value that indicates if any touch input is currently active.
+ ///
+ public bool HasTouches => CurrentState.Count > 0;
+
+ ///
+ /// Gets the number of current active touches.
+ ///
+ public int TouchCount => CurrentState.Count;
+
+ ///
+ /// Gets the primary touch location, or Vector2.Zero if no touches are active.
+ ///
+ public Vector2 PrimaryTouchPosition => HasTouches ? CurrentState[0].Position : Vector2.Zero;
+
+ ///
+ /// Creates a new TouchInfo.
+ ///
+ public TouchInfo()
+ {
+ PreviousState = new TouchCollection();
+ CurrentState = TouchPanel.GetState();
+ }
+
+ ///
+ /// Updates the state information about touch input.
+ ///
+ public void Update()
+ {
+ PreviousState = CurrentState;
+ CurrentState = TouchPanel.GetState();
+ }
+
+ ///
+ /// Returns a value that indicates if a touch was just started on the current frame.
+ ///
+ /// true if a touch was just started on the current frame; otherwise, false.
+ public bool WasTouchJustPressed()
+ {
+ return CurrentState.Any(t => t.State == TouchLocationState.Pressed) &&
+ !PreviousState.Any(t => t.State == TouchLocationState.Pressed || t.State == TouchLocationState.Moved);
+ }
+
+ ///
+ /// Returns a value that indicates if a touch was just released on the current frame.
+ ///
+ /// true if a touch was just released on the current frame; otherwise, false.
+ public bool WasTouchJustReleased()
+ {
+ return CurrentState.Any(t => t.State == TouchLocationState.Released) ||
+ (PreviousState.Count > 0 && CurrentState.Count == 0);
+ }
+
+ ///
+ /// Returns a value that indicates if any touch is currently active (pressed or moved).
+ ///
+ /// true if any touch is currently active; otherwise, false.
+ public bool IsTouchDown()
+ {
+ return CurrentState.Any(t => t.State == TouchLocationState.Pressed || t.State == TouchLocationState.Moved);
+ }
+
+ ///
+ /// Gets all current touch locations.
+ ///
+ /// An enumerable of all current touch locations.
+ public IEnumerable GetTouchLocations()
+ {
+ return CurrentState;
+ }
+
+ ///
+ /// Gets a specific touch by its ID.
+ ///
+ /// The ID of the touch to find.
+ /// The touch location if found; otherwise, a default TouchLocation.
+ public TouchLocation GetTouchById(int id)
+ {
+ return CurrentState.FirstOrDefault(t => t.Id == id);
+ }
+
+ ///
+ /// Returns a value that indicates if a touch moved between the previous and current frames.
+ ///
+ /// true if any touch moved; otherwise, false.
+ public bool WasTouchMoved()
+ {
+ return CurrentState.Any(t => t.State == TouchLocationState.Moved);
+ }
+
+ ///
+ /// Gets the delta movement of the primary touch between frames.
+ ///
+ /// The movement delta, or Vector2.Zero if no primary touch or movement.
+ public Vector2 GetPrimaryTouchDelta()
+ {
+ if (!HasTouches) return Vector2.Zero;
+
+ var currentTouch = CurrentState[0];
+
+ // Try to find the corresponding touch in the previous state
+ foreach (var prevTouch in PreviousState)
+ {
+ if (prevTouch.Id == currentTouch.Id)
+ {
+ return currentTouch.Position - prevTouch.Position;
+ }
+ }
+
+ return Vector2.Zero;
+ }
+}
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/06-Publishing/MonoGameLibrary/MonoGameLibrary.csproj b/Tutorials/MobileDeployment/06-Publishing/MonoGameLibrary/MonoGameLibrary.csproj
new file mode 100644
index 00000000..314517c3
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/MonoGameLibrary/MonoGameLibrary.csproj
@@ -0,0 +1,17 @@
+
+
+ net9.0;net9.0-ios;net9.0-android
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Tutorials/MobileDeployment/06-Publishing/MonoGameLibrary/Scenes/Scene.cs b/Tutorials/MobileDeployment/06-Publishing/MonoGameLibrary/Scenes/Scene.cs
new file mode 100644
index 00000000..627d220f
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/MonoGameLibrary/Scenes/Scene.cs
@@ -0,0 +1,104 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content;
+
+namespace MonoGameLibrary.Scenes;
+
+public abstract class Scene : IDisposable
+{
+ ///
+ /// Gets the ContentManager used for loading scene-specific assets.
+ ///
+ ///
+ /// Assets loaded through this ContentManager will be automatically unloaded when this scene ends.
+ ///
+ protected ContentManager Content { get; }
+
+ ///
+ /// Gets a value that indicates if the scene has been disposed of.
+ ///
+ public bool IsDisposed { get; private set; }
+
+ ///
+ /// Creates a new scene instance.
+ ///
+ public Scene()
+ {
+ // Create a content manager for the scene
+ Content = new ContentManager(Core.Content.ServiceProvider);
+
+ // Set the root directory for content to the same as the root directory
+ // for the game's content.
+ Content.RootDirectory = Core.Content.RootDirectory;
+ }
+
+ // Finalizer, called when object is cleaned up by garbage collector.
+ ~Scene() => Dispose(false);
+
+ ///
+ /// Initializes the scene.
+ ///
+ ///
+ /// When overriding this in a derived class, ensure that base.Initialize()
+ /// still called as this is when LoadContent is called.
+ ///
+ public virtual void Initialize()
+ {
+ LoadContent();
+ }
+
+ ///
+ /// Override to provide logic to load content for the scene.
+ ///
+ public virtual void LoadContent() { }
+
+ ///
+ /// Unloads scene-specific content.
+ ///
+ public virtual void UnloadContent()
+ {
+ Content.Unload();
+ }
+
+ ///
+ /// Updates this scene.
+ ///
+ /// A snapshot of the timing values for the current frame.
+ public virtual void Update(GameTime gameTime) { }
+
+ ///
+ /// Draws this scene.
+ ///
+ /// A snapshot of the timing values for the current frame.
+ public virtual void Draw(GameTime gameTime) { }
+
+ ///
+ /// Disposes of this scene.
+ ///
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ /// Disposes of this scene.
+ ///
+ /// '
+ /// Indicates whether managed resources should be disposed. This value is only true when called from the main
+ /// Dispose method. When called from the finalizer, this will be false.
+ ///
+ protected virtual void Dispose(bool disposing)
+ {
+ if (IsDisposed)
+ {
+ return;
+ }
+
+ if (disposing)
+ {
+ UnloadContent();
+ Content.Dispose();
+ }
+ }
+}
diff --git a/Tutorials/MobileDeployment/06-Publishing/README.md b/Tutorials/MobileDeployment/06-Publishing/README.md
new file mode 100644
index 00000000..2221fa5e
--- /dev/null
+++ b/Tutorials/MobileDeployment/06-Publishing/README.md
@@ -0,0 +1,36 @@
+# Chapter 6: Publishing to App Stores
+
+This chapter demonstrates how to convert a Windows-only MonoGame project to support iOS and Android platforms using a shared codebase architecture.
+
+The chapter covers:
+
+* Converting a single-platform project to multi-platform structure.
+* Setting up a shared common library with multi-targeting.
+* Creating platform-specific project shells for Windows, iOS, and Android.
+* Configuring conditional package references for each platform.
+* Understanding cross-platform project architecture and naming conventions.
+* Updating third-party libraries for cross-platform compatibility.
+* The assets and configuration to package and deploy to the app stores.
+
+## Project Structure
+
+This sample includes:
+
+* **DungeonSlime.Common** - Shared game logic library with multi-targeting support
+* **DungeonSlime.Windows** - Windows desktop project shell
+* **DungeonSlime.iOS** - iOS mobile project shell
+* **DungeonSlime.Android** - Android mobile project shell
+
+## Prerequisites
+
+* Completed the MonoGame 2D tutorial
+* Development environment set up
+* For iOS: Mac with Xcode and Apple Developer account
+* For Android: Android SDK and development tools
+
+## Key Features Demonstrated
+
+* Multi-targeting framework configuration (`net8.0;net8.0-ios;net8.0-android`)
+* Platform-specific MonoGame package references
+* Shared code architecture for cross-platform development
+* Modern .NET project management with Central Package Management