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