diff --git a/Tutorials/2dShaders/AllChapters.sln b/Tutorials/2dShaders/AllChapters.sln new file mode 100644 index 00000000..1457a1e6 --- /dev/null +++ b/Tutorials/2dShaders/AllChapters.sln @@ -0,0 +1,144 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "02-Hot-Reload-System", "02-Hot-Reload-System", "{5C8ECEB8-3113-41E1-9F86-CD33F50E900C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MonoGameLibrary", "src\02-Hot-Reload-System\MonoGameLibrary\MonoGameLibrary.csproj", "{53F894E0-1907-4EAE-A72D-A31D09D6912A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DungeonSlime", "src\02-Hot-Reload-System\DungeonSlime\DungeonSlime.csproj", "{632223D1-6067-40DB-9F2D-301621574301}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "03-The-Material-Class", "03-The-Material-Class", "{A959F3EE-A738-DA1E-8657-F797276622F9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DungeonSlime", "src\03-The-Material-Class\DungeonSlime\DungeonSlime.csproj", "{47ABB502-5EB7-4676-AC44-57466F64EA86}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MonoGameLibrary", "src\03-The-Material-Class\MonoGameLibrary\MonoGameLibrary.csproj", "{2712BAA4-87E6-4243-9798-A0747AFEEF53}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "04-Debug-UI", "04-Debug-UI", "{8CCC4EC4-B284-4ECF-9C08-958C770F3259}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DungeonSlime", "src\04-Debug-UI\DungeonSlime\DungeonSlime.csproj", "{7F741013-A83D-4755-BF7F-CA7FE1D0D834}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MonoGameLibrary", "src\04-Debug-UI\MonoGameLibrary\MonoGameLibrary.csproj", "{5C713E76-6394-4D2B-913F-180ECD5FA4CF}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "05-Transition-Effect", "05-Transition-Effect", "{31C31A95-F2DE-4879-B6BB-2A5EBACDE187}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DungeonSlime", "src\05-Transition-Effect\DungeonSlime\DungeonSlime.csproj", "{137FADC7-AFEF-41D3-9492-58E734982D38}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MonoGameLibrary", "src\05-Transition-Effect\MonoGameLibrary\MonoGameLibrary.csproj", "{38651908-A22C-4589-A884-07B09EAAF489}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "06-Color-Swap-Effect", "06-Color-Swap-Effect", "{77792598-E7A1-4C95-BC0B-49EBBEB0C4C7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MonoGameLibrary", "src\06-Color-Swap-Effect\MonoGameLibrary\MonoGameLibrary.csproj", "{1EA33E74-FB01-42D8-9922-182CBC2F3936}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DungeonSlime", "src\06-Color-Swap-Effect\DungeonSlime\DungeonSlime.csproj", "{0CF4EC16-238F-407C-9472-63EFF6C40CCC}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "07-Sprite-Vertex-Effect", "07-Sprite-Vertex-Effect", "{23616150-B165-4CFE-BC26-44ED64DE0565}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MonoGameLibrary", "src\07-Sprite-Vertex-Effect\MonoGameLibrary\MonoGameLibrary.csproj", "{1DC76E50-88F5-4ACC-9DF7-4353D5C4AA5F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DungeonSlime", "src\07-Sprite-Vertex-Effect\DungeonSlime\DungeonSlime.csproj", "{7AE4D6EA-583E-4046-B94C-0860CDDEBE5D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "08-Light-Effect", "08-Light-Effect", "{B43F0FC2-1923-4FFA-9E82-D8814BF3F974}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MonoGameLibrary", "src\08-Light-Effect\MonoGameLibrary\MonoGameLibrary.csproj", "{5671DA7C-B84C-4672-8ED7-766C43BE35F1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DungeonSlime", "src\08-Light-Effect\DungeonSlime\DungeonSlime.csproj", "{DBFBD5B7-BAC6-42D7-BFA5-70C4AB6F6F14}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "09-Shadows-Effect", "09-Shadows-Effect", "{BB01A8A7-B315-4E08-8A6F-2C49335FDF03}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MonoGameLibrary", "src\09-Shadows-Effect\MonoGameLibrary\MonoGameLibrary.csproj", "{D5D9227B-DB9A-4FC2-8D74-BA3C310105D5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DungeonSlime", "src\09-Shadows-Effect\DungeonSlime\DungeonSlime.csproj", "{811AC0B0-0858-4387-96D2-60D842299F68}" +EndProject + +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {53F894E0-1907-4EAE-A72D-A31D09D6912A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {53F894E0-1907-4EAE-A72D-A31D09D6912A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {53F894E0-1907-4EAE-A72D-A31D09D6912A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {53F894E0-1907-4EAE-A72D-A31D09D6912A}.Release|Any CPU.Build.0 = Release|Any CPU + {632223D1-6067-40DB-9F2D-301621574301}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {632223D1-6067-40DB-9F2D-301621574301}.Debug|Any CPU.Build.0 = Debug|Any CPU + {632223D1-6067-40DB-9F2D-301621574301}.Release|Any CPU.ActiveCfg = Release|Any CPU + {632223D1-6067-40DB-9F2D-301621574301}.Release|Any CPU.Build.0 = Release|Any CPU + {7F741013-A83D-4755-BF7F-CA7FE1D0D834}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7F741013-A83D-4755-BF7F-CA7FE1D0D834}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7F741013-A83D-4755-BF7F-CA7FE1D0D834}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7F741013-A83D-4755-BF7F-CA7FE1D0D834}.Release|Any CPU.Build.0 = Release|Any CPU + {5C713E76-6394-4D2B-913F-180ECD5FA4CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5C713E76-6394-4D2B-913F-180ECD5FA4CF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5C713E76-6394-4D2B-913F-180ECD5FA4CF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5C713E76-6394-4D2B-913F-180ECD5FA4CF}.Release|Any CPU.Build.0 = Release|Any CPU + {47ABB502-5EB7-4676-AC44-57466F64EA86}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {47ABB502-5EB7-4676-AC44-57466F64EA86}.Debug|Any CPU.Build.0 = Debug|Any CPU + {47ABB502-5EB7-4676-AC44-57466F64EA86}.Release|Any CPU.ActiveCfg = Release|Any CPU + {47ABB502-5EB7-4676-AC44-57466F64EA86}.Release|Any CPU.Build.0 = Release|Any CPU + {2712BAA4-87E6-4243-9798-A0747AFEEF53}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2712BAA4-87E6-4243-9798-A0747AFEEF53}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2712BAA4-87E6-4243-9798-A0747AFEEF53}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2712BAA4-87E6-4243-9798-A0747AFEEF53}.Release|Any CPU.Build.0 = Release|Any CPU + {137FADC7-AFEF-41D3-9492-58E734982D38}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {137FADC7-AFEF-41D3-9492-58E734982D38}.Debug|Any CPU.Build.0 = Debug|Any CPU + {137FADC7-AFEF-41D3-9492-58E734982D38}.Release|Any CPU.ActiveCfg = Release|Any CPU + {137FADC7-AFEF-41D3-9492-58E734982D38}.Release|Any CPU.Build.0 = Release|Any CPU + {38651908-A22C-4589-A884-07B09EAAF489}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {38651908-A22C-4589-A884-07B09EAAF489}.Debug|Any CPU.Build.0 = Debug|Any CPU + {38651908-A22C-4589-A884-07B09EAAF489}.Release|Any CPU.ActiveCfg = Release|Any CPU + {38651908-A22C-4589-A884-07B09EAAF489}.Release|Any CPU.Build.0 = Release|Any CPU + {1EA33E74-FB01-42D8-9922-182CBC2F3936}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1EA33E74-FB01-42D8-9922-182CBC2F3936}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1EA33E74-FB01-42D8-9922-182CBC2F3936}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1EA33E74-FB01-42D8-9922-182CBC2F3936}.Release|Any CPU.Build.0 = Release|Any CPU + {0CF4EC16-238F-407C-9472-63EFF6C40CCC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0CF4EC16-238F-407C-9472-63EFF6C40CCC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0CF4EC16-238F-407C-9472-63EFF6C40CCC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0CF4EC16-238F-407C-9472-63EFF6C40CCC}.Release|Any CPU.Build.0 = Release|Any CPU + {1DC76E50-88F5-4ACC-9DF7-4353D5C4AA5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1DC76E50-88F5-4ACC-9DF7-4353D5C4AA5F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1DC76E50-88F5-4ACC-9DF7-4353D5C4AA5F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1DC76E50-88F5-4ACC-9DF7-4353D5C4AA5F}.Release|Any CPU.Build.0 = Release|Any CPU + {7AE4D6EA-583E-4046-B94C-0860CDDEBE5D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7AE4D6EA-583E-4046-B94C-0860CDDEBE5D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7AE4D6EA-583E-4046-B94C-0860CDDEBE5D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7AE4D6EA-583E-4046-B94C-0860CDDEBE5D}.Release|Any CPU.Build.0 = Release|Any CPU + {5671DA7C-B84C-4672-8ED7-766C43BE35F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5671DA7C-B84C-4672-8ED7-766C43BE35F1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5671DA7C-B84C-4672-8ED7-766C43BE35F1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5671DA7C-B84C-4672-8ED7-766C43BE35F1}.Release|Any CPU.Build.0 = Release|Any CPU + {DBFBD5B7-BAC6-42D7-BFA5-70C4AB6F6F14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DBFBD5B7-BAC6-42D7-BFA5-70C4AB6F6F14}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DBFBD5B7-BAC6-42D7-BFA5-70C4AB6F6F14}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DBFBD5B7-BAC6-42D7-BFA5-70C4AB6F6F14}.Release|Any CPU.Build.0 = Release|Any CPU + {D5D9227B-DB9A-4FC2-8D74-BA3C310105D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D5D9227B-DB9A-4FC2-8D74-BA3C310105D5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D5D9227B-DB9A-4FC2-8D74-BA3C310105D5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D5D9227B-DB9A-4FC2-8D74-BA3C310105D5}.Release|Any CPU.Build.0 = Release|Any CPU + {811AC0B0-0858-4387-96D2-60D842299F68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {811AC0B0-0858-4387-96D2-60D842299F68}.Debug|Any CPU.Build.0 = Debug|Any CPU + {811AC0B0-0858-4387-96D2-60D842299F68}.Release|Any CPU.ActiveCfg = Release|Any CPU + {811AC0B0-0858-4387-96D2-60D842299F68}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {53F894E0-1907-4EAE-A72D-A31D09D6912A} = {5C8ECEB8-3113-41E1-9F86-CD33F50E900C} + {632223D1-6067-40DB-9F2D-301621574301} = {5C8ECEB8-3113-41E1-9F86-CD33F50E900C} + {7F741013-A83D-4755-BF7F-CA7FE1D0D834} = {8CCC4EC4-B284-4ECF-9C08-958C770F3259} + {5C713E76-6394-4D2B-913F-180ECD5FA4CF} = {8CCC4EC4-B284-4ECF-9C08-958C770F3259} + {47ABB502-5EB7-4676-AC44-57466F64EA86} = {A959F3EE-A738-DA1E-8657-F797276622F9} + {2712BAA4-87E6-4243-9798-A0747AFEEF53} = {A959F3EE-A738-DA1E-8657-F797276622F9} + {137FADC7-AFEF-41D3-9492-58E734982D38} = {31C31A95-F2DE-4879-B6BB-2A5EBACDE187} + {38651908-A22C-4589-A884-07B09EAAF489} = {31C31A95-F2DE-4879-B6BB-2A5EBACDE187} + {1EA33E74-FB01-42D8-9922-182CBC2F3936} = {77792598-E7A1-4C95-BC0B-49EBBEB0C4C7} + {0CF4EC16-238F-407C-9472-63EFF6C40CCC} = {77792598-E7A1-4C95-BC0B-49EBBEB0C4C7} + {1DC76E50-88F5-4ACC-9DF7-4353D5C4AA5F} = {23616150-B165-4CFE-BC26-44ED64DE0565} + {7AE4D6EA-583E-4046-B94C-0860CDDEBE5D} = {23616150-B165-4CFE-BC26-44ED64DE0565} + {5671DA7C-B84C-4672-8ED7-766C43BE35F1} = {B43F0FC2-1923-4FFA-9E82-D8814BF3F974} + {DBFBD5B7-BAC6-42D7-BFA5-70C4AB6F6F14} = {B43F0FC2-1923-4FFA-9E82-D8814BF3F974} + {D5D9227B-DB9A-4FC2-8D74-BA3C310105D5} = {BB01A8A7-B315-4E08-8A6F-2C49335FDF03} + {811AC0B0-0858-4387-96D2-60D842299F68} = {BB01A8A7-B315-4E08-8A6F-2C49335FDF03} + EndGlobalSection +EndGlobal diff --git a/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime.sln b/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime.sln new file mode 100644 index 00000000..077462d5 --- /dev/null +++ b/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DungeonSlime", "DungeonSlime\DungeonSlime.csproj", "{88BD3FF9-D3D6-4680-A274-F866E0DFCAC4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MonoGameLibrary", "MonoGameLibrary\MonoGameLibrary.csproj", "{AB85CEEE-6D97-4438-AEC4-797D2806F44A}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {88BD3FF9-D3D6-4680-A274-F866E0DFCAC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {88BD3FF9-D3D6-4680-A274-F866E0DFCAC4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {88BD3FF9-D3D6-4680-A274-F866E0DFCAC4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {88BD3FF9-D3D6-4680-A274-F866E0DFCAC4}.Release|Any CPU.Build.0 = Release|Any CPU + {AB85CEEE-6D97-4438-AEC4-797D2806F44A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AB85CEEE-6D97-4438-AEC4-797D2806F44A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AB85CEEE-6D97-4438-AEC4-797D2806F44A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AB85CEEE-6D97-4438-AEC4-797D2806F44A}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/.config/dotnet-tools.json b/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/.config/dotnet-tools.json new file mode 100644 index 00000000..afd4e2c4 --- /dev/null +++ b/Tutorials/2dShaders/src/02-Hot-Reload-System/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/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Content/Content.mgcb b/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Content/Content.mgcb new file mode 100644 index 00000000..d26ea4f1 --- /dev/null +++ b/Tutorials/2dShaders/src/02-Hot-Reload-System/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/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Content/audio/bounce.wav b/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Content/audio/bounce.wav new file mode 100644 index 00000000..baa7a47b Binary files /dev/null and b/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Content/audio/bounce.wav differ diff --git a/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Content/audio/collect.wav b/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Content/audio/collect.wav new file mode 100644 index 00000000..506220de Binary files /dev/null and b/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Content/audio/collect.wav differ diff --git a/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Content/audio/theme.ogg b/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Content/audio/theme.ogg new file mode 100644 index 00000000..72e1fd3b Binary files /dev/null and b/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Content/audio/theme.ogg differ diff --git a/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Content/audio/ui.wav b/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Content/audio/ui.wav new file mode 100644 index 00000000..63e8941e Binary files /dev/null and b/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Content/audio/ui.wav differ diff --git a/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Content/effects/grayscaleEffect.fx b/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Content/effects/grayscaleEffect.fx new file mode 100644 index 00000000..5dd0d8b6 --- /dev/null +++ b/Tutorials/2dShaders/src/02-Hot-Reload-System/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/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Content/fonts/04B_30.spritefont b/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Content/fonts/04B_30.spritefont new file mode 100644 index 00000000..63d4728c --- /dev/null +++ b/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Content/fonts/04B_30.spritefont @@ -0,0 +1,16 @@ + + + + 04B_30.ttf + 17.5 + 0 + true + + + + + ~ + + + + diff --git a/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Content/fonts/04B_30.ttf b/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Content/fonts/04B_30.ttf new file mode 100644 index 00000000..4b93740c Binary files /dev/null and b/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Content/fonts/04B_30.ttf differ diff --git a/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Content/fonts/04B_30_5x.spritefont b/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Content/fonts/04B_30_5x.spritefont new file mode 100644 index 00000000..dd239a53 --- /dev/null +++ b/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Content/fonts/04B_30_5x.spritefont @@ -0,0 +1,16 @@ + + + + 04B_30.ttf + 87.5 + 0 + true + + + + + ~ + + + + diff --git a/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Content/fonts/04b_30.fnt b/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Content/fonts/04b_30.fnt new file mode 100644 index 00000000..772f8c54 --- /dev/null +++ b/Tutorials/2dShaders/src/02-Hot-Reload-System/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/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Content/images/atlas-definition.xml b/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Content/images/atlas-definition.xml new file mode 100644 index 00000000..21772022 --- /dev/null +++ b/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Content/images/atlas-definition.xml @@ -0,0 +1,34 @@ + + + images/atlas + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Content/images/atlas.png b/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Content/images/atlas.png new file mode 100644 index 00000000..f7def20f Binary files /dev/null and b/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Content/images/atlas.png differ diff --git a/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Content/images/background-pattern.png b/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Content/images/background-pattern.png new file mode 100644 index 00000000..2d8d878e Binary files /dev/null and b/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Content/images/background-pattern.png differ diff --git a/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Content/images/logo.png b/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Content/images/logo.png new file mode 100644 index 00000000..1509036c Binary files /dev/null and b/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Content/images/logo.png differ diff --git a/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Content/images/tilemap-definition.xml b/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Content/images/tilemap-definition.xml new file mode 100644 index 00000000..85658c60 --- /dev/null +++ b/Tutorials/2dShaders/src/02-Hot-Reload-System/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/2dShaders/src/02-Hot-Reload-System/DungeonSlime/DungeonSlime.csproj b/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/DungeonSlime.csproj new file mode 100644 index 00000000..7f067a0d --- /dev/null +++ b/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/DungeonSlime.csproj @@ -0,0 +1,69 @@ + + + WinExe + net8.0 + Major + false + false + + + app.manifest + Icon.ico + + + bin/$(Configuration)/$(TargetFramework) + + + + + + + + Icon.ico + + + Icon.bmp + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Game1.cs b/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Game1.cs new file mode 100644 index 00000000..3ecde311 --- /dev/null +++ b/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Game1.cs @@ -0,0 +1,72 @@ +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() + { + 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/2dShaders/src/02-Hot-Reload-System/DungeonSlime/GameController.cs b/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/GameController.cs new file mode 100644 index 00000000..a85df08f --- /dev/null +++ b/Tutorials/2dShaders/src/02-Hot-Reload-System/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/2dShaders/src/02-Hot-Reload-System/DungeonSlime/GameObjects/Bat.cs b/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/GameObjects/Bat.cs new file mode 100644 index 00000000..ddc855ed --- /dev/null +++ b/Tutorials/2dShaders/src/02-Hot-Reload-System/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/2dShaders/src/02-Hot-Reload-System/DungeonSlime/GameObjects/Slime.cs b/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/GameObjects/Slime.cs new file mode 100644 index 00000000..08b5a63d --- /dev/null +++ b/Tutorials/2dShaders/src/02-Hot-Reload-System/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/2dShaders/src/02-Hot-Reload-System/DungeonSlime/GameObjects/SlimeSegment.cs b/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/GameObjects/SlimeSegment.cs new file mode 100644 index 00000000..b00189eb --- /dev/null +++ b/Tutorials/2dShaders/src/02-Hot-Reload-System/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/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Icon.bmp b/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Icon.bmp new file mode 100644 index 00000000..2b481653 Binary files /dev/null and b/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Icon.bmp differ diff --git a/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Icon.ico b/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Icon.ico new file mode 100644 index 00000000..7d9dec18 Binary files /dev/null and b/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Icon.ico differ diff --git a/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Program.cs b/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Program.cs new file mode 100644 index 00000000..b883ea93 --- /dev/null +++ b/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Program.cs @@ -0,0 +1,3 @@ +MonoGameLibrary.Content.ContentManagerExtensions.StartContentWatcherTask(); +using var game = new DungeonSlime.Game1(); +game.Run(); diff --git a/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Scenes/GameScene.cs b/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Scenes/GameScene.cs new file mode 100644 index 00000000..48a3394a --- /dev/null +++ b/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Scenes/GameScene.cs @@ -0,0 +1,432 @@ +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.Content; +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 WatchedAsset _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.Watch("effects/grayscaleEffect"); + } + + public override void Update(GameTime gameTime) + { + // Ensure the UI is always updated + _ui.Update(gameTime); + + // Update the grayscale effect if it was changed + _grayscaleEffect.TryRefresh(out _); + + 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.Asset.Parameters["Saturation"].SetValue(_saturation); + + // And begin the sprite batch using the grayscale effect. + Core.SpriteBatch.Begin(samplerState: SamplerState.PointClamp, effect: _grayscaleEffect.Asset); + } + 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/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Scenes/TitleScene.cs b/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Scenes/TitleScene.cs new file mode 100644 index 00000000..8a4dacea --- /dev/null +++ b/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/Scenes/TitleScene.cs @@ -0,0 +1,345 @@ +using System; +using DungeonSlime.UI; +using Gum.Forms.Controls; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Audio; +using Microsoft.Xna.Framework.Graphics; +using MonoGameGum; +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/2dShaders/src/02-Hot-Reload-System/DungeonSlime/UI/AnimatedButton.cs b/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/UI/AnimatedButton.cs new file mode 100644 index 00000000..4cce6ee5 --- /dev/null +++ b/Tutorials/2dShaders/src/02-Hot-Reload-System/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 by AnimationChains 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/2dShaders/src/02-Hot-Reload-System/DungeonSlime/UI/GameSceneUI.cs b/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/UI/GameSceneUI.cs new file mode 100644 index 00000000..498655c2 --- /dev/null +++ b/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/UI/GameSceneUI.cs @@ -0,0 +1,340 @@ +using System; +using Gum.DataTypes; +using Gum.Forms.Controls; +using Gum.Managers; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Audio; +using Microsoft.Xna.Framework.Content; +using MonoGameGum; +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/2dShaders/src/02-Hot-Reload-System/DungeonSlime/UI/OptionsSlider.cs b/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/UI/OptionsSlider.cs new file mode 100644 index 00000000..53d6ee94 --- /dev/null +++ b/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/UI/OptionsSlider.cs @@ -0,0 +1,253 @@ +using System; +using Gum.DataTypes; +using Gum.DataTypes.Variables; +using Gum.Forms.Controls; +using Gum.Managers; +using Microsoft.Xna.Framework; +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/2dShaders/src/02-Hot-Reload-System/DungeonSlime/app.manifest b/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/app.manifest new file mode 100644 index 00000000..caf45166 --- /dev/null +++ b/Tutorials/2dShaders/src/02-Hot-Reload-System/DungeonSlime/app.manifest @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true/pm + permonitorv2,permonitor + + + + diff --git a/Tutorials/2dShaders/src/02-Hot-Reload-System/MonoGameLibrary/Audio/AudioController.cs b/Tutorials/2dShaders/src/02-Hot-Reload-System/MonoGameLibrary/Audio/AudioController.cs new file mode 100644 index 00000000..1bffd636 --- /dev/null +++ b/Tutorials/2dShaders/src/02-Hot-Reload-System/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/2dShaders/src/02-Hot-Reload-System/MonoGameLibrary/Circle.cs b/Tutorials/2dShaders/src/02-Hot-Reload-System/MonoGameLibrary/Circle.cs new file mode 100644 index 00000000..0bb691bc --- /dev/null +++ b/Tutorials/2dShaders/src/02-Hot-Reload-System/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/2dShaders/src/02-Hot-Reload-System/MonoGameLibrary/Content/ContentManagerExtensions.cs b/Tutorials/2dShaders/src/02-Hot-Reload-System/MonoGameLibrary/Content/ContentManagerExtensions.cs new file mode 100644 index 00000000..810b7933 --- /dev/null +++ b/Tutorials/2dShaders/src/02-Hot-Reload-System/MonoGameLibrary/Content/ContentManagerExtensions.cs @@ -0,0 +1,155 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using Microsoft.Xna.Framework.Content; + +namespace MonoGameLibrary.Content; + +public static class ContentManagerExtensions +{ + /// + /// Check if the given xnb file has a newer write-time than the last loaded version of the asset. + /// If the local file has been updated, reload the asset and return true. + /// + /// The that loaded the asset originally + /// The asset that will be reloaded if the xnb file is newer + /// If the asset has been reloaded, this out parameter will be set to the previous version of the asset before the newer version was loaded. + /// + /// true when asset was reloaded; false otherwise. + /// + public static bool TryRefresh(this ContentManager manager, WatchedAsset watchedAsset, out T oldAsset) + { + oldAsset = default; + + if (manager != watchedAsset.Owner) + throw new ArgumentException($"Used the wrong ContentManager to refresh {watchedAsset.AssetName}"); + + var path = Path.Combine(manager.RootDirectory, watchedAsset.AssetName) + ".xnb"; + var lastWriteTime = File.GetLastWriteTime(path); + + if (lastWriteTime <= watchedAsset.UpdatedAt) + { + return false; + } + + if (IsFileLocked(path)) return false; // wait for the file to not be locked. + + manager.UnloadAsset(watchedAsset.AssetName); + oldAsset = watchedAsset.Asset; + watchedAsset.Asset = manager.Load(watchedAsset.AssetName); + watchedAsset.UpdatedAt = lastWriteTime; + + return true; + } + + private static bool IsFileLocked(string path) + { + try + { + using FileStream _ = new FileStream(path, FileMode.Open, FileAccess.ReadWrite, FileShare.None); + // File is not locked + return false; + } + catch (IOException) + { + // File is locked or inaccessible + return true; + } + } + + /// + /// Load an asset and wrap it with the metadata required to refresh it later using the function + /// + /// + /// + /// + /// + public static WatchedAsset Watch(this ContentManager manager, string assetName) + { + var asset = manager.Load(assetName); + return new WatchedAsset + { + AssetName = assetName, + Asset = asset, + UpdatedAt = DateTimeOffset.Now, + Owner = manager + }; + } + + [Conditional("DEBUG")] + public static void StartContentWatcherTask() + { + var args = Environment.GetCommandLineArgs(); + foreach (var arg in args) + { + // if the application was started with the --no-reload option, then do not start the watcher. + if (arg == "--no-reload") return; + } + + // identify the project directory + var projectFile = Assembly.GetEntryAssembly().GetName().Name + ".csproj"; + var current = Directory.GetCurrentDirectory(); + string projectDirectory = null; + + while (current != null && projectDirectory == null) + { + if (File.Exists(Path.Combine(current, projectFile))) + { + // the valid project csproj exists in the directory + projectDirectory = current; + } + else + { + // try looking in the parent directory. + // When there is no parent directory, the variable becomes 'null' + current = Path.GetDirectoryName(current); + } + } + + // if no valid project was identified, then it is impossible to start the watcher + if (string.IsNullOrEmpty(projectDirectory)) return; + + // start the watcher process + var process = Process.Start(new ProcessStartInfo + { + FileName = "dotnet", + Arguments = "build -t:WatchContent --tl:off", + WorkingDirectory = projectDirectory, + WindowStyle = ProcessWindowStyle.Normal, + UseShellExecute = false, + CreateNoWindow = false + }); + + // when this program exits, make sure to emit a kill signal to the watcher process + AppDomain.CurrentDomain.ProcessExit += (_, __) => + { + try + { + if (!process.HasExited) + { + process.Kill(entireProcessTree: true); + } + } + catch + { + /* ignore */ + } + }; + AppDomain.CurrentDomain.UnhandledException += (sender, e) => + { + try + { + if (!process.HasExited) + { + process.Kill(entireProcessTree: true); + } + } + catch + { + /* ignore */ + } + }; + } + +} \ No newline at end of file diff --git a/Tutorials/2dShaders/src/02-Hot-Reload-System/MonoGameLibrary/Content/WatchedAsset.cs b/Tutorials/2dShaders/src/02-Hot-Reload-System/MonoGameLibrary/Content/WatchedAsset.cs new file mode 100644 index 00000000..39008666 --- /dev/null +++ b/Tutorials/2dShaders/src/02-Hot-Reload-System/MonoGameLibrary/Content/WatchedAsset.cs @@ -0,0 +1,33 @@ +using System; +using Microsoft.Xna.Framework.Content; + +namespace MonoGameLibrary.Content; + +public class WatchedAsset +{ + /// + /// The latest version of the asset. + /// + public T Asset { get; set; } + + /// + /// The last time the was loaded into memory. + /// + public DateTimeOffset UpdatedAt { get; set; } + + /// + /// The name of the . This is the name used to load the asset from disk. + /// + public string AssetName { get; init; } + + /// + /// The instance that loaded the asset. + /// + public ContentManager Owner { get; init; } + + + public bool TryRefresh(out T oldAsset) + { + return Owner.TryRefresh(this, out oldAsset); + } +} \ No newline at end of file diff --git a/Tutorials/2dShaders/src/02-Hot-Reload-System/MonoGameLibrary/Core.cs b/Tutorials/2dShaders/src/02-Hot-Reload-System/MonoGameLibrary/Core.cs new file mode 100644 index 00000000..d83c54fe --- /dev/null +++ b/Tutorials/2dShaders/src/02-Hot-Reload-System/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/2dShaders/src/02-Hot-Reload-System/MonoGameLibrary/Graphics/AnimatedSprite.cs b/Tutorials/2dShaders/src/02-Hot-Reload-System/MonoGameLibrary/Graphics/AnimatedSprite.cs new file mode 100644 index 00000000..a1a3594e --- /dev/null +++ b/Tutorials/2dShaders/src/02-Hot-Reload-System/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/2dShaders/src/02-Hot-Reload-System/MonoGameLibrary/Graphics/Animation.cs b/Tutorials/2dShaders/src/02-Hot-Reload-System/MonoGameLibrary/Graphics/Animation.cs new file mode 100644 index 00000000..44d61b65 --- /dev/null +++ b/Tutorials/2dShaders/src/02-Hot-Reload-System/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/2dShaders/src/02-Hot-Reload-System/MonoGameLibrary/Graphics/Sprite.cs b/Tutorials/2dShaders/src/02-Hot-Reload-System/MonoGameLibrary/Graphics/Sprite.cs new file mode 100644 index 00000000..20c44f0b --- /dev/null +++ b/Tutorials/2dShaders/src/02-Hot-Reload-System/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/2dShaders/src/02-Hot-Reload-System/MonoGameLibrary/Graphics/TextureAtlas.cs b/Tutorials/2dShaders/src/02-Hot-Reload-System/MonoGameLibrary/Graphics/TextureAtlas.cs new file mode 100644 index 00000000..e48c9abd --- /dev/null +++ b/Tutorials/2dShaders/src/02-Hot-Reload-System/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/2dShaders/src/02-Hot-Reload-System/MonoGameLibrary/Graphics/TextureRegion.cs b/Tutorials/2dShaders/src/02-Hot-Reload-System/MonoGameLibrary/Graphics/TextureRegion.cs new file mode 100644 index 00000000..ecd69030 --- /dev/null +++ b/Tutorials/2dShaders/src/02-Hot-Reload-System/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/2dShaders/src/02-Hot-Reload-System/MonoGameLibrary/Graphics/Tilemap.cs b/Tutorials/2dShaders/src/02-Hot-Reload-System/MonoGameLibrary/Graphics/Tilemap.cs new file mode 100644 index 00000000..96e1ee5e --- /dev/null +++ b/Tutorials/2dShaders/src/02-Hot-Reload-System/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/2dShaders/src/02-Hot-Reload-System/MonoGameLibrary/Graphics/Tileset.cs b/Tutorials/2dShaders/src/02-Hot-Reload-System/MonoGameLibrary/Graphics/Tileset.cs new file mode 100644 index 00000000..80c2e65a --- /dev/null +++ b/Tutorials/2dShaders/src/02-Hot-Reload-System/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/2dShaders/src/02-Hot-Reload-System/MonoGameLibrary/Input/GamePadInfo.cs b/Tutorials/2dShaders/src/02-Hot-Reload-System/MonoGameLibrary/Input/GamePadInfo.cs new file mode 100644 index 00000000..7fd16126 --- /dev/null +++ b/Tutorials/2dShaders/src/02-Hot-Reload-System/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/2dShaders/src/02-Hot-Reload-System/MonoGameLibrary/Input/InputManager.cs b/Tutorials/2dShaders/src/02-Hot-Reload-System/MonoGameLibrary/Input/InputManager.cs new file mode 100644 index 00000000..1790eb70 --- /dev/null +++ b/Tutorials/2dShaders/src/02-Hot-Reload-System/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/2dShaders/src/02-Hot-Reload-System/MonoGameLibrary/Input/KeyboardInfo.cs b/Tutorials/2dShaders/src/02-Hot-Reload-System/MonoGameLibrary/Input/KeyboardInfo.cs new file mode 100644 index 00000000..c6770cb0 --- /dev/null +++ b/Tutorials/2dShaders/src/02-Hot-Reload-System/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/2dShaders/src/02-Hot-Reload-System/MonoGameLibrary/Input/MouseButton.cs b/Tutorials/2dShaders/src/02-Hot-Reload-System/MonoGameLibrary/Input/MouseButton.cs new file mode 100644 index 00000000..5b041f80 --- /dev/null +++ b/Tutorials/2dShaders/src/02-Hot-Reload-System/MonoGameLibrary/Input/MouseButton.cs @@ -0,0 +1,10 @@ +namespace MonoGameLibrary.Input; + +public enum MouseButton +{ + Left, + Middle, + Right, + XButton1, + XButton2 +} diff --git a/Tutorials/2dShaders/src/02-Hot-Reload-System/MonoGameLibrary/Input/MouseInfo.cs b/Tutorials/2dShaders/src/02-Hot-Reload-System/MonoGameLibrary/Input/MouseInfo.cs new file mode 100644 index 00000000..09d6207c --- /dev/null +++ b/Tutorials/2dShaders/src/02-Hot-Reload-System/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/2dShaders/src/02-Hot-Reload-System/MonoGameLibrary/MonoGameLibrary.csproj b/Tutorials/2dShaders/src/02-Hot-Reload-System/MonoGameLibrary/MonoGameLibrary.csproj new file mode 100644 index 00000000..d4941464 --- /dev/null +++ b/Tutorials/2dShaders/src/02-Hot-Reload-System/MonoGameLibrary/MonoGameLibrary.csproj @@ -0,0 +1,10 @@ + + + net8.0 + + + + All + + + \ No newline at end of file diff --git a/Tutorials/2dShaders/src/02-Hot-Reload-System/MonoGameLibrary/Scenes/Scene.cs b/Tutorials/2dShaders/src/02-Hot-Reload-System/MonoGameLibrary/Scenes/Scene.cs new file mode 100644 index 00000000..627d220f --- /dev/null +++ b/Tutorials/2dShaders/src/02-Hot-Reload-System/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/2dShaders/src/03-The-Material-Class/DungeonSlime.sln b/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime.sln new file mode 100644 index 00000000..077462d5 --- /dev/null +++ b/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DungeonSlime", "DungeonSlime\DungeonSlime.csproj", "{88BD3FF9-D3D6-4680-A274-F866E0DFCAC4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MonoGameLibrary", "MonoGameLibrary\MonoGameLibrary.csproj", "{AB85CEEE-6D97-4438-AEC4-797D2806F44A}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {88BD3FF9-D3D6-4680-A274-F866E0DFCAC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {88BD3FF9-D3D6-4680-A274-F866E0DFCAC4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {88BD3FF9-D3D6-4680-A274-F866E0DFCAC4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {88BD3FF9-D3D6-4680-A274-F866E0DFCAC4}.Release|Any CPU.Build.0 = Release|Any CPU + {AB85CEEE-6D97-4438-AEC4-797D2806F44A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AB85CEEE-6D97-4438-AEC4-797D2806F44A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AB85CEEE-6D97-4438-AEC4-797D2806F44A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AB85CEEE-6D97-4438-AEC4-797D2806F44A}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/.config/dotnet-tools.json b/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/.config/dotnet-tools.json new file mode 100644 index 00000000..afd4e2c4 --- /dev/null +++ b/Tutorials/2dShaders/src/03-The-Material-Class/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/2dShaders/src/03-The-Material-Class/DungeonSlime/Content/Content.mgcb b/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/Content/Content.mgcb new file mode 100644 index 00000000..d26ea4f1 --- /dev/null +++ b/Tutorials/2dShaders/src/03-The-Material-Class/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/2dShaders/src/03-The-Material-Class/DungeonSlime/Content/audio/bounce.wav b/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/Content/audio/bounce.wav new file mode 100644 index 00000000..baa7a47b Binary files /dev/null and b/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/Content/audio/bounce.wav differ diff --git a/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/Content/audio/collect.wav b/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/Content/audio/collect.wav new file mode 100644 index 00000000..506220de Binary files /dev/null and b/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/Content/audio/collect.wav differ diff --git a/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/Content/audio/theme.ogg b/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/Content/audio/theme.ogg new file mode 100644 index 00000000..72e1fd3b Binary files /dev/null and b/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/Content/audio/theme.ogg differ diff --git a/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/Content/audio/ui.wav b/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/Content/audio/ui.wav new file mode 100644 index 00000000..63e8941e Binary files /dev/null and b/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/Content/audio/ui.wav differ diff --git a/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/Content/effects/grayscaleEffect.fx b/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/Content/effects/grayscaleEffect.fx new file mode 100644 index 00000000..5dd0d8b6 --- /dev/null +++ b/Tutorials/2dShaders/src/03-The-Material-Class/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/2dShaders/src/03-The-Material-Class/DungeonSlime/Content/fonts/04B_30.spritefont b/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/Content/fonts/04B_30.spritefont new file mode 100644 index 00000000..63d4728c --- /dev/null +++ b/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/Content/fonts/04B_30.spritefont @@ -0,0 +1,16 @@ + + + + 04B_30.ttf + 17.5 + 0 + true + + + + + ~ + + + + diff --git a/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/Content/fonts/04B_30.ttf b/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/Content/fonts/04B_30.ttf new file mode 100644 index 00000000..4b93740c Binary files /dev/null and b/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/Content/fonts/04B_30.ttf differ diff --git a/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/Content/fonts/04B_30_5x.spritefont b/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/Content/fonts/04B_30_5x.spritefont new file mode 100644 index 00000000..dd239a53 --- /dev/null +++ b/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/Content/fonts/04B_30_5x.spritefont @@ -0,0 +1,16 @@ + + + + 04B_30.ttf + 87.5 + 0 + true + + + + + ~ + + + + diff --git a/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/Content/fonts/04b_30.fnt b/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/Content/fonts/04b_30.fnt new file mode 100644 index 00000000..772f8c54 --- /dev/null +++ b/Tutorials/2dShaders/src/03-The-Material-Class/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/2dShaders/src/03-The-Material-Class/DungeonSlime/Content/images/atlas-definition.xml b/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/Content/images/atlas-definition.xml new file mode 100644 index 00000000..21772022 --- /dev/null +++ b/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/Content/images/atlas-definition.xml @@ -0,0 +1,34 @@ + + + images/atlas + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/Content/images/atlas.png b/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/Content/images/atlas.png new file mode 100644 index 00000000..f7def20f Binary files /dev/null and b/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/Content/images/atlas.png differ diff --git a/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/Content/images/background-pattern.png b/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/Content/images/background-pattern.png new file mode 100644 index 00000000..2d8d878e Binary files /dev/null and b/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/Content/images/background-pattern.png differ diff --git a/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/Content/images/logo.png b/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/Content/images/logo.png new file mode 100644 index 00000000..1509036c Binary files /dev/null and b/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/Content/images/logo.png differ diff --git a/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/Content/images/tilemap-definition.xml b/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/Content/images/tilemap-definition.xml new file mode 100644 index 00000000..85658c60 --- /dev/null +++ b/Tutorials/2dShaders/src/03-The-Material-Class/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/2dShaders/src/03-The-Material-Class/DungeonSlime/DungeonSlime.csproj b/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/DungeonSlime.csproj new file mode 100644 index 00000000..7f067a0d --- /dev/null +++ b/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/DungeonSlime.csproj @@ -0,0 +1,69 @@ + + + WinExe + net8.0 + Major + false + false + + + app.manifest + Icon.ico + + + bin/$(Configuration)/$(TargetFramework) + + + + + + + + Icon.ico + + + Icon.bmp + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/Game1.cs b/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/Game1.cs new file mode 100644 index 00000000..3d6d44ec --- /dev/null +++ b/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/Game1.cs @@ -0,0 +1,71 @@ +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/2dShaders/src/03-The-Material-Class/DungeonSlime/GameController.cs b/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/GameController.cs new file mode 100644 index 00000000..a85df08f --- /dev/null +++ b/Tutorials/2dShaders/src/03-The-Material-Class/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/2dShaders/src/03-The-Material-Class/DungeonSlime/GameObjects/Bat.cs b/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/GameObjects/Bat.cs new file mode 100644 index 00000000..ddc855ed --- /dev/null +++ b/Tutorials/2dShaders/src/03-The-Material-Class/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/2dShaders/src/03-The-Material-Class/DungeonSlime/GameObjects/Slime.cs b/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/GameObjects/Slime.cs new file mode 100644 index 00000000..08b5a63d --- /dev/null +++ b/Tutorials/2dShaders/src/03-The-Material-Class/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/2dShaders/src/03-The-Material-Class/DungeonSlime/GameObjects/SlimeSegment.cs b/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/GameObjects/SlimeSegment.cs new file mode 100644 index 00000000..b00189eb --- /dev/null +++ b/Tutorials/2dShaders/src/03-The-Material-Class/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/2dShaders/src/03-The-Material-Class/DungeonSlime/Icon.bmp b/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/Icon.bmp new file mode 100644 index 00000000..2b481653 Binary files /dev/null and b/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/Icon.bmp differ diff --git a/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/Icon.ico b/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/Icon.ico new file mode 100644 index 00000000..7d9dec18 Binary files /dev/null and b/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/Icon.ico differ diff --git a/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/Program.cs b/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/Program.cs new file mode 100644 index 00000000..4d9be314 --- /dev/null +++ b/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/Program.cs @@ -0,0 +1,3 @@ +MonoGameLibrary.Content.ContentManagerExtensions.StartContentWatcherTask(); +using var game = new DungeonSlime.Game1(); +game.Run(); \ No newline at end of file diff --git a/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/Scenes/GameScene.cs b/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/Scenes/GameScene.cs new file mode 100644 index 00000000..58bade7e --- /dev/null +++ b/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/Scenes/GameScene.cs @@ -0,0 +1,432 @@ +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.Content; +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 Material _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.WatchMaterial("effects/grayscaleEffect"); + } + + public override void Update(GameTime gameTime) + { + // Ensure the UI is always updated + _ui.Update(gameTime); + + // Update the grayscale effect if it was changed + _grayscaleEffect.Update(); + + 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.SetParameter("Saturation", _saturation); + + // And begin the sprite batch using the grayscale effect. + Core.SpriteBatch.Begin(samplerState: SamplerState.PointClamp, effect: _grayscaleEffect.Effect); + } + 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/2dShaders/src/03-The-Material-Class/DungeonSlime/Scenes/TitleScene.cs b/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/Scenes/TitleScene.cs new file mode 100644 index 00000000..8a4dacea --- /dev/null +++ b/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/Scenes/TitleScene.cs @@ -0,0 +1,345 @@ +using System; +using DungeonSlime.UI; +using Gum.Forms.Controls; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Audio; +using Microsoft.Xna.Framework.Graphics; +using MonoGameGum; +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/2dShaders/src/03-The-Material-Class/DungeonSlime/UI/AnimatedButton.cs b/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/UI/AnimatedButton.cs new file mode 100644 index 00000000..4cce6ee5 --- /dev/null +++ b/Tutorials/2dShaders/src/03-The-Material-Class/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 by AnimationChains 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/2dShaders/src/03-The-Material-Class/DungeonSlime/UI/GameSceneUI.cs b/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/UI/GameSceneUI.cs new file mode 100644 index 00000000..498655c2 --- /dev/null +++ b/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/UI/GameSceneUI.cs @@ -0,0 +1,340 @@ +using System; +using Gum.DataTypes; +using Gum.Forms.Controls; +using Gum.Managers; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Audio; +using Microsoft.Xna.Framework.Content; +using MonoGameGum; +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/2dShaders/src/03-The-Material-Class/DungeonSlime/UI/OptionsSlider.cs b/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/UI/OptionsSlider.cs new file mode 100644 index 00000000..53d6ee94 --- /dev/null +++ b/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/UI/OptionsSlider.cs @@ -0,0 +1,253 @@ +using System; +using Gum.DataTypes; +using Gum.DataTypes.Variables; +using Gum.Forms.Controls; +using Gum.Managers; +using Microsoft.Xna.Framework; +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/2dShaders/src/03-The-Material-Class/DungeonSlime/app.manifest b/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/app.manifest new file mode 100644 index 00000000..caf45166 --- /dev/null +++ b/Tutorials/2dShaders/src/03-The-Material-Class/DungeonSlime/app.manifest @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true/pm + permonitorv2,permonitor + + + + diff --git a/Tutorials/2dShaders/src/03-The-Material-Class/MonoGameLibrary/Audio/AudioController.cs b/Tutorials/2dShaders/src/03-The-Material-Class/MonoGameLibrary/Audio/AudioController.cs new file mode 100644 index 00000000..1bffd636 --- /dev/null +++ b/Tutorials/2dShaders/src/03-The-Material-Class/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/2dShaders/src/03-The-Material-Class/MonoGameLibrary/Circle.cs b/Tutorials/2dShaders/src/03-The-Material-Class/MonoGameLibrary/Circle.cs new file mode 100644 index 00000000..0bb691bc --- /dev/null +++ b/Tutorials/2dShaders/src/03-The-Material-Class/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/2dShaders/src/03-The-Material-Class/MonoGameLibrary/Content/ContentManagerExtensions.cs b/Tutorials/2dShaders/src/03-The-Material-Class/MonoGameLibrary/Content/ContentManagerExtensions.cs new file mode 100644 index 00000000..e012836c --- /dev/null +++ b/Tutorials/2dShaders/src/03-The-Material-Class/MonoGameLibrary/Content/ContentManagerExtensions.cs @@ -0,0 +1,168 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; +using MonoGameLibrary.Graphics; + +namespace MonoGameLibrary.Content; + +public static class ContentManagerExtensions +{ + /// + /// Check if the given xnb file has a newer write-time than the last loaded version of the asset. + /// If the local file has been updated, reload the asset and return true. + /// + /// The that loaded the asset originally + /// The asset that will be reloaded if the xnb file is newer + /// If the asset has been reloaded, this out parameter will be set to the previous version of the asset before the newer version was loaded. + /// + /// true when asset was reloaded; false otherwise. + /// + public static bool TryRefresh(this ContentManager manager, WatchedAsset watchedAsset, out T oldAsset) + { + oldAsset = default; + + if (manager != watchedAsset.Owner) + throw new ArgumentException($"Used the wrong ContentManager to refresh {watchedAsset.AssetName}"); + + var path = Path.Combine(manager.RootDirectory, watchedAsset.AssetName) + ".xnb"; + var lastWriteTime = File.GetLastWriteTime(path); + + if (lastWriteTime <= watchedAsset.UpdatedAt) + { + return false; + } + + if (IsFileLocked(path)) return false; // wait for the file to not be locked. + + manager.UnloadAsset(watchedAsset.AssetName); + oldAsset = watchedAsset.Asset; + watchedAsset.Asset = manager.Load(watchedAsset.AssetName); + watchedAsset.UpdatedAt = lastWriteTime; + + return true; + } + + private static bool IsFileLocked(string path) + { + try + { + using FileStream _ = new FileStream(path, FileMode.Open, FileAccess.ReadWrite, FileShare.None); + // File is not locked + return false; + } + catch (IOException) + { + // File is locked or inaccessible + return true; + } + } + + /// + /// Load an asset and wrap it with the metadata required to refresh it later using the function + /// + /// + /// + /// + /// + public static WatchedAsset Watch(this ContentManager manager, string assetName) + { + var asset = manager.Load(assetName); + return new WatchedAsset + { + AssetName = assetName, + Asset = asset, + UpdatedAt = DateTimeOffset.Now, + Owner = manager + }; + } + + /// + /// Load an Effect into the wrapper class + /// + /// + /// + /// + public static Material WatchMaterial(this ContentManager manager, string assetName) + { + return new Material(manager.Watch(assetName)); + } + + + [Conditional("DEBUG")] + public static void StartContentWatcherTask() + { + var args = Environment.GetCommandLineArgs(); + foreach (var arg in args) + { + // if the application was started with the --no-reload option, then do not start the watcher. + if (arg == "--no-reload") return; + } + + // identify the project directory + var projectFile = Assembly.GetEntryAssembly().GetName().Name + ".csproj"; + var current = Directory.GetCurrentDirectory(); + string projectDirectory = null; + + while (current != null && projectDirectory == null) + { + if (File.Exists(Path.Combine(current, projectFile))) + { + // the valid project csproj exists in the directory + projectDirectory = current; + } + else + { + // try looking in the parent directory. + // When there is no parent directory, the variable becomes 'null' + current = Path.GetDirectoryName(current); + } + } + + // if no valid project was identified, then it is impossible to start the watcher + if (string.IsNullOrEmpty(projectDirectory)) return; + + // start the watcher process + var process = Process.Start(new ProcessStartInfo + { + FileName = "dotnet", + Arguments = "build -t:WatchContent --tl:off", + WorkingDirectory = projectDirectory, + WindowStyle = ProcessWindowStyle.Normal, + UseShellExecute = false, + CreateNoWindow = false + }); + + // when this program exits, make sure to emit a kill signal to the watcher process + AppDomain.CurrentDomain.ProcessExit += (_, __) => + { + try + { + if (!process.HasExited) + { + process.Kill(entireProcessTree: true); + } + } + catch + { + /* ignore */ + } + }; + AppDomain.CurrentDomain.UnhandledException += (sender, e) => + { + try + { + if (!process.HasExited) + { + process.Kill(entireProcessTree: true); + } + } + catch + { + /* ignore */ + } + }; + } +} \ No newline at end of file diff --git a/Tutorials/2dShaders/src/03-The-Material-Class/MonoGameLibrary/Content/WatchedAsset.cs b/Tutorials/2dShaders/src/03-The-Material-Class/MonoGameLibrary/Content/WatchedAsset.cs new file mode 100644 index 00000000..39008666 --- /dev/null +++ b/Tutorials/2dShaders/src/03-The-Material-Class/MonoGameLibrary/Content/WatchedAsset.cs @@ -0,0 +1,33 @@ +using System; +using Microsoft.Xna.Framework.Content; + +namespace MonoGameLibrary.Content; + +public class WatchedAsset +{ + /// + /// The latest version of the asset. + /// + public T Asset { get; set; } + + /// + /// The last time the was loaded into memory. + /// + public DateTimeOffset UpdatedAt { get; set; } + + /// + /// The name of the . This is the name used to load the asset from disk. + /// + public string AssetName { get; init; } + + /// + /// The instance that loaded the asset. + /// + public ContentManager Owner { get; init; } + + + public bool TryRefresh(out T oldAsset) + { + return Owner.TryRefresh(this, out oldAsset); + } +} \ No newline at end of file diff --git a/Tutorials/2dShaders/src/03-The-Material-Class/MonoGameLibrary/Core.cs b/Tutorials/2dShaders/src/03-The-Material-Class/MonoGameLibrary/Core.cs new file mode 100644 index 00000000..d83c54fe --- /dev/null +++ b/Tutorials/2dShaders/src/03-The-Material-Class/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/2dShaders/src/03-The-Material-Class/MonoGameLibrary/Graphics/AnimatedSprite.cs b/Tutorials/2dShaders/src/03-The-Material-Class/MonoGameLibrary/Graphics/AnimatedSprite.cs new file mode 100644 index 00000000..a1a3594e --- /dev/null +++ b/Tutorials/2dShaders/src/03-The-Material-Class/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/2dShaders/src/03-The-Material-Class/MonoGameLibrary/Graphics/Animation.cs b/Tutorials/2dShaders/src/03-The-Material-Class/MonoGameLibrary/Graphics/Animation.cs new file mode 100644 index 00000000..44d61b65 --- /dev/null +++ b/Tutorials/2dShaders/src/03-The-Material-Class/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/2dShaders/src/03-The-Material-Class/MonoGameLibrary/Graphics/Material.cs b/Tutorials/2dShaders/src/03-The-Material-Class/MonoGameLibrary/Graphics/Material.cs new file mode 100644 index 00000000..9624f14a --- /dev/null +++ b/Tutorials/2dShaders/src/03-The-Material-Class/MonoGameLibrary/Graphics/Material.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using MonoGameLibrary.Content; +namespace MonoGameLibrary.Graphics; + +public class Material +{ + /// + /// The hot-reloadable asset that this material is using + /// + public WatchedAsset Asset; + + /// + /// A cached version of the parameters available in the shader + /// + public Dictionary ParameterMap; + + /// + /// The currently loaded Effect that this material is using + /// + public Effect Effect => Asset.Asset; + + public Material(WatchedAsset asset) + { + Asset = asset; + UpdateParameterCache(); + } + + public void SetParameter(string name, float value) + { + if (TryGetParameter(name, out var parameter)) + { + parameter.SetValue(value); + } + else + { + Console.WriteLine($"Warning: cannot set shader parameter=[{name}] because it does not exist in the compiled shader=[{Asset.AssetName}]"); + } + } + + public void SetParameter(string name, Matrix value) + { + if (TryGetParameter(name, out var parameter)) + { + parameter.SetValue(value); + } + else + { + Console.WriteLine($"Warning: cannot set shader parameter=[{name}] because it does not exist in the compiled shader=[{Asset.AssetName}]"); + } + } + + public void SetParameter(string name, Vector2 value) + { + if (TryGetParameter(name, out var parameter)) + { + parameter.SetValue(value); + } + else + { + Console.WriteLine($"Warning: cannot set shader parameter=[{name}] because it does not exist in the compiled shader=[{Asset.AssetName}]"); + } + } + + public void SetParameter(string name, Texture2D value) + { + if (TryGetParameter(name, out var parameter)) + { + parameter.SetValue(value); + } + else + { + Console.WriteLine($"Warning: cannot set shader parameter=[{name}] because it does not exist in the compiled shader=[{Asset.AssetName}]"); + } + } + + /// + /// Check if the given parameter name is available in the compiled shader code. + /// Remember that a parameter will be optimized out of a shader if it is not being used + /// in the shader's return value. + /// + /// + /// + /// + public bool TryGetParameter(string name, out EffectParameter parameter) + { + return ParameterMap.TryGetValue(name, out parameter); + } + + /// + /// Rebuild the based on the current parameters available in the effect instance + /// + public void UpdateParameterCache() + { + ParameterMap = Effect.Parameters.ToDictionary(p => p.Name); + } + + [Conditional("DEBUG")] + public void Update() + { + if (Asset.TryRefresh(out var oldAsset)) + { + UpdateParameterCache(); + + foreach (var oldParam in oldAsset.Parameters) + { + if (!TryGetParameter(oldParam.Name, out var newParam)) + { + continue; + } + + switch (oldParam.ParameterClass) + { + case EffectParameterClass.Scalar: + newParam.SetValue(oldParam.GetValueSingle()); + break; + case EffectParameterClass.Matrix: + newParam.SetValue(oldParam.GetValueMatrix()); + break; + case EffectParameterClass.Vector when oldParam.ColumnCount == 2: // float2 + newParam.SetValue(oldParam.GetValueVector2()); + break; + case EffectParameterClass.Object: + newParam.SetValue(oldParam.GetValueTexture2D()); + break; + default: + Console.WriteLine("Warning: shader reload system was not able to re-apply property. " + + $"shader=[{Effect.Name}] " + + $"property=[{oldParam.Name}] " + + $"class=[{oldParam.ParameterClass}]"); + break; + } + } + } + } +} \ No newline at end of file diff --git a/Tutorials/2dShaders/src/03-The-Material-Class/MonoGameLibrary/Graphics/Sprite.cs b/Tutorials/2dShaders/src/03-The-Material-Class/MonoGameLibrary/Graphics/Sprite.cs new file mode 100644 index 00000000..20c44f0b --- /dev/null +++ b/Tutorials/2dShaders/src/03-The-Material-Class/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/2dShaders/src/03-The-Material-Class/MonoGameLibrary/Graphics/TextureAtlas.cs b/Tutorials/2dShaders/src/03-The-Material-Class/MonoGameLibrary/Graphics/TextureAtlas.cs new file mode 100644 index 00000000..e48c9abd --- /dev/null +++ b/Tutorials/2dShaders/src/03-The-Material-Class/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/2dShaders/src/03-The-Material-Class/MonoGameLibrary/Graphics/TextureRegion.cs b/Tutorials/2dShaders/src/03-The-Material-Class/MonoGameLibrary/Graphics/TextureRegion.cs new file mode 100644 index 00000000..ecd69030 --- /dev/null +++ b/Tutorials/2dShaders/src/03-The-Material-Class/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/2dShaders/src/03-The-Material-Class/MonoGameLibrary/Graphics/Tilemap.cs b/Tutorials/2dShaders/src/03-The-Material-Class/MonoGameLibrary/Graphics/Tilemap.cs new file mode 100644 index 00000000..96e1ee5e --- /dev/null +++ b/Tutorials/2dShaders/src/03-The-Material-Class/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/2dShaders/src/03-The-Material-Class/MonoGameLibrary/Graphics/Tileset.cs b/Tutorials/2dShaders/src/03-The-Material-Class/MonoGameLibrary/Graphics/Tileset.cs new file mode 100644 index 00000000..80c2e65a --- /dev/null +++ b/Tutorials/2dShaders/src/03-The-Material-Class/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/2dShaders/src/03-The-Material-Class/MonoGameLibrary/Input/GamePadInfo.cs b/Tutorials/2dShaders/src/03-The-Material-Class/MonoGameLibrary/Input/GamePadInfo.cs new file mode 100644 index 00000000..7fd16126 --- /dev/null +++ b/Tutorials/2dShaders/src/03-The-Material-Class/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/2dShaders/src/03-The-Material-Class/MonoGameLibrary/Input/InputManager.cs b/Tutorials/2dShaders/src/03-The-Material-Class/MonoGameLibrary/Input/InputManager.cs new file mode 100644 index 00000000..1790eb70 --- /dev/null +++ b/Tutorials/2dShaders/src/03-The-Material-Class/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/2dShaders/src/03-The-Material-Class/MonoGameLibrary/Input/KeyboardInfo.cs b/Tutorials/2dShaders/src/03-The-Material-Class/MonoGameLibrary/Input/KeyboardInfo.cs new file mode 100644 index 00000000..c6770cb0 --- /dev/null +++ b/Tutorials/2dShaders/src/03-The-Material-Class/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/2dShaders/src/03-The-Material-Class/MonoGameLibrary/Input/MouseButton.cs b/Tutorials/2dShaders/src/03-The-Material-Class/MonoGameLibrary/Input/MouseButton.cs new file mode 100644 index 00000000..5b041f80 --- /dev/null +++ b/Tutorials/2dShaders/src/03-The-Material-Class/MonoGameLibrary/Input/MouseButton.cs @@ -0,0 +1,10 @@ +namespace MonoGameLibrary.Input; + +public enum MouseButton +{ + Left, + Middle, + Right, + XButton1, + XButton2 +} diff --git a/Tutorials/2dShaders/src/03-The-Material-Class/MonoGameLibrary/Input/MouseInfo.cs b/Tutorials/2dShaders/src/03-The-Material-Class/MonoGameLibrary/Input/MouseInfo.cs new file mode 100644 index 00000000..09d6207c --- /dev/null +++ b/Tutorials/2dShaders/src/03-The-Material-Class/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/2dShaders/src/03-The-Material-Class/MonoGameLibrary/MonoGameLibrary.csproj b/Tutorials/2dShaders/src/03-The-Material-Class/MonoGameLibrary/MonoGameLibrary.csproj new file mode 100644 index 00000000..d4941464 --- /dev/null +++ b/Tutorials/2dShaders/src/03-The-Material-Class/MonoGameLibrary/MonoGameLibrary.csproj @@ -0,0 +1,10 @@ + + + net8.0 + + + + All + + + \ No newline at end of file diff --git a/Tutorials/2dShaders/src/03-The-Material-Class/MonoGameLibrary/Scenes/Scene.cs b/Tutorials/2dShaders/src/03-The-Material-Class/MonoGameLibrary/Scenes/Scene.cs new file mode 100644 index 00000000..627d220f --- /dev/null +++ b/Tutorials/2dShaders/src/03-The-Material-Class/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/2dShaders/src/04-Debug-UI/DungeonSlime.sln b/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime.sln new file mode 100644 index 00000000..077462d5 --- /dev/null +++ b/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DungeonSlime", "DungeonSlime\DungeonSlime.csproj", "{88BD3FF9-D3D6-4680-A274-F866E0DFCAC4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MonoGameLibrary", "MonoGameLibrary\MonoGameLibrary.csproj", "{AB85CEEE-6D97-4438-AEC4-797D2806F44A}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {88BD3FF9-D3D6-4680-A274-F866E0DFCAC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {88BD3FF9-D3D6-4680-A274-F866E0DFCAC4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {88BD3FF9-D3D6-4680-A274-F866E0DFCAC4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {88BD3FF9-D3D6-4680-A274-F866E0DFCAC4}.Release|Any CPU.Build.0 = Release|Any CPU + {AB85CEEE-6D97-4438-AEC4-797D2806F44A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AB85CEEE-6D97-4438-AEC4-797D2806F44A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AB85CEEE-6D97-4438-AEC4-797D2806F44A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AB85CEEE-6D97-4438-AEC4-797D2806F44A}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/.config/dotnet-tools.json b/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/.config/dotnet-tools.json new file mode 100644 index 00000000..afd4e2c4 --- /dev/null +++ b/Tutorials/2dShaders/src/04-Debug-UI/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/2dShaders/src/04-Debug-UI/DungeonSlime/Content/Content.mgcb b/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/Content/Content.mgcb new file mode 100644 index 00000000..d26ea4f1 --- /dev/null +++ b/Tutorials/2dShaders/src/04-Debug-UI/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/2dShaders/src/04-Debug-UI/DungeonSlime/Content/audio/bounce.wav b/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/Content/audio/bounce.wav new file mode 100644 index 00000000..baa7a47b Binary files /dev/null and b/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/Content/audio/bounce.wav differ diff --git a/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/Content/audio/collect.wav b/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/Content/audio/collect.wav new file mode 100644 index 00000000..506220de Binary files /dev/null and b/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/Content/audio/collect.wav differ diff --git a/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/Content/audio/theme.ogg b/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/Content/audio/theme.ogg new file mode 100644 index 00000000..72e1fd3b Binary files /dev/null and b/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/Content/audio/theme.ogg differ diff --git a/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/Content/audio/ui.wav b/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/Content/audio/ui.wav new file mode 100644 index 00000000..63e8941e Binary files /dev/null and b/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/Content/audio/ui.wav differ diff --git a/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/Content/effects/grayscaleEffect.fx b/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/Content/effects/grayscaleEffect.fx new file mode 100644 index 00000000..5dd0d8b6 --- /dev/null +++ b/Tutorials/2dShaders/src/04-Debug-UI/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/2dShaders/src/04-Debug-UI/DungeonSlime/Content/fonts/04B_30.spritefont b/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/Content/fonts/04B_30.spritefont new file mode 100644 index 00000000..63d4728c --- /dev/null +++ b/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/Content/fonts/04B_30.spritefont @@ -0,0 +1,16 @@ + + + + 04B_30.ttf + 17.5 + 0 + true + + + + + ~ + + + + diff --git a/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/Content/fonts/04B_30.ttf b/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/Content/fonts/04B_30.ttf new file mode 100644 index 00000000..4b93740c Binary files /dev/null and b/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/Content/fonts/04B_30.ttf differ diff --git a/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/Content/fonts/04B_30_5x.spritefont b/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/Content/fonts/04B_30_5x.spritefont new file mode 100644 index 00000000..dd239a53 --- /dev/null +++ b/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/Content/fonts/04B_30_5x.spritefont @@ -0,0 +1,16 @@ + + + + 04B_30.ttf + 87.5 + 0 + true + + + + + ~ + + + + diff --git a/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/Content/fonts/04b_30.fnt b/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/Content/fonts/04b_30.fnt new file mode 100644 index 00000000..772f8c54 --- /dev/null +++ b/Tutorials/2dShaders/src/04-Debug-UI/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/2dShaders/src/04-Debug-UI/DungeonSlime/Content/images/atlas-definition.xml b/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/Content/images/atlas-definition.xml new file mode 100644 index 00000000..21772022 --- /dev/null +++ b/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/Content/images/atlas-definition.xml @@ -0,0 +1,34 @@ + + + images/atlas + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/Content/images/atlas.png b/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/Content/images/atlas.png new file mode 100644 index 00000000..f7def20f Binary files /dev/null and b/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/Content/images/atlas.png differ diff --git a/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/Content/images/background-pattern.png b/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/Content/images/background-pattern.png new file mode 100644 index 00000000..2d8d878e Binary files /dev/null and b/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/Content/images/background-pattern.png differ diff --git a/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/Content/images/logo.png b/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/Content/images/logo.png new file mode 100644 index 00000000..1509036c Binary files /dev/null and b/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/Content/images/logo.png differ diff --git a/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/Content/images/tilemap-definition.xml b/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/Content/images/tilemap-definition.xml new file mode 100644 index 00000000..85658c60 --- /dev/null +++ b/Tutorials/2dShaders/src/04-Debug-UI/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/2dShaders/src/04-Debug-UI/DungeonSlime/DungeonSlime.csproj b/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/DungeonSlime.csproj new file mode 100644 index 00000000..7f067a0d --- /dev/null +++ b/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/DungeonSlime.csproj @@ -0,0 +1,69 @@ + + + WinExe + net8.0 + Major + false + false + + + app.manifest + Icon.ico + + + bin/$(Configuration)/$(TargetFramework) + + + + + + + + Icon.ico + + + Icon.bmp + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/Game1.cs b/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/Game1.cs new file mode 100644 index 00000000..3d6d44ec --- /dev/null +++ b/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/Game1.cs @@ -0,0 +1,71 @@ +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/2dShaders/src/04-Debug-UI/DungeonSlime/GameController.cs b/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/GameController.cs new file mode 100644 index 00000000..a85df08f --- /dev/null +++ b/Tutorials/2dShaders/src/04-Debug-UI/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/2dShaders/src/04-Debug-UI/DungeonSlime/GameObjects/Bat.cs b/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/GameObjects/Bat.cs new file mode 100644 index 00000000..ddc855ed --- /dev/null +++ b/Tutorials/2dShaders/src/04-Debug-UI/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/2dShaders/src/04-Debug-UI/DungeonSlime/GameObjects/Slime.cs b/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/GameObjects/Slime.cs new file mode 100644 index 00000000..08b5a63d --- /dev/null +++ b/Tutorials/2dShaders/src/04-Debug-UI/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/2dShaders/src/04-Debug-UI/DungeonSlime/GameObjects/SlimeSegment.cs b/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/GameObjects/SlimeSegment.cs new file mode 100644 index 00000000..b00189eb --- /dev/null +++ b/Tutorials/2dShaders/src/04-Debug-UI/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/2dShaders/src/04-Debug-UI/DungeonSlime/Icon.bmp b/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/Icon.bmp new file mode 100644 index 00000000..2b481653 Binary files /dev/null and b/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/Icon.bmp differ diff --git a/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/Icon.ico b/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/Icon.ico new file mode 100644 index 00000000..7d9dec18 Binary files /dev/null and b/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/Icon.ico differ diff --git a/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/Program.cs b/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/Program.cs new file mode 100644 index 00000000..4d9be314 --- /dev/null +++ b/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/Program.cs @@ -0,0 +1,3 @@ +MonoGameLibrary.Content.ContentManagerExtensions.StartContentWatcherTask(); +using var game = new DungeonSlime.Game1(); +game.Run(); \ No newline at end of file diff --git a/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/Scenes/GameScene.cs b/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/Scenes/GameScene.cs new file mode 100644 index 00000000..0e0eeb9f --- /dev/null +++ b/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/Scenes/GameScene.cs @@ -0,0 +1,434 @@ +using System; +using DungeonSlime.GameObjects; +using DungeonSlime.UI; +using ImGuiNET; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Audio; +using Microsoft.Xna.Framework.Graphics; +using MonoGameGum; +using MonoGameLibrary; +using MonoGameLibrary.Content; +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 Material _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.WatchMaterial("effects/grayscaleEffect"); + _grayscaleEffect.IsDebugVisible = true; + } + + public override void Update(GameTime gameTime) + { + // Ensure the UI is always updated + _ui.Update(gameTime); + + // Update the grayscale effect if it was changed + _grayscaleEffect.Update(); + + 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.SetParameter("Saturation", _saturation); + + // And begin the sprite batch using the grayscale effect. + Core.SpriteBatch.Begin(samplerState: SamplerState.PointClamp, effect: _grayscaleEffect.Effect); + } + 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/2dShaders/src/04-Debug-UI/DungeonSlime/Scenes/TitleScene.cs b/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/Scenes/TitleScene.cs new file mode 100644 index 00000000..8a4dacea --- /dev/null +++ b/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/Scenes/TitleScene.cs @@ -0,0 +1,345 @@ +using System; +using DungeonSlime.UI; +using Gum.Forms.Controls; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Audio; +using Microsoft.Xna.Framework.Graphics; +using MonoGameGum; +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/2dShaders/src/04-Debug-UI/DungeonSlime/UI/AnimatedButton.cs b/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/UI/AnimatedButton.cs new file mode 100644 index 00000000..4cce6ee5 --- /dev/null +++ b/Tutorials/2dShaders/src/04-Debug-UI/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 by AnimationChains 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/2dShaders/src/04-Debug-UI/DungeonSlime/UI/GameSceneUI.cs b/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/UI/GameSceneUI.cs new file mode 100644 index 00000000..498655c2 --- /dev/null +++ b/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/UI/GameSceneUI.cs @@ -0,0 +1,340 @@ +using System; +using Gum.DataTypes; +using Gum.Forms.Controls; +using Gum.Managers; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Audio; +using Microsoft.Xna.Framework.Content; +using MonoGameGum; +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/2dShaders/src/04-Debug-UI/DungeonSlime/UI/OptionsSlider.cs b/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/UI/OptionsSlider.cs new file mode 100644 index 00000000..53d6ee94 --- /dev/null +++ b/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/UI/OptionsSlider.cs @@ -0,0 +1,253 @@ +using System; +using Gum.DataTypes; +using Gum.DataTypes.Variables; +using Gum.Forms.Controls; +using Gum.Managers; +using Microsoft.Xna.Framework; +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/2dShaders/src/04-Debug-UI/DungeonSlime/app.manifest b/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/app.manifest new file mode 100644 index 00000000..caf45166 --- /dev/null +++ b/Tutorials/2dShaders/src/04-Debug-UI/DungeonSlime/app.manifest @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true/pm + permonitorv2,permonitor + + + + diff --git a/Tutorials/2dShaders/src/04-Debug-UI/MonoGameLibrary/Audio/AudioController.cs b/Tutorials/2dShaders/src/04-Debug-UI/MonoGameLibrary/Audio/AudioController.cs new file mode 100644 index 00000000..1bffd636 --- /dev/null +++ b/Tutorials/2dShaders/src/04-Debug-UI/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/2dShaders/src/04-Debug-UI/MonoGameLibrary/Circle.cs b/Tutorials/2dShaders/src/04-Debug-UI/MonoGameLibrary/Circle.cs new file mode 100644 index 00000000..0bb691bc --- /dev/null +++ b/Tutorials/2dShaders/src/04-Debug-UI/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/2dShaders/src/04-Debug-UI/MonoGameLibrary/Content/ContentManagerExtensions.cs b/Tutorials/2dShaders/src/04-Debug-UI/MonoGameLibrary/Content/ContentManagerExtensions.cs new file mode 100644 index 00000000..e012836c --- /dev/null +++ b/Tutorials/2dShaders/src/04-Debug-UI/MonoGameLibrary/Content/ContentManagerExtensions.cs @@ -0,0 +1,168 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; +using MonoGameLibrary.Graphics; + +namespace MonoGameLibrary.Content; + +public static class ContentManagerExtensions +{ + /// + /// Check if the given xnb file has a newer write-time than the last loaded version of the asset. + /// If the local file has been updated, reload the asset and return true. + /// + /// The that loaded the asset originally + /// The asset that will be reloaded if the xnb file is newer + /// If the asset has been reloaded, this out parameter will be set to the previous version of the asset before the newer version was loaded. + /// + /// true when asset was reloaded; false otherwise. + /// + public static bool TryRefresh(this ContentManager manager, WatchedAsset watchedAsset, out T oldAsset) + { + oldAsset = default; + + if (manager != watchedAsset.Owner) + throw new ArgumentException($"Used the wrong ContentManager to refresh {watchedAsset.AssetName}"); + + var path = Path.Combine(manager.RootDirectory, watchedAsset.AssetName) + ".xnb"; + var lastWriteTime = File.GetLastWriteTime(path); + + if (lastWriteTime <= watchedAsset.UpdatedAt) + { + return false; + } + + if (IsFileLocked(path)) return false; // wait for the file to not be locked. + + manager.UnloadAsset(watchedAsset.AssetName); + oldAsset = watchedAsset.Asset; + watchedAsset.Asset = manager.Load(watchedAsset.AssetName); + watchedAsset.UpdatedAt = lastWriteTime; + + return true; + } + + private static bool IsFileLocked(string path) + { + try + { + using FileStream _ = new FileStream(path, FileMode.Open, FileAccess.ReadWrite, FileShare.None); + // File is not locked + return false; + } + catch (IOException) + { + // File is locked or inaccessible + return true; + } + } + + /// + /// Load an asset and wrap it with the metadata required to refresh it later using the function + /// + /// + /// + /// + /// + public static WatchedAsset Watch(this ContentManager manager, string assetName) + { + var asset = manager.Load(assetName); + return new WatchedAsset + { + AssetName = assetName, + Asset = asset, + UpdatedAt = DateTimeOffset.Now, + Owner = manager + }; + } + + /// + /// Load an Effect into the wrapper class + /// + /// + /// + /// + public static Material WatchMaterial(this ContentManager manager, string assetName) + { + return new Material(manager.Watch(assetName)); + } + + + [Conditional("DEBUG")] + public static void StartContentWatcherTask() + { + var args = Environment.GetCommandLineArgs(); + foreach (var arg in args) + { + // if the application was started with the --no-reload option, then do not start the watcher. + if (arg == "--no-reload") return; + } + + // identify the project directory + var projectFile = Assembly.GetEntryAssembly().GetName().Name + ".csproj"; + var current = Directory.GetCurrentDirectory(); + string projectDirectory = null; + + while (current != null && projectDirectory == null) + { + if (File.Exists(Path.Combine(current, projectFile))) + { + // the valid project csproj exists in the directory + projectDirectory = current; + } + else + { + // try looking in the parent directory. + // When there is no parent directory, the variable becomes 'null' + current = Path.GetDirectoryName(current); + } + } + + // if no valid project was identified, then it is impossible to start the watcher + if (string.IsNullOrEmpty(projectDirectory)) return; + + // start the watcher process + var process = Process.Start(new ProcessStartInfo + { + FileName = "dotnet", + Arguments = "build -t:WatchContent --tl:off", + WorkingDirectory = projectDirectory, + WindowStyle = ProcessWindowStyle.Normal, + UseShellExecute = false, + CreateNoWindow = false + }); + + // when this program exits, make sure to emit a kill signal to the watcher process + AppDomain.CurrentDomain.ProcessExit += (_, __) => + { + try + { + if (!process.HasExited) + { + process.Kill(entireProcessTree: true); + } + } + catch + { + /* ignore */ + } + }; + AppDomain.CurrentDomain.UnhandledException += (sender, e) => + { + try + { + if (!process.HasExited) + { + process.Kill(entireProcessTree: true); + } + } + catch + { + /* ignore */ + } + }; + } +} \ No newline at end of file diff --git a/Tutorials/2dShaders/src/04-Debug-UI/MonoGameLibrary/Content/WatchedAsset.cs b/Tutorials/2dShaders/src/04-Debug-UI/MonoGameLibrary/Content/WatchedAsset.cs new file mode 100644 index 00000000..39008666 --- /dev/null +++ b/Tutorials/2dShaders/src/04-Debug-UI/MonoGameLibrary/Content/WatchedAsset.cs @@ -0,0 +1,33 @@ +using System; +using Microsoft.Xna.Framework.Content; + +namespace MonoGameLibrary.Content; + +public class WatchedAsset +{ + /// + /// The latest version of the asset. + /// + public T Asset { get; set; } + + /// + /// The last time the was loaded into memory. + /// + public DateTimeOffset UpdatedAt { get; set; } + + /// + /// The name of the . This is the name used to load the asset from disk. + /// + public string AssetName { get; init; } + + /// + /// The instance that loaded the asset. + /// + public ContentManager Owner { get; init; } + + + public bool TryRefresh(out T oldAsset) + { + return Owner.TryRefresh(this, out oldAsset); + } +} \ No newline at end of file diff --git a/Tutorials/2dShaders/src/04-Debug-UI/MonoGameLibrary/Core.cs b/Tutorials/2dShaders/src/04-Debug-UI/MonoGameLibrary/Core.cs new file mode 100644 index 00000000..3ff316f5 --- /dev/null +++ b/Tutorials/2dShaders/src/04-Debug-UI/MonoGameLibrary/Core.cs @@ -0,0 +1,219 @@ +using System; +using ImGuiNET.SampleProgram.XNA; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; +using MonoGameLibrary.Audio; +using MonoGameLibrary.Graphics; +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 ImGui renderer used for debug UIs. + /// + public static ImGuiRenderer ImGuiRenderer { 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 the ImGui renderer. + ImGuiRenderer = new ImGuiRenderer(this); + ImGuiRenderer.RebuildFontAtlas(); + + // 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); + } + + Material.DrawVisibleDebugUi(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/2dShaders/src/04-Debug-UI/MonoGameLibrary/Graphics/AnimatedSprite.cs b/Tutorials/2dShaders/src/04-Debug-UI/MonoGameLibrary/Graphics/AnimatedSprite.cs new file mode 100644 index 00000000..a1a3594e --- /dev/null +++ b/Tutorials/2dShaders/src/04-Debug-UI/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/2dShaders/src/04-Debug-UI/MonoGameLibrary/Graphics/Animation.cs b/Tutorials/2dShaders/src/04-Debug-UI/MonoGameLibrary/Graphics/Animation.cs new file mode 100644 index 00000000..44d61b65 --- /dev/null +++ b/Tutorials/2dShaders/src/04-Debug-UI/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/2dShaders/src/04-Debug-UI/MonoGameLibrary/Graphics/Material.cs b/Tutorials/2dShaders/src/04-Debug-UI/MonoGameLibrary/Graphics/Material.cs new file mode 100644 index 00000000..f1a22a83 --- /dev/null +++ b/Tutorials/2dShaders/src/04-Debug-UI/MonoGameLibrary/Graphics/Material.cs @@ -0,0 +1,303 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using ImGuiNET; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using MonoGameLibrary.Content; +namespace MonoGameLibrary.Graphics; + +public class Material +{ + // materials that will be drawn during the standard debug UI pass. + private static HashSet s_debugMaterials = new HashSet(); + + /// + /// The hot-reloadable asset that this material is using + /// + public WatchedAsset Asset; + + /// + /// A cached version of the parameters available in the shader + /// + public Dictionary ParameterMap; + + /// + /// The currently loaded Effect that this material is using + /// + public Effect Effect => Asset.Asset; + + /// + /// Enable this variable to visualize the debugUI for the material + /// + public bool IsDebugVisible + { + get + { + return s_debugMaterials.Contains(this); + } + set + { + if (!value) + { + s_debugMaterials.Remove(this); + } + else + { + s_debugMaterials.Add(this); + } + } + } + + /// + /// When true, the debug UI will override parameters + /// + public bool DebugOverride; + + public Material(WatchedAsset asset) + { + Asset = asset; + UpdateParameterCache(); + } + + public void SetParameter(string name, float value) + { + if (DebugOverride) return; + + if (TryGetParameter(name, out var parameter)) + { + parameter.SetValue(value); + } + else + { + Console.WriteLine($"Warning: cannot set shader parameter=[{name}] because it does not exist in the compiled shader=[{Asset.AssetName}]"); + } + } + + public void SetParameter(string name, Matrix value) + { + if (DebugOverride) return; + + if (TryGetParameter(name, out var parameter)) + { + parameter.SetValue(value); + } + else + { + Console.WriteLine($"Warning: cannot set shader parameter=[{name}] because it does not exist in the compiled shader=[{Asset.AssetName}]"); + } + } + + public void SetParameter(string name, Vector2 value) + { + if (DebugOverride) return; + + if (TryGetParameter(name, out var parameter)) + { + parameter.SetValue(value); + } + else + { + Console.WriteLine($"Warning: cannot set shader parameter=[{name}] because it does not exist in the compiled shader=[{Asset.AssetName}]"); + } + } + + public void SetParameter(string name, Texture2D value) + { + if (DebugOverride) return; + + if (TryGetParameter(name, out var parameter)) + { + parameter.SetValue(value); + } + else + { + Console.WriteLine($"Warning: cannot set shader parameter=[{name}] because it does not exist in the compiled shader=[{Asset.AssetName}]"); + } + } + + /// + /// Check if the given parameter name is available in the compiled shader code. + /// Remember that a parameter will be optimized out of a shader if it is not being used + /// in the shader's return value. + /// + /// + /// + /// + public bool TryGetParameter(string name, out EffectParameter parameter) + { + return ParameterMap.TryGetValue(name, out parameter); + } + + /// + /// Rebuild the based on the current parameters available in the effect instance + /// + public void UpdateParameterCache() + { + ParameterMap = Effect.Parameters.ToDictionary(p => p.Name); + } + + [Conditional("DEBUG")] + public void Update() + { + if (Asset.TryRefresh(out var oldAsset)) + { + UpdateParameterCache(); + + foreach (var oldParam in oldAsset.Parameters) + { + if (!TryGetParameter(oldParam.Name, out var newParam)) + { + continue; + } + + switch (oldParam.ParameterClass) + { + case EffectParameterClass.Scalar: + newParam.SetValue(oldParam.GetValueSingle()); + break; + case EffectParameterClass.Matrix: + newParam.SetValue(oldParam.GetValueMatrix()); + break; + case EffectParameterClass.Vector when oldParam.ColumnCount == 2: // float2 + newParam.SetValue(oldParam.GetValueVector2()); + break; + case EffectParameterClass.Object: + newParam.SetValue(oldParam.GetValueTexture2D()); + break; + default: + Console.WriteLine("Warning: shader reload system was not able to re-apply property. " + + $"shader=[{Effect.Name}] " + + $"property=[{oldParam.Name}] " + + $"class=[{oldParam.ParameterClass}]"); + break; + } + } + } + } + + + + [Conditional("DEBUG")] + public void DrawDebug() + { + ImGui.Begin(Effect.Name); + + var currentSize = ImGui.GetWindowSize(); + ImGui.SetWindowSize(Effect.Name, new System.Numerics.Vector2(MathHelper.Max(100, currentSize.X), MathHelper.Max(100, currentSize.Y))); + + ImGui.AlignTextToFramePadding(); + ImGui.Text("Last Updated"); + ImGui.SameLine(); + ImGui.LabelText("##last-updated", Asset.UpdatedAt.ToString() + $" ({(DateTimeOffset.Now - Asset.UpdatedAt).ToString(@"h\:mm\:ss")} ago)"); + + ImGui.AlignTextToFramePadding(); + ImGui.Text("Override Values"); + ImGui.SameLine(); + ImGui.Checkbox("##override-values", ref DebugOverride); + + ImGui.NewLine(); + + bool ScalarSlider(string key, ref float value) + { + float min = 0; + float max = 1; + + return ImGui.SliderFloat($"##_prop{key}", ref value, min, max); + } + + foreach (var prop in ParameterMap) + { + switch (prop.Value.ParameterType, prop.Value.ParameterClass) + { + case (EffectParameterType.Single, EffectParameterClass.Scalar): + ImGui.AlignTextToFramePadding(); + ImGui.Text(prop.Key); + ImGui.SameLine(); + + var value = prop.Value.GetValueSingle(); + if (ScalarSlider(prop.Key, ref value)) + { + prop.Value.SetValue(value); + } + break; + + case (EffectParameterType.Single, EffectParameterClass.Vector): + ImGui.AlignTextToFramePadding(); + ImGui.Text(prop.Key); + + var vec2Value = prop.Value.GetValueVector2(); + ImGui.Indent(); + + ImGui.Text("X"); + ImGui.SameLine(); + + if (ScalarSlider(prop.Key + ".x", ref vec2Value.X)) + { + prop.Value.SetValue(vec2Value); + } + + ImGui.Text("Y"); + ImGui.SameLine(); + if (ScalarSlider(prop.Key + ".y", ref vec2Value.Y)) + { + prop.Value.SetValue(vec2Value); + } + ImGui.Unindent(); + break; + + case (EffectParameterType.Texture2D, EffectParameterClass.Object): + ImGui.AlignTextToFramePadding(); + ImGui.Text(prop.Key); + ImGui.SameLine(); + + var texture = prop.Value.GetValueTexture2D(); + if (texture != null) + { + var texturePtr = Core.ImGuiRenderer.BindTexture(texture); + ImGui.Image(texturePtr, new System.Numerics.Vector2(texture.Width, texture.Height)); + } + else + { + ImGui.Text("(null)"); + } + break; + + default: + ImGui.AlignTextToFramePadding(); + ImGui.Text(prop.Key); + ImGui.SameLine(); + ImGui.Text($"(unsupported {prop.Value.ParameterType}, {prop.Value.ParameterClass})"); + break; + } + } + ImGui.End(); + } + + [Conditional("DEBUG")] + public static void DrawVisibleDebugUi(GameTime gameTime) + { + // first, cull any materials that are not visible, or disposed. + var toRemove = new List(); + foreach (var material in s_debugMaterials) + { + if (material.Effect.IsDisposed) + { + toRemove.Add(material); + } + } + + foreach (var material in toRemove) + { + s_debugMaterials.Remove(material); + } + + Core.ImGuiRenderer.BeforeLayout(gameTime); + foreach (var material in s_debugMaterials) + { + material.DrawDebug(); + } + Core.ImGuiRenderer.AfterLayout(); + } +} \ No newline at end of file diff --git a/Tutorials/2dShaders/src/04-Debug-UI/MonoGameLibrary/Graphics/Sprite.cs b/Tutorials/2dShaders/src/04-Debug-UI/MonoGameLibrary/Graphics/Sprite.cs new file mode 100644 index 00000000..20c44f0b --- /dev/null +++ b/Tutorials/2dShaders/src/04-Debug-UI/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/2dShaders/src/04-Debug-UI/MonoGameLibrary/Graphics/TextureAtlas.cs b/Tutorials/2dShaders/src/04-Debug-UI/MonoGameLibrary/Graphics/TextureAtlas.cs new file mode 100644 index 00000000..e48c9abd --- /dev/null +++ b/Tutorials/2dShaders/src/04-Debug-UI/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/2dShaders/src/04-Debug-UI/MonoGameLibrary/Graphics/TextureRegion.cs b/Tutorials/2dShaders/src/04-Debug-UI/MonoGameLibrary/Graphics/TextureRegion.cs new file mode 100644 index 00000000..ecd69030 --- /dev/null +++ b/Tutorials/2dShaders/src/04-Debug-UI/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/2dShaders/src/04-Debug-UI/MonoGameLibrary/Graphics/Tilemap.cs b/Tutorials/2dShaders/src/04-Debug-UI/MonoGameLibrary/Graphics/Tilemap.cs new file mode 100644 index 00000000..96e1ee5e --- /dev/null +++ b/Tutorials/2dShaders/src/04-Debug-UI/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/2dShaders/src/04-Debug-UI/MonoGameLibrary/Graphics/Tileset.cs b/Tutorials/2dShaders/src/04-Debug-UI/MonoGameLibrary/Graphics/Tileset.cs new file mode 100644 index 00000000..80c2e65a --- /dev/null +++ b/Tutorials/2dShaders/src/04-Debug-UI/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/2dShaders/src/04-Debug-UI/MonoGameLibrary/ImGui/DrawVertDeclaration.cs b/Tutorials/2dShaders/src/04-Debug-UI/MonoGameLibrary/ImGui/DrawVertDeclaration.cs new file mode 100644 index 00000000..d846e7da --- /dev/null +++ b/Tutorials/2dShaders/src/04-Debug-UI/MonoGameLibrary/ImGui/DrawVertDeclaration.cs @@ -0,0 +1,29 @@ +using Microsoft.Xna.Framework.Graphics; + +namespace ImGuiNET.SampleProgram.XNA +{ + public static class DrawVertDeclaration + { + public static readonly VertexDeclaration Declaration; + + public static readonly int Size; + + static DrawVertDeclaration() + { + unsafe { Size = sizeof(ImDrawVert); } + + Declaration = new VertexDeclaration( + Size, + + // Position + new VertexElement(0, VertexElementFormat.Vector2, VertexElementUsage.Position, 0), + + // UV + new VertexElement(8, VertexElementFormat.Vector2, VertexElementUsage.TextureCoordinate, 0), + + // Color + new VertexElement(16, VertexElementFormat.Color, VertexElementUsage.Color, 0) + ); + } + } +} \ No newline at end of file diff --git a/Tutorials/2dShaders/src/04-Debug-UI/MonoGameLibrary/ImGui/ImGuiRenderer.cs b/Tutorials/2dShaders/src/04-Debug-UI/MonoGameLibrary/ImGui/ImGuiRenderer.cs new file mode 100644 index 00000000..e2cc1a29 --- /dev/null +++ b/Tutorials/2dShaders/src/04-Debug-UI/MonoGameLibrary/ImGui/ImGuiRenderer.cs @@ -0,0 +1,436 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace ImGuiNET.SampleProgram.XNA +{ + /// + /// ImGui renderer for use with XNA-likes (FNA & MonoGame) + /// + public class ImGuiRenderer + { + private Game _game; + + // Graphics + private GraphicsDevice _graphicsDevice; + + private BasicEffect _effect; + private RasterizerState _rasterizerState; + + private byte[] _vertexData; + private VertexBuffer _vertexBuffer; + private int _vertexBufferSize; + + private byte[] _indexData; + private IndexBuffer _indexBuffer; + private int _indexBufferSize; + + // Textures + private Dictionary _loadedTextures; + + private int _textureId; + private IntPtr? _fontTextureId; + + // Input + private int _scrollWheelValue; + private int _horizontalScrollWheelValue; + private readonly float WHEEL_DELTA = 120; + private Keys[] _allKeys = Enum.GetValues(); + + public ImGuiRenderer(Game game) + { + var context = ImGui.CreateContext(); + ImGui.SetCurrentContext(context); + + _game = game ?? throw new ArgumentNullException(nameof(game)); + _graphicsDevice = game.GraphicsDevice; + + _loadedTextures = new Dictionary(); + + _rasterizerState = new RasterizerState() + { + CullMode = CullMode.None, + DepthBias = 0, + FillMode = FillMode.Solid, + MultiSampleAntiAlias = false, + ScissorTestEnable = true, + SlopeScaleDepthBias = 0 + }; + + SetupInput(); + } + + #region ImGuiRenderer + + /// + /// Creates a texture and loads the font data from ImGui. Should be called when the is initialized but before any rendering is done + /// + public virtual unsafe void RebuildFontAtlas() + { + // Get font texture from ImGui + var io = ImGui.GetIO(); + io.Fonts.GetTexDataAsRGBA32(out byte* pixelData, out int width, out int height, out int bytesPerPixel); + + // Copy the data to a managed array + var pixels = new byte[width * height * bytesPerPixel]; + unsafe { Marshal.Copy(new IntPtr(pixelData), pixels, 0, pixels.Length); } + + // Create and register the texture as an XNA texture + var tex2d = new Texture2D(_graphicsDevice, width, height, false, SurfaceFormat.Color); + tex2d.SetData(pixels); + + // Should a texture already have been build previously, unbind it first so it can be deallocated + if (_fontTextureId.HasValue) UnbindTexture(_fontTextureId.Value); + + // Bind the new texture to an ImGui-friendly id + _fontTextureId = BindTexture(tex2d); + + // Let ImGui know where to find the texture + io.Fonts.SetTexID(_fontTextureId.Value); + io.Fonts.ClearTexData(); // Clears CPU side texture data + } + + /// + /// Creates a pointer to a texture, which can be passed through ImGui calls such as . That pointer is then used by ImGui to let us know what texture to draw + /// + public virtual IntPtr BindTexture(Texture2D texture) + { + var id = new IntPtr(_textureId++); + + _loadedTextures.Add(id, texture); + + return id; + } + + /// + /// Removes a previously created texture pointer, releasing its reference and allowing it to be deallocated + /// + public virtual void UnbindTexture(IntPtr textureId) + { + _loadedTextures.Remove(textureId); + } + + /// + /// Sets up ImGui for a new frame, should be called at frame start + /// + public virtual void BeforeLayout(GameTime gameTime) + { + ImGui.GetIO().DeltaTime = (float)gameTime.ElapsedGameTime.TotalSeconds; + + UpdateInput(); + + ImGui.NewFrame(); + } + + /// + /// Asks ImGui for the generated geometry data and sends it to the graphics pipeline, should be called after the UI is drawn using ImGui.** calls + /// + public virtual void AfterLayout() + { + ImGui.Render(); + + unsafe { RenderDrawData(ImGui.GetDrawData()); } + } + + #endregion ImGuiRenderer + + #region Setup & Update + + /// + /// Setup key input event handler. + /// + protected virtual void SetupInput() + { + var io = ImGui.GetIO(); + + // MonoGame-specific ////////////////////// + _game.Window.TextInput += (s, a) => + { + if (a.Character == '\t') return; + io.AddInputCharacter(a.Character); + }; + + /////////////////////////////////////////// + + // FNA-specific /////////////////////////// + //TextInputEXT.TextInput += c => + //{ + // if (c == '\t') return; + + // ImGui.GetIO().AddInputCharacter(c); + //}; + /////////////////////////////////////////// + } + + /// + /// Updates the to the current matrices and texture + /// + protected virtual Effect UpdateEffect(Texture2D texture) + { + _effect = _effect ?? new BasicEffect(_graphicsDevice); + + var io = ImGui.GetIO(); + + _effect.World = Matrix.Identity; + _effect.View = Matrix.Identity; + _effect.Projection = Matrix.CreateOrthographicOffCenter(0f, io.DisplaySize.X, io.DisplaySize.Y, 0f, -1f, 1f); + _effect.TextureEnabled = true; + _effect.Texture = texture; + _effect.VertexColorEnabled = true; + + return _effect; + } + + /// + /// Sends XNA input state to ImGui + /// + protected virtual void UpdateInput() + { + if (!_game.IsActive) return; + + var io = ImGui.GetIO(); + + var mouse = Mouse.GetState(); + var keyboard = Keyboard.GetState(); + io.AddMousePosEvent(mouse.X, mouse.Y); + io.AddMouseButtonEvent(0, mouse.LeftButton == ButtonState.Pressed); + io.AddMouseButtonEvent(1, mouse.RightButton == ButtonState.Pressed); + io.AddMouseButtonEvent(2, mouse.MiddleButton == ButtonState.Pressed); + io.AddMouseButtonEvent(3, mouse.XButton1 == ButtonState.Pressed); + io.AddMouseButtonEvent(4, mouse.XButton2 == ButtonState.Pressed); + + io.AddMouseWheelEvent( + (mouse.HorizontalScrollWheelValue - _horizontalScrollWheelValue) / WHEEL_DELTA, + (mouse.ScrollWheelValue - _scrollWheelValue) / WHEEL_DELTA); + _scrollWheelValue = mouse.ScrollWheelValue; + _horizontalScrollWheelValue = mouse.HorizontalScrollWheelValue; + + foreach (var key in _allKeys) + { + if (TryMapKeys(key, out ImGuiKey imguikey)) + { + io.AddKeyEvent(imguikey, keyboard.IsKeyDown(key)); + } + } + + io.DisplaySize = new System.Numerics.Vector2(_graphicsDevice.PresentationParameters.BackBufferWidth, _graphicsDevice.PresentationParameters.BackBufferHeight); + io.DisplayFramebufferScale = new System.Numerics.Vector2(1f, 1f); + } + + private bool TryMapKeys(Keys key, out ImGuiKey imguikey) + { + //Special case not handed in the switch... + //If the actual key we put in is "None", return none and true. + //otherwise, return none and false. + if (key == Keys.None) + { + imguikey = ImGuiKey.None; + return true; + } + + imguikey = key switch + { + Keys.Back => ImGuiKey.Backspace, + Keys.Tab => ImGuiKey.Tab, + Keys.Enter => ImGuiKey.Enter, + Keys.CapsLock => ImGuiKey.CapsLock, + Keys.Escape => ImGuiKey.Escape, + Keys.Space => ImGuiKey.Space, + Keys.PageUp => ImGuiKey.PageUp, + Keys.PageDown => ImGuiKey.PageDown, + Keys.End => ImGuiKey.End, + Keys.Home => ImGuiKey.Home, + Keys.Left => ImGuiKey.LeftArrow, + Keys.Right => ImGuiKey.RightArrow, + Keys.Up => ImGuiKey.UpArrow, + Keys.Down => ImGuiKey.DownArrow, + Keys.PrintScreen => ImGuiKey.PrintScreen, + Keys.Insert => ImGuiKey.Insert, + Keys.Delete => ImGuiKey.Delete, + >= Keys.D0 and <= Keys.D9 => ImGuiKey._0 + (key - Keys.D0), + >= Keys.A and <= Keys.Z => ImGuiKey.A + (key - Keys.A), + >= Keys.NumPad0 and <= Keys.NumPad9 => ImGuiKey.Keypad0 + (key - Keys.NumPad0), + Keys.Multiply => ImGuiKey.KeypadMultiply, + Keys.Add => ImGuiKey.KeypadAdd, + Keys.Subtract => ImGuiKey.KeypadSubtract, + Keys.Decimal => ImGuiKey.KeypadDecimal, + Keys.Divide => ImGuiKey.KeypadDivide, + >= Keys.F1 and <= Keys.F24 => ImGuiKey.F1 + (key - Keys.F1), + Keys.NumLock => ImGuiKey.NumLock, + Keys.Scroll => ImGuiKey.ScrollLock, + Keys.LeftShift => ImGuiKey.ModShift, + Keys.LeftControl => ImGuiKey.ModCtrl, + Keys.LeftAlt => ImGuiKey.ModAlt, + Keys.OemSemicolon => ImGuiKey.Semicolon, + Keys.OemPlus => ImGuiKey.Equal, + Keys.OemComma => ImGuiKey.Comma, + Keys.OemMinus => ImGuiKey.Minus, + Keys.OemPeriod => ImGuiKey.Period, + Keys.OemQuestion => ImGuiKey.Slash, + Keys.OemTilde => ImGuiKey.GraveAccent, + Keys.OemOpenBrackets => ImGuiKey.LeftBracket, + Keys.OemCloseBrackets => ImGuiKey.RightBracket, + Keys.OemPipe => ImGuiKey.Backslash, + Keys.OemQuotes => ImGuiKey.Apostrophe, + Keys.BrowserBack => ImGuiKey.AppBack, + Keys.BrowserForward => ImGuiKey.AppForward, + _ => ImGuiKey.None, + }; + + return imguikey != ImGuiKey.None; + } + + #endregion Setup & Update + + #region Internals + + /// + /// Gets the geometry as set up by ImGui and sends it to the graphics device + /// + private void RenderDrawData(ImDrawDataPtr drawData) + { + // Setup render state: alpha-blending enabled, no face culling, no depth testing, scissor enabled, vertex/texcoord/color pointers + var lastViewport = _graphicsDevice.Viewport; + var lastScissorBox = _graphicsDevice.ScissorRectangle; + var lastRasterizer = _graphicsDevice.RasterizerState; + var lastDepthStencil = _graphicsDevice.DepthStencilState; + var lastBlendFactor = _graphicsDevice.BlendFactor; + var lastBlendState = _graphicsDevice.BlendState; + + _graphicsDevice.BlendFactor = Color.White; + _graphicsDevice.BlendState = BlendState.NonPremultiplied; + _graphicsDevice.RasterizerState = _rasterizerState; + _graphicsDevice.DepthStencilState = DepthStencilState.DepthRead; + + // Handle cases of screen coordinates != from framebuffer coordinates (e.g. retina displays) + drawData.ScaleClipRects(ImGui.GetIO().DisplayFramebufferScale); + + // Setup projection + _graphicsDevice.Viewport = new Viewport(0, 0, _graphicsDevice.PresentationParameters.BackBufferWidth, _graphicsDevice.PresentationParameters.BackBufferHeight); + + UpdateBuffers(drawData); + + RenderCommandLists(drawData); + + // Restore modified state + _graphicsDevice.Viewport = lastViewport; + _graphicsDevice.ScissorRectangle = lastScissorBox; + _graphicsDevice.RasterizerState = lastRasterizer; + _graphicsDevice.DepthStencilState = lastDepthStencil; + _graphicsDevice.BlendState = lastBlendState; + _graphicsDevice.BlendFactor = lastBlendFactor; + } + + private unsafe void UpdateBuffers(ImDrawDataPtr drawData) + { + if (drawData.TotalVtxCount == 0) + { + return; + } + + // Expand buffers if we need more room + if (drawData.TotalVtxCount > _vertexBufferSize) + { + _vertexBuffer?.Dispose(); + + _vertexBufferSize = (int)(drawData.TotalVtxCount * 1.5f); + _vertexBuffer = new VertexBuffer(_graphicsDevice, DrawVertDeclaration.Declaration, _vertexBufferSize, BufferUsage.None); + _vertexData = new byte[_vertexBufferSize * DrawVertDeclaration.Size]; + } + + if (drawData.TotalIdxCount > _indexBufferSize) + { + _indexBuffer?.Dispose(); + + _indexBufferSize = (int)(drawData.TotalIdxCount * 1.5f); + _indexBuffer = new IndexBuffer(_graphicsDevice, IndexElementSize.SixteenBits, _indexBufferSize, BufferUsage.None); + _indexData = new byte[_indexBufferSize * sizeof(ushort)]; + } + + // Copy ImGui's vertices and indices to a set of managed byte arrays + int vtxOffset = 0; + int idxOffset = 0; + + for (int n = 0; n < drawData.CmdListsCount; n++) + { + ImDrawListPtr cmdList = drawData.CmdLists[n]; + + fixed (void* vtxDstPtr = &_vertexData[vtxOffset * DrawVertDeclaration.Size]) + fixed (void* idxDstPtr = &_indexData[idxOffset * sizeof(ushort)]) + { + Buffer.MemoryCopy((void*)cmdList.VtxBuffer.Data, vtxDstPtr, _vertexData.Length, cmdList.VtxBuffer.Size * DrawVertDeclaration.Size); + Buffer.MemoryCopy((void*)cmdList.IdxBuffer.Data, idxDstPtr, _indexData.Length, cmdList.IdxBuffer.Size * sizeof(ushort)); + } + + vtxOffset += cmdList.VtxBuffer.Size; + idxOffset += cmdList.IdxBuffer.Size; + } + + // Copy the managed byte arrays to the gpu vertex- and index buffers + _vertexBuffer.SetData(_vertexData, 0, drawData.TotalVtxCount * DrawVertDeclaration.Size); + _indexBuffer.SetData(_indexData, 0, drawData.TotalIdxCount * sizeof(ushort)); + } + + private unsafe void RenderCommandLists(ImDrawDataPtr drawData) + { + _graphicsDevice.SetVertexBuffer(_vertexBuffer); + _graphicsDevice.Indices = _indexBuffer; + + int vtxOffset = 0; + int idxOffset = 0; + + for (int n = 0; n < drawData.CmdListsCount; n++) + { + ImDrawListPtr cmdList = drawData.CmdLists[n]; + + for (int cmdi = 0; cmdi < cmdList.CmdBuffer.Size; cmdi++) + { + ImDrawCmdPtr drawCmd = cmdList.CmdBuffer[cmdi]; + + if (drawCmd.ElemCount == 0) + { + continue; + } + + if (!_loadedTextures.ContainsKey(drawCmd.TextureId)) + { + throw new InvalidOperationException($"Could not find a texture with id '{drawCmd.TextureId}', please check your bindings"); + } + + _graphicsDevice.ScissorRectangle = new Rectangle( + (int)drawCmd.ClipRect.X, + (int)drawCmd.ClipRect.Y, + (int)(drawCmd.ClipRect.Z - drawCmd.ClipRect.X), + (int)(drawCmd.ClipRect.W - drawCmd.ClipRect.Y) + ); + + var effect = UpdateEffect(_loadedTextures[drawCmd.TextureId]); + + foreach (var pass in effect.CurrentTechnique.Passes) + { + pass.Apply(); + +#pragma warning disable CS0618 // // FNA does not expose an alternative method. + _graphicsDevice.DrawIndexedPrimitives( + primitiveType: PrimitiveType.TriangleList, + baseVertex: (int)drawCmd.VtxOffset + vtxOffset, + minVertexIndex: 0, + numVertices: cmdList.VtxBuffer.Size, + startIndex: (int)drawCmd.IdxOffset + idxOffset, + primitiveCount: (int)drawCmd.ElemCount / 3 + ); +#pragma warning restore CS0618 + } + } + + vtxOffset += cmdList.VtxBuffer.Size; + idxOffset += cmdList.IdxBuffer.Size; + } + } + + #endregion Internals + } +} \ No newline at end of file diff --git a/Tutorials/2dShaders/src/04-Debug-UI/MonoGameLibrary/Input/GamePadInfo.cs b/Tutorials/2dShaders/src/04-Debug-UI/MonoGameLibrary/Input/GamePadInfo.cs new file mode 100644 index 00000000..7fd16126 --- /dev/null +++ b/Tutorials/2dShaders/src/04-Debug-UI/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/2dShaders/src/04-Debug-UI/MonoGameLibrary/Input/InputManager.cs b/Tutorials/2dShaders/src/04-Debug-UI/MonoGameLibrary/Input/InputManager.cs new file mode 100644 index 00000000..1790eb70 --- /dev/null +++ b/Tutorials/2dShaders/src/04-Debug-UI/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/2dShaders/src/04-Debug-UI/MonoGameLibrary/Input/KeyboardInfo.cs b/Tutorials/2dShaders/src/04-Debug-UI/MonoGameLibrary/Input/KeyboardInfo.cs new file mode 100644 index 00000000..c6770cb0 --- /dev/null +++ b/Tutorials/2dShaders/src/04-Debug-UI/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/2dShaders/src/04-Debug-UI/MonoGameLibrary/Input/MouseButton.cs b/Tutorials/2dShaders/src/04-Debug-UI/MonoGameLibrary/Input/MouseButton.cs new file mode 100644 index 00000000..5b041f80 --- /dev/null +++ b/Tutorials/2dShaders/src/04-Debug-UI/MonoGameLibrary/Input/MouseButton.cs @@ -0,0 +1,10 @@ +namespace MonoGameLibrary.Input; + +public enum MouseButton +{ + Left, + Middle, + Right, + XButton1, + XButton2 +} diff --git a/Tutorials/2dShaders/src/04-Debug-UI/MonoGameLibrary/Input/MouseInfo.cs b/Tutorials/2dShaders/src/04-Debug-UI/MonoGameLibrary/Input/MouseInfo.cs new file mode 100644 index 00000000..09d6207c --- /dev/null +++ b/Tutorials/2dShaders/src/04-Debug-UI/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/2dShaders/src/04-Debug-UI/MonoGameLibrary/MonoGameLibrary.csproj b/Tutorials/2dShaders/src/04-Debug-UI/MonoGameLibrary/MonoGameLibrary.csproj new file mode 100644 index 00000000..69adcc21 --- /dev/null +++ b/Tutorials/2dShaders/src/04-Debug-UI/MonoGameLibrary/MonoGameLibrary.csproj @@ -0,0 +1,12 @@ + + + net8.0 + true + + + + + All + + + \ No newline at end of file diff --git a/Tutorials/2dShaders/src/04-Debug-UI/MonoGameLibrary/Scenes/Scene.cs b/Tutorials/2dShaders/src/04-Debug-UI/MonoGameLibrary/Scenes/Scene.cs new file mode 100644 index 00000000..627d220f --- /dev/null +++ b/Tutorials/2dShaders/src/04-Debug-UI/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/2dShaders/src/05-Transition-Effect/DungeonSlime.sln b/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime.sln new file mode 100644 index 00000000..077462d5 --- /dev/null +++ b/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DungeonSlime", "DungeonSlime\DungeonSlime.csproj", "{88BD3FF9-D3D6-4680-A274-F866E0DFCAC4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MonoGameLibrary", "MonoGameLibrary\MonoGameLibrary.csproj", "{AB85CEEE-6D97-4438-AEC4-797D2806F44A}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {88BD3FF9-D3D6-4680-A274-F866E0DFCAC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {88BD3FF9-D3D6-4680-A274-F866E0DFCAC4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {88BD3FF9-D3D6-4680-A274-F866E0DFCAC4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {88BD3FF9-D3D6-4680-A274-F866E0DFCAC4}.Release|Any CPU.Build.0 = Release|Any CPU + {AB85CEEE-6D97-4438-AEC4-797D2806F44A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AB85CEEE-6D97-4438-AEC4-797D2806F44A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AB85CEEE-6D97-4438-AEC4-797D2806F44A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AB85CEEE-6D97-4438-AEC4-797D2806F44A}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/.config/dotnet-tools.json b/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/.config/dotnet-tools.json new file mode 100644 index 00000000..afd4e2c4 --- /dev/null +++ b/Tutorials/2dShaders/src/05-Transition-Effect/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/2dShaders/src/05-Transition-Effect/DungeonSlime/Content/Content.mgcb b/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/Content/Content.mgcb new file mode 100644 index 00000000..d26ea4f1 --- /dev/null +++ b/Tutorials/2dShaders/src/05-Transition-Effect/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/2dShaders/src/05-Transition-Effect/DungeonSlime/Content/audio/bounce.wav b/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/Content/audio/bounce.wav new file mode 100644 index 00000000..baa7a47b Binary files /dev/null and b/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/Content/audio/bounce.wav differ diff --git a/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/Content/audio/collect.wav b/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/Content/audio/collect.wav new file mode 100644 index 00000000..506220de Binary files /dev/null and b/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/Content/audio/collect.wav differ diff --git a/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/Content/audio/theme.ogg b/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/Content/audio/theme.ogg new file mode 100644 index 00000000..72e1fd3b Binary files /dev/null and b/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/Content/audio/theme.ogg differ diff --git a/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/Content/audio/ui.wav b/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/Content/audio/ui.wav new file mode 100644 index 00000000..63e8941e Binary files /dev/null and b/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/Content/audio/ui.wav differ diff --git a/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/Content/effects/grayscaleEffect.fx b/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/Content/effects/grayscaleEffect.fx new file mode 100644 index 00000000..5dd0d8b6 --- /dev/null +++ b/Tutorials/2dShaders/src/05-Transition-Effect/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/2dShaders/src/05-Transition-Effect/DungeonSlime/Content/fonts/04B_30.spritefont b/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/Content/fonts/04B_30.spritefont new file mode 100644 index 00000000..63d4728c --- /dev/null +++ b/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/Content/fonts/04B_30.spritefont @@ -0,0 +1,16 @@ + + + + 04B_30.ttf + 17.5 + 0 + true + + + + + ~ + + + + diff --git a/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/Content/fonts/04B_30.ttf b/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/Content/fonts/04B_30.ttf new file mode 100644 index 00000000..4b93740c Binary files /dev/null and b/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/Content/fonts/04B_30.ttf differ diff --git a/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/Content/fonts/04B_30_5x.spritefont b/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/Content/fonts/04B_30_5x.spritefont new file mode 100644 index 00000000..dd239a53 --- /dev/null +++ b/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/Content/fonts/04B_30_5x.spritefont @@ -0,0 +1,16 @@ + + + + 04B_30.ttf + 87.5 + 0 + true + + + + + ~ + + + + diff --git a/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/Content/fonts/04b_30.fnt b/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/Content/fonts/04b_30.fnt new file mode 100644 index 00000000..772f8c54 --- /dev/null +++ b/Tutorials/2dShaders/src/05-Transition-Effect/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/2dShaders/src/05-Transition-Effect/DungeonSlime/Content/images/atlas-definition.xml b/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/Content/images/atlas-definition.xml new file mode 100644 index 00000000..21772022 --- /dev/null +++ b/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/Content/images/atlas-definition.xml @@ -0,0 +1,34 @@ + + + images/atlas + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/Content/images/atlas.png b/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/Content/images/atlas.png new file mode 100644 index 00000000..f7def20f Binary files /dev/null and b/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/Content/images/atlas.png differ diff --git a/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/Content/images/background-pattern.png b/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/Content/images/background-pattern.png new file mode 100644 index 00000000..2d8d878e Binary files /dev/null and b/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/Content/images/background-pattern.png differ diff --git a/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/Content/images/logo.png b/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/Content/images/logo.png new file mode 100644 index 00000000..1509036c Binary files /dev/null and b/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/Content/images/logo.png differ diff --git a/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/Content/images/tilemap-definition.xml b/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/Content/images/tilemap-definition.xml new file mode 100644 index 00000000..85658c60 --- /dev/null +++ b/Tutorials/2dShaders/src/05-Transition-Effect/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/2dShaders/src/05-Transition-Effect/DungeonSlime/DungeonSlime.csproj b/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/DungeonSlime.csproj new file mode 100644 index 00000000..2cd4cbe7 --- /dev/null +++ b/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/DungeonSlime.csproj @@ -0,0 +1,73 @@ + + + WinExe + net8.0 + Major + false + false + + + app.manifest + Icon.ico + + + bin/$(Configuration)/$(TargetFramework) + + + + + + + + Icon.ico + + + Icon.bmp + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/Game1.cs b/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/Game1.cs new file mode 100644 index 00000000..1aa42bac --- /dev/null +++ b/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/Game1.cs @@ -0,0 +1,77 @@ +using DungeonSlime.Scenes; +using Microsoft.Xna.Framework; +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); + + SceneTransitionMaterial.IsDebugVisible = true; + + // 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() + { + // Allow the Core class to load any content. + base.LoadContent(); + + // Load the background theme music + _themeSong = Content.Load("audio/theme"); + } +} diff --git a/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/GameController.cs b/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/GameController.cs new file mode 100644 index 00000000..a85df08f --- /dev/null +++ b/Tutorials/2dShaders/src/05-Transition-Effect/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/2dShaders/src/05-Transition-Effect/DungeonSlime/GameObjects/Bat.cs b/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/GameObjects/Bat.cs new file mode 100644 index 00000000..ddc855ed --- /dev/null +++ b/Tutorials/2dShaders/src/05-Transition-Effect/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/2dShaders/src/05-Transition-Effect/DungeonSlime/GameObjects/Slime.cs b/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/GameObjects/Slime.cs new file mode 100644 index 00000000..08b5a63d --- /dev/null +++ b/Tutorials/2dShaders/src/05-Transition-Effect/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/2dShaders/src/05-Transition-Effect/DungeonSlime/GameObjects/SlimeSegment.cs b/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/GameObjects/SlimeSegment.cs new file mode 100644 index 00000000..b00189eb --- /dev/null +++ b/Tutorials/2dShaders/src/05-Transition-Effect/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/2dShaders/src/05-Transition-Effect/DungeonSlime/Icon.bmp b/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/Icon.bmp new file mode 100644 index 00000000..2b481653 Binary files /dev/null and b/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/Icon.bmp differ diff --git a/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/Icon.ico b/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/Icon.ico new file mode 100644 index 00000000..7d9dec18 Binary files /dev/null and b/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/Icon.ico differ diff --git a/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/Program.cs b/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/Program.cs new file mode 100644 index 00000000..4d9be314 --- /dev/null +++ b/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/Program.cs @@ -0,0 +1,3 @@ +MonoGameLibrary.Content.ContentManagerExtensions.StartContentWatcherTask(); +using var game = new DungeonSlime.Game1(); +game.Run(); \ No newline at end of file diff --git a/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/Scenes/GameScene.cs b/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/Scenes/GameScene.cs new file mode 100644 index 00000000..b47f80c0 --- /dev/null +++ b/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/Scenes/GameScene.cs @@ -0,0 +1,433 @@ +using System; +using DungeonSlime.GameObjects; +using DungeonSlime.UI; +using ImGuiNET; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Audio; +using Microsoft.Xna.Framework.Graphics; +using MonoGameGum; +using MonoGameLibrary; +using MonoGameLibrary.Content; +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 Material _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.WatchMaterial("effects/grayscaleEffect"); + } + + public override void Update(GameTime gameTime) + { + // Ensure the UI is always updated + _ui.Update(gameTime); + + // Update the grayscale effect if it was changed + _grayscaleEffect.Update(); + + 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.SetParameter("Saturation", _saturation); + + // And begin the sprite batch using the grayscale effect. + Core.SpriteBatch.Begin(samplerState: SamplerState.PointClamp, effect: _grayscaleEffect.Effect); + } + 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/2dShaders/src/05-Transition-Effect/DungeonSlime/Scenes/TitleScene.cs b/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/Scenes/TitleScene.cs new file mode 100644 index 00000000..8a4dacea --- /dev/null +++ b/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/Scenes/TitleScene.cs @@ -0,0 +1,345 @@ +using System; +using DungeonSlime.UI; +using Gum.Forms.Controls; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Audio; +using Microsoft.Xna.Framework.Graphics; +using MonoGameGum; +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/2dShaders/src/05-Transition-Effect/DungeonSlime/UI/AnimatedButton.cs b/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/UI/AnimatedButton.cs new file mode 100644 index 00000000..4cce6ee5 --- /dev/null +++ b/Tutorials/2dShaders/src/05-Transition-Effect/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 by AnimationChains 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/2dShaders/src/05-Transition-Effect/DungeonSlime/UI/GameSceneUI.cs b/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/UI/GameSceneUI.cs new file mode 100644 index 00000000..498655c2 --- /dev/null +++ b/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/UI/GameSceneUI.cs @@ -0,0 +1,340 @@ +using System; +using Gum.DataTypes; +using Gum.Forms.Controls; +using Gum.Managers; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Audio; +using Microsoft.Xna.Framework.Content; +using MonoGameGum; +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/2dShaders/src/05-Transition-Effect/DungeonSlime/UI/OptionsSlider.cs b/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/UI/OptionsSlider.cs new file mode 100644 index 00000000..53d6ee94 --- /dev/null +++ b/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/UI/OptionsSlider.cs @@ -0,0 +1,253 @@ +using System; +using Gum.DataTypes; +using Gum.DataTypes.Variables; +using Gum.Forms.Controls; +using Gum.Managers; +using Microsoft.Xna.Framework; +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/2dShaders/src/05-Transition-Effect/DungeonSlime/app.manifest b/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/app.manifest new file mode 100644 index 00000000..caf45166 --- /dev/null +++ b/Tutorials/2dShaders/src/05-Transition-Effect/DungeonSlime/app.manifest @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true/pm + permonitorv2,permonitor + + + + diff --git a/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/Audio/AudioController.cs b/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/Audio/AudioController.cs new file mode 100644 index 00000000..1bffd636 --- /dev/null +++ b/Tutorials/2dShaders/src/05-Transition-Effect/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/2dShaders/src/05-Transition-Effect/MonoGameLibrary/Circle.cs b/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/Circle.cs new file mode 100644 index 00000000..0bb691bc --- /dev/null +++ b/Tutorials/2dShaders/src/05-Transition-Effect/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/2dShaders/src/05-Transition-Effect/MonoGameLibrary/Content/ContentManagerExtensions.cs b/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/Content/ContentManagerExtensions.cs new file mode 100644 index 00000000..e012836c --- /dev/null +++ b/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/Content/ContentManagerExtensions.cs @@ -0,0 +1,168 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; +using MonoGameLibrary.Graphics; + +namespace MonoGameLibrary.Content; + +public static class ContentManagerExtensions +{ + /// + /// Check if the given xnb file has a newer write-time than the last loaded version of the asset. + /// If the local file has been updated, reload the asset and return true. + /// + /// The that loaded the asset originally + /// The asset that will be reloaded if the xnb file is newer + /// If the asset has been reloaded, this out parameter will be set to the previous version of the asset before the newer version was loaded. + /// + /// true when asset was reloaded; false otherwise. + /// + public static bool TryRefresh(this ContentManager manager, WatchedAsset watchedAsset, out T oldAsset) + { + oldAsset = default; + + if (manager != watchedAsset.Owner) + throw new ArgumentException($"Used the wrong ContentManager to refresh {watchedAsset.AssetName}"); + + var path = Path.Combine(manager.RootDirectory, watchedAsset.AssetName) + ".xnb"; + var lastWriteTime = File.GetLastWriteTime(path); + + if (lastWriteTime <= watchedAsset.UpdatedAt) + { + return false; + } + + if (IsFileLocked(path)) return false; // wait for the file to not be locked. + + manager.UnloadAsset(watchedAsset.AssetName); + oldAsset = watchedAsset.Asset; + watchedAsset.Asset = manager.Load(watchedAsset.AssetName); + watchedAsset.UpdatedAt = lastWriteTime; + + return true; + } + + private static bool IsFileLocked(string path) + { + try + { + using FileStream _ = new FileStream(path, FileMode.Open, FileAccess.ReadWrite, FileShare.None); + // File is not locked + return false; + } + catch (IOException) + { + // File is locked or inaccessible + return true; + } + } + + /// + /// Load an asset and wrap it with the metadata required to refresh it later using the function + /// + /// + /// + /// + /// + public static WatchedAsset Watch(this ContentManager manager, string assetName) + { + var asset = manager.Load(assetName); + return new WatchedAsset + { + AssetName = assetName, + Asset = asset, + UpdatedAt = DateTimeOffset.Now, + Owner = manager + }; + } + + /// + /// Load an Effect into the wrapper class + /// + /// + /// + /// + public static Material WatchMaterial(this ContentManager manager, string assetName) + { + return new Material(manager.Watch(assetName)); + } + + + [Conditional("DEBUG")] + public static void StartContentWatcherTask() + { + var args = Environment.GetCommandLineArgs(); + foreach (var arg in args) + { + // if the application was started with the --no-reload option, then do not start the watcher. + if (arg == "--no-reload") return; + } + + // identify the project directory + var projectFile = Assembly.GetEntryAssembly().GetName().Name + ".csproj"; + var current = Directory.GetCurrentDirectory(); + string projectDirectory = null; + + while (current != null && projectDirectory == null) + { + if (File.Exists(Path.Combine(current, projectFile))) + { + // the valid project csproj exists in the directory + projectDirectory = current; + } + else + { + // try looking in the parent directory. + // When there is no parent directory, the variable becomes 'null' + current = Path.GetDirectoryName(current); + } + } + + // if no valid project was identified, then it is impossible to start the watcher + if (string.IsNullOrEmpty(projectDirectory)) return; + + // start the watcher process + var process = Process.Start(new ProcessStartInfo + { + FileName = "dotnet", + Arguments = "build -t:WatchContent --tl:off", + WorkingDirectory = projectDirectory, + WindowStyle = ProcessWindowStyle.Normal, + UseShellExecute = false, + CreateNoWindow = false + }); + + // when this program exits, make sure to emit a kill signal to the watcher process + AppDomain.CurrentDomain.ProcessExit += (_, __) => + { + try + { + if (!process.HasExited) + { + process.Kill(entireProcessTree: true); + } + } + catch + { + /* ignore */ + } + }; + AppDomain.CurrentDomain.UnhandledException += (sender, e) => + { + try + { + if (!process.HasExited) + { + process.Kill(entireProcessTree: true); + } + } + catch + { + /* ignore */ + } + }; + } +} \ No newline at end of file diff --git a/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/Content/WatchedAsset.cs b/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/Content/WatchedAsset.cs new file mode 100644 index 00000000..39008666 --- /dev/null +++ b/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/Content/WatchedAsset.cs @@ -0,0 +1,33 @@ +using System; +using Microsoft.Xna.Framework.Content; + +namespace MonoGameLibrary.Content; + +public class WatchedAsset +{ + /// + /// The latest version of the asset. + /// + public T Asset { get; set; } + + /// + /// The last time the was loaded into memory. + /// + public DateTimeOffset UpdatedAt { get; set; } + + /// + /// The name of the . This is the name used to load the asset from disk. + /// + public string AssetName { get; init; } + + /// + /// The instance that loaded the asset. + /// + public ContentManager Owner { get; init; } + + + public bool TryRefresh(out T oldAsset) + { + return Owner.TryRefresh(this, out oldAsset); + } +} \ No newline at end of file diff --git a/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/Core.cs b/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/Core.cs new file mode 100644 index 00000000..1bcd962a --- /dev/null +++ b/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/Core.cs @@ -0,0 +1,278 @@ +using System; +using System.Collections.Generic; +using ImGuiNET.SampleProgram.XNA; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; +using MonoGameLibrary.Audio; +using MonoGameLibrary.Content; +using MonoGameLibrary.Graphics; +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; + + /// + /// The material that is used when changing scenes + /// + public static Material SceneTransitionMaterial { get; private set; } + + /// + /// A set of grayscale gradient textures to use as transition guides + /// + public static List SceneTransitionTextures { get; private set; } + + /// + /// The current transition between scenes + /// + public static SceneTransition SceneTransition { get; protected set; } = SceneTransition.Open(1000); + + /// + /// 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 a runtime generated 1x1 pixel texture. + /// + public static Texture2D Pixel { get; private set; } + + /// + /// Gets the ImGui renderer used for debug UIs. + /// + public static ImGuiRenderer ImGuiRenderer { get; private set; } + + /// + /// Gets the content manager used to load global assets. + /// + public static new ContentManager Content { get; private set; } + + /// + /// Gets the content manager that can load global assets from the SharedContent folder. + /// + public static ContentManager SharedContent { 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"; + + // Set the core's shared content manager, pointing to the SharedContent folder. + SharedContent = new ContentManager(Services, "SharedContent"); + + // 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 the ImGui renderer. + ImGuiRenderer = new ImGuiRenderer(this); + ImGuiRenderer.RebuildFontAtlas(); + + // Create a new input manager + Input = new InputManager(); + + // Create a new audio controller. + Audio = new AudioController(); + + // Create a 1x1 white pixel texture for drawing quads. + Pixel = new Texture2D(GraphicsDevice, 1, 1); + Pixel.SetData(new Color[]{ Color.White }); + } + + protected override void LoadContent() + { + base.LoadContent(); + SceneTransitionMaterial = SharedContent.WatchMaterial("effects/sceneTransitionEffect"); + SceneTransitionMaterial.SetParameter("EdgeWidth", .05f); + + SceneTransitionTextures = new List(); + SceneTransitionTextures.Add(SharedContent.Load("images/angled")); + SceneTransitionTextures.Add(SharedContent.Load("images/concave")); + SceneTransitionTextures.Add(SharedContent.Load("images/radial")); + SceneTransitionTextures.Add(SharedContent.Load("images/ripple")); + } + + 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 && SceneTransition.IsComplete) + { + TransitionScene(); + } + + // If there is an active scene, update it. + if (s_activeScene != null) + { + s_activeScene.Update(gameTime); + } + + // Check if the scene transition material needs to be reloaded. + SceneTransitionMaterial.SetParameter("Progress", SceneTransition.DirectionalRatio); + SceneTransitionMaterial.Update(); + + 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); + } + + // Draw the scene transition quad + SpriteBatch.Begin(effect: SceneTransitionMaterial.Effect); + SpriteBatch.Draw(SceneTransitionTextures[SceneTransition.TextureIndex % SceneTransitionTextures.Count], GraphicsDevice.Viewport.Bounds, Color.White); + SpriteBatch.End(); + + Material.DrawVisibleDebugUi(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; + SceneTransition = SceneTransition.Close(250); + } + } + + private static void TransitionScene() + { + SceneTransition = SceneTransition.Open(500); + + // 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/2dShaders/src/05-Transition-Effect/MonoGameLibrary/Graphics/AnimatedSprite.cs b/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/Graphics/AnimatedSprite.cs new file mode 100644 index 00000000..a1a3594e --- /dev/null +++ b/Tutorials/2dShaders/src/05-Transition-Effect/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/2dShaders/src/05-Transition-Effect/MonoGameLibrary/Graphics/Animation.cs b/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/Graphics/Animation.cs new file mode 100644 index 00000000..44d61b65 --- /dev/null +++ b/Tutorials/2dShaders/src/05-Transition-Effect/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/2dShaders/src/05-Transition-Effect/MonoGameLibrary/Graphics/Material.cs b/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/Graphics/Material.cs new file mode 100644 index 00000000..f1a22a83 --- /dev/null +++ b/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/Graphics/Material.cs @@ -0,0 +1,303 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using ImGuiNET; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using MonoGameLibrary.Content; +namespace MonoGameLibrary.Graphics; + +public class Material +{ + // materials that will be drawn during the standard debug UI pass. + private static HashSet s_debugMaterials = new HashSet(); + + /// + /// The hot-reloadable asset that this material is using + /// + public WatchedAsset Asset; + + /// + /// A cached version of the parameters available in the shader + /// + public Dictionary ParameterMap; + + /// + /// The currently loaded Effect that this material is using + /// + public Effect Effect => Asset.Asset; + + /// + /// Enable this variable to visualize the debugUI for the material + /// + public bool IsDebugVisible + { + get + { + return s_debugMaterials.Contains(this); + } + set + { + if (!value) + { + s_debugMaterials.Remove(this); + } + else + { + s_debugMaterials.Add(this); + } + } + } + + /// + /// When true, the debug UI will override parameters + /// + public bool DebugOverride; + + public Material(WatchedAsset asset) + { + Asset = asset; + UpdateParameterCache(); + } + + public void SetParameter(string name, float value) + { + if (DebugOverride) return; + + if (TryGetParameter(name, out var parameter)) + { + parameter.SetValue(value); + } + else + { + Console.WriteLine($"Warning: cannot set shader parameter=[{name}] because it does not exist in the compiled shader=[{Asset.AssetName}]"); + } + } + + public void SetParameter(string name, Matrix value) + { + if (DebugOverride) return; + + if (TryGetParameter(name, out var parameter)) + { + parameter.SetValue(value); + } + else + { + Console.WriteLine($"Warning: cannot set shader parameter=[{name}] because it does not exist in the compiled shader=[{Asset.AssetName}]"); + } + } + + public void SetParameter(string name, Vector2 value) + { + if (DebugOverride) return; + + if (TryGetParameter(name, out var parameter)) + { + parameter.SetValue(value); + } + else + { + Console.WriteLine($"Warning: cannot set shader parameter=[{name}] because it does not exist in the compiled shader=[{Asset.AssetName}]"); + } + } + + public void SetParameter(string name, Texture2D value) + { + if (DebugOverride) return; + + if (TryGetParameter(name, out var parameter)) + { + parameter.SetValue(value); + } + else + { + Console.WriteLine($"Warning: cannot set shader parameter=[{name}] because it does not exist in the compiled shader=[{Asset.AssetName}]"); + } + } + + /// + /// Check if the given parameter name is available in the compiled shader code. + /// Remember that a parameter will be optimized out of a shader if it is not being used + /// in the shader's return value. + /// + /// + /// + /// + public bool TryGetParameter(string name, out EffectParameter parameter) + { + return ParameterMap.TryGetValue(name, out parameter); + } + + /// + /// Rebuild the based on the current parameters available in the effect instance + /// + public void UpdateParameterCache() + { + ParameterMap = Effect.Parameters.ToDictionary(p => p.Name); + } + + [Conditional("DEBUG")] + public void Update() + { + if (Asset.TryRefresh(out var oldAsset)) + { + UpdateParameterCache(); + + foreach (var oldParam in oldAsset.Parameters) + { + if (!TryGetParameter(oldParam.Name, out var newParam)) + { + continue; + } + + switch (oldParam.ParameterClass) + { + case EffectParameterClass.Scalar: + newParam.SetValue(oldParam.GetValueSingle()); + break; + case EffectParameterClass.Matrix: + newParam.SetValue(oldParam.GetValueMatrix()); + break; + case EffectParameterClass.Vector when oldParam.ColumnCount == 2: // float2 + newParam.SetValue(oldParam.GetValueVector2()); + break; + case EffectParameterClass.Object: + newParam.SetValue(oldParam.GetValueTexture2D()); + break; + default: + Console.WriteLine("Warning: shader reload system was not able to re-apply property. " + + $"shader=[{Effect.Name}] " + + $"property=[{oldParam.Name}] " + + $"class=[{oldParam.ParameterClass}]"); + break; + } + } + } + } + + + + [Conditional("DEBUG")] + public void DrawDebug() + { + ImGui.Begin(Effect.Name); + + var currentSize = ImGui.GetWindowSize(); + ImGui.SetWindowSize(Effect.Name, new System.Numerics.Vector2(MathHelper.Max(100, currentSize.X), MathHelper.Max(100, currentSize.Y))); + + ImGui.AlignTextToFramePadding(); + ImGui.Text("Last Updated"); + ImGui.SameLine(); + ImGui.LabelText("##last-updated", Asset.UpdatedAt.ToString() + $" ({(DateTimeOffset.Now - Asset.UpdatedAt).ToString(@"h\:mm\:ss")} ago)"); + + ImGui.AlignTextToFramePadding(); + ImGui.Text("Override Values"); + ImGui.SameLine(); + ImGui.Checkbox("##override-values", ref DebugOverride); + + ImGui.NewLine(); + + bool ScalarSlider(string key, ref float value) + { + float min = 0; + float max = 1; + + return ImGui.SliderFloat($"##_prop{key}", ref value, min, max); + } + + foreach (var prop in ParameterMap) + { + switch (prop.Value.ParameterType, prop.Value.ParameterClass) + { + case (EffectParameterType.Single, EffectParameterClass.Scalar): + ImGui.AlignTextToFramePadding(); + ImGui.Text(prop.Key); + ImGui.SameLine(); + + var value = prop.Value.GetValueSingle(); + if (ScalarSlider(prop.Key, ref value)) + { + prop.Value.SetValue(value); + } + break; + + case (EffectParameterType.Single, EffectParameterClass.Vector): + ImGui.AlignTextToFramePadding(); + ImGui.Text(prop.Key); + + var vec2Value = prop.Value.GetValueVector2(); + ImGui.Indent(); + + ImGui.Text("X"); + ImGui.SameLine(); + + if (ScalarSlider(prop.Key + ".x", ref vec2Value.X)) + { + prop.Value.SetValue(vec2Value); + } + + ImGui.Text("Y"); + ImGui.SameLine(); + if (ScalarSlider(prop.Key + ".y", ref vec2Value.Y)) + { + prop.Value.SetValue(vec2Value); + } + ImGui.Unindent(); + break; + + case (EffectParameterType.Texture2D, EffectParameterClass.Object): + ImGui.AlignTextToFramePadding(); + ImGui.Text(prop.Key); + ImGui.SameLine(); + + var texture = prop.Value.GetValueTexture2D(); + if (texture != null) + { + var texturePtr = Core.ImGuiRenderer.BindTexture(texture); + ImGui.Image(texturePtr, new System.Numerics.Vector2(texture.Width, texture.Height)); + } + else + { + ImGui.Text("(null)"); + } + break; + + default: + ImGui.AlignTextToFramePadding(); + ImGui.Text(prop.Key); + ImGui.SameLine(); + ImGui.Text($"(unsupported {prop.Value.ParameterType}, {prop.Value.ParameterClass})"); + break; + } + } + ImGui.End(); + } + + [Conditional("DEBUG")] + public static void DrawVisibleDebugUi(GameTime gameTime) + { + // first, cull any materials that are not visible, or disposed. + var toRemove = new List(); + foreach (var material in s_debugMaterials) + { + if (material.Effect.IsDisposed) + { + toRemove.Add(material); + } + } + + foreach (var material in toRemove) + { + s_debugMaterials.Remove(material); + } + + Core.ImGuiRenderer.BeforeLayout(gameTime); + foreach (var material in s_debugMaterials) + { + material.DrawDebug(); + } + Core.ImGuiRenderer.AfterLayout(); + } +} \ No newline at end of file diff --git a/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/Graphics/Sprite.cs b/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/Graphics/Sprite.cs new file mode 100644 index 00000000..20c44f0b --- /dev/null +++ b/Tutorials/2dShaders/src/05-Transition-Effect/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/2dShaders/src/05-Transition-Effect/MonoGameLibrary/Graphics/TextureAtlas.cs b/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/Graphics/TextureAtlas.cs new file mode 100644 index 00000000..e48c9abd --- /dev/null +++ b/Tutorials/2dShaders/src/05-Transition-Effect/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/2dShaders/src/05-Transition-Effect/MonoGameLibrary/Graphics/TextureRegion.cs b/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/Graphics/TextureRegion.cs new file mode 100644 index 00000000..ecd69030 --- /dev/null +++ b/Tutorials/2dShaders/src/05-Transition-Effect/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/2dShaders/src/05-Transition-Effect/MonoGameLibrary/Graphics/Tilemap.cs b/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/Graphics/Tilemap.cs new file mode 100644 index 00000000..96e1ee5e --- /dev/null +++ b/Tutorials/2dShaders/src/05-Transition-Effect/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/2dShaders/src/05-Transition-Effect/MonoGameLibrary/Graphics/Tileset.cs b/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/Graphics/Tileset.cs new file mode 100644 index 00000000..80c2e65a --- /dev/null +++ b/Tutorials/2dShaders/src/05-Transition-Effect/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/2dShaders/src/05-Transition-Effect/MonoGameLibrary/ImGui/DrawVertDeclaration.cs b/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/ImGui/DrawVertDeclaration.cs new file mode 100644 index 00000000..d846e7da --- /dev/null +++ b/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/ImGui/DrawVertDeclaration.cs @@ -0,0 +1,29 @@ +using Microsoft.Xna.Framework.Graphics; + +namespace ImGuiNET.SampleProgram.XNA +{ + public static class DrawVertDeclaration + { + public static readonly VertexDeclaration Declaration; + + public static readonly int Size; + + static DrawVertDeclaration() + { + unsafe { Size = sizeof(ImDrawVert); } + + Declaration = new VertexDeclaration( + Size, + + // Position + new VertexElement(0, VertexElementFormat.Vector2, VertexElementUsage.Position, 0), + + // UV + new VertexElement(8, VertexElementFormat.Vector2, VertexElementUsage.TextureCoordinate, 0), + + // Color + new VertexElement(16, VertexElementFormat.Color, VertexElementUsage.Color, 0) + ); + } + } +} \ No newline at end of file diff --git a/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/ImGui/ImGuiRenderer.cs b/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/ImGui/ImGuiRenderer.cs new file mode 100644 index 00000000..e2cc1a29 --- /dev/null +++ b/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/ImGui/ImGuiRenderer.cs @@ -0,0 +1,436 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace ImGuiNET.SampleProgram.XNA +{ + /// + /// ImGui renderer for use with XNA-likes (FNA & MonoGame) + /// + public class ImGuiRenderer + { + private Game _game; + + // Graphics + private GraphicsDevice _graphicsDevice; + + private BasicEffect _effect; + private RasterizerState _rasterizerState; + + private byte[] _vertexData; + private VertexBuffer _vertexBuffer; + private int _vertexBufferSize; + + private byte[] _indexData; + private IndexBuffer _indexBuffer; + private int _indexBufferSize; + + // Textures + private Dictionary _loadedTextures; + + private int _textureId; + private IntPtr? _fontTextureId; + + // Input + private int _scrollWheelValue; + private int _horizontalScrollWheelValue; + private readonly float WHEEL_DELTA = 120; + private Keys[] _allKeys = Enum.GetValues(); + + public ImGuiRenderer(Game game) + { + var context = ImGui.CreateContext(); + ImGui.SetCurrentContext(context); + + _game = game ?? throw new ArgumentNullException(nameof(game)); + _graphicsDevice = game.GraphicsDevice; + + _loadedTextures = new Dictionary(); + + _rasterizerState = new RasterizerState() + { + CullMode = CullMode.None, + DepthBias = 0, + FillMode = FillMode.Solid, + MultiSampleAntiAlias = false, + ScissorTestEnable = true, + SlopeScaleDepthBias = 0 + }; + + SetupInput(); + } + + #region ImGuiRenderer + + /// + /// Creates a texture and loads the font data from ImGui. Should be called when the is initialized but before any rendering is done + /// + public virtual unsafe void RebuildFontAtlas() + { + // Get font texture from ImGui + var io = ImGui.GetIO(); + io.Fonts.GetTexDataAsRGBA32(out byte* pixelData, out int width, out int height, out int bytesPerPixel); + + // Copy the data to a managed array + var pixels = new byte[width * height * bytesPerPixel]; + unsafe { Marshal.Copy(new IntPtr(pixelData), pixels, 0, pixels.Length); } + + // Create and register the texture as an XNA texture + var tex2d = new Texture2D(_graphicsDevice, width, height, false, SurfaceFormat.Color); + tex2d.SetData(pixels); + + // Should a texture already have been build previously, unbind it first so it can be deallocated + if (_fontTextureId.HasValue) UnbindTexture(_fontTextureId.Value); + + // Bind the new texture to an ImGui-friendly id + _fontTextureId = BindTexture(tex2d); + + // Let ImGui know where to find the texture + io.Fonts.SetTexID(_fontTextureId.Value); + io.Fonts.ClearTexData(); // Clears CPU side texture data + } + + /// + /// Creates a pointer to a texture, which can be passed through ImGui calls such as . That pointer is then used by ImGui to let us know what texture to draw + /// + public virtual IntPtr BindTexture(Texture2D texture) + { + var id = new IntPtr(_textureId++); + + _loadedTextures.Add(id, texture); + + return id; + } + + /// + /// Removes a previously created texture pointer, releasing its reference and allowing it to be deallocated + /// + public virtual void UnbindTexture(IntPtr textureId) + { + _loadedTextures.Remove(textureId); + } + + /// + /// Sets up ImGui for a new frame, should be called at frame start + /// + public virtual void BeforeLayout(GameTime gameTime) + { + ImGui.GetIO().DeltaTime = (float)gameTime.ElapsedGameTime.TotalSeconds; + + UpdateInput(); + + ImGui.NewFrame(); + } + + /// + /// Asks ImGui for the generated geometry data and sends it to the graphics pipeline, should be called after the UI is drawn using ImGui.** calls + /// + public virtual void AfterLayout() + { + ImGui.Render(); + + unsafe { RenderDrawData(ImGui.GetDrawData()); } + } + + #endregion ImGuiRenderer + + #region Setup & Update + + /// + /// Setup key input event handler. + /// + protected virtual void SetupInput() + { + var io = ImGui.GetIO(); + + // MonoGame-specific ////////////////////// + _game.Window.TextInput += (s, a) => + { + if (a.Character == '\t') return; + io.AddInputCharacter(a.Character); + }; + + /////////////////////////////////////////// + + // FNA-specific /////////////////////////// + //TextInputEXT.TextInput += c => + //{ + // if (c == '\t') return; + + // ImGui.GetIO().AddInputCharacter(c); + //}; + /////////////////////////////////////////// + } + + /// + /// Updates the to the current matrices and texture + /// + protected virtual Effect UpdateEffect(Texture2D texture) + { + _effect = _effect ?? new BasicEffect(_graphicsDevice); + + var io = ImGui.GetIO(); + + _effect.World = Matrix.Identity; + _effect.View = Matrix.Identity; + _effect.Projection = Matrix.CreateOrthographicOffCenter(0f, io.DisplaySize.X, io.DisplaySize.Y, 0f, -1f, 1f); + _effect.TextureEnabled = true; + _effect.Texture = texture; + _effect.VertexColorEnabled = true; + + return _effect; + } + + /// + /// Sends XNA input state to ImGui + /// + protected virtual void UpdateInput() + { + if (!_game.IsActive) return; + + var io = ImGui.GetIO(); + + var mouse = Mouse.GetState(); + var keyboard = Keyboard.GetState(); + io.AddMousePosEvent(mouse.X, mouse.Y); + io.AddMouseButtonEvent(0, mouse.LeftButton == ButtonState.Pressed); + io.AddMouseButtonEvent(1, mouse.RightButton == ButtonState.Pressed); + io.AddMouseButtonEvent(2, mouse.MiddleButton == ButtonState.Pressed); + io.AddMouseButtonEvent(3, mouse.XButton1 == ButtonState.Pressed); + io.AddMouseButtonEvent(4, mouse.XButton2 == ButtonState.Pressed); + + io.AddMouseWheelEvent( + (mouse.HorizontalScrollWheelValue - _horizontalScrollWheelValue) / WHEEL_DELTA, + (mouse.ScrollWheelValue - _scrollWheelValue) / WHEEL_DELTA); + _scrollWheelValue = mouse.ScrollWheelValue; + _horizontalScrollWheelValue = mouse.HorizontalScrollWheelValue; + + foreach (var key in _allKeys) + { + if (TryMapKeys(key, out ImGuiKey imguikey)) + { + io.AddKeyEvent(imguikey, keyboard.IsKeyDown(key)); + } + } + + io.DisplaySize = new System.Numerics.Vector2(_graphicsDevice.PresentationParameters.BackBufferWidth, _graphicsDevice.PresentationParameters.BackBufferHeight); + io.DisplayFramebufferScale = new System.Numerics.Vector2(1f, 1f); + } + + private bool TryMapKeys(Keys key, out ImGuiKey imguikey) + { + //Special case not handed in the switch... + //If the actual key we put in is "None", return none and true. + //otherwise, return none and false. + if (key == Keys.None) + { + imguikey = ImGuiKey.None; + return true; + } + + imguikey = key switch + { + Keys.Back => ImGuiKey.Backspace, + Keys.Tab => ImGuiKey.Tab, + Keys.Enter => ImGuiKey.Enter, + Keys.CapsLock => ImGuiKey.CapsLock, + Keys.Escape => ImGuiKey.Escape, + Keys.Space => ImGuiKey.Space, + Keys.PageUp => ImGuiKey.PageUp, + Keys.PageDown => ImGuiKey.PageDown, + Keys.End => ImGuiKey.End, + Keys.Home => ImGuiKey.Home, + Keys.Left => ImGuiKey.LeftArrow, + Keys.Right => ImGuiKey.RightArrow, + Keys.Up => ImGuiKey.UpArrow, + Keys.Down => ImGuiKey.DownArrow, + Keys.PrintScreen => ImGuiKey.PrintScreen, + Keys.Insert => ImGuiKey.Insert, + Keys.Delete => ImGuiKey.Delete, + >= Keys.D0 and <= Keys.D9 => ImGuiKey._0 + (key - Keys.D0), + >= Keys.A and <= Keys.Z => ImGuiKey.A + (key - Keys.A), + >= Keys.NumPad0 and <= Keys.NumPad9 => ImGuiKey.Keypad0 + (key - Keys.NumPad0), + Keys.Multiply => ImGuiKey.KeypadMultiply, + Keys.Add => ImGuiKey.KeypadAdd, + Keys.Subtract => ImGuiKey.KeypadSubtract, + Keys.Decimal => ImGuiKey.KeypadDecimal, + Keys.Divide => ImGuiKey.KeypadDivide, + >= Keys.F1 and <= Keys.F24 => ImGuiKey.F1 + (key - Keys.F1), + Keys.NumLock => ImGuiKey.NumLock, + Keys.Scroll => ImGuiKey.ScrollLock, + Keys.LeftShift => ImGuiKey.ModShift, + Keys.LeftControl => ImGuiKey.ModCtrl, + Keys.LeftAlt => ImGuiKey.ModAlt, + Keys.OemSemicolon => ImGuiKey.Semicolon, + Keys.OemPlus => ImGuiKey.Equal, + Keys.OemComma => ImGuiKey.Comma, + Keys.OemMinus => ImGuiKey.Minus, + Keys.OemPeriod => ImGuiKey.Period, + Keys.OemQuestion => ImGuiKey.Slash, + Keys.OemTilde => ImGuiKey.GraveAccent, + Keys.OemOpenBrackets => ImGuiKey.LeftBracket, + Keys.OemCloseBrackets => ImGuiKey.RightBracket, + Keys.OemPipe => ImGuiKey.Backslash, + Keys.OemQuotes => ImGuiKey.Apostrophe, + Keys.BrowserBack => ImGuiKey.AppBack, + Keys.BrowserForward => ImGuiKey.AppForward, + _ => ImGuiKey.None, + }; + + return imguikey != ImGuiKey.None; + } + + #endregion Setup & Update + + #region Internals + + /// + /// Gets the geometry as set up by ImGui and sends it to the graphics device + /// + private void RenderDrawData(ImDrawDataPtr drawData) + { + // Setup render state: alpha-blending enabled, no face culling, no depth testing, scissor enabled, vertex/texcoord/color pointers + var lastViewport = _graphicsDevice.Viewport; + var lastScissorBox = _graphicsDevice.ScissorRectangle; + var lastRasterizer = _graphicsDevice.RasterizerState; + var lastDepthStencil = _graphicsDevice.DepthStencilState; + var lastBlendFactor = _graphicsDevice.BlendFactor; + var lastBlendState = _graphicsDevice.BlendState; + + _graphicsDevice.BlendFactor = Color.White; + _graphicsDevice.BlendState = BlendState.NonPremultiplied; + _graphicsDevice.RasterizerState = _rasterizerState; + _graphicsDevice.DepthStencilState = DepthStencilState.DepthRead; + + // Handle cases of screen coordinates != from framebuffer coordinates (e.g. retina displays) + drawData.ScaleClipRects(ImGui.GetIO().DisplayFramebufferScale); + + // Setup projection + _graphicsDevice.Viewport = new Viewport(0, 0, _graphicsDevice.PresentationParameters.BackBufferWidth, _graphicsDevice.PresentationParameters.BackBufferHeight); + + UpdateBuffers(drawData); + + RenderCommandLists(drawData); + + // Restore modified state + _graphicsDevice.Viewport = lastViewport; + _graphicsDevice.ScissorRectangle = lastScissorBox; + _graphicsDevice.RasterizerState = lastRasterizer; + _graphicsDevice.DepthStencilState = lastDepthStencil; + _graphicsDevice.BlendState = lastBlendState; + _graphicsDevice.BlendFactor = lastBlendFactor; + } + + private unsafe void UpdateBuffers(ImDrawDataPtr drawData) + { + if (drawData.TotalVtxCount == 0) + { + return; + } + + // Expand buffers if we need more room + if (drawData.TotalVtxCount > _vertexBufferSize) + { + _vertexBuffer?.Dispose(); + + _vertexBufferSize = (int)(drawData.TotalVtxCount * 1.5f); + _vertexBuffer = new VertexBuffer(_graphicsDevice, DrawVertDeclaration.Declaration, _vertexBufferSize, BufferUsage.None); + _vertexData = new byte[_vertexBufferSize * DrawVertDeclaration.Size]; + } + + if (drawData.TotalIdxCount > _indexBufferSize) + { + _indexBuffer?.Dispose(); + + _indexBufferSize = (int)(drawData.TotalIdxCount * 1.5f); + _indexBuffer = new IndexBuffer(_graphicsDevice, IndexElementSize.SixteenBits, _indexBufferSize, BufferUsage.None); + _indexData = new byte[_indexBufferSize * sizeof(ushort)]; + } + + // Copy ImGui's vertices and indices to a set of managed byte arrays + int vtxOffset = 0; + int idxOffset = 0; + + for (int n = 0; n < drawData.CmdListsCount; n++) + { + ImDrawListPtr cmdList = drawData.CmdLists[n]; + + fixed (void* vtxDstPtr = &_vertexData[vtxOffset * DrawVertDeclaration.Size]) + fixed (void* idxDstPtr = &_indexData[idxOffset * sizeof(ushort)]) + { + Buffer.MemoryCopy((void*)cmdList.VtxBuffer.Data, vtxDstPtr, _vertexData.Length, cmdList.VtxBuffer.Size * DrawVertDeclaration.Size); + Buffer.MemoryCopy((void*)cmdList.IdxBuffer.Data, idxDstPtr, _indexData.Length, cmdList.IdxBuffer.Size * sizeof(ushort)); + } + + vtxOffset += cmdList.VtxBuffer.Size; + idxOffset += cmdList.IdxBuffer.Size; + } + + // Copy the managed byte arrays to the gpu vertex- and index buffers + _vertexBuffer.SetData(_vertexData, 0, drawData.TotalVtxCount * DrawVertDeclaration.Size); + _indexBuffer.SetData(_indexData, 0, drawData.TotalIdxCount * sizeof(ushort)); + } + + private unsafe void RenderCommandLists(ImDrawDataPtr drawData) + { + _graphicsDevice.SetVertexBuffer(_vertexBuffer); + _graphicsDevice.Indices = _indexBuffer; + + int vtxOffset = 0; + int idxOffset = 0; + + for (int n = 0; n < drawData.CmdListsCount; n++) + { + ImDrawListPtr cmdList = drawData.CmdLists[n]; + + for (int cmdi = 0; cmdi < cmdList.CmdBuffer.Size; cmdi++) + { + ImDrawCmdPtr drawCmd = cmdList.CmdBuffer[cmdi]; + + if (drawCmd.ElemCount == 0) + { + continue; + } + + if (!_loadedTextures.ContainsKey(drawCmd.TextureId)) + { + throw new InvalidOperationException($"Could not find a texture with id '{drawCmd.TextureId}', please check your bindings"); + } + + _graphicsDevice.ScissorRectangle = new Rectangle( + (int)drawCmd.ClipRect.X, + (int)drawCmd.ClipRect.Y, + (int)(drawCmd.ClipRect.Z - drawCmd.ClipRect.X), + (int)(drawCmd.ClipRect.W - drawCmd.ClipRect.Y) + ); + + var effect = UpdateEffect(_loadedTextures[drawCmd.TextureId]); + + foreach (var pass in effect.CurrentTechnique.Passes) + { + pass.Apply(); + +#pragma warning disable CS0618 // // FNA does not expose an alternative method. + _graphicsDevice.DrawIndexedPrimitives( + primitiveType: PrimitiveType.TriangleList, + baseVertex: (int)drawCmd.VtxOffset + vtxOffset, + minVertexIndex: 0, + numVertices: cmdList.VtxBuffer.Size, + startIndex: (int)drawCmd.IdxOffset + idxOffset, + primitiveCount: (int)drawCmd.ElemCount / 3 + ); +#pragma warning restore CS0618 + } + } + + vtxOffset += cmdList.VtxBuffer.Size; + idxOffset += cmdList.IdxBuffer.Size; + } + } + + #endregion Internals + } +} \ No newline at end of file diff --git a/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/Input/GamePadInfo.cs b/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/Input/GamePadInfo.cs new file mode 100644 index 00000000..7fd16126 --- /dev/null +++ b/Tutorials/2dShaders/src/05-Transition-Effect/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/2dShaders/src/05-Transition-Effect/MonoGameLibrary/Input/InputManager.cs b/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/Input/InputManager.cs new file mode 100644 index 00000000..1790eb70 --- /dev/null +++ b/Tutorials/2dShaders/src/05-Transition-Effect/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/2dShaders/src/05-Transition-Effect/MonoGameLibrary/Input/KeyboardInfo.cs b/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/Input/KeyboardInfo.cs new file mode 100644 index 00000000..c6770cb0 --- /dev/null +++ b/Tutorials/2dShaders/src/05-Transition-Effect/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/2dShaders/src/05-Transition-Effect/MonoGameLibrary/Input/MouseButton.cs b/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/Input/MouseButton.cs new file mode 100644 index 00000000..5b041f80 --- /dev/null +++ b/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/Input/MouseButton.cs @@ -0,0 +1,10 @@ +namespace MonoGameLibrary.Input; + +public enum MouseButton +{ + Left, + Middle, + Right, + XButton1, + XButton2 +} diff --git a/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/Input/MouseInfo.cs b/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/Input/MouseInfo.cs new file mode 100644 index 00000000..09d6207c --- /dev/null +++ b/Tutorials/2dShaders/src/05-Transition-Effect/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/2dShaders/src/05-Transition-Effect/MonoGameLibrary/MonoGameLibrary.csproj b/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/MonoGameLibrary.csproj new file mode 100644 index 00000000..69adcc21 --- /dev/null +++ b/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/MonoGameLibrary.csproj @@ -0,0 +1,12 @@ + + + net8.0 + true + + + + + All + + + \ No newline at end of file diff --git a/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/Scenes/Scene.cs b/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/Scenes/Scene.cs new file mode 100644 index 00000000..627d220f --- /dev/null +++ b/Tutorials/2dShaders/src/05-Transition-Effect/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/2dShaders/src/05-Transition-Effect/MonoGameLibrary/Scenes/SceneTransition.cs b/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/Scenes/SceneTransition.cs new file mode 100644 index 00000000..bbd1f7d5 --- /dev/null +++ b/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/Scenes/SceneTransition.cs @@ -0,0 +1,56 @@ +using System; +using Microsoft.Xna.Framework; + +namespace MonoGameLibrary.Scenes; + +public class SceneTransition +{ + public DateTimeOffset StartTime; + public TimeSpan Duration; + + /// + /// true when the transition is progressing from 0 to 1. + /// false when the transition is progressing from 1 to 0. + /// + public bool IsForwards; + + /// + /// The index into the + /// + public int TextureIndex; + + /// + /// The 0 to 1 value representing the progress of the transition. + /// + public float ProgressRatio => MathHelper.Clamp((float)(EndTime - DateTimeOffset.Now).TotalMilliseconds / (float)Duration.TotalMilliseconds, 0, 1); + + public float DirectionalRatio => IsForwards ? 1 - ProgressRatio : ProgressRatio; + + public DateTimeOffset EndTime => StartTime + Duration; + public bool IsComplete => DateTimeOffset.Now >= EndTime; + + + /// + /// Create a new transition + /// + /// + /// how long will the transition last in milliseconds? + /// + /// + /// should the transition be animating the Progress parameter from 0 to 1, or 1 to 0? + /// + /// + public static SceneTransition Create(int durationMs, bool isForwards) + { + return new SceneTransition + { + Duration = TimeSpan.FromMilliseconds(durationMs), + StartTime = DateTimeOffset.Now, + TextureIndex = Random.Shared.Next(), + IsForwards = isForwards + }; + } + + public static SceneTransition Open(int durationMs) => Create(durationMs, true); + public static SceneTransition Close(int durationMs) => Create(durationMs, false); +} \ No newline at end of file diff --git a/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/SharedContent/SharedContent.mgcb b/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/SharedContent/SharedContent.mgcb new file mode 100644 index 00000000..a3ddec1f --- /dev/null +++ b/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/SharedContent/SharedContent.mgcb @@ -0,0 +1,69 @@ + +#----------------------------- Global Properties ----------------------------# + +/outputDir:bin/$(Platform) +/intermediateDir:obj/$(Platform) +/platform:DesktopGL +/config: +/profile:Reach +/compress:False + +#-------------------------------- References --------------------------------# + + +#---------------------------------- Content ---------------------------------# + +#begin effects/sceneTransitionEffect.fx +/importer:EffectImporter +/processor:EffectProcessor +/processorParam:DebugMode=Auto +/build:effects/sceneTransitionEffect.fx + +#begin images/angled.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/angled.png + +#begin images/concave.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/concave.png + +#begin images/radial.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/radial.png + +#begin images/ripple.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/ripple.png + diff --git a/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/SharedContent/effects/sceneTransitionEffect.fx b/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/SharedContent/effects/sceneTransitionEffect.fx new file mode 100644 index 00000000..0d87d021 --- /dev/null +++ b/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/SharedContent/effects/sceneTransitionEffect.fx @@ -0,0 +1,42 @@ +#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; + +float Progress; +float EdgeWidth; + +sampler2D SpriteTextureSampler = sampler_state +{ + Texture = ; +}; + +struct VertexShaderOutput +{ + float4 Position : SV_POSITION; + float4 Color : COLOR0; + float2 TextureCoordinates : TEXCOORD0; +}; + +float4 MainPS(VertexShaderOutput input) : COLOR +{ + float2 uv = input.TextureCoordinates; + float value = tex2D(SpriteTextureSampler, uv).r; + float transitioned = smoothstep(Progress, Progress + EdgeWidth, value); + return float4(0, 0, 0, transitioned); +} + + +technique SpriteDrawing +{ + pass P0 + { + PixelShader = compile PS_SHADERMODEL MainPS(); + } +}; \ No newline at end of file diff --git a/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/SharedContent/images/angled.png b/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/SharedContent/images/angled.png new file mode 100644 index 00000000..de0160f2 Binary files /dev/null and b/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/SharedContent/images/angled.png differ diff --git a/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/SharedContent/images/concave.png b/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/SharedContent/images/concave.png new file mode 100644 index 00000000..826e2207 Binary files /dev/null and b/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/SharedContent/images/concave.png differ diff --git a/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/SharedContent/images/radial.png b/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/SharedContent/images/radial.png new file mode 100644 index 00000000..bd1207cf Binary files /dev/null and b/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/SharedContent/images/radial.png differ diff --git a/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/SharedContent/images/ripple.png b/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/SharedContent/images/ripple.png new file mode 100644 index 00000000..e137653a Binary files /dev/null and b/Tutorials/2dShaders/src/05-Transition-Effect/MonoGameLibrary/SharedContent/images/ripple.png differ diff --git a/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime.sln b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime.sln new file mode 100644 index 00000000..077462d5 --- /dev/null +++ b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DungeonSlime", "DungeonSlime\DungeonSlime.csproj", "{88BD3FF9-D3D6-4680-A274-F866E0DFCAC4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MonoGameLibrary", "MonoGameLibrary\MonoGameLibrary.csproj", "{AB85CEEE-6D97-4438-AEC4-797D2806F44A}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {88BD3FF9-D3D6-4680-A274-F866E0DFCAC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {88BD3FF9-D3D6-4680-A274-F866E0DFCAC4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {88BD3FF9-D3D6-4680-A274-F866E0DFCAC4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {88BD3FF9-D3D6-4680-A274-F866E0DFCAC4}.Release|Any CPU.Build.0 = Release|Any CPU + {AB85CEEE-6D97-4438-AEC4-797D2806F44A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AB85CEEE-6D97-4438-AEC4-797D2806F44A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AB85CEEE-6D97-4438-AEC4-797D2806F44A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AB85CEEE-6D97-4438-AEC4-797D2806F44A}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/.config/dotnet-tools.json b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/.config/dotnet-tools.json new file mode 100644 index 00000000..afd4e2c4 --- /dev/null +++ b/Tutorials/2dShaders/src/06-Color-Swap-Effect/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/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Content/Content.mgcb b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Content/Content.mgcb new file mode 100644 index 00000000..0e916dbc --- /dev/null +++ b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Content/Content.mgcb @@ -0,0 +1,158 @@ + +#----------------------------- 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 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/color-map-1.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/color-map-1.png + +#begin images/color-map-2.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/color-map-2.png + +#begin images/color-map-dark-purple.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/color-map-dark-purple.png + +#begin images/color-map-green.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/color-map-green.png + +#begin images/color-map-pink.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/color-map-pink.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/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Content/audio/bounce.wav b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Content/audio/bounce.wav new file mode 100644 index 00000000..baa7a47b Binary files /dev/null and b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Content/audio/bounce.wav differ diff --git a/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Content/audio/collect.wav b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Content/audio/collect.wav new file mode 100644 index 00000000..506220de Binary files /dev/null and b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Content/audio/collect.wav differ diff --git a/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Content/audio/theme.ogg b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Content/audio/theme.ogg new file mode 100644 index 00000000..72e1fd3b Binary files /dev/null and b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Content/audio/theme.ogg differ diff --git a/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Content/audio/ui.wav b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Content/audio/ui.wav new file mode 100644 index 00000000..63e8941e Binary files /dev/null and b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Content/audio/ui.wav differ diff --git a/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Content/fonts/04B_30.spritefont b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Content/fonts/04B_30.spritefont new file mode 100644 index 00000000..63d4728c --- /dev/null +++ b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Content/fonts/04B_30.spritefont @@ -0,0 +1,16 @@ + + + + 04B_30.ttf + 17.5 + 0 + true + + + + + ~ + + + + diff --git a/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Content/fonts/04B_30.ttf b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Content/fonts/04B_30.ttf new file mode 100644 index 00000000..4b93740c Binary files /dev/null and b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Content/fonts/04B_30.ttf differ diff --git a/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Content/fonts/04B_30_5x.spritefont b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Content/fonts/04B_30_5x.spritefont new file mode 100644 index 00000000..dd239a53 --- /dev/null +++ b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Content/fonts/04B_30_5x.spritefont @@ -0,0 +1,16 @@ + + + + 04B_30.ttf + 87.5 + 0 + true + + + + + ~ + + + + diff --git a/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Content/fonts/04b_30.fnt b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Content/fonts/04b_30.fnt new file mode 100644 index 00000000..772f8c54 --- /dev/null +++ b/Tutorials/2dShaders/src/06-Color-Swap-Effect/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/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Content/images/atlas-definition.xml b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Content/images/atlas-definition.xml new file mode 100644 index 00000000..21772022 --- /dev/null +++ b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Content/images/atlas-definition.xml @@ -0,0 +1,34 @@ + + + images/atlas + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Content/images/atlas.png b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Content/images/atlas.png new file mode 100644 index 00000000..f7def20f Binary files /dev/null and b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Content/images/atlas.png differ diff --git a/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Content/images/background-pattern.png b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Content/images/background-pattern.png new file mode 100644 index 00000000..2d8d878e Binary files /dev/null and b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Content/images/background-pattern.png differ diff --git a/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Content/images/color-map-1.png b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Content/images/color-map-1.png new file mode 100644 index 00000000..b5e3dc5a Binary files /dev/null and b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Content/images/color-map-1.png differ diff --git a/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Content/images/color-map-2.png b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Content/images/color-map-2.png new file mode 100644 index 00000000..2789bee8 Binary files /dev/null and b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Content/images/color-map-2.png differ diff --git a/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Content/images/color-map-dark-purple.png b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Content/images/color-map-dark-purple.png new file mode 100644 index 00000000..ffe9516e Binary files /dev/null and b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Content/images/color-map-dark-purple.png differ diff --git a/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Content/images/color-map-green.png b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Content/images/color-map-green.png new file mode 100644 index 00000000..87656c81 Binary files /dev/null and b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Content/images/color-map-green.png differ diff --git a/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Content/images/color-map-pink.png b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Content/images/color-map-pink.png new file mode 100644 index 00000000..e8910ded Binary files /dev/null and b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Content/images/color-map-pink.png differ diff --git a/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Content/images/logo.png b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Content/images/logo.png new file mode 100644 index 00000000..1509036c Binary files /dev/null and b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Content/images/logo.png differ diff --git a/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Content/images/tilemap-definition.xml b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Content/images/tilemap-definition.xml new file mode 100644 index 00000000..85658c60 --- /dev/null +++ b/Tutorials/2dShaders/src/06-Color-Swap-Effect/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/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/DungeonSlime.csproj b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/DungeonSlime.csproj new file mode 100644 index 00000000..644e242c --- /dev/null +++ b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/DungeonSlime.csproj @@ -0,0 +1,78 @@ + + + WinExe + net8.0 + Major + false + false + + + app.manifest + Icon.ico + + + bin/$(Configuration)/$(TargetFramework) + + + + + + + + Icon.ico + + + Icon.bmp + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Game1.cs b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Game1.cs new file mode 100644 index 00000000..981a4c55 --- /dev/null +++ b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Game1.cs @@ -0,0 +1,75 @@ +using DungeonSlime.Scenes; +using Microsoft.Xna.Framework; +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() + { + // Allow the Core class to load any content. + base.LoadContent(); + + // Load the background theme music + _themeSong = Content.Load("audio/theme"); + } +} diff --git a/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/GameController.cs b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/GameController.cs new file mode 100644 index 00000000..a85df08f --- /dev/null +++ b/Tutorials/2dShaders/src/06-Color-Swap-Effect/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/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/GameObjects/Bat.cs b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/GameObjects/Bat.cs new file mode 100644 index 00000000..ddc855ed --- /dev/null +++ b/Tutorials/2dShaders/src/06-Color-Swap-Effect/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/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/GameObjects/Slime.cs b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/GameObjects/Slime.cs new file mode 100644 index 00000000..2198fc9a --- /dev/null +++ b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/GameObjects/Slime.cs @@ -0,0 +1,272 @@ +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 size of the slime + public int Size => _segments.Count; + + // 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(Action configureSpriteBatch) + { + // Iterate through each segment and draw it + for (var i = 0 ; i < _segments.Count; i ++) + { + var segment = _segments[i]; + // 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); + + // Allow the sprite batch to be configured before each call. + configureSpriteBatch(i); + + // 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/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/GameObjects/SlimeSegment.cs b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/GameObjects/SlimeSegment.cs new file mode 100644 index 00000000..b00189eb --- /dev/null +++ b/Tutorials/2dShaders/src/06-Color-Swap-Effect/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/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Icon.bmp b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Icon.bmp new file mode 100644 index 00000000..2b481653 Binary files /dev/null and b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Icon.bmp differ diff --git a/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Icon.ico b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Icon.ico new file mode 100644 index 00000000..7d9dec18 Binary files /dev/null and b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Icon.ico differ diff --git a/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Program.cs b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Program.cs new file mode 100644 index 00000000..4d9be314 --- /dev/null +++ b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Program.cs @@ -0,0 +1,3 @@ +MonoGameLibrary.Content.ContentManagerExtensions.StartContentWatcherTask(); +using var game = new DungeonSlime.Game1(); +game.Run(); \ No newline at end of file diff --git a/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Scenes/GameScene.cs b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Scenes/GameScene.cs new file mode 100644 index 00000000..354044e1 --- /dev/null +++ b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Scenes/GameScene.cs @@ -0,0 +1,467 @@ +using System; +using System.Collections.Generic; +using DungeonSlime.GameObjects; +using DungeonSlime.UI; +using ImGuiNET; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Audio; +using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; +using MonoGameGum; +using MonoGameLibrary; +using MonoGameLibrary.Content; +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 color swap shader material. + private Material _colorSwapMaterial; + + // The amount of saturation to provide the grayscale shader effect + private float _saturation = 1.0f; + private Texture2D _colorMap; + private RedColorMap _slimeColorMap; + private TimeSpan _lastGrowTime; + + // 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 colorSwap material + _colorSwapMaterial = Core.SharedContent.WatchMaterial("effects/colorSwapEffect"); + _colorSwapMaterial.IsDebugVisible = true; + + _colorMap = Content.Load("images/color-map-dark-purple"); + _colorSwapMaterial.SetParameter("ColorMap", _colorMap); + + _slimeColorMap = new RedColorMap(); + _slimeColorMap.SetColorsByExistingColorMap(_colorMap); + _slimeColorMap.SetColorsByRedValue(new Dictionary + { + // main color + [32] = Color.LightSteelBlue, + }, false); + + _colorSwapMaterial.SetParameter("ColorMap", _slimeColorMap.ColorMap); + } + + public override void Update(GameTime gameTime) + { + // Ensure the UI is always updated + _ui.Update(gameTime); + + // Update the colorSwap material if it was changed + _colorSwapMaterial.Update(); + + 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; + } + } + else + { + _saturation = 1; + } + + // 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(gameTime); + } + + private void CollisionChecks(GameTime gameTime) + { + // 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(); + + // Remember when the last time the slime grew + _lastGrowTime = gameTime.TotalGameTime; + + // 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); + + _colorSwapMaterial.SetParameter("Saturation", _saturation); + Core.SpriteBatch.Begin( + samplerState: SamplerState.PointClamp, + sortMode: SpriteSortMode.Immediate, + effect: _colorSwapMaterial.Effect); + + // Update the colorMap + _colorSwapMaterial.SetParameter("ColorMap", _colorMap); + + // Draw the tilemap + _tilemap.Draw(Core.SpriteBatch); + + // Draw the bat. + _bat.Draw(); + + // Draw the slime. + _slime.Draw(segmentIndex => + { + const int flashTimeMs = 125; + var map = _colorMap; + var elapsedMs = (gameTime.TotalGameTime.TotalMilliseconds - _lastGrowTime.TotalMilliseconds); + var intervalsAgo = (int)(elapsedMs / flashTimeMs); + + if (intervalsAgo < _slime.Size && (intervalsAgo - segmentIndex) % _slime.Size == 0) + { + map = _slimeColorMap.ColorMap; + } + + _colorSwapMaterial.SetParameter("ColorMap", map); + }); + + // Always end the sprite batch when finished. + Core.SpriteBatch.End(); + + // Draw the UI + _ui.Draw(); + } +} diff --git a/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Scenes/TitleScene.cs b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Scenes/TitleScene.cs new file mode 100644 index 00000000..8a4dacea --- /dev/null +++ b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/Scenes/TitleScene.cs @@ -0,0 +1,345 @@ +using System; +using DungeonSlime.UI; +using Gum.Forms.Controls; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Audio; +using Microsoft.Xna.Framework.Graphics; +using MonoGameGum; +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/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/UI/AnimatedButton.cs b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/UI/AnimatedButton.cs new file mode 100644 index 00000000..4cce6ee5 --- /dev/null +++ b/Tutorials/2dShaders/src/06-Color-Swap-Effect/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 by AnimationChains 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/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/UI/GameSceneUI.cs b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/UI/GameSceneUI.cs new file mode 100644 index 00000000..498655c2 --- /dev/null +++ b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/UI/GameSceneUI.cs @@ -0,0 +1,340 @@ +using System; +using Gum.DataTypes; +using Gum.Forms.Controls; +using Gum.Managers; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Audio; +using Microsoft.Xna.Framework.Content; +using MonoGameGum; +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/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/UI/OptionsSlider.cs b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/UI/OptionsSlider.cs new file mode 100644 index 00000000..53d6ee94 --- /dev/null +++ b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/UI/OptionsSlider.cs @@ -0,0 +1,253 @@ +using System; +using Gum.DataTypes; +using Gum.DataTypes.Variables; +using Gum.Forms.Controls; +using Gum.Managers; +using Microsoft.Xna.Framework; +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/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/app.manifest b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/app.manifest new file mode 100644 index 00000000..caf45166 --- /dev/null +++ b/Tutorials/2dShaders/src/06-Color-Swap-Effect/DungeonSlime/app.manifest @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true/pm + permonitorv2,permonitor + + + + diff --git a/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/Audio/AudioController.cs b/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/Audio/AudioController.cs new file mode 100644 index 00000000..1bffd636 --- /dev/null +++ b/Tutorials/2dShaders/src/06-Color-Swap-Effect/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/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/Circle.cs b/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/Circle.cs new file mode 100644 index 00000000..0bb691bc --- /dev/null +++ b/Tutorials/2dShaders/src/06-Color-Swap-Effect/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/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/Content/ContentManagerExtensions.cs b/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/Content/ContentManagerExtensions.cs new file mode 100644 index 00000000..e012836c --- /dev/null +++ b/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/Content/ContentManagerExtensions.cs @@ -0,0 +1,168 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; +using MonoGameLibrary.Graphics; + +namespace MonoGameLibrary.Content; + +public static class ContentManagerExtensions +{ + /// + /// Check if the given xnb file has a newer write-time than the last loaded version of the asset. + /// If the local file has been updated, reload the asset and return true. + /// + /// The that loaded the asset originally + /// The asset that will be reloaded if the xnb file is newer + /// If the asset has been reloaded, this out parameter will be set to the previous version of the asset before the newer version was loaded. + /// + /// true when asset was reloaded; false otherwise. + /// + public static bool TryRefresh(this ContentManager manager, WatchedAsset watchedAsset, out T oldAsset) + { + oldAsset = default; + + if (manager != watchedAsset.Owner) + throw new ArgumentException($"Used the wrong ContentManager to refresh {watchedAsset.AssetName}"); + + var path = Path.Combine(manager.RootDirectory, watchedAsset.AssetName) + ".xnb"; + var lastWriteTime = File.GetLastWriteTime(path); + + if (lastWriteTime <= watchedAsset.UpdatedAt) + { + return false; + } + + if (IsFileLocked(path)) return false; // wait for the file to not be locked. + + manager.UnloadAsset(watchedAsset.AssetName); + oldAsset = watchedAsset.Asset; + watchedAsset.Asset = manager.Load(watchedAsset.AssetName); + watchedAsset.UpdatedAt = lastWriteTime; + + return true; + } + + private static bool IsFileLocked(string path) + { + try + { + using FileStream _ = new FileStream(path, FileMode.Open, FileAccess.ReadWrite, FileShare.None); + // File is not locked + return false; + } + catch (IOException) + { + // File is locked or inaccessible + return true; + } + } + + /// + /// Load an asset and wrap it with the metadata required to refresh it later using the function + /// + /// + /// + /// + /// + public static WatchedAsset Watch(this ContentManager manager, string assetName) + { + var asset = manager.Load(assetName); + return new WatchedAsset + { + AssetName = assetName, + Asset = asset, + UpdatedAt = DateTimeOffset.Now, + Owner = manager + }; + } + + /// + /// Load an Effect into the wrapper class + /// + /// + /// + /// + public static Material WatchMaterial(this ContentManager manager, string assetName) + { + return new Material(manager.Watch(assetName)); + } + + + [Conditional("DEBUG")] + public static void StartContentWatcherTask() + { + var args = Environment.GetCommandLineArgs(); + foreach (var arg in args) + { + // if the application was started with the --no-reload option, then do not start the watcher. + if (arg == "--no-reload") return; + } + + // identify the project directory + var projectFile = Assembly.GetEntryAssembly().GetName().Name + ".csproj"; + var current = Directory.GetCurrentDirectory(); + string projectDirectory = null; + + while (current != null && projectDirectory == null) + { + if (File.Exists(Path.Combine(current, projectFile))) + { + // the valid project csproj exists in the directory + projectDirectory = current; + } + else + { + // try looking in the parent directory. + // When there is no parent directory, the variable becomes 'null' + current = Path.GetDirectoryName(current); + } + } + + // if no valid project was identified, then it is impossible to start the watcher + if (string.IsNullOrEmpty(projectDirectory)) return; + + // start the watcher process + var process = Process.Start(new ProcessStartInfo + { + FileName = "dotnet", + Arguments = "build -t:WatchContent --tl:off", + WorkingDirectory = projectDirectory, + WindowStyle = ProcessWindowStyle.Normal, + UseShellExecute = false, + CreateNoWindow = false + }); + + // when this program exits, make sure to emit a kill signal to the watcher process + AppDomain.CurrentDomain.ProcessExit += (_, __) => + { + try + { + if (!process.HasExited) + { + process.Kill(entireProcessTree: true); + } + } + catch + { + /* ignore */ + } + }; + AppDomain.CurrentDomain.UnhandledException += (sender, e) => + { + try + { + if (!process.HasExited) + { + process.Kill(entireProcessTree: true); + } + } + catch + { + /* ignore */ + } + }; + } +} \ No newline at end of file diff --git a/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/Content/WatchedAsset.cs b/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/Content/WatchedAsset.cs new file mode 100644 index 00000000..39008666 --- /dev/null +++ b/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/Content/WatchedAsset.cs @@ -0,0 +1,33 @@ +using System; +using Microsoft.Xna.Framework.Content; + +namespace MonoGameLibrary.Content; + +public class WatchedAsset +{ + /// + /// The latest version of the asset. + /// + public T Asset { get; set; } + + /// + /// The last time the was loaded into memory. + /// + public DateTimeOffset UpdatedAt { get; set; } + + /// + /// The name of the . This is the name used to load the asset from disk. + /// + public string AssetName { get; init; } + + /// + /// The instance that loaded the asset. + /// + public ContentManager Owner { get; init; } + + + public bool TryRefresh(out T oldAsset) + { + return Owner.TryRefresh(this, out oldAsset); + } +} \ No newline at end of file diff --git a/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/Core.cs b/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/Core.cs new file mode 100644 index 00000000..1bcd962a --- /dev/null +++ b/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/Core.cs @@ -0,0 +1,278 @@ +using System; +using System.Collections.Generic; +using ImGuiNET.SampleProgram.XNA; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; +using MonoGameLibrary.Audio; +using MonoGameLibrary.Content; +using MonoGameLibrary.Graphics; +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; + + /// + /// The material that is used when changing scenes + /// + public static Material SceneTransitionMaterial { get; private set; } + + /// + /// A set of grayscale gradient textures to use as transition guides + /// + public static List SceneTransitionTextures { get; private set; } + + /// + /// The current transition between scenes + /// + public static SceneTransition SceneTransition { get; protected set; } = SceneTransition.Open(1000); + + /// + /// 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 a runtime generated 1x1 pixel texture. + /// + public static Texture2D Pixel { get; private set; } + + /// + /// Gets the ImGui renderer used for debug UIs. + /// + public static ImGuiRenderer ImGuiRenderer { get; private set; } + + /// + /// Gets the content manager used to load global assets. + /// + public static new ContentManager Content { get; private set; } + + /// + /// Gets the content manager that can load global assets from the SharedContent folder. + /// + public static ContentManager SharedContent { 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"; + + // Set the core's shared content manager, pointing to the SharedContent folder. + SharedContent = new ContentManager(Services, "SharedContent"); + + // 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 the ImGui renderer. + ImGuiRenderer = new ImGuiRenderer(this); + ImGuiRenderer.RebuildFontAtlas(); + + // Create a new input manager + Input = new InputManager(); + + // Create a new audio controller. + Audio = new AudioController(); + + // Create a 1x1 white pixel texture for drawing quads. + Pixel = new Texture2D(GraphicsDevice, 1, 1); + Pixel.SetData(new Color[]{ Color.White }); + } + + protected override void LoadContent() + { + base.LoadContent(); + SceneTransitionMaterial = SharedContent.WatchMaterial("effects/sceneTransitionEffect"); + SceneTransitionMaterial.SetParameter("EdgeWidth", .05f); + + SceneTransitionTextures = new List(); + SceneTransitionTextures.Add(SharedContent.Load("images/angled")); + SceneTransitionTextures.Add(SharedContent.Load("images/concave")); + SceneTransitionTextures.Add(SharedContent.Load("images/radial")); + SceneTransitionTextures.Add(SharedContent.Load("images/ripple")); + } + + 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 && SceneTransition.IsComplete) + { + TransitionScene(); + } + + // If there is an active scene, update it. + if (s_activeScene != null) + { + s_activeScene.Update(gameTime); + } + + // Check if the scene transition material needs to be reloaded. + SceneTransitionMaterial.SetParameter("Progress", SceneTransition.DirectionalRatio); + SceneTransitionMaterial.Update(); + + 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); + } + + // Draw the scene transition quad + SpriteBatch.Begin(effect: SceneTransitionMaterial.Effect); + SpriteBatch.Draw(SceneTransitionTextures[SceneTransition.TextureIndex % SceneTransitionTextures.Count], GraphicsDevice.Viewport.Bounds, Color.White); + SpriteBatch.End(); + + Material.DrawVisibleDebugUi(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; + SceneTransition = SceneTransition.Close(250); + } + } + + private static void TransitionScene() + { + SceneTransition = SceneTransition.Open(500); + + // 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/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/Graphics/AnimatedSprite.cs b/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/Graphics/AnimatedSprite.cs new file mode 100644 index 00000000..a1a3594e --- /dev/null +++ b/Tutorials/2dShaders/src/06-Color-Swap-Effect/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/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/Graphics/Animation.cs b/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/Graphics/Animation.cs new file mode 100644 index 00000000..44d61b65 --- /dev/null +++ b/Tutorials/2dShaders/src/06-Color-Swap-Effect/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/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/Graphics/Material.cs b/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/Graphics/Material.cs new file mode 100644 index 00000000..f1a22a83 --- /dev/null +++ b/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/Graphics/Material.cs @@ -0,0 +1,303 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using ImGuiNET; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using MonoGameLibrary.Content; +namespace MonoGameLibrary.Graphics; + +public class Material +{ + // materials that will be drawn during the standard debug UI pass. + private static HashSet s_debugMaterials = new HashSet(); + + /// + /// The hot-reloadable asset that this material is using + /// + public WatchedAsset Asset; + + /// + /// A cached version of the parameters available in the shader + /// + public Dictionary ParameterMap; + + /// + /// The currently loaded Effect that this material is using + /// + public Effect Effect => Asset.Asset; + + /// + /// Enable this variable to visualize the debugUI for the material + /// + public bool IsDebugVisible + { + get + { + return s_debugMaterials.Contains(this); + } + set + { + if (!value) + { + s_debugMaterials.Remove(this); + } + else + { + s_debugMaterials.Add(this); + } + } + } + + /// + /// When true, the debug UI will override parameters + /// + public bool DebugOverride; + + public Material(WatchedAsset asset) + { + Asset = asset; + UpdateParameterCache(); + } + + public void SetParameter(string name, float value) + { + if (DebugOverride) return; + + if (TryGetParameter(name, out var parameter)) + { + parameter.SetValue(value); + } + else + { + Console.WriteLine($"Warning: cannot set shader parameter=[{name}] because it does not exist in the compiled shader=[{Asset.AssetName}]"); + } + } + + public void SetParameter(string name, Matrix value) + { + if (DebugOverride) return; + + if (TryGetParameter(name, out var parameter)) + { + parameter.SetValue(value); + } + else + { + Console.WriteLine($"Warning: cannot set shader parameter=[{name}] because it does not exist in the compiled shader=[{Asset.AssetName}]"); + } + } + + public void SetParameter(string name, Vector2 value) + { + if (DebugOverride) return; + + if (TryGetParameter(name, out var parameter)) + { + parameter.SetValue(value); + } + else + { + Console.WriteLine($"Warning: cannot set shader parameter=[{name}] because it does not exist in the compiled shader=[{Asset.AssetName}]"); + } + } + + public void SetParameter(string name, Texture2D value) + { + if (DebugOverride) return; + + if (TryGetParameter(name, out var parameter)) + { + parameter.SetValue(value); + } + else + { + Console.WriteLine($"Warning: cannot set shader parameter=[{name}] because it does not exist in the compiled shader=[{Asset.AssetName}]"); + } + } + + /// + /// Check if the given parameter name is available in the compiled shader code. + /// Remember that a parameter will be optimized out of a shader if it is not being used + /// in the shader's return value. + /// + /// + /// + /// + public bool TryGetParameter(string name, out EffectParameter parameter) + { + return ParameterMap.TryGetValue(name, out parameter); + } + + /// + /// Rebuild the based on the current parameters available in the effect instance + /// + public void UpdateParameterCache() + { + ParameterMap = Effect.Parameters.ToDictionary(p => p.Name); + } + + [Conditional("DEBUG")] + public void Update() + { + if (Asset.TryRefresh(out var oldAsset)) + { + UpdateParameterCache(); + + foreach (var oldParam in oldAsset.Parameters) + { + if (!TryGetParameter(oldParam.Name, out var newParam)) + { + continue; + } + + switch (oldParam.ParameterClass) + { + case EffectParameterClass.Scalar: + newParam.SetValue(oldParam.GetValueSingle()); + break; + case EffectParameterClass.Matrix: + newParam.SetValue(oldParam.GetValueMatrix()); + break; + case EffectParameterClass.Vector when oldParam.ColumnCount == 2: // float2 + newParam.SetValue(oldParam.GetValueVector2()); + break; + case EffectParameterClass.Object: + newParam.SetValue(oldParam.GetValueTexture2D()); + break; + default: + Console.WriteLine("Warning: shader reload system was not able to re-apply property. " + + $"shader=[{Effect.Name}] " + + $"property=[{oldParam.Name}] " + + $"class=[{oldParam.ParameterClass}]"); + break; + } + } + } + } + + + + [Conditional("DEBUG")] + public void DrawDebug() + { + ImGui.Begin(Effect.Name); + + var currentSize = ImGui.GetWindowSize(); + ImGui.SetWindowSize(Effect.Name, new System.Numerics.Vector2(MathHelper.Max(100, currentSize.X), MathHelper.Max(100, currentSize.Y))); + + ImGui.AlignTextToFramePadding(); + ImGui.Text("Last Updated"); + ImGui.SameLine(); + ImGui.LabelText("##last-updated", Asset.UpdatedAt.ToString() + $" ({(DateTimeOffset.Now - Asset.UpdatedAt).ToString(@"h\:mm\:ss")} ago)"); + + ImGui.AlignTextToFramePadding(); + ImGui.Text("Override Values"); + ImGui.SameLine(); + ImGui.Checkbox("##override-values", ref DebugOverride); + + ImGui.NewLine(); + + bool ScalarSlider(string key, ref float value) + { + float min = 0; + float max = 1; + + return ImGui.SliderFloat($"##_prop{key}", ref value, min, max); + } + + foreach (var prop in ParameterMap) + { + switch (prop.Value.ParameterType, prop.Value.ParameterClass) + { + case (EffectParameterType.Single, EffectParameterClass.Scalar): + ImGui.AlignTextToFramePadding(); + ImGui.Text(prop.Key); + ImGui.SameLine(); + + var value = prop.Value.GetValueSingle(); + if (ScalarSlider(prop.Key, ref value)) + { + prop.Value.SetValue(value); + } + break; + + case (EffectParameterType.Single, EffectParameterClass.Vector): + ImGui.AlignTextToFramePadding(); + ImGui.Text(prop.Key); + + var vec2Value = prop.Value.GetValueVector2(); + ImGui.Indent(); + + ImGui.Text("X"); + ImGui.SameLine(); + + if (ScalarSlider(prop.Key + ".x", ref vec2Value.X)) + { + prop.Value.SetValue(vec2Value); + } + + ImGui.Text("Y"); + ImGui.SameLine(); + if (ScalarSlider(prop.Key + ".y", ref vec2Value.Y)) + { + prop.Value.SetValue(vec2Value); + } + ImGui.Unindent(); + break; + + case (EffectParameterType.Texture2D, EffectParameterClass.Object): + ImGui.AlignTextToFramePadding(); + ImGui.Text(prop.Key); + ImGui.SameLine(); + + var texture = prop.Value.GetValueTexture2D(); + if (texture != null) + { + var texturePtr = Core.ImGuiRenderer.BindTexture(texture); + ImGui.Image(texturePtr, new System.Numerics.Vector2(texture.Width, texture.Height)); + } + else + { + ImGui.Text("(null)"); + } + break; + + default: + ImGui.AlignTextToFramePadding(); + ImGui.Text(prop.Key); + ImGui.SameLine(); + ImGui.Text($"(unsupported {prop.Value.ParameterType}, {prop.Value.ParameterClass})"); + break; + } + } + ImGui.End(); + } + + [Conditional("DEBUG")] + public static void DrawVisibleDebugUi(GameTime gameTime) + { + // first, cull any materials that are not visible, or disposed. + var toRemove = new List(); + foreach (var material in s_debugMaterials) + { + if (material.Effect.IsDisposed) + { + toRemove.Add(material); + } + } + + foreach (var material in toRemove) + { + s_debugMaterials.Remove(material); + } + + Core.ImGuiRenderer.BeforeLayout(gameTime); + foreach (var material in s_debugMaterials) + { + material.DrawDebug(); + } + Core.ImGuiRenderer.AfterLayout(); + } +} \ No newline at end of file diff --git a/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/Graphics/RedColorMap.cs b/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/Graphics/RedColorMap.cs new file mode 100644 index 00000000..d6e0bf3f --- /dev/null +++ b/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/Graphics/RedColorMap.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace MonoGameLibrary.Graphics; + +public class RedColorMap +{ + public Texture2D ColorMap { get; set; } + + public RedColorMap() + { + ColorMap = new Texture2D(Core.GraphicsDevice, 256, 1, false, SurfaceFormat.Color); + } + + /// + /// Given a dictionary of red-color values (0 to 255) to swapColors, + /// Set the values of the so that it can be used + /// As the ColorMap parameter in the colorSwapEffect. + /// + public void SetColorsByRedValue(Dictionary map, bool overWrite = true) + { + var pixelData = new Color[ColorMap.Width]; + ColorMap.GetData(pixelData); + + for (var i = 0; i < pixelData.Length; i++) + { + // if the given color dictionary contains a color value for this red index, use it. + if (map.TryGetValue(i, out var swapColor)) + { + pixelData[i] = swapColor; + } + else if (overWrite) + { + // otherwise, default the pixel to transparent + pixelData[i] = Color.Transparent; + } + } + + ColorMap.SetData(pixelData); + } + + public void SetColorsByExistingColorMap(Texture2D existingColorMap) + { + var existingPixels = new Color[256]; + existingColorMap.GetData(existingPixels); + + var map = new Dictionary(); + for (var i = 0; i < existingPixels.Length; i++) + { + map[i] = existingPixels[i]; + } + + SetColorsByRedValue(map); + } +} \ No newline at end of file diff --git a/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/Graphics/Sprite.cs b/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/Graphics/Sprite.cs new file mode 100644 index 00000000..20c44f0b --- /dev/null +++ b/Tutorials/2dShaders/src/06-Color-Swap-Effect/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/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/Graphics/TextureAtlas.cs b/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/Graphics/TextureAtlas.cs new file mode 100644 index 00000000..e48c9abd --- /dev/null +++ b/Tutorials/2dShaders/src/06-Color-Swap-Effect/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/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/Graphics/TextureRegion.cs b/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/Graphics/TextureRegion.cs new file mode 100644 index 00000000..ecd69030 --- /dev/null +++ b/Tutorials/2dShaders/src/06-Color-Swap-Effect/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/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/Graphics/Tilemap.cs b/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/Graphics/Tilemap.cs new file mode 100644 index 00000000..96e1ee5e --- /dev/null +++ b/Tutorials/2dShaders/src/06-Color-Swap-Effect/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/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/Graphics/Tileset.cs b/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/Graphics/Tileset.cs new file mode 100644 index 00000000..80c2e65a --- /dev/null +++ b/Tutorials/2dShaders/src/06-Color-Swap-Effect/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/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/ImGui/DrawVertDeclaration.cs b/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/ImGui/DrawVertDeclaration.cs new file mode 100644 index 00000000..d846e7da --- /dev/null +++ b/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/ImGui/DrawVertDeclaration.cs @@ -0,0 +1,29 @@ +using Microsoft.Xna.Framework.Graphics; + +namespace ImGuiNET.SampleProgram.XNA +{ + public static class DrawVertDeclaration + { + public static readonly VertexDeclaration Declaration; + + public static readonly int Size; + + static DrawVertDeclaration() + { + unsafe { Size = sizeof(ImDrawVert); } + + Declaration = new VertexDeclaration( + Size, + + // Position + new VertexElement(0, VertexElementFormat.Vector2, VertexElementUsage.Position, 0), + + // UV + new VertexElement(8, VertexElementFormat.Vector2, VertexElementUsage.TextureCoordinate, 0), + + // Color + new VertexElement(16, VertexElementFormat.Color, VertexElementUsage.Color, 0) + ); + } + } +} \ No newline at end of file diff --git a/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/ImGui/ImGuiRenderer.cs b/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/ImGui/ImGuiRenderer.cs new file mode 100644 index 00000000..e2cc1a29 --- /dev/null +++ b/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/ImGui/ImGuiRenderer.cs @@ -0,0 +1,436 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace ImGuiNET.SampleProgram.XNA +{ + /// + /// ImGui renderer for use with XNA-likes (FNA & MonoGame) + /// + public class ImGuiRenderer + { + private Game _game; + + // Graphics + private GraphicsDevice _graphicsDevice; + + private BasicEffect _effect; + private RasterizerState _rasterizerState; + + private byte[] _vertexData; + private VertexBuffer _vertexBuffer; + private int _vertexBufferSize; + + private byte[] _indexData; + private IndexBuffer _indexBuffer; + private int _indexBufferSize; + + // Textures + private Dictionary _loadedTextures; + + private int _textureId; + private IntPtr? _fontTextureId; + + // Input + private int _scrollWheelValue; + private int _horizontalScrollWheelValue; + private readonly float WHEEL_DELTA = 120; + private Keys[] _allKeys = Enum.GetValues(); + + public ImGuiRenderer(Game game) + { + var context = ImGui.CreateContext(); + ImGui.SetCurrentContext(context); + + _game = game ?? throw new ArgumentNullException(nameof(game)); + _graphicsDevice = game.GraphicsDevice; + + _loadedTextures = new Dictionary(); + + _rasterizerState = new RasterizerState() + { + CullMode = CullMode.None, + DepthBias = 0, + FillMode = FillMode.Solid, + MultiSampleAntiAlias = false, + ScissorTestEnable = true, + SlopeScaleDepthBias = 0 + }; + + SetupInput(); + } + + #region ImGuiRenderer + + /// + /// Creates a texture and loads the font data from ImGui. Should be called when the is initialized but before any rendering is done + /// + public virtual unsafe void RebuildFontAtlas() + { + // Get font texture from ImGui + var io = ImGui.GetIO(); + io.Fonts.GetTexDataAsRGBA32(out byte* pixelData, out int width, out int height, out int bytesPerPixel); + + // Copy the data to a managed array + var pixels = new byte[width * height * bytesPerPixel]; + unsafe { Marshal.Copy(new IntPtr(pixelData), pixels, 0, pixels.Length); } + + // Create and register the texture as an XNA texture + var tex2d = new Texture2D(_graphicsDevice, width, height, false, SurfaceFormat.Color); + tex2d.SetData(pixels); + + // Should a texture already have been build previously, unbind it first so it can be deallocated + if (_fontTextureId.HasValue) UnbindTexture(_fontTextureId.Value); + + // Bind the new texture to an ImGui-friendly id + _fontTextureId = BindTexture(tex2d); + + // Let ImGui know where to find the texture + io.Fonts.SetTexID(_fontTextureId.Value); + io.Fonts.ClearTexData(); // Clears CPU side texture data + } + + /// + /// Creates a pointer to a texture, which can be passed through ImGui calls such as . That pointer is then used by ImGui to let us know what texture to draw + /// + public virtual IntPtr BindTexture(Texture2D texture) + { + var id = new IntPtr(_textureId++); + + _loadedTextures.Add(id, texture); + + return id; + } + + /// + /// Removes a previously created texture pointer, releasing its reference and allowing it to be deallocated + /// + public virtual void UnbindTexture(IntPtr textureId) + { + _loadedTextures.Remove(textureId); + } + + /// + /// Sets up ImGui for a new frame, should be called at frame start + /// + public virtual void BeforeLayout(GameTime gameTime) + { + ImGui.GetIO().DeltaTime = (float)gameTime.ElapsedGameTime.TotalSeconds; + + UpdateInput(); + + ImGui.NewFrame(); + } + + /// + /// Asks ImGui for the generated geometry data and sends it to the graphics pipeline, should be called after the UI is drawn using ImGui.** calls + /// + public virtual void AfterLayout() + { + ImGui.Render(); + + unsafe { RenderDrawData(ImGui.GetDrawData()); } + } + + #endregion ImGuiRenderer + + #region Setup & Update + + /// + /// Setup key input event handler. + /// + protected virtual void SetupInput() + { + var io = ImGui.GetIO(); + + // MonoGame-specific ////////////////////// + _game.Window.TextInput += (s, a) => + { + if (a.Character == '\t') return; + io.AddInputCharacter(a.Character); + }; + + /////////////////////////////////////////// + + // FNA-specific /////////////////////////// + //TextInputEXT.TextInput += c => + //{ + // if (c == '\t') return; + + // ImGui.GetIO().AddInputCharacter(c); + //}; + /////////////////////////////////////////// + } + + /// + /// Updates the to the current matrices and texture + /// + protected virtual Effect UpdateEffect(Texture2D texture) + { + _effect = _effect ?? new BasicEffect(_graphicsDevice); + + var io = ImGui.GetIO(); + + _effect.World = Matrix.Identity; + _effect.View = Matrix.Identity; + _effect.Projection = Matrix.CreateOrthographicOffCenter(0f, io.DisplaySize.X, io.DisplaySize.Y, 0f, -1f, 1f); + _effect.TextureEnabled = true; + _effect.Texture = texture; + _effect.VertexColorEnabled = true; + + return _effect; + } + + /// + /// Sends XNA input state to ImGui + /// + protected virtual void UpdateInput() + { + if (!_game.IsActive) return; + + var io = ImGui.GetIO(); + + var mouse = Mouse.GetState(); + var keyboard = Keyboard.GetState(); + io.AddMousePosEvent(mouse.X, mouse.Y); + io.AddMouseButtonEvent(0, mouse.LeftButton == ButtonState.Pressed); + io.AddMouseButtonEvent(1, mouse.RightButton == ButtonState.Pressed); + io.AddMouseButtonEvent(2, mouse.MiddleButton == ButtonState.Pressed); + io.AddMouseButtonEvent(3, mouse.XButton1 == ButtonState.Pressed); + io.AddMouseButtonEvent(4, mouse.XButton2 == ButtonState.Pressed); + + io.AddMouseWheelEvent( + (mouse.HorizontalScrollWheelValue - _horizontalScrollWheelValue) / WHEEL_DELTA, + (mouse.ScrollWheelValue - _scrollWheelValue) / WHEEL_DELTA); + _scrollWheelValue = mouse.ScrollWheelValue; + _horizontalScrollWheelValue = mouse.HorizontalScrollWheelValue; + + foreach (var key in _allKeys) + { + if (TryMapKeys(key, out ImGuiKey imguikey)) + { + io.AddKeyEvent(imguikey, keyboard.IsKeyDown(key)); + } + } + + io.DisplaySize = new System.Numerics.Vector2(_graphicsDevice.PresentationParameters.BackBufferWidth, _graphicsDevice.PresentationParameters.BackBufferHeight); + io.DisplayFramebufferScale = new System.Numerics.Vector2(1f, 1f); + } + + private bool TryMapKeys(Keys key, out ImGuiKey imguikey) + { + //Special case not handed in the switch... + //If the actual key we put in is "None", return none and true. + //otherwise, return none and false. + if (key == Keys.None) + { + imguikey = ImGuiKey.None; + return true; + } + + imguikey = key switch + { + Keys.Back => ImGuiKey.Backspace, + Keys.Tab => ImGuiKey.Tab, + Keys.Enter => ImGuiKey.Enter, + Keys.CapsLock => ImGuiKey.CapsLock, + Keys.Escape => ImGuiKey.Escape, + Keys.Space => ImGuiKey.Space, + Keys.PageUp => ImGuiKey.PageUp, + Keys.PageDown => ImGuiKey.PageDown, + Keys.End => ImGuiKey.End, + Keys.Home => ImGuiKey.Home, + Keys.Left => ImGuiKey.LeftArrow, + Keys.Right => ImGuiKey.RightArrow, + Keys.Up => ImGuiKey.UpArrow, + Keys.Down => ImGuiKey.DownArrow, + Keys.PrintScreen => ImGuiKey.PrintScreen, + Keys.Insert => ImGuiKey.Insert, + Keys.Delete => ImGuiKey.Delete, + >= Keys.D0 and <= Keys.D9 => ImGuiKey._0 + (key - Keys.D0), + >= Keys.A and <= Keys.Z => ImGuiKey.A + (key - Keys.A), + >= Keys.NumPad0 and <= Keys.NumPad9 => ImGuiKey.Keypad0 + (key - Keys.NumPad0), + Keys.Multiply => ImGuiKey.KeypadMultiply, + Keys.Add => ImGuiKey.KeypadAdd, + Keys.Subtract => ImGuiKey.KeypadSubtract, + Keys.Decimal => ImGuiKey.KeypadDecimal, + Keys.Divide => ImGuiKey.KeypadDivide, + >= Keys.F1 and <= Keys.F24 => ImGuiKey.F1 + (key - Keys.F1), + Keys.NumLock => ImGuiKey.NumLock, + Keys.Scroll => ImGuiKey.ScrollLock, + Keys.LeftShift => ImGuiKey.ModShift, + Keys.LeftControl => ImGuiKey.ModCtrl, + Keys.LeftAlt => ImGuiKey.ModAlt, + Keys.OemSemicolon => ImGuiKey.Semicolon, + Keys.OemPlus => ImGuiKey.Equal, + Keys.OemComma => ImGuiKey.Comma, + Keys.OemMinus => ImGuiKey.Minus, + Keys.OemPeriod => ImGuiKey.Period, + Keys.OemQuestion => ImGuiKey.Slash, + Keys.OemTilde => ImGuiKey.GraveAccent, + Keys.OemOpenBrackets => ImGuiKey.LeftBracket, + Keys.OemCloseBrackets => ImGuiKey.RightBracket, + Keys.OemPipe => ImGuiKey.Backslash, + Keys.OemQuotes => ImGuiKey.Apostrophe, + Keys.BrowserBack => ImGuiKey.AppBack, + Keys.BrowserForward => ImGuiKey.AppForward, + _ => ImGuiKey.None, + }; + + return imguikey != ImGuiKey.None; + } + + #endregion Setup & Update + + #region Internals + + /// + /// Gets the geometry as set up by ImGui and sends it to the graphics device + /// + private void RenderDrawData(ImDrawDataPtr drawData) + { + // Setup render state: alpha-blending enabled, no face culling, no depth testing, scissor enabled, vertex/texcoord/color pointers + var lastViewport = _graphicsDevice.Viewport; + var lastScissorBox = _graphicsDevice.ScissorRectangle; + var lastRasterizer = _graphicsDevice.RasterizerState; + var lastDepthStencil = _graphicsDevice.DepthStencilState; + var lastBlendFactor = _graphicsDevice.BlendFactor; + var lastBlendState = _graphicsDevice.BlendState; + + _graphicsDevice.BlendFactor = Color.White; + _graphicsDevice.BlendState = BlendState.NonPremultiplied; + _graphicsDevice.RasterizerState = _rasterizerState; + _graphicsDevice.DepthStencilState = DepthStencilState.DepthRead; + + // Handle cases of screen coordinates != from framebuffer coordinates (e.g. retina displays) + drawData.ScaleClipRects(ImGui.GetIO().DisplayFramebufferScale); + + // Setup projection + _graphicsDevice.Viewport = new Viewport(0, 0, _graphicsDevice.PresentationParameters.BackBufferWidth, _graphicsDevice.PresentationParameters.BackBufferHeight); + + UpdateBuffers(drawData); + + RenderCommandLists(drawData); + + // Restore modified state + _graphicsDevice.Viewport = lastViewport; + _graphicsDevice.ScissorRectangle = lastScissorBox; + _graphicsDevice.RasterizerState = lastRasterizer; + _graphicsDevice.DepthStencilState = lastDepthStencil; + _graphicsDevice.BlendState = lastBlendState; + _graphicsDevice.BlendFactor = lastBlendFactor; + } + + private unsafe void UpdateBuffers(ImDrawDataPtr drawData) + { + if (drawData.TotalVtxCount == 0) + { + return; + } + + // Expand buffers if we need more room + if (drawData.TotalVtxCount > _vertexBufferSize) + { + _vertexBuffer?.Dispose(); + + _vertexBufferSize = (int)(drawData.TotalVtxCount * 1.5f); + _vertexBuffer = new VertexBuffer(_graphicsDevice, DrawVertDeclaration.Declaration, _vertexBufferSize, BufferUsage.None); + _vertexData = new byte[_vertexBufferSize * DrawVertDeclaration.Size]; + } + + if (drawData.TotalIdxCount > _indexBufferSize) + { + _indexBuffer?.Dispose(); + + _indexBufferSize = (int)(drawData.TotalIdxCount * 1.5f); + _indexBuffer = new IndexBuffer(_graphicsDevice, IndexElementSize.SixteenBits, _indexBufferSize, BufferUsage.None); + _indexData = new byte[_indexBufferSize * sizeof(ushort)]; + } + + // Copy ImGui's vertices and indices to a set of managed byte arrays + int vtxOffset = 0; + int idxOffset = 0; + + for (int n = 0; n < drawData.CmdListsCount; n++) + { + ImDrawListPtr cmdList = drawData.CmdLists[n]; + + fixed (void* vtxDstPtr = &_vertexData[vtxOffset * DrawVertDeclaration.Size]) + fixed (void* idxDstPtr = &_indexData[idxOffset * sizeof(ushort)]) + { + Buffer.MemoryCopy((void*)cmdList.VtxBuffer.Data, vtxDstPtr, _vertexData.Length, cmdList.VtxBuffer.Size * DrawVertDeclaration.Size); + Buffer.MemoryCopy((void*)cmdList.IdxBuffer.Data, idxDstPtr, _indexData.Length, cmdList.IdxBuffer.Size * sizeof(ushort)); + } + + vtxOffset += cmdList.VtxBuffer.Size; + idxOffset += cmdList.IdxBuffer.Size; + } + + // Copy the managed byte arrays to the gpu vertex- and index buffers + _vertexBuffer.SetData(_vertexData, 0, drawData.TotalVtxCount * DrawVertDeclaration.Size); + _indexBuffer.SetData(_indexData, 0, drawData.TotalIdxCount * sizeof(ushort)); + } + + private unsafe void RenderCommandLists(ImDrawDataPtr drawData) + { + _graphicsDevice.SetVertexBuffer(_vertexBuffer); + _graphicsDevice.Indices = _indexBuffer; + + int vtxOffset = 0; + int idxOffset = 0; + + for (int n = 0; n < drawData.CmdListsCount; n++) + { + ImDrawListPtr cmdList = drawData.CmdLists[n]; + + for (int cmdi = 0; cmdi < cmdList.CmdBuffer.Size; cmdi++) + { + ImDrawCmdPtr drawCmd = cmdList.CmdBuffer[cmdi]; + + if (drawCmd.ElemCount == 0) + { + continue; + } + + if (!_loadedTextures.ContainsKey(drawCmd.TextureId)) + { + throw new InvalidOperationException($"Could not find a texture with id '{drawCmd.TextureId}', please check your bindings"); + } + + _graphicsDevice.ScissorRectangle = new Rectangle( + (int)drawCmd.ClipRect.X, + (int)drawCmd.ClipRect.Y, + (int)(drawCmd.ClipRect.Z - drawCmd.ClipRect.X), + (int)(drawCmd.ClipRect.W - drawCmd.ClipRect.Y) + ); + + var effect = UpdateEffect(_loadedTextures[drawCmd.TextureId]); + + foreach (var pass in effect.CurrentTechnique.Passes) + { + pass.Apply(); + +#pragma warning disable CS0618 // // FNA does not expose an alternative method. + _graphicsDevice.DrawIndexedPrimitives( + primitiveType: PrimitiveType.TriangleList, + baseVertex: (int)drawCmd.VtxOffset + vtxOffset, + minVertexIndex: 0, + numVertices: cmdList.VtxBuffer.Size, + startIndex: (int)drawCmd.IdxOffset + idxOffset, + primitiveCount: (int)drawCmd.ElemCount / 3 + ); +#pragma warning restore CS0618 + } + } + + vtxOffset += cmdList.VtxBuffer.Size; + idxOffset += cmdList.IdxBuffer.Size; + } + } + + #endregion Internals + } +} \ No newline at end of file diff --git a/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/Input/GamePadInfo.cs b/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/Input/GamePadInfo.cs new file mode 100644 index 00000000..7fd16126 --- /dev/null +++ b/Tutorials/2dShaders/src/06-Color-Swap-Effect/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/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/Input/InputManager.cs b/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/Input/InputManager.cs new file mode 100644 index 00000000..1790eb70 --- /dev/null +++ b/Tutorials/2dShaders/src/06-Color-Swap-Effect/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/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/Input/KeyboardInfo.cs b/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/Input/KeyboardInfo.cs new file mode 100644 index 00000000..c6770cb0 --- /dev/null +++ b/Tutorials/2dShaders/src/06-Color-Swap-Effect/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/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/Input/MouseButton.cs b/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/Input/MouseButton.cs new file mode 100644 index 00000000..5b041f80 --- /dev/null +++ b/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/Input/MouseButton.cs @@ -0,0 +1,10 @@ +namespace MonoGameLibrary.Input; + +public enum MouseButton +{ + Left, + Middle, + Right, + XButton1, + XButton2 +} diff --git a/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/Input/MouseInfo.cs b/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/Input/MouseInfo.cs new file mode 100644 index 00000000..09d6207c --- /dev/null +++ b/Tutorials/2dShaders/src/06-Color-Swap-Effect/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/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/MonoGameLibrary.csproj b/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/MonoGameLibrary.csproj new file mode 100644 index 00000000..69adcc21 --- /dev/null +++ b/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/MonoGameLibrary.csproj @@ -0,0 +1,12 @@ + + + net8.0 + true + + + + + All + + + \ No newline at end of file diff --git a/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/Scenes/Scene.cs b/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/Scenes/Scene.cs new file mode 100644 index 00000000..627d220f --- /dev/null +++ b/Tutorials/2dShaders/src/06-Color-Swap-Effect/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/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/Scenes/SceneTransition.cs b/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/Scenes/SceneTransition.cs new file mode 100644 index 00000000..bbd1f7d5 --- /dev/null +++ b/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/Scenes/SceneTransition.cs @@ -0,0 +1,56 @@ +using System; +using Microsoft.Xna.Framework; + +namespace MonoGameLibrary.Scenes; + +public class SceneTransition +{ + public DateTimeOffset StartTime; + public TimeSpan Duration; + + /// + /// true when the transition is progressing from 0 to 1. + /// false when the transition is progressing from 1 to 0. + /// + public bool IsForwards; + + /// + /// The index into the + /// + public int TextureIndex; + + /// + /// The 0 to 1 value representing the progress of the transition. + /// + public float ProgressRatio => MathHelper.Clamp((float)(EndTime - DateTimeOffset.Now).TotalMilliseconds / (float)Duration.TotalMilliseconds, 0, 1); + + public float DirectionalRatio => IsForwards ? 1 - ProgressRatio : ProgressRatio; + + public DateTimeOffset EndTime => StartTime + Duration; + public bool IsComplete => DateTimeOffset.Now >= EndTime; + + + /// + /// Create a new transition + /// + /// + /// how long will the transition last in milliseconds? + /// + /// + /// should the transition be animating the Progress parameter from 0 to 1, or 1 to 0? + /// + /// + public static SceneTransition Create(int durationMs, bool isForwards) + { + return new SceneTransition + { + Duration = TimeSpan.FromMilliseconds(durationMs), + StartTime = DateTimeOffset.Now, + TextureIndex = Random.Shared.Next(), + IsForwards = isForwards + }; + } + + public static SceneTransition Open(int durationMs) => Create(durationMs, true); + public static SceneTransition Close(int durationMs) => Create(durationMs, false); +} \ No newline at end of file diff --git a/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/SharedContent/SharedContent.mgcb b/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/SharedContent/SharedContent.mgcb new file mode 100644 index 00000000..2c7cbed1 --- /dev/null +++ b/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/SharedContent/SharedContent.mgcb @@ -0,0 +1,75 @@ + +#----------------------------- Global Properties ----------------------------# + +/outputDir:bin/$(Platform) +/intermediateDir:obj/$(Platform) +/platform:DesktopGL +/config: +/profile:Reach +/compress:False + +#-------------------------------- References --------------------------------# + + +#---------------------------------- Content ---------------------------------# + +#begin effects/colorSwapEffect.fx +/importer:EffectImporter +/processor:EffectProcessor +/processorParam:DebugMode=Auto +/build:effects/colorSwapEffect.fx + +#begin effects/sceneTransitionEffect.fx +/importer:EffectImporter +/processor:EffectProcessor +/processorParam:DebugMode=Auto +/build:effects/sceneTransitionEffect.fx + +#begin images/angled.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/angled.png + +#begin images/concave.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/concave.png + +#begin images/radial.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/radial.png + +#begin images/ripple.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/ripple.png + diff --git a/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/SharedContent/effects/colorSwapEffect.fx b/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/SharedContent/effects/colorSwapEffect.fx new file mode 100644 index 00000000..b6f67eaf --- /dev/null +++ b/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/SharedContent/effects/colorSwapEffect.fx @@ -0,0 +1,92 @@ +#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 + +// the main Sprite texture passed to SpriteBatch.Draw() +Texture2D SpriteTexture; +sampler2D SpriteTextureSampler = sampler_state +{ + Texture = ; +}; + +// the custom color map passed to the Material.SetParameter() +Texture2D ColorMap; +sampler2D ColorMapSampler = sampler_state +{ + Texture = ; + MinFilter = Point; + MagFilter = Point; + MipFilter = Point; + AddressU = Clamp; + AddressV = Clamp; +}; + +// a control variable to lerp between original color and swapped color +float OriginalAmount; +float Saturation; + +struct VertexShaderOutput +{ + float4 Position : SV_POSITION; + float4 Color : COLOR0; + float2 TextureCoordinates : TEXCOORD0; +}; + +float4 Grayscale(float4 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); +} + +float4 SwapColors(float4 color) +{ + // produce the key location + float2 keyUv = float2(color.r, 0); + + // read the swap color value + float4 swappedColor = tex2D(ColorMapSampler, keyUv) * color.a; + + // ignore the swap if the map does not have a value + bool hasSwapColor = swappedColor.a > 0; + if (!hasSwapColor) + { + return color; + } + + // return the result color + return lerp(swappedColor, color, OriginalAmount); +} + +float4 MainPS(VertexShaderOutput input) : COLOR +{ + // read the original color value + float4 originalColor = tex2D(SpriteTextureSampler,input.TextureCoordinates); + + float4 swapped = SwapColors(originalColor); + float4 saturated = Grayscale(swapped); + + return saturated; +} + +technique SpriteDrawing +{ + pass P0 + { + PixelShader = compile PS_SHADERMODEL MainPS(); + } +}; \ No newline at end of file diff --git a/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/SharedContent/effects/sceneTransitionEffect.fx b/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/SharedContent/effects/sceneTransitionEffect.fx new file mode 100644 index 00000000..0d87d021 --- /dev/null +++ b/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/SharedContent/effects/sceneTransitionEffect.fx @@ -0,0 +1,42 @@ +#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; + +float Progress; +float EdgeWidth; + +sampler2D SpriteTextureSampler = sampler_state +{ + Texture = ; +}; + +struct VertexShaderOutput +{ + float4 Position : SV_POSITION; + float4 Color : COLOR0; + float2 TextureCoordinates : TEXCOORD0; +}; + +float4 MainPS(VertexShaderOutput input) : COLOR +{ + float2 uv = input.TextureCoordinates; + float value = tex2D(SpriteTextureSampler, uv).r; + float transitioned = smoothstep(Progress, Progress + EdgeWidth, value); + return float4(0, 0, 0, transitioned); +} + + +technique SpriteDrawing +{ + pass P0 + { + PixelShader = compile PS_SHADERMODEL MainPS(); + } +}; \ No newline at end of file diff --git a/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/SharedContent/images/angled.png b/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/SharedContent/images/angled.png new file mode 100644 index 00000000..de0160f2 Binary files /dev/null and b/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/SharedContent/images/angled.png differ diff --git a/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/SharedContent/images/concave.png b/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/SharedContent/images/concave.png new file mode 100644 index 00000000..826e2207 Binary files /dev/null and b/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/SharedContent/images/concave.png differ diff --git a/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/SharedContent/images/radial.png b/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/SharedContent/images/radial.png new file mode 100644 index 00000000..bd1207cf Binary files /dev/null and b/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/SharedContent/images/radial.png differ diff --git a/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/SharedContent/images/ripple.png b/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/SharedContent/images/ripple.png new file mode 100644 index 00000000..e137653a Binary files /dev/null and b/Tutorials/2dShaders/src/06-Color-Swap-Effect/MonoGameLibrary/SharedContent/images/ripple.png differ diff --git a/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime.sln b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime.sln new file mode 100644 index 00000000..077462d5 --- /dev/null +++ b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DungeonSlime", "DungeonSlime\DungeonSlime.csproj", "{88BD3FF9-D3D6-4680-A274-F866E0DFCAC4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MonoGameLibrary", "MonoGameLibrary\MonoGameLibrary.csproj", "{AB85CEEE-6D97-4438-AEC4-797D2806F44A}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {88BD3FF9-D3D6-4680-A274-F866E0DFCAC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {88BD3FF9-D3D6-4680-A274-F866E0DFCAC4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {88BD3FF9-D3D6-4680-A274-F866E0DFCAC4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {88BD3FF9-D3D6-4680-A274-F866E0DFCAC4}.Release|Any CPU.Build.0 = Release|Any CPU + {AB85CEEE-6D97-4438-AEC4-797D2806F44A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AB85CEEE-6D97-4438-AEC4-797D2806F44A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AB85CEEE-6D97-4438-AEC4-797D2806F44A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AB85CEEE-6D97-4438-AEC4-797D2806F44A}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/.config/dotnet-tools.json b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/.config/dotnet-tools.json new file mode 100644 index 00000000..afd4e2c4 --- /dev/null +++ b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/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/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/.mgstats b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/.mgstats new file mode 100644 index 00000000..eab26b31 --- /dev/null +++ b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/.mgstats @@ -0,0 +1 @@ +Source File,Dest File,Processor Type,Content Type,Source File Size,Dest File Size,Build Seconds diff --git a/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/Content.mgcb b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/Content.mgcb new file mode 100644 index 00000000..4b109714 --- /dev/null +++ b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/Content.mgcb @@ -0,0 +1,164 @@ + +#----------------------------- 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/gameEffect.fx +/importer:EffectImporter +/processor:EffectProcessor +/processorParam:DebugMode=Auto +/build:effects/gameEffect.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/color-map-1.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/color-map-1.png + +#begin images/color-map-2.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/color-map-2.png + +#begin images/color-map-dark-purple.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/color-map-dark-purple.png + +#begin images/color-map-green.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/color-map-green.png + +#begin images/color-map-pink.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/color-map-pink.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/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/audio/bounce.wav b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/audio/bounce.wav new file mode 100644 index 00000000..baa7a47b Binary files /dev/null and b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/audio/bounce.wav differ diff --git a/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/audio/collect.wav b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/audio/collect.wav new file mode 100644 index 00000000..506220de Binary files /dev/null and b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/audio/collect.wav differ diff --git a/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/audio/theme.ogg b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/audio/theme.ogg new file mode 100644 index 00000000..72e1fd3b Binary files /dev/null and b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/audio/theme.ogg differ diff --git a/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/audio/ui.wav b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/audio/ui.wav new file mode 100644 index 00000000..63e8941e Binary files /dev/null and b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/audio/ui.wav differ diff --git a/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/effects/gameEffect.fx b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/effects/gameEffect.fx new file mode 100644 index 00000000..35aeedb7 --- /dev/null +++ b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/effects/gameEffect.fx @@ -0,0 +1,27 @@ +#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; + +sampler2D SpriteTextureSampler = sampler_state +{ + Texture = ; +}; + +#include "../../../MonoGameLibrary/SharedContent/effects/3dEffect.fxh" +#include "../../../MonoGameLibrary/SharedContent/effects/colors.fxh" + +technique SpriteDrawing +{ + pass P0 + { + VertexShader = compile VS_SHADERMODEL MainVS(); + PixelShader = compile PS_SHADERMODEL ColorSwapPS(); + } +}; \ No newline at end of file diff --git a/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/fonts/04B_30.spritefont b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/fonts/04B_30.spritefont new file mode 100644 index 00000000..63d4728c --- /dev/null +++ b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/fonts/04B_30.spritefont @@ -0,0 +1,16 @@ + + + + 04B_30.ttf + 17.5 + 0 + true + + + + + ~ + + + + diff --git a/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/fonts/04B_30.ttf b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/fonts/04B_30.ttf new file mode 100644 index 00000000..4b93740c Binary files /dev/null and b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/fonts/04B_30.ttf differ diff --git a/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/fonts/04B_30_5x.spritefont b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/fonts/04B_30_5x.spritefont new file mode 100644 index 00000000..dd239a53 --- /dev/null +++ b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/fonts/04B_30_5x.spritefont @@ -0,0 +1,16 @@ + + + + 04B_30.ttf + 87.5 + 0 + true + + + + + ~ + + + + diff --git a/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/fonts/04b_30.fnt b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/fonts/04b_30.fnt new file mode 100644 index 00000000..772f8c54 --- /dev/null +++ b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/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/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/images/atlas-definition.xml b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/images/atlas-definition.xml new file mode 100644 index 00000000..21772022 --- /dev/null +++ b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/images/atlas-definition.xml @@ -0,0 +1,34 @@ + + + images/atlas + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/images/atlas.png b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/images/atlas.png new file mode 100644 index 00000000..f7def20f Binary files /dev/null and b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/images/atlas.png differ diff --git a/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/images/background-pattern.png b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/images/background-pattern.png new file mode 100644 index 00000000..2d8d878e Binary files /dev/null and b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/images/background-pattern.png differ diff --git a/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/images/color-map-1.png b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/images/color-map-1.png new file mode 100644 index 00000000..b5e3dc5a Binary files /dev/null and b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/images/color-map-1.png differ diff --git a/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/images/color-map-2.png b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/images/color-map-2.png new file mode 100644 index 00000000..2789bee8 Binary files /dev/null and b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/images/color-map-2.png differ diff --git a/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/images/color-map-dark-purple.png b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/images/color-map-dark-purple.png new file mode 100644 index 00000000..ffe9516e Binary files /dev/null and b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/images/color-map-dark-purple.png differ diff --git a/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/images/color-map-green.png b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/images/color-map-green.png new file mode 100644 index 00000000..87656c81 Binary files /dev/null and b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/images/color-map-green.png differ diff --git a/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/images/color-map-pink.png b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/images/color-map-pink.png new file mode 100644 index 00000000..e8910ded Binary files /dev/null and b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/images/color-map-pink.png differ diff --git a/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/images/logo.png b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/images/logo.png new file mode 100644 index 00000000..1509036c Binary files /dev/null and b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/images/logo.png differ diff --git a/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/images/tilemap-definition.xml b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Content/images/tilemap-definition.xml new file mode 100644 index 00000000..85658c60 --- /dev/null +++ b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/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/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/DungeonSlime.csproj b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/DungeonSlime.csproj new file mode 100644 index 00000000..ab01c538 --- /dev/null +++ b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/DungeonSlime.csproj @@ -0,0 +1,77 @@ + + + WinExe + net8.0 + Major + false + false + + + app.manifest + Icon.ico + + + bin/$(Configuration)/$(TargetFramework) + + + + + + + + Icon.ico + + + Icon.bmp + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Game1.cs b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Game1.cs new file mode 100644 index 00000000..981a4c55 --- /dev/null +++ b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Game1.cs @@ -0,0 +1,75 @@ +using DungeonSlime.Scenes; +using Microsoft.Xna.Framework; +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() + { + // Allow the Core class to load any content. + base.LoadContent(); + + // Load the background theme music + _themeSong = Content.Load("audio/theme"); + } +} diff --git a/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/GameController.cs b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/GameController.cs new file mode 100644 index 00000000..a85df08f --- /dev/null +++ b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/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/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/GameObjects/Bat.cs b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/GameObjects/Bat.cs new file mode 100644 index 00000000..ddc855ed --- /dev/null +++ b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/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/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/GameObjects/Slime.cs b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/GameObjects/Slime.cs new file mode 100644 index 00000000..2198fc9a --- /dev/null +++ b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/GameObjects/Slime.cs @@ -0,0 +1,272 @@ +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 size of the slime + public int Size => _segments.Count; + + // 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(Action configureSpriteBatch) + { + // Iterate through each segment and draw it + for (var i = 0 ; i < _segments.Count; i ++) + { + var segment = _segments[i]; + // 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); + + // Allow the sprite batch to be configured before each call. + configureSpriteBatch(i); + + // 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/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/GameObjects/SlimeSegment.cs b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/GameObjects/SlimeSegment.cs new file mode 100644 index 00000000..b00189eb --- /dev/null +++ b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/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/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Icon.bmp b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Icon.bmp new file mode 100644 index 00000000..2b481653 Binary files /dev/null and b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Icon.bmp differ diff --git a/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Icon.ico b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Icon.ico new file mode 100644 index 00000000..7d9dec18 Binary files /dev/null and b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Icon.ico differ diff --git a/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Program.cs b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Program.cs new file mode 100644 index 00000000..4d9be314 --- /dev/null +++ b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Program.cs @@ -0,0 +1,3 @@ +MonoGameLibrary.Content.ContentManagerExtensions.StartContentWatcherTask(); +using var game = new DungeonSlime.Game1(); +game.Run(); \ No newline at end of file diff --git a/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Scenes/GameScene.cs b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Scenes/GameScene.cs new file mode 100644 index 00000000..5b7c5436 --- /dev/null +++ b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Scenes/GameScene.cs @@ -0,0 +1,478 @@ +using System; +using System.Collections.Generic; +using DungeonSlime.GameObjects; +using DungeonSlime.UI; +using ImGuiNET; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Audio; +using Microsoft.Xna.Framework.Graphics; +using MonoGameGum; +using MonoGameLibrary; +using MonoGameLibrary.Content; +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 amount of saturation to provide the grayscale shader effect + private float _saturation = 1.0f; + private Texture2D _colorMap; + private RedColorMap _slimeColorMap; + private TimeSpan _lastGrowTime; + + // The uber material for the game objects + private Material _gameMaterial; + private SpriteCamera3d _camera; + + // 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 colorSwap map + _colorMap = Content.Load("images/color-map-dark-purple"); + _slimeColorMap = new RedColorMap(); + _slimeColorMap.SetColorsByExistingColorMap(_colorMap); + _slimeColorMap.SetColorsByRedValue(new Dictionary + { + // main color + [32] = Color.LightSteelBlue, + }, false); + + // Load the game material + _gameMaterial = Content.WatchMaterial("effects/gameEffect"); + _gameMaterial.IsDebugVisible = true; + _gameMaterial.SetParameter("ColorMap", _colorMap); + _camera = new SpriteCamera3d(); + _gameMaterial.SetParameter("MatrixTransform", _camera.CalculateMatrixTransform()); + _gameMaterial.SetParameter("ScreenSize", new Vector2(Core.GraphicsDevice.Viewport.Width, Core.GraphicsDevice.Viewport.Height)); + + } + + public override void Update(GameTime gameTime) + { + // Ensure the UI is always updated + _ui.Update(gameTime); + + // Set the camera view to look at the player slime + var viewport = Core.GraphicsDevice.Viewport; + var center = .5f * new Vector2(viewport.Width, viewport.Height); + var slimePosition = new Vector2(_slime?.GetBounds().X ?? center.X, _slime?.GetBounds().Y ?? center.Y); + var offset = .01f * (slimePosition - center); + _camera.LookOffset = offset; + _gameMaterial.SetParameter("MatrixTransform", _camera.CalculateMatrixTransform()); + + // Update the colorSwap material if it was changed + _gameMaterial.Update(); + + 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; + } + } + else + { + _saturation = 1; + } + + // 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(gameTime); + } + + private void CollisionChecks(GameTime gameTime) + { + // 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(); + + // Remember when the last time the slime grew + _lastGrowTime = gameTime.TotalGameTime; + + // 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(new Color(32, 16, 20)); + + _gameMaterial.SetParameter("Saturation", _saturation); + Core.SpriteBatch.Begin( + samplerState: SamplerState.PointClamp, + sortMode: SpriteSortMode.Immediate, + rasterizerState: RasterizerState.CullNone, + effect: _gameMaterial.Effect); + + // Update the colorMap + _gameMaterial.SetParameter("ColorMap", _colorMap); + + // Draw the tilemap + _tilemap.Draw(Core.SpriteBatch); + + // Draw the bat. + _bat.Draw(); + + // Draw the slime. + _slime.Draw(segmentIndex => + { + const int flashTimeMs = 125; + var map = _colorMap; + var elapsedMs = (gameTime.TotalGameTime.TotalMilliseconds - _lastGrowTime.TotalMilliseconds); + var intervalsAgo = (int)(elapsedMs / flashTimeMs); + + if (intervalsAgo < _slime.Size && (intervalsAgo - segmentIndex) % _slime.Size == 0) + { + map = _slimeColorMap.ColorMap; + } + + _gameMaterial.SetParameter("ColorMap", map); + }); + + // Always end the sprite batch when finished. + Core.SpriteBatch.End(); + + // Draw the UI + _ui.Draw(); + } +} diff --git a/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Scenes/TitleScene.cs b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Scenes/TitleScene.cs new file mode 100644 index 00000000..6c35e1dc --- /dev/null +++ b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/Scenes/TitleScene.cs @@ -0,0 +1,369 @@ +using System; +using DungeonSlime.UI; +using Gum.Forms.Controls; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Audio; +using Microsoft.Xna.Framework.Graphics; +using MonoGameGum; +using MonoGameGum.GueDeriving; +using MonoGameLibrary; +using MonoGameLibrary.Content; +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; + + // The 3d material + private Material _3dMaterial; + + 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"); + + // Load the 3d effect + _3dMaterial = Core.SharedContent.WatchMaterial("effects/3dEffect"); + _3dMaterial.IsDebugVisible = true; + + var camera = new SpriteCamera3d + { + Fov = 40 + }; + _3dMaterial.SetParameter("MatrixTransform", camera.CalculateMatrixTransform()); + _3dMaterial.SetParameter("ScreenSize", new Vector2(Core.GraphicsDevice.Viewport.Width, Core.GraphicsDevice.Viewport.Height)); + } + + 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) + { + _3dMaterial.Update(); + + var spinAmount = Core.Input.Mouse.X / (float)Core.GraphicsDevice.Viewport.Width; + spinAmount = MathHelper.SmoothStep(-.1f, .1f, spinAmount); + _3dMaterial.SetParameter("SpinAmount", spinAmount); + + // 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, + rasterizerState: RasterizerState.CullNone, + effect: _3dMaterial.Effect); + + // 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/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/UI/AnimatedButton.cs b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/UI/AnimatedButton.cs new file mode 100644 index 00000000..4cce6ee5 --- /dev/null +++ b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/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 by AnimationChains 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/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/UI/GameSceneUI.cs b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/UI/GameSceneUI.cs new file mode 100644 index 00000000..498655c2 --- /dev/null +++ b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/UI/GameSceneUI.cs @@ -0,0 +1,340 @@ +using System; +using Gum.DataTypes; +using Gum.Forms.Controls; +using Gum.Managers; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Audio; +using Microsoft.Xna.Framework.Content; +using MonoGameGum; +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/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/UI/OptionsSlider.cs b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/UI/OptionsSlider.cs new file mode 100644 index 00000000..53d6ee94 --- /dev/null +++ b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/UI/OptionsSlider.cs @@ -0,0 +1,253 @@ +using System; +using Gum.DataTypes; +using Gum.DataTypes.Variables; +using Gum.Forms.Controls; +using Gum.Managers; +using Microsoft.Xna.Framework; +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/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/app.manifest b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/app.manifest new file mode 100644 index 00000000..caf45166 --- /dev/null +++ b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/DungeonSlime/app.manifest @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true/pm + permonitorv2,permonitor + + + + diff --git a/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/Audio/AudioController.cs b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/Audio/AudioController.cs new file mode 100644 index 00000000..1bffd636 --- /dev/null +++ b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/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/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/Circle.cs b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/Circle.cs new file mode 100644 index 00000000..0bb691bc --- /dev/null +++ b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/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/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/Content/ContentManagerExtensions.cs b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/Content/ContentManagerExtensions.cs new file mode 100644 index 00000000..e012836c --- /dev/null +++ b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/Content/ContentManagerExtensions.cs @@ -0,0 +1,168 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; +using MonoGameLibrary.Graphics; + +namespace MonoGameLibrary.Content; + +public static class ContentManagerExtensions +{ + /// + /// Check if the given xnb file has a newer write-time than the last loaded version of the asset. + /// If the local file has been updated, reload the asset and return true. + /// + /// The that loaded the asset originally + /// The asset that will be reloaded if the xnb file is newer + /// If the asset has been reloaded, this out parameter will be set to the previous version of the asset before the newer version was loaded. + /// + /// true when asset was reloaded; false otherwise. + /// + public static bool TryRefresh(this ContentManager manager, WatchedAsset watchedAsset, out T oldAsset) + { + oldAsset = default; + + if (manager != watchedAsset.Owner) + throw new ArgumentException($"Used the wrong ContentManager to refresh {watchedAsset.AssetName}"); + + var path = Path.Combine(manager.RootDirectory, watchedAsset.AssetName) + ".xnb"; + var lastWriteTime = File.GetLastWriteTime(path); + + if (lastWriteTime <= watchedAsset.UpdatedAt) + { + return false; + } + + if (IsFileLocked(path)) return false; // wait for the file to not be locked. + + manager.UnloadAsset(watchedAsset.AssetName); + oldAsset = watchedAsset.Asset; + watchedAsset.Asset = manager.Load(watchedAsset.AssetName); + watchedAsset.UpdatedAt = lastWriteTime; + + return true; + } + + private static bool IsFileLocked(string path) + { + try + { + using FileStream _ = new FileStream(path, FileMode.Open, FileAccess.ReadWrite, FileShare.None); + // File is not locked + return false; + } + catch (IOException) + { + // File is locked or inaccessible + return true; + } + } + + /// + /// Load an asset and wrap it with the metadata required to refresh it later using the function + /// + /// + /// + /// + /// + public static WatchedAsset Watch(this ContentManager manager, string assetName) + { + var asset = manager.Load(assetName); + return new WatchedAsset + { + AssetName = assetName, + Asset = asset, + UpdatedAt = DateTimeOffset.Now, + Owner = manager + }; + } + + /// + /// Load an Effect into the wrapper class + /// + /// + /// + /// + public static Material WatchMaterial(this ContentManager manager, string assetName) + { + return new Material(manager.Watch(assetName)); + } + + + [Conditional("DEBUG")] + public static void StartContentWatcherTask() + { + var args = Environment.GetCommandLineArgs(); + foreach (var arg in args) + { + // if the application was started with the --no-reload option, then do not start the watcher. + if (arg == "--no-reload") return; + } + + // identify the project directory + var projectFile = Assembly.GetEntryAssembly().GetName().Name + ".csproj"; + var current = Directory.GetCurrentDirectory(); + string projectDirectory = null; + + while (current != null && projectDirectory == null) + { + if (File.Exists(Path.Combine(current, projectFile))) + { + // the valid project csproj exists in the directory + projectDirectory = current; + } + else + { + // try looking in the parent directory. + // When there is no parent directory, the variable becomes 'null' + current = Path.GetDirectoryName(current); + } + } + + // if no valid project was identified, then it is impossible to start the watcher + if (string.IsNullOrEmpty(projectDirectory)) return; + + // start the watcher process + var process = Process.Start(new ProcessStartInfo + { + FileName = "dotnet", + Arguments = "build -t:WatchContent --tl:off", + WorkingDirectory = projectDirectory, + WindowStyle = ProcessWindowStyle.Normal, + UseShellExecute = false, + CreateNoWindow = false + }); + + // when this program exits, make sure to emit a kill signal to the watcher process + AppDomain.CurrentDomain.ProcessExit += (_, __) => + { + try + { + if (!process.HasExited) + { + process.Kill(entireProcessTree: true); + } + } + catch + { + /* ignore */ + } + }; + AppDomain.CurrentDomain.UnhandledException += (sender, e) => + { + try + { + if (!process.HasExited) + { + process.Kill(entireProcessTree: true); + } + } + catch + { + /* ignore */ + } + }; + } +} \ No newline at end of file diff --git a/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/Content/WatchedAsset.cs b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/Content/WatchedAsset.cs new file mode 100644 index 00000000..39008666 --- /dev/null +++ b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/Content/WatchedAsset.cs @@ -0,0 +1,33 @@ +using System; +using Microsoft.Xna.Framework.Content; + +namespace MonoGameLibrary.Content; + +public class WatchedAsset +{ + /// + /// The latest version of the asset. + /// + public T Asset { get; set; } + + /// + /// The last time the was loaded into memory. + /// + public DateTimeOffset UpdatedAt { get; set; } + + /// + /// The name of the . This is the name used to load the asset from disk. + /// + public string AssetName { get; init; } + + /// + /// The instance that loaded the asset. + /// + public ContentManager Owner { get; init; } + + + public bool TryRefresh(out T oldAsset) + { + return Owner.TryRefresh(this, out oldAsset); + } +} \ No newline at end of file diff --git a/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/Core.cs b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/Core.cs new file mode 100644 index 00000000..1bcd962a --- /dev/null +++ b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/Core.cs @@ -0,0 +1,278 @@ +using System; +using System.Collections.Generic; +using ImGuiNET.SampleProgram.XNA; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; +using MonoGameLibrary.Audio; +using MonoGameLibrary.Content; +using MonoGameLibrary.Graphics; +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; + + /// + /// The material that is used when changing scenes + /// + public static Material SceneTransitionMaterial { get; private set; } + + /// + /// A set of grayscale gradient textures to use as transition guides + /// + public static List SceneTransitionTextures { get; private set; } + + /// + /// The current transition between scenes + /// + public static SceneTransition SceneTransition { get; protected set; } = SceneTransition.Open(1000); + + /// + /// 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 a runtime generated 1x1 pixel texture. + /// + public static Texture2D Pixel { get; private set; } + + /// + /// Gets the ImGui renderer used for debug UIs. + /// + public static ImGuiRenderer ImGuiRenderer { get; private set; } + + /// + /// Gets the content manager used to load global assets. + /// + public static new ContentManager Content { get; private set; } + + /// + /// Gets the content manager that can load global assets from the SharedContent folder. + /// + public static ContentManager SharedContent { 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"; + + // Set the core's shared content manager, pointing to the SharedContent folder. + SharedContent = new ContentManager(Services, "SharedContent"); + + // 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 the ImGui renderer. + ImGuiRenderer = new ImGuiRenderer(this); + ImGuiRenderer.RebuildFontAtlas(); + + // Create a new input manager + Input = new InputManager(); + + // Create a new audio controller. + Audio = new AudioController(); + + // Create a 1x1 white pixel texture for drawing quads. + Pixel = new Texture2D(GraphicsDevice, 1, 1); + Pixel.SetData(new Color[]{ Color.White }); + } + + protected override void LoadContent() + { + base.LoadContent(); + SceneTransitionMaterial = SharedContent.WatchMaterial("effects/sceneTransitionEffect"); + SceneTransitionMaterial.SetParameter("EdgeWidth", .05f); + + SceneTransitionTextures = new List(); + SceneTransitionTextures.Add(SharedContent.Load("images/angled")); + SceneTransitionTextures.Add(SharedContent.Load("images/concave")); + SceneTransitionTextures.Add(SharedContent.Load("images/radial")); + SceneTransitionTextures.Add(SharedContent.Load("images/ripple")); + } + + 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 && SceneTransition.IsComplete) + { + TransitionScene(); + } + + // If there is an active scene, update it. + if (s_activeScene != null) + { + s_activeScene.Update(gameTime); + } + + // Check if the scene transition material needs to be reloaded. + SceneTransitionMaterial.SetParameter("Progress", SceneTransition.DirectionalRatio); + SceneTransitionMaterial.Update(); + + 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); + } + + // Draw the scene transition quad + SpriteBatch.Begin(effect: SceneTransitionMaterial.Effect); + SpriteBatch.Draw(SceneTransitionTextures[SceneTransition.TextureIndex % SceneTransitionTextures.Count], GraphicsDevice.Viewport.Bounds, Color.White); + SpriteBatch.End(); + + Material.DrawVisibleDebugUi(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; + SceneTransition = SceneTransition.Close(250); + } + } + + private static void TransitionScene() + { + SceneTransition = SceneTransition.Open(500); + + // 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/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/Graphics/AnimatedSprite.cs b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/Graphics/AnimatedSprite.cs new file mode 100644 index 00000000..a1a3594e --- /dev/null +++ b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/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/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/Graphics/Animation.cs b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/Graphics/Animation.cs new file mode 100644 index 00000000..44d61b65 --- /dev/null +++ b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/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/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/Graphics/Material.cs b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/Graphics/Material.cs new file mode 100644 index 00000000..f1a22a83 --- /dev/null +++ b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/Graphics/Material.cs @@ -0,0 +1,303 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using ImGuiNET; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using MonoGameLibrary.Content; +namespace MonoGameLibrary.Graphics; + +public class Material +{ + // materials that will be drawn during the standard debug UI pass. + private static HashSet s_debugMaterials = new HashSet(); + + /// + /// The hot-reloadable asset that this material is using + /// + public WatchedAsset Asset; + + /// + /// A cached version of the parameters available in the shader + /// + public Dictionary ParameterMap; + + /// + /// The currently loaded Effect that this material is using + /// + public Effect Effect => Asset.Asset; + + /// + /// Enable this variable to visualize the debugUI for the material + /// + public bool IsDebugVisible + { + get + { + return s_debugMaterials.Contains(this); + } + set + { + if (!value) + { + s_debugMaterials.Remove(this); + } + else + { + s_debugMaterials.Add(this); + } + } + } + + /// + /// When true, the debug UI will override parameters + /// + public bool DebugOverride; + + public Material(WatchedAsset asset) + { + Asset = asset; + UpdateParameterCache(); + } + + public void SetParameter(string name, float value) + { + if (DebugOverride) return; + + if (TryGetParameter(name, out var parameter)) + { + parameter.SetValue(value); + } + else + { + Console.WriteLine($"Warning: cannot set shader parameter=[{name}] because it does not exist in the compiled shader=[{Asset.AssetName}]"); + } + } + + public void SetParameter(string name, Matrix value) + { + if (DebugOverride) return; + + if (TryGetParameter(name, out var parameter)) + { + parameter.SetValue(value); + } + else + { + Console.WriteLine($"Warning: cannot set shader parameter=[{name}] because it does not exist in the compiled shader=[{Asset.AssetName}]"); + } + } + + public void SetParameter(string name, Vector2 value) + { + if (DebugOverride) return; + + if (TryGetParameter(name, out var parameter)) + { + parameter.SetValue(value); + } + else + { + Console.WriteLine($"Warning: cannot set shader parameter=[{name}] because it does not exist in the compiled shader=[{Asset.AssetName}]"); + } + } + + public void SetParameter(string name, Texture2D value) + { + if (DebugOverride) return; + + if (TryGetParameter(name, out var parameter)) + { + parameter.SetValue(value); + } + else + { + Console.WriteLine($"Warning: cannot set shader parameter=[{name}] because it does not exist in the compiled shader=[{Asset.AssetName}]"); + } + } + + /// + /// Check if the given parameter name is available in the compiled shader code. + /// Remember that a parameter will be optimized out of a shader if it is not being used + /// in the shader's return value. + /// + /// + /// + /// + public bool TryGetParameter(string name, out EffectParameter parameter) + { + return ParameterMap.TryGetValue(name, out parameter); + } + + /// + /// Rebuild the based on the current parameters available in the effect instance + /// + public void UpdateParameterCache() + { + ParameterMap = Effect.Parameters.ToDictionary(p => p.Name); + } + + [Conditional("DEBUG")] + public void Update() + { + if (Asset.TryRefresh(out var oldAsset)) + { + UpdateParameterCache(); + + foreach (var oldParam in oldAsset.Parameters) + { + if (!TryGetParameter(oldParam.Name, out var newParam)) + { + continue; + } + + switch (oldParam.ParameterClass) + { + case EffectParameterClass.Scalar: + newParam.SetValue(oldParam.GetValueSingle()); + break; + case EffectParameterClass.Matrix: + newParam.SetValue(oldParam.GetValueMatrix()); + break; + case EffectParameterClass.Vector when oldParam.ColumnCount == 2: // float2 + newParam.SetValue(oldParam.GetValueVector2()); + break; + case EffectParameterClass.Object: + newParam.SetValue(oldParam.GetValueTexture2D()); + break; + default: + Console.WriteLine("Warning: shader reload system was not able to re-apply property. " + + $"shader=[{Effect.Name}] " + + $"property=[{oldParam.Name}] " + + $"class=[{oldParam.ParameterClass}]"); + break; + } + } + } + } + + + + [Conditional("DEBUG")] + public void DrawDebug() + { + ImGui.Begin(Effect.Name); + + var currentSize = ImGui.GetWindowSize(); + ImGui.SetWindowSize(Effect.Name, new System.Numerics.Vector2(MathHelper.Max(100, currentSize.X), MathHelper.Max(100, currentSize.Y))); + + ImGui.AlignTextToFramePadding(); + ImGui.Text("Last Updated"); + ImGui.SameLine(); + ImGui.LabelText("##last-updated", Asset.UpdatedAt.ToString() + $" ({(DateTimeOffset.Now - Asset.UpdatedAt).ToString(@"h\:mm\:ss")} ago)"); + + ImGui.AlignTextToFramePadding(); + ImGui.Text("Override Values"); + ImGui.SameLine(); + ImGui.Checkbox("##override-values", ref DebugOverride); + + ImGui.NewLine(); + + bool ScalarSlider(string key, ref float value) + { + float min = 0; + float max = 1; + + return ImGui.SliderFloat($"##_prop{key}", ref value, min, max); + } + + foreach (var prop in ParameterMap) + { + switch (prop.Value.ParameterType, prop.Value.ParameterClass) + { + case (EffectParameterType.Single, EffectParameterClass.Scalar): + ImGui.AlignTextToFramePadding(); + ImGui.Text(prop.Key); + ImGui.SameLine(); + + var value = prop.Value.GetValueSingle(); + if (ScalarSlider(prop.Key, ref value)) + { + prop.Value.SetValue(value); + } + break; + + case (EffectParameterType.Single, EffectParameterClass.Vector): + ImGui.AlignTextToFramePadding(); + ImGui.Text(prop.Key); + + var vec2Value = prop.Value.GetValueVector2(); + ImGui.Indent(); + + ImGui.Text("X"); + ImGui.SameLine(); + + if (ScalarSlider(prop.Key + ".x", ref vec2Value.X)) + { + prop.Value.SetValue(vec2Value); + } + + ImGui.Text("Y"); + ImGui.SameLine(); + if (ScalarSlider(prop.Key + ".y", ref vec2Value.Y)) + { + prop.Value.SetValue(vec2Value); + } + ImGui.Unindent(); + break; + + case (EffectParameterType.Texture2D, EffectParameterClass.Object): + ImGui.AlignTextToFramePadding(); + ImGui.Text(prop.Key); + ImGui.SameLine(); + + var texture = prop.Value.GetValueTexture2D(); + if (texture != null) + { + var texturePtr = Core.ImGuiRenderer.BindTexture(texture); + ImGui.Image(texturePtr, new System.Numerics.Vector2(texture.Width, texture.Height)); + } + else + { + ImGui.Text("(null)"); + } + break; + + default: + ImGui.AlignTextToFramePadding(); + ImGui.Text(prop.Key); + ImGui.SameLine(); + ImGui.Text($"(unsupported {prop.Value.ParameterType}, {prop.Value.ParameterClass})"); + break; + } + } + ImGui.End(); + } + + [Conditional("DEBUG")] + public static void DrawVisibleDebugUi(GameTime gameTime) + { + // first, cull any materials that are not visible, or disposed. + var toRemove = new List(); + foreach (var material in s_debugMaterials) + { + if (material.Effect.IsDisposed) + { + toRemove.Add(material); + } + } + + foreach (var material in toRemove) + { + s_debugMaterials.Remove(material); + } + + Core.ImGuiRenderer.BeforeLayout(gameTime); + foreach (var material in s_debugMaterials) + { + material.DrawDebug(); + } + Core.ImGuiRenderer.AfterLayout(); + } +} \ No newline at end of file diff --git a/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/Graphics/RedColorMap.cs b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/Graphics/RedColorMap.cs new file mode 100644 index 00000000..d6e0bf3f --- /dev/null +++ b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/Graphics/RedColorMap.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace MonoGameLibrary.Graphics; + +public class RedColorMap +{ + public Texture2D ColorMap { get; set; } + + public RedColorMap() + { + ColorMap = new Texture2D(Core.GraphicsDevice, 256, 1, false, SurfaceFormat.Color); + } + + /// + /// Given a dictionary of red-color values (0 to 255) to swapColors, + /// Set the values of the so that it can be used + /// As the ColorMap parameter in the colorSwapEffect. + /// + public void SetColorsByRedValue(Dictionary map, bool overWrite = true) + { + var pixelData = new Color[ColorMap.Width]; + ColorMap.GetData(pixelData); + + for (var i = 0; i < pixelData.Length; i++) + { + // if the given color dictionary contains a color value for this red index, use it. + if (map.TryGetValue(i, out var swapColor)) + { + pixelData[i] = swapColor; + } + else if (overWrite) + { + // otherwise, default the pixel to transparent + pixelData[i] = Color.Transparent; + } + } + + ColorMap.SetData(pixelData); + } + + public void SetColorsByExistingColorMap(Texture2D existingColorMap) + { + var existingPixels = new Color[256]; + existingColorMap.GetData(existingPixels); + + var map = new Dictionary(); + for (var i = 0; i < existingPixels.Length; i++) + { + map[i] = existingPixels[i]; + } + + SetColorsByRedValue(map); + } +} \ No newline at end of file diff --git a/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/Graphics/Sprite.cs b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/Graphics/Sprite.cs new file mode 100644 index 00000000..20c44f0b --- /dev/null +++ b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/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/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/Graphics/SpriteCamera3d.cs b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/Graphics/SpriteCamera3d.cs new file mode 100644 index 00000000..0602eb57 --- /dev/null +++ b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/Graphics/SpriteCamera3d.cs @@ -0,0 +1,51 @@ +using System; +using Microsoft.Xna.Framework; + +namespace MonoGameLibrary.Graphics; + +public class SpriteCamera3d +{ + /// + /// The field of view for the camera. + /// + public int Fov { get; set; } = 120; + + /// + /// By default, the camera is looking at the center of the screen. + /// This offset value can be used to "turn" the camera from the center towards the given vector value. + /// + public Vector2 LookOffset { get; set; } = Vector2.Zero; + + /// + /// Produce a matrix that will transform world-space coordinates into clip-space coordinates. + /// + /// + public Matrix CalculateMatrixTransform() + { + var viewport = Core.GraphicsDevice.Viewport; + + // start by creating the projection matrix + var projection = Matrix.CreatePerspectiveFieldOfView( + fieldOfView: MathHelper.ToRadians(Fov), + aspectRatio: Core.GraphicsDevice.Viewport.AspectRatio, + nearPlaneDistance: 0.0001f, + farPlaneDistance: 10000f + ); + + // position the camera far enough away to see the entire contents of the screen + var cameraZ = (viewport.Height * 0.5f) / (float)Math.Tan(MathHelper.ToRadians(Fov) * 0.5f); + + // create a view that is centered on the screen + var center = .5f * new Vector2(viewport.Width, viewport.Height); + var look = center + LookOffset; + var view = Matrix.CreateLookAt( + cameraPosition: new Vector3(center.X, center.Y, -cameraZ), + cameraTarget: new Vector3(look.X, look.Y, 0), + cameraUpVector: Vector3.Down + ); + + // the standard matrix format is world*view*projection, + // but given that we are skipping the world matrix, its just view*projection + return view * projection; + } +} \ No newline at end of file diff --git a/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/Graphics/TextureAtlas.cs b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/Graphics/TextureAtlas.cs new file mode 100644 index 00000000..e48c9abd --- /dev/null +++ b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/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/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/Graphics/TextureRegion.cs b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/Graphics/TextureRegion.cs new file mode 100644 index 00000000..ecd69030 --- /dev/null +++ b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/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/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/Graphics/Tilemap.cs b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/Graphics/Tilemap.cs new file mode 100644 index 00000000..96e1ee5e --- /dev/null +++ b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/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/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/Graphics/Tileset.cs b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/Graphics/Tileset.cs new file mode 100644 index 00000000..80c2e65a --- /dev/null +++ b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/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/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/ImGui/DrawVertDeclaration.cs b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/ImGui/DrawVertDeclaration.cs new file mode 100644 index 00000000..d846e7da --- /dev/null +++ b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/ImGui/DrawVertDeclaration.cs @@ -0,0 +1,29 @@ +using Microsoft.Xna.Framework.Graphics; + +namespace ImGuiNET.SampleProgram.XNA +{ + public static class DrawVertDeclaration + { + public static readonly VertexDeclaration Declaration; + + public static readonly int Size; + + static DrawVertDeclaration() + { + unsafe { Size = sizeof(ImDrawVert); } + + Declaration = new VertexDeclaration( + Size, + + // Position + new VertexElement(0, VertexElementFormat.Vector2, VertexElementUsage.Position, 0), + + // UV + new VertexElement(8, VertexElementFormat.Vector2, VertexElementUsage.TextureCoordinate, 0), + + // Color + new VertexElement(16, VertexElementFormat.Color, VertexElementUsage.Color, 0) + ); + } + } +} \ No newline at end of file diff --git a/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/ImGui/ImGuiRenderer.cs b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/ImGui/ImGuiRenderer.cs new file mode 100644 index 00000000..e2cc1a29 --- /dev/null +++ b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/ImGui/ImGuiRenderer.cs @@ -0,0 +1,436 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace ImGuiNET.SampleProgram.XNA +{ + /// + /// ImGui renderer for use with XNA-likes (FNA & MonoGame) + /// + public class ImGuiRenderer + { + private Game _game; + + // Graphics + private GraphicsDevice _graphicsDevice; + + private BasicEffect _effect; + private RasterizerState _rasterizerState; + + private byte[] _vertexData; + private VertexBuffer _vertexBuffer; + private int _vertexBufferSize; + + private byte[] _indexData; + private IndexBuffer _indexBuffer; + private int _indexBufferSize; + + // Textures + private Dictionary _loadedTextures; + + private int _textureId; + private IntPtr? _fontTextureId; + + // Input + private int _scrollWheelValue; + private int _horizontalScrollWheelValue; + private readonly float WHEEL_DELTA = 120; + private Keys[] _allKeys = Enum.GetValues(); + + public ImGuiRenderer(Game game) + { + var context = ImGui.CreateContext(); + ImGui.SetCurrentContext(context); + + _game = game ?? throw new ArgumentNullException(nameof(game)); + _graphicsDevice = game.GraphicsDevice; + + _loadedTextures = new Dictionary(); + + _rasterizerState = new RasterizerState() + { + CullMode = CullMode.None, + DepthBias = 0, + FillMode = FillMode.Solid, + MultiSampleAntiAlias = false, + ScissorTestEnable = true, + SlopeScaleDepthBias = 0 + }; + + SetupInput(); + } + + #region ImGuiRenderer + + /// + /// Creates a texture and loads the font data from ImGui. Should be called when the is initialized but before any rendering is done + /// + public virtual unsafe void RebuildFontAtlas() + { + // Get font texture from ImGui + var io = ImGui.GetIO(); + io.Fonts.GetTexDataAsRGBA32(out byte* pixelData, out int width, out int height, out int bytesPerPixel); + + // Copy the data to a managed array + var pixels = new byte[width * height * bytesPerPixel]; + unsafe { Marshal.Copy(new IntPtr(pixelData), pixels, 0, pixels.Length); } + + // Create and register the texture as an XNA texture + var tex2d = new Texture2D(_graphicsDevice, width, height, false, SurfaceFormat.Color); + tex2d.SetData(pixels); + + // Should a texture already have been build previously, unbind it first so it can be deallocated + if (_fontTextureId.HasValue) UnbindTexture(_fontTextureId.Value); + + // Bind the new texture to an ImGui-friendly id + _fontTextureId = BindTexture(tex2d); + + // Let ImGui know where to find the texture + io.Fonts.SetTexID(_fontTextureId.Value); + io.Fonts.ClearTexData(); // Clears CPU side texture data + } + + /// + /// Creates a pointer to a texture, which can be passed through ImGui calls such as . That pointer is then used by ImGui to let us know what texture to draw + /// + public virtual IntPtr BindTexture(Texture2D texture) + { + var id = new IntPtr(_textureId++); + + _loadedTextures.Add(id, texture); + + return id; + } + + /// + /// Removes a previously created texture pointer, releasing its reference and allowing it to be deallocated + /// + public virtual void UnbindTexture(IntPtr textureId) + { + _loadedTextures.Remove(textureId); + } + + /// + /// Sets up ImGui for a new frame, should be called at frame start + /// + public virtual void BeforeLayout(GameTime gameTime) + { + ImGui.GetIO().DeltaTime = (float)gameTime.ElapsedGameTime.TotalSeconds; + + UpdateInput(); + + ImGui.NewFrame(); + } + + /// + /// Asks ImGui for the generated geometry data and sends it to the graphics pipeline, should be called after the UI is drawn using ImGui.** calls + /// + public virtual void AfterLayout() + { + ImGui.Render(); + + unsafe { RenderDrawData(ImGui.GetDrawData()); } + } + + #endregion ImGuiRenderer + + #region Setup & Update + + /// + /// Setup key input event handler. + /// + protected virtual void SetupInput() + { + var io = ImGui.GetIO(); + + // MonoGame-specific ////////////////////// + _game.Window.TextInput += (s, a) => + { + if (a.Character == '\t') return; + io.AddInputCharacter(a.Character); + }; + + /////////////////////////////////////////// + + // FNA-specific /////////////////////////// + //TextInputEXT.TextInput += c => + //{ + // if (c == '\t') return; + + // ImGui.GetIO().AddInputCharacter(c); + //}; + /////////////////////////////////////////// + } + + /// + /// Updates the to the current matrices and texture + /// + protected virtual Effect UpdateEffect(Texture2D texture) + { + _effect = _effect ?? new BasicEffect(_graphicsDevice); + + var io = ImGui.GetIO(); + + _effect.World = Matrix.Identity; + _effect.View = Matrix.Identity; + _effect.Projection = Matrix.CreateOrthographicOffCenter(0f, io.DisplaySize.X, io.DisplaySize.Y, 0f, -1f, 1f); + _effect.TextureEnabled = true; + _effect.Texture = texture; + _effect.VertexColorEnabled = true; + + return _effect; + } + + /// + /// Sends XNA input state to ImGui + /// + protected virtual void UpdateInput() + { + if (!_game.IsActive) return; + + var io = ImGui.GetIO(); + + var mouse = Mouse.GetState(); + var keyboard = Keyboard.GetState(); + io.AddMousePosEvent(mouse.X, mouse.Y); + io.AddMouseButtonEvent(0, mouse.LeftButton == ButtonState.Pressed); + io.AddMouseButtonEvent(1, mouse.RightButton == ButtonState.Pressed); + io.AddMouseButtonEvent(2, mouse.MiddleButton == ButtonState.Pressed); + io.AddMouseButtonEvent(3, mouse.XButton1 == ButtonState.Pressed); + io.AddMouseButtonEvent(4, mouse.XButton2 == ButtonState.Pressed); + + io.AddMouseWheelEvent( + (mouse.HorizontalScrollWheelValue - _horizontalScrollWheelValue) / WHEEL_DELTA, + (mouse.ScrollWheelValue - _scrollWheelValue) / WHEEL_DELTA); + _scrollWheelValue = mouse.ScrollWheelValue; + _horizontalScrollWheelValue = mouse.HorizontalScrollWheelValue; + + foreach (var key in _allKeys) + { + if (TryMapKeys(key, out ImGuiKey imguikey)) + { + io.AddKeyEvent(imguikey, keyboard.IsKeyDown(key)); + } + } + + io.DisplaySize = new System.Numerics.Vector2(_graphicsDevice.PresentationParameters.BackBufferWidth, _graphicsDevice.PresentationParameters.BackBufferHeight); + io.DisplayFramebufferScale = new System.Numerics.Vector2(1f, 1f); + } + + private bool TryMapKeys(Keys key, out ImGuiKey imguikey) + { + //Special case not handed in the switch... + //If the actual key we put in is "None", return none and true. + //otherwise, return none and false. + if (key == Keys.None) + { + imguikey = ImGuiKey.None; + return true; + } + + imguikey = key switch + { + Keys.Back => ImGuiKey.Backspace, + Keys.Tab => ImGuiKey.Tab, + Keys.Enter => ImGuiKey.Enter, + Keys.CapsLock => ImGuiKey.CapsLock, + Keys.Escape => ImGuiKey.Escape, + Keys.Space => ImGuiKey.Space, + Keys.PageUp => ImGuiKey.PageUp, + Keys.PageDown => ImGuiKey.PageDown, + Keys.End => ImGuiKey.End, + Keys.Home => ImGuiKey.Home, + Keys.Left => ImGuiKey.LeftArrow, + Keys.Right => ImGuiKey.RightArrow, + Keys.Up => ImGuiKey.UpArrow, + Keys.Down => ImGuiKey.DownArrow, + Keys.PrintScreen => ImGuiKey.PrintScreen, + Keys.Insert => ImGuiKey.Insert, + Keys.Delete => ImGuiKey.Delete, + >= Keys.D0 and <= Keys.D9 => ImGuiKey._0 + (key - Keys.D0), + >= Keys.A and <= Keys.Z => ImGuiKey.A + (key - Keys.A), + >= Keys.NumPad0 and <= Keys.NumPad9 => ImGuiKey.Keypad0 + (key - Keys.NumPad0), + Keys.Multiply => ImGuiKey.KeypadMultiply, + Keys.Add => ImGuiKey.KeypadAdd, + Keys.Subtract => ImGuiKey.KeypadSubtract, + Keys.Decimal => ImGuiKey.KeypadDecimal, + Keys.Divide => ImGuiKey.KeypadDivide, + >= Keys.F1 and <= Keys.F24 => ImGuiKey.F1 + (key - Keys.F1), + Keys.NumLock => ImGuiKey.NumLock, + Keys.Scroll => ImGuiKey.ScrollLock, + Keys.LeftShift => ImGuiKey.ModShift, + Keys.LeftControl => ImGuiKey.ModCtrl, + Keys.LeftAlt => ImGuiKey.ModAlt, + Keys.OemSemicolon => ImGuiKey.Semicolon, + Keys.OemPlus => ImGuiKey.Equal, + Keys.OemComma => ImGuiKey.Comma, + Keys.OemMinus => ImGuiKey.Minus, + Keys.OemPeriod => ImGuiKey.Period, + Keys.OemQuestion => ImGuiKey.Slash, + Keys.OemTilde => ImGuiKey.GraveAccent, + Keys.OemOpenBrackets => ImGuiKey.LeftBracket, + Keys.OemCloseBrackets => ImGuiKey.RightBracket, + Keys.OemPipe => ImGuiKey.Backslash, + Keys.OemQuotes => ImGuiKey.Apostrophe, + Keys.BrowserBack => ImGuiKey.AppBack, + Keys.BrowserForward => ImGuiKey.AppForward, + _ => ImGuiKey.None, + }; + + return imguikey != ImGuiKey.None; + } + + #endregion Setup & Update + + #region Internals + + /// + /// Gets the geometry as set up by ImGui and sends it to the graphics device + /// + private void RenderDrawData(ImDrawDataPtr drawData) + { + // Setup render state: alpha-blending enabled, no face culling, no depth testing, scissor enabled, vertex/texcoord/color pointers + var lastViewport = _graphicsDevice.Viewport; + var lastScissorBox = _graphicsDevice.ScissorRectangle; + var lastRasterizer = _graphicsDevice.RasterizerState; + var lastDepthStencil = _graphicsDevice.DepthStencilState; + var lastBlendFactor = _graphicsDevice.BlendFactor; + var lastBlendState = _graphicsDevice.BlendState; + + _graphicsDevice.BlendFactor = Color.White; + _graphicsDevice.BlendState = BlendState.NonPremultiplied; + _graphicsDevice.RasterizerState = _rasterizerState; + _graphicsDevice.DepthStencilState = DepthStencilState.DepthRead; + + // Handle cases of screen coordinates != from framebuffer coordinates (e.g. retina displays) + drawData.ScaleClipRects(ImGui.GetIO().DisplayFramebufferScale); + + // Setup projection + _graphicsDevice.Viewport = new Viewport(0, 0, _graphicsDevice.PresentationParameters.BackBufferWidth, _graphicsDevice.PresentationParameters.BackBufferHeight); + + UpdateBuffers(drawData); + + RenderCommandLists(drawData); + + // Restore modified state + _graphicsDevice.Viewport = lastViewport; + _graphicsDevice.ScissorRectangle = lastScissorBox; + _graphicsDevice.RasterizerState = lastRasterizer; + _graphicsDevice.DepthStencilState = lastDepthStencil; + _graphicsDevice.BlendState = lastBlendState; + _graphicsDevice.BlendFactor = lastBlendFactor; + } + + private unsafe void UpdateBuffers(ImDrawDataPtr drawData) + { + if (drawData.TotalVtxCount == 0) + { + return; + } + + // Expand buffers if we need more room + if (drawData.TotalVtxCount > _vertexBufferSize) + { + _vertexBuffer?.Dispose(); + + _vertexBufferSize = (int)(drawData.TotalVtxCount * 1.5f); + _vertexBuffer = new VertexBuffer(_graphicsDevice, DrawVertDeclaration.Declaration, _vertexBufferSize, BufferUsage.None); + _vertexData = new byte[_vertexBufferSize * DrawVertDeclaration.Size]; + } + + if (drawData.TotalIdxCount > _indexBufferSize) + { + _indexBuffer?.Dispose(); + + _indexBufferSize = (int)(drawData.TotalIdxCount * 1.5f); + _indexBuffer = new IndexBuffer(_graphicsDevice, IndexElementSize.SixteenBits, _indexBufferSize, BufferUsage.None); + _indexData = new byte[_indexBufferSize * sizeof(ushort)]; + } + + // Copy ImGui's vertices and indices to a set of managed byte arrays + int vtxOffset = 0; + int idxOffset = 0; + + for (int n = 0; n < drawData.CmdListsCount; n++) + { + ImDrawListPtr cmdList = drawData.CmdLists[n]; + + fixed (void* vtxDstPtr = &_vertexData[vtxOffset * DrawVertDeclaration.Size]) + fixed (void* idxDstPtr = &_indexData[idxOffset * sizeof(ushort)]) + { + Buffer.MemoryCopy((void*)cmdList.VtxBuffer.Data, vtxDstPtr, _vertexData.Length, cmdList.VtxBuffer.Size * DrawVertDeclaration.Size); + Buffer.MemoryCopy((void*)cmdList.IdxBuffer.Data, idxDstPtr, _indexData.Length, cmdList.IdxBuffer.Size * sizeof(ushort)); + } + + vtxOffset += cmdList.VtxBuffer.Size; + idxOffset += cmdList.IdxBuffer.Size; + } + + // Copy the managed byte arrays to the gpu vertex- and index buffers + _vertexBuffer.SetData(_vertexData, 0, drawData.TotalVtxCount * DrawVertDeclaration.Size); + _indexBuffer.SetData(_indexData, 0, drawData.TotalIdxCount * sizeof(ushort)); + } + + private unsafe void RenderCommandLists(ImDrawDataPtr drawData) + { + _graphicsDevice.SetVertexBuffer(_vertexBuffer); + _graphicsDevice.Indices = _indexBuffer; + + int vtxOffset = 0; + int idxOffset = 0; + + for (int n = 0; n < drawData.CmdListsCount; n++) + { + ImDrawListPtr cmdList = drawData.CmdLists[n]; + + for (int cmdi = 0; cmdi < cmdList.CmdBuffer.Size; cmdi++) + { + ImDrawCmdPtr drawCmd = cmdList.CmdBuffer[cmdi]; + + if (drawCmd.ElemCount == 0) + { + continue; + } + + if (!_loadedTextures.ContainsKey(drawCmd.TextureId)) + { + throw new InvalidOperationException($"Could not find a texture with id '{drawCmd.TextureId}', please check your bindings"); + } + + _graphicsDevice.ScissorRectangle = new Rectangle( + (int)drawCmd.ClipRect.X, + (int)drawCmd.ClipRect.Y, + (int)(drawCmd.ClipRect.Z - drawCmd.ClipRect.X), + (int)(drawCmd.ClipRect.W - drawCmd.ClipRect.Y) + ); + + var effect = UpdateEffect(_loadedTextures[drawCmd.TextureId]); + + foreach (var pass in effect.CurrentTechnique.Passes) + { + pass.Apply(); + +#pragma warning disable CS0618 // // FNA does not expose an alternative method. + _graphicsDevice.DrawIndexedPrimitives( + primitiveType: PrimitiveType.TriangleList, + baseVertex: (int)drawCmd.VtxOffset + vtxOffset, + minVertexIndex: 0, + numVertices: cmdList.VtxBuffer.Size, + startIndex: (int)drawCmd.IdxOffset + idxOffset, + primitiveCount: (int)drawCmd.ElemCount / 3 + ); +#pragma warning restore CS0618 + } + } + + vtxOffset += cmdList.VtxBuffer.Size; + idxOffset += cmdList.IdxBuffer.Size; + } + } + + #endregion Internals + } +} \ No newline at end of file diff --git a/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/Input/GamePadInfo.cs b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/Input/GamePadInfo.cs new file mode 100644 index 00000000..7fd16126 --- /dev/null +++ b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/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/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/Input/InputManager.cs b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/Input/InputManager.cs new file mode 100644 index 00000000..1790eb70 --- /dev/null +++ b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/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/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/Input/KeyboardInfo.cs b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/Input/KeyboardInfo.cs new file mode 100644 index 00000000..c6770cb0 --- /dev/null +++ b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/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/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/Input/MouseButton.cs b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/Input/MouseButton.cs new file mode 100644 index 00000000..5b041f80 --- /dev/null +++ b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/Input/MouseButton.cs @@ -0,0 +1,10 @@ +namespace MonoGameLibrary.Input; + +public enum MouseButton +{ + Left, + Middle, + Right, + XButton1, + XButton2 +} diff --git a/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/Input/MouseInfo.cs b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/Input/MouseInfo.cs new file mode 100644 index 00000000..09d6207c --- /dev/null +++ b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/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/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/MonoGameLibrary.csproj b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/MonoGameLibrary.csproj new file mode 100644 index 00000000..69adcc21 --- /dev/null +++ b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/MonoGameLibrary.csproj @@ -0,0 +1,12 @@ + + + net8.0 + true + + + + + All + + + \ No newline at end of file diff --git a/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/Scenes/Scene.cs b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/Scenes/Scene.cs new file mode 100644 index 00000000..627d220f --- /dev/null +++ b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/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/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/Scenes/SceneTransition.cs b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/Scenes/SceneTransition.cs new file mode 100644 index 00000000..bbd1f7d5 --- /dev/null +++ b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/Scenes/SceneTransition.cs @@ -0,0 +1,56 @@ +using System; +using Microsoft.Xna.Framework; + +namespace MonoGameLibrary.Scenes; + +public class SceneTransition +{ + public DateTimeOffset StartTime; + public TimeSpan Duration; + + /// + /// true when the transition is progressing from 0 to 1. + /// false when the transition is progressing from 1 to 0. + /// + public bool IsForwards; + + /// + /// The index into the + /// + public int TextureIndex; + + /// + /// The 0 to 1 value representing the progress of the transition. + /// + public float ProgressRatio => MathHelper.Clamp((float)(EndTime - DateTimeOffset.Now).TotalMilliseconds / (float)Duration.TotalMilliseconds, 0, 1); + + public float DirectionalRatio => IsForwards ? 1 - ProgressRatio : ProgressRatio; + + public DateTimeOffset EndTime => StartTime + Duration; + public bool IsComplete => DateTimeOffset.Now >= EndTime; + + + /// + /// Create a new transition + /// + /// + /// how long will the transition last in milliseconds? + /// + /// + /// should the transition be animating the Progress parameter from 0 to 1, or 1 to 0? + /// + /// + public static SceneTransition Create(int durationMs, bool isForwards) + { + return new SceneTransition + { + Duration = TimeSpan.FromMilliseconds(durationMs), + StartTime = DateTimeOffset.Now, + TextureIndex = Random.Shared.Next(), + IsForwards = isForwards + }; + } + + public static SceneTransition Open(int durationMs) => Create(durationMs, true); + public static SceneTransition Close(int durationMs) => Create(durationMs, false); +} \ No newline at end of file diff --git a/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/SharedContent/SharedContent.mgcb b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/SharedContent/SharedContent.mgcb new file mode 100644 index 00000000..8c44711d --- /dev/null +++ b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/SharedContent/SharedContent.mgcb @@ -0,0 +1,81 @@ + +#----------------------------- Global Properties ----------------------------# + +/outputDir:bin/$(Platform) +/intermediateDir:obj/$(Platform) +/platform:DesktopGL +/config: +/profile:Reach +/compress:False + +#-------------------------------- References --------------------------------# + + +#---------------------------------- Content ---------------------------------# + +#begin effects/3dEffect.fx +/importer:EffectImporter +/processor:EffectProcessor +/processorParam:DebugMode=Auto +/build:effects/3dEffect.fx + +#begin effects/colorSwapEffect.fx +/importer:EffectImporter +/processor:EffectProcessor +/processorParam:DebugMode=Auto +/build:effects/colorSwapEffect.fx + +#begin effects/sceneTransitionEffect.fx +/importer:EffectImporter +/processor:EffectProcessor +/processorParam:DebugMode=Auto +/build:effects/sceneTransitionEffect.fx + +#begin images/angled.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/angled.png + +#begin images/concave.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/concave.png + +#begin images/radial.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/radial.png + +#begin images/ripple.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/ripple.png + diff --git a/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/SharedContent/effects/3dEffect.fx b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/SharedContent/effects/3dEffect.fx new file mode 100644 index 00000000..454e0b37 --- /dev/null +++ b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/SharedContent/effects/3dEffect.fx @@ -0,0 +1,31 @@ +#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; + +sampler2D SpriteTextureSampler = sampler_state +{ + Texture = ; +}; + +#include "3dEffect.fxh" + +float4 MainPS(VertexShaderOutput input) : COLOR +{ + return tex2D(SpriteTextureSampler,input.TextureCoordinates) * input.Color; +} + +technique SpriteDrawing +{ + pass P0 + { + PixelShader = compile PS_SHADERMODEL MainPS(); + VertexShader = compile VS_SHADERMODEL MainVS(); + } +}; \ No newline at end of file diff --git a/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/SharedContent/effects/3dEffect.fxh b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/SharedContent/effects/3dEffect.fxh new file mode 100644 index 00000000..304503fe --- /dev/null +++ b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/SharedContent/effects/3dEffect.fxh @@ -0,0 +1,45 @@ +#ifndef EFFECT_3DEFFECT +#define EFFECT_3DEFFECT +#include "common.fxh" + +float4x4 MatrixTransform; +float2 ScreenSize; +float SpinAmount; + +VertexShaderOutput MainVS(VertexShaderInput input) +{ + VertexShaderOutput output; + + float4 pos = input.Position; + + // create the center of rotation + float2 centerXZ = float2(ScreenSize.x * .5, 0); + + // convert the debug variable into an angle from 0 to 2 pi. + // shaders use radians for angles, so 2 pi = 360 degrees + float angle = SpinAmount * 6.28; + + // pre-compute the cos and sin of the angle + float cosA = cos(angle); + float sinA = sin(angle); + + // shift the position to the center of rotation + pos.xz -= centerXZ; + + // compute the rotation + float nextX = pos.x * cosA - pos.z * sinA; + float nextZ = pos.x * sinA + pos.z * cosA; + + // apply the rotation + pos.x = nextX; + pos.z = nextZ; + + // shift the position away from the center of rotation + pos.xz += centerXZ; + + output.Position = mul(pos, MatrixTransform); + output.Color = input.Color; + output.TextureCoordinates = input.TexCoord; + return output; +} +#endif \ No newline at end of file diff --git a/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/SharedContent/effects/colorSwapEffect.fx b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/SharedContent/effects/colorSwapEffect.fx new file mode 100644 index 00000000..8e4d4f2f --- /dev/null +++ b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/SharedContent/effects/colorSwapEffect.fx @@ -0,0 +1,25 @@ +#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 + +// the main Sprite texture passed to SpriteBatch.Draw() +Texture2D SpriteTexture; +sampler2D SpriteTextureSampler = sampler_state +{ + Texture = ; +}; + +#include "colors.fxh" + +technique SpriteDrawing +{ + pass P0 + { + PixelShader = compile PS_SHADERMODEL ColorSwapPS(); + } +}; \ No newline at end of file diff --git a/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/SharedContent/effects/colors.fxh b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/SharedContent/effects/colors.fxh new file mode 100644 index 00000000..81e3c79e --- /dev/null +++ b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/SharedContent/effects/colors.fxh @@ -0,0 +1,68 @@ +#ifndef COLORS +#define COLORS + +#include "common.fxh" + +// the custom color map passed to the Material.SetParameter() +Texture2D ColorMap; +sampler2D ColorMapSampler = sampler_state +{ + Texture = ; + MinFilter = Point; + MagFilter = Point; + MipFilter = Point; + AddressU = Clamp; + AddressV = Clamp; +}; + +// a control variable to lerp between original color and swapped color +float OriginalAmount; +float Saturation; + +float4 Grayscale(float4 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); +} + +float4 SwapColors(float4 color) +{ + // produce the key location + float2 keyUv = float2(color.r , 0); + + // read the swap color value + float4 swappedColor = tex2D(ColorMapSampler, keyUv) * color.a; + + // ignore the swap if the map does not have a value + bool hasSwapColor = swappedColor.a > 0; + if (!hasSwapColor) + { + return color; + } + + // return the result color + return lerp(swappedColor, color, OriginalAmount); +} + +float4 ColorSwapPS(VertexShaderOutput input) : COLOR +{ + // read the original color value + float4 originalColor = tex2D(SpriteTextureSampler,input.TextureCoordinates); + + float4 swapped = SwapColors(originalColor); + float4 saturated = Grayscale(swapped); + + return saturated; +} + +#endif \ No newline at end of file diff --git a/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/SharedContent/effects/common.fxh b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/SharedContent/effects/common.fxh new file mode 100644 index 00000000..e0e849c7 --- /dev/null +++ b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/SharedContent/effects/common.fxh @@ -0,0 +1,17 @@ +#ifndef COMMON +#define COMMON + +struct VertexShaderInput +{ + float4 Position : POSITION0; + float4 Color : COLOR0; + float2 TexCoord : TEXCOORD0; +}; + +struct VertexShaderOutput +{ + float4 Position : SV_POSITION; + float4 Color : COLOR0; + float2 TextureCoordinates : TEXCOORD0; +}; +#endif \ No newline at end of file diff --git a/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/SharedContent/effects/sceneTransitionEffect.fx b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/SharedContent/effects/sceneTransitionEffect.fx new file mode 100644 index 00000000..0d87d021 --- /dev/null +++ b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/SharedContent/effects/sceneTransitionEffect.fx @@ -0,0 +1,42 @@ +#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; + +float Progress; +float EdgeWidth; + +sampler2D SpriteTextureSampler = sampler_state +{ + Texture = ; +}; + +struct VertexShaderOutput +{ + float4 Position : SV_POSITION; + float4 Color : COLOR0; + float2 TextureCoordinates : TEXCOORD0; +}; + +float4 MainPS(VertexShaderOutput input) : COLOR +{ + float2 uv = input.TextureCoordinates; + float value = tex2D(SpriteTextureSampler, uv).r; + float transitioned = smoothstep(Progress, Progress + EdgeWidth, value); + return float4(0, 0, 0, transitioned); +} + + +technique SpriteDrawing +{ + pass P0 + { + PixelShader = compile PS_SHADERMODEL MainPS(); + } +}; \ No newline at end of file diff --git a/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/SharedContent/images/angled.png b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/SharedContent/images/angled.png new file mode 100644 index 00000000..de0160f2 Binary files /dev/null and b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/SharedContent/images/angled.png differ diff --git a/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/SharedContent/images/concave.png b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/SharedContent/images/concave.png new file mode 100644 index 00000000..826e2207 Binary files /dev/null and b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/SharedContent/images/concave.png differ diff --git a/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/SharedContent/images/radial.png b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/SharedContent/images/radial.png new file mode 100644 index 00000000..bd1207cf Binary files /dev/null and b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/SharedContent/images/radial.png differ diff --git a/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/SharedContent/images/ripple.png b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/SharedContent/images/ripple.png new file mode 100644 index 00000000..e137653a Binary files /dev/null and b/Tutorials/2dShaders/src/07-Sprite-Vertex-Effect/MonoGameLibrary/SharedContent/images/ripple.png differ diff --git a/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime.sln b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime.sln new file mode 100644 index 00000000..077462d5 --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DungeonSlime", "DungeonSlime\DungeonSlime.csproj", "{88BD3FF9-D3D6-4680-A274-F866E0DFCAC4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MonoGameLibrary", "MonoGameLibrary\MonoGameLibrary.csproj", "{AB85CEEE-6D97-4438-AEC4-797D2806F44A}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {88BD3FF9-D3D6-4680-A274-F866E0DFCAC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {88BD3FF9-D3D6-4680-A274-F866E0DFCAC4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {88BD3FF9-D3D6-4680-A274-F866E0DFCAC4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {88BD3FF9-D3D6-4680-A274-F866E0DFCAC4}.Release|Any CPU.Build.0 = Release|Any CPU + {AB85CEEE-6D97-4438-AEC4-797D2806F44A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AB85CEEE-6D97-4438-AEC4-797D2806F44A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AB85CEEE-6D97-4438-AEC4-797D2806F44A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AB85CEEE-6D97-4438-AEC4-797D2806F44A}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/.config/dotnet-tools.json b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/.config/dotnet-tools.json new file mode 100644 index 00000000..afd4e2c4 --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/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/2dShaders/src/08-Light-Effect/DungeonSlime/.mgstats b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/.mgstats new file mode 100644 index 00000000..eab26b31 --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/.mgstats @@ -0,0 +1 @@ +Source File,Dest File,Processor Type,Content Type,Source File Size,Dest File Size,Build Seconds diff --git a/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/Content.mgcb b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/Content.mgcb new file mode 100644 index 00000000..24b5916f --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/Content.mgcb @@ -0,0 +1,176 @@ + +#----------------------------- 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/gameEffect.fx +/importer:EffectImporter +/processor:EffectProcessor +/processorParam:DebugMode=Auto +/build:effects/gameEffect.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-normal.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-normal.png + +#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/color-map-1.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/color-map-1.png + +#begin images/color-map-2.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/color-map-2.png + +#begin images/color-map-dark-purple.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/color-map-dark-purple.png + +#begin images/color-map-green.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/color-map-green.png + +#begin images/color-map-pink.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/color-map-pink.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/2dShaders/src/08-Light-Effect/DungeonSlime/Content/audio/bounce.wav b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/audio/bounce.wav new file mode 100644 index 00000000..baa7a47b Binary files /dev/null and b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/audio/bounce.wav differ diff --git a/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/audio/collect.wav b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/audio/collect.wav new file mode 100644 index 00000000..506220de Binary files /dev/null and b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/audio/collect.wav differ diff --git a/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/audio/theme.ogg b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/audio/theme.ogg new file mode 100644 index 00000000..72e1fd3b Binary files /dev/null and b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/audio/theme.ogg differ diff --git a/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/audio/ui.wav b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/audio/ui.wav new file mode 100644 index 00000000..63e8941e Binary files /dev/null and b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/audio/ui.wav differ diff --git a/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/effects/gameEffect.fx b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/effects/gameEffect.fx new file mode 100644 index 00000000..10f5690a --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/effects/gameEffect.fx @@ -0,0 +1,51 @@ +#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; + +sampler2D SpriteTextureSampler = sampler_state +{ + Texture = ; +}; + +Texture2D NormalMap; +sampler2D NormalMapSampler = sampler_state +{ + Texture = ; +}; + +#include "../../../MonoGameLibrary/SharedContent/effects/3dEffect.fxh" +#include "../../../MonoGameLibrary/SharedContent/effects/colors.fxh" + +struct PixelShaderOutput { + float4 color: COLOR0; + float4 normal: COLOR1; +}; + +PixelShaderOutput MainPS(VertexShaderOutput input) +{ + PixelShaderOutput output; + output.color = ColorSwapPS(input); + + // read the normal data from the NormalMap + float4 normal = tex2D(NormalMapSampler,input.TextureCoordinates); + output.normal = normal; + + return output; +} + + +technique SpriteDrawing +{ + pass P0 + { + VertexShader = compile VS_SHADERMODEL MainVS(); + PixelShader = compile PS_SHADERMODEL MainPS(); + } +}; \ No newline at end of file diff --git a/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/fonts/04B_30.spritefont b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/fonts/04B_30.spritefont new file mode 100644 index 00000000..63d4728c --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/fonts/04B_30.spritefont @@ -0,0 +1,16 @@ + + + + 04B_30.ttf + 17.5 + 0 + true + + + + + ~ + + + + diff --git a/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/fonts/04B_30.ttf b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/fonts/04B_30.ttf new file mode 100644 index 00000000..4b93740c Binary files /dev/null and b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/fonts/04B_30.ttf differ diff --git a/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/fonts/04B_30_5x.spritefont b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/fonts/04B_30_5x.spritefont new file mode 100644 index 00000000..dd239a53 --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/fonts/04B_30_5x.spritefont @@ -0,0 +1,16 @@ + + + + 04B_30.ttf + 87.5 + 0 + true + + + + + ~ + + + + diff --git a/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/fonts/04b_30.fnt b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/fonts/04b_30.fnt new file mode 100644 index 00000000..772f8c54 --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/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/2dShaders/src/08-Light-Effect/DungeonSlime/Content/images/atlas-definition.xml b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/images/atlas-definition.xml new file mode 100644 index 00000000..21772022 --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/images/atlas-definition.xml @@ -0,0 +1,34 @@ + + + images/atlas + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/images/atlas-normal.png b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/images/atlas-normal.png new file mode 100644 index 00000000..ae3ae78a Binary files /dev/null and b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/images/atlas-normal.png differ diff --git a/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/images/atlas.png b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/images/atlas.png new file mode 100644 index 00000000..f7def20f Binary files /dev/null and b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/images/atlas.png differ diff --git a/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/images/background-pattern.png b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/images/background-pattern.png new file mode 100644 index 00000000..2d8d878e Binary files /dev/null and b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/images/background-pattern.png differ diff --git a/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/images/color-map-1.png b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/images/color-map-1.png new file mode 100644 index 00000000..b5e3dc5a Binary files /dev/null and b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/images/color-map-1.png differ diff --git a/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/images/color-map-2.png b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/images/color-map-2.png new file mode 100644 index 00000000..2789bee8 Binary files /dev/null and b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/images/color-map-2.png differ diff --git a/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/images/color-map-dark-purple.png b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/images/color-map-dark-purple.png new file mode 100644 index 00000000..ffe9516e Binary files /dev/null and b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/images/color-map-dark-purple.png differ diff --git a/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/images/color-map-green.png b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/images/color-map-green.png new file mode 100644 index 00000000..87656c81 Binary files /dev/null and b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/images/color-map-green.png differ diff --git a/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/images/color-map-pink.png b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/images/color-map-pink.png new file mode 100644 index 00000000..e8910ded Binary files /dev/null and b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/images/color-map-pink.png differ diff --git a/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/images/logo.png b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/images/logo.png new file mode 100644 index 00000000..1509036c Binary files /dev/null and b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/images/logo.png differ diff --git a/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/images/tilemap-definition.xml b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Content/images/tilemap-definition.xml new file mode 100644 index 00000000..85658c60 --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/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/2dShaders/src/08-Light-Effect/DungeonSlime/DungeonSlime.csproj b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/DungeonSlime.csproj new file mode 100644 index 00000000..ab01c538 --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/DungeonSlime.csproj @@ -0,0 +1,77 @@ + + + WinExe + net8.0 + Major + false + false + + + app.manifest + Icon.ico + + + bin/$(Configuration)/$(TargetFramework) + + + + + + + + Icon.ico + + + Icon.bmp + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Game1.cs b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Game1.cs new file mode 100644 index 00000000..981a4c55 --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Game1.cs @@ -0,0 +1,75 @@ +using DungeonSlime.Scenes; +using Microsoft.Xna.Framework; +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() + { + // Allow the Core class to load any content. + base.LoadContent(); + + // Load the background theme music + _themeSong = Content.Load("audio/theme"); + } +} diff --git a/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/GameController.cs b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/GameController.cs new file mode 100644 index 00000000..a85df08f --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/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/2dShaders/src/08-Light-Effect/DungeonSlime/GameObjects/Bat.cs b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/GameObjects/Bat.cs new file mode 100644 index 00000000..ddc855ed --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/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/2dShaders/src/08-Light-Effect/DungeonSlime/GameObjects/Slime.cs b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/GameObjects/Slime.cs new file mode 100644 index 00000000..2198fc9a --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/GameObjects/Slime.cs @@ -0,0 +1,272 @@ +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 size of the slime + public int Size => _segments.Count; + + // 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(Action configureSpriteBatch) + { + // Iterate through each segment and draw it + for (var i = 0 ; i < _segments.Count; i ++) + { + var segment = _segments[i]; + // 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); + + // Allow the sprite batch to be configured before each call. + configureSpriteBatch(i); + + // 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/2dShaders/src/08-Light-Effect/DungeonSlime/GameObjects/SlimeSegment.cs b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/GameObjects/SlimeSegment.cs new file mode 100644 index 00000000..b00189eb --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/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/2dShaders/src/08-Light-Effect/DungeonSlime/Icon.bmp b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Icon.bmp new file mode 100644 index 00000000..2b481653 Binary files /dev/null and b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Icon.bmp differ diff --git a/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Icon.ico b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Icon.ico new file mode 100644 index 00000000..7d9dec18 Binary files /dev/null and b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Icon.ico differ diff --git a/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Program.cs b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Program.cs new file mode 100644 index 00000000..4d9be314 --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Program.cs @@ -0,0 +1,3 @@ +MonoGameLibrary.Content.ContentManagerExtensions.StartContentWatcherTask(); +using var game = new DungeonSlime.Game1(); +game.Run(); \ No newline at end of file diff --git a/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Scenes/GameScene.cs b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Scenes/GameScene.cs new file mode 100644 index 00000000..49431a24 --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Scenes/GameScene.cs @@ -0,0 +1,575 @@ +using System; +using System.Collections.Generic; +using DungeonSlime.GameObjects; +using DungeonSlime.UI; +using ImGuiNET; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Audio; +using Microsoft.Xna.Framework.Graphics; +using MonoGameGum; +using MonoGameLibrary; +using MonoGameLibrary.Content; +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; + + // The normal texture atlas + private Texture2D _normalAtlas; + + // 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 amount of saturation to provide the grayscale shader effect + private float _saturation = 1.0f; + private Texture2D _colorMap; + private RedColorMap _slimeColorMap; + private TimeSpan _lastGrowTime; + + // The uber material for the game objects + private Material _gameMaterial; + private SpriteCamera3d _camera; + + // The deferred rendering resources + private DeferredRenderer _deferredRenderer; + + // A list of point lights to be rendered + private List _lights = new List(); + + // 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(); + + // Create the deferred rendering resources + _deferredRenderer = new DeferredRenderer(); + InitializeLights(); + } + + private void InitializeLights() + { + // torch 1 + _lights.Add(new PointLight + { + Position = new Vector2(260, 100), + Color = Color.CornflowerBlue, + Radius = 500 + }); + // torch 2 + _lights.Add(new PointLight + { + Position = new Vector2(520, 100), + Color = Color.CornflowerBlue, + Radius = 500 + }); + // torch 3 + _lights.Add(new PointLight + { + Position = new Vector2(740, 100), + Color = Color.CornflowerBlue, + Radius = 500 + }); + // torch 4 + _lights.Add(new PointLight + { + Position = new Vector2(1000, 100), + Color = Color.CornflowerBlue, + Radius = 500 + }); + + // random lights + _lights.Add(new PointLight + { + Position = new Vector2(Random.Shared.Next(50, 400),400), + Color = Color.MonoGameOrange, + Radius = 500 + }); + _lights.Add(new PointLight + { + Position = new Vector2(Random.Shared.Next(650, 1200),300), + Color = Color.MonoGameOrange, + Radius = 500 + }); + } + + 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 colorSwap map + _colorMap = Content.Load("images/color-map-dark-purple"); + _slimeColorMap = new RedColorMap(); + _slimeColorMap.SetColorsByExistingColorMap(_colorMap); + _slimeColorMap.SetColorsByRedValue(new Dictionary + { + // main color + [32] = Color.LightSteelBlue, + }, false); + + // Load the normal maps + _normalAtlas = Content.Load("images/atlas-normal"); + + // Load the game material + _gameMaterial = Content.WatchMaterial("effects/gameEffect"); + _gameMaterial.SetParameter("ColorMap", _colorMap); + _camera = new SpriteCamera3d(); + _gameMaterial.SetParameter("MatrixTransform", _camera.CalculateMatrixTransform()); + _gameMaterial.SetParameter("ScreenSize", new Vector2(Core.GraphicsDevice.Viewport.Width, Core.GraphicsDevice.Viewport.Height)); + _gameMaterial.SetParameter("NormalMap", _normalAtlas); + } + + public override void Update(GameTime gameTime) + { + // Ensure the UI is always updated + _ui.Update(gameTime); + + // Set the camera view to look at the player slime + var viewport = Core.GraphicsDevice.Viewport; + var center = .5f * new Vector2(viewport.Width, viewport.Height); + var slimePosition = new Vector2(_slime?.GetBounds().X ?? center.X, _slime?.GetBounds().Y ?? center.Y); + var offset = .01f * (slimePosition - center); + _camera.LookOffset = offset; + + var matrixTransform = _camera.CalculateMatrixTransform(); + _gameMaterial.SetParameter("MatrixTransform", matrixTransform); + Core.PointLightMaterial.SetParameter("MatrixTransform", matrixTransform); + Core.PointLightMaterial.SetParameter("ScreenSize", new Vector2(Core.GraphicsDevice.Viewport.Width, Core.GraphicsDevice.Viewport.Height)); + + // Update the colorSwap material if it was changed + _gameMaterial.Update(); + //return; + + 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; + } + } + else + { + _saturation = 1; + } + + // 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(gameTime); + + // Move some lights around for artistic effect + MoveLightsAround(gameTime); + } + + private void MoveLightsAround(GameTime gameTime) + { + var t = (float)gameTime.TotalGameTime.TotalSeconds * .25f; + var bounds = Core.GraphicsDevice.Viewport.Bounds; + bounds.Inflate(-100, -100); + + var halfWidth = bounds.Width / 2; + var halfHeight = bounds.Height / 2; + var center = new Vector2(halfWidth, halfHeight); + _lights[^1].Position = center + new Vector2(halfWidth * MathF.Cos(t), .7f * halfHeight * MathF.Sin(t * 1.1f)); + _lights[^2].Position = center + new Vector2(halfWidth * MathF.Cos(t + MathHelper.Pi), halfHeight * MathF.Sin(t - MathHelper.Pi)); + } + + private void CollisionChecks(GameTime gameTime) + { + // 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(); + + // Remember when the last time the slime grew + _lastGrowTime = gameTime.TotalGameTime; + + // 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(new Color(32, 16, 20)); + + _gameMaterial.SetParameter("Saturation", _saturation); + + // Start rendering to the deferred renderer + _deferredRenderer.StartColorPhase(); + Core.SpriteBatch.Begin( + samplerState: SamplerState.PointClamp, + sortMode: SpriteSortMode.Immediate, + rasterizerState: RasterizerState.CullNone, + effect: _gameMaterial.Effect); + + // Update the colorMap + _gameMaterial.SetParameter("ColorMap", _colorMap); + + // Draw the tilemap + _tilemap.Draw(Core.SpriteBatch); + + // Draw the bat. + _bat.Draw(); + + // Draw the slime. + _slime.Draw(segmentIndex => + { + const int flashTimeMs = 125; + var map = _colorMap; + var elapsedMs = (gameTime.TotalGameTime.TotalMilliseconds - _lastGrowTime.TotalMilliseconds); + var intervalsAgo = (int)(elapsedMs / flashTimeMs); + + if (intervalsAgo < _slime.Size && (intervalsAgo - segmentIndex) % _slime.Size == 0) + { + map = _slimeColorMap.ColorMap; + } + + _gameMaterial.SetParameter("ColorMap", map); + }); + + // Always end the sprite batch when finished. + Core.SpriteBatch.End(); + + // start rendering the lights + _deferredRenderer.StartLightPhase(); + PointLight.Draw(Core.SpriteBatch, _lights, _deferredRenderer.NormalBuffer); + + // finish the deferred rendering + _deferredRenderer.Finish(); + _deferredRenderer.DrawComposite(); + + // Draw the UI + _ui.Draw(); + + // Render the debug view for the game + // _deferredRenderer.DebugDraw(); + + } +} diff --git a/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Scenes/TitleScene.cs b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Scenes/TitleScene.cs new file mode 100644 index 00000000..b0f4d794 --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/Scenes/TitleScene.cs @@ -0,0 +1,368 @@ +using System; +using DungeonSlime.UI; +using Gum.Forms.Controls; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Audio; +using Microsoft.Xna.Framework.Graphics; +using MonoGameGum; +using MonoGameGum.GueDeriving; +using MonoGameLibrary; +using MonoGameLibrary.Content; +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; + + // The 3d material + private Material _3dMaterial; + + 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"); + + // Load the 3d effect + _3dMaterial = Core.SharedContent.WatchMaterial("effects/3dEffect"); + + var camera = new SpriteCamera3d + { + Fov = 40 + }; + _3dMaterial.SetParameter("MatrixTransform", camera.CalculateMatrixTransform()); + _3dMaterial.SetParameter("ScreenSize", new Vector2(Core.GraphicsDevice.Viewport.Width, Core.GraphicsDevice.Viewport.Height)); + } + + 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) + { + _3dMaterial.Update(); + + var spinAmount = Core.Input.Mouse.X / (float)Core.GraphicsDevice.Viewport.Width; + spinAmount = MathHelper.SmoothStep(-.1f, .1f, spinAmount); + _3dMaterial.SetParameter("SpinAmount", spinAmount); + + // 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, + rasterizerState: RasterizerState.CullNone, + effect: _3dMaterial.Effect); + + // 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/2dShaders/src/08-Light-Effect/DungeonSlime/UI/AnimatedButton.cs b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/UI/AnimatedButton.cs new file mode 100644 index 00000000..4cce6ee5 --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/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 by AnimationChains 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/2dShaders/src/08-Light-Effect/DungeonSlime/UI/GameSceneUI.cs b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/UI/GameSceneUI.cs new file mode 100644 index 00000000..498655c2 --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/UI/GameSceneUI.cs @@ -0,0 +1,340 @@ +using System; +using Gum.DataTypes; +using Gum.Forms.Controls; +using Gum.Managers; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Audio; +using Microsoft.Xna.Framework.Content; +using MonoGameGum; +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/2dShaders/src/08-Light-Effect/DungeonSlime/UI/OptionsSlider.cs b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/UI/OptionsSlider.cs new file mode 100644 index 00000000..53d6ee94 --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/UI/OptionsSlider.cs @@ -0,0 +1,253 @@ +using System; +using Gum.DataTypes; +using Gum.DataTypes.Variables; +using Gum.Forms.Controls; +using Gum.Managers; +using Microsoft.Xna.Framework; +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/2dShaders/src/08-Light-Effect/DungeonSlime/app.manifest b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/app.manifest new file mode 100644 index 00000000..caf45166 --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/DungeonSlime/app.manifest @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true/pm + permonitorv2,permonitor + + + + diff --git a/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/Audio/AudioController.cs b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/Audio/AudioController.cs new file mode 100644 index 00000000..1bffd636 --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/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/2dShaders/src/08-Light-Effect/MonoGameLibrary/Circle.cs b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/Circle.cs new file mode 100644 index 00000000..0bb691bc --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/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/2dShaders/src/08-Light-Effect/MonoGameLibrary/Content/ContentManagerExtensions.cs b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/Content/ContentManagerExtensions.cs new file mode 100644 index 00000000..e012836c --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/Content/ContentManagerExtensions.cs @@ -0,0 +1,168 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; +using MonoGameLibrary.Graphics; + +namespace MonoGameLibrary.Content; + +public static class ContentManagerExtensions +{ + /// + /// Check if the given xnb file has a newer write-time than the last loaded version of the asset. + /// If the local file has been updated, reload the asset and return true. + /// + /// The that loaded the asset originally + /// The asset that will be reloaded if the xnb file is newer + /// If the asset has been reloaded, this out parameter will be set to the previous version of the asset before the newer version was loaded. + /// + /// true when asset was reloaded; false otherwise. + /// + public static bool TryRefresh(this ContentManager manager, WatchedAsset watchedAsset, out T oldAsset) + { + oldAsset = default; + + if (manager != watchedAsset.Owner) + throw new ArgumentException($"Used the wrong ContentManager to refresh {watchedAsset.AssetName}"); + + var path = Path.Combine(manager.RootDirectory, watchedAsset.AssetName) + ".xnb"; + var lastWriteTime = File.GetLastWriteTime(path); + + if (lastWriteTime <= watchedAsset.UpdatedAt) + { + return false; + } + + if (IsFileLocked(path)) return false; // wait for the file to not be locked. + + manager.UnloadAsset(watchedAsset.AssetName); + oldAsset = watchedAsset.Asset; + watchedAsset.Asset = manager.Load(watchedAsset.AssetName); + watchedAsset.UpdatedAt = lastWriteTime; + + return true; + } + + private static bool IsFileLocked(string path) + { + try + { + using FileStream _ = new FileStream(path, FileMode.Open, FileAccess.ReadWrite, FileShare.None); + // File is not locked + return false; + } + catch (IOException) + { + // File is locked or inaccessible + return true; + } + } + + /// + /// Load an asset and wrap it with the metadata required to refresh it later using the function + /// + /// + /// + /// + /// + public static WatchedAsset Watch(this ContentManager manager, string assetName) + { + var asset = manager.Load(assetName); + return new WatchedAsset + { + AssetName = assetName, + Asset = asset, + UpdatedAt = DateTimeOffset.Now, + Owner = manager + }; + } + + /// + /// Load an Effect into the wrapper class + /// + /// + /// + /// + public static Material WatchMaterial(this ContentManager manager, string assetName) + { + return new Material(manager.Watch(assetName)); + } + + + [Conditional("DEBUG")] + public static void StartContentWatcherTask() + { + var args = Environment.GetCommandLineArgs(); + foreach (var arg in args) + { + // if the application was started with the --no-reload option, then do not start the watcher. + if (arg == "--no-reload") return; + } + + // identify the project directory + var projectFile = Assembly.GetEntryAssembly().GetName().Name + ".csproj"; + var current = Directory.GetCurrentDirectory(); + string projectDirectory = null; + + while (current != null && projectDirectory == null) + { + if (File.Exists(Path.Combine(current, projectFile))) + { + // the valid project csproj exists in the directory + projectDirectory = current; + } + else + { + // try looking in the parent directory. + // When there is no parent directory, the variable becomes 'null' + current = Path.GetDirectoryName(current); + } + } + + // if no valid project was identified, then it is impossible to start the watcher + if (string.IsNullOrEmpty(projectDirectory)) return; + + // start the watcher process + var process = Process.Start(new ProcessStartInfo + { + FileName = "dotnet", + Arguments = "build -t:WatchContent --tl:off", + WorkingDirectory = projectDirectory, + WindowStyle = ProcessWindowStyle.Normal, + UseShellExecute = false, + CreateNoWindow = false + }); + + // when this program exits, make sure to emit a kill signal to the watcher process + AppDomain.CurrentDomain.ProcessExit += (_, __) => + { + try + { + if (!process.HasExited) + { + process.Kill(entireProcessTree: true); + } + } + catch + { + /* ignore */ + } + }; + AppDomain.CurrentDomain.UnhandledException += (sender, e) => + { + try + { + if (!process.HasExited) + { + process.Kill(entireProcessTree: true); + } + } + catch + { + /* ignore */ + } + }; + } +} \ No newline at end of file diff --git a/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/Content/WatchedAsset.cs b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/Content/WatchedAsset.cs new file mode 100644 index 00000000..39008666 --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/Content/WatchedAsset.cs @@ -0,0 +1,33 @@ +using System; +using Microsoft.Xna.Framework.Content; + +namespace MonoGameLibrary.Content; + +public class WatchedAsset +{ + /// + /// The latest version of the asset. + /// + public T Asset { get; set; } + + /// + /// The last time the was loaded into memory. + /// + public DateTimeOffset UpdatedAt { get; set; } + + /// + /// The name of the . This is the name used to load the asset from disk. + /// + public string AssetName { get; init; } + + /// + /// The instance that loaded the asset. + /// + public ContentManager Owner { get; init; } + + + public bool TryRefresh(out T oldAsset) + { + return Owner.TryRefresh(this, out oldAsset); + } +} \ No newline at end of file diff --git a/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/Core.cs b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/Core.cs new file mode 100644 index 00000000..34f6384f --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/Core.cs @@ -0,0 +1,299 @@ +using System; +using System.Collections.Generic; +using ImGuiNET.SampleProgram.XNA; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; +using MonoGameLibrary.Audio; +using MonoGameLibrary.Content; +using MonoGameLibrary.Graphics; +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; + + /// + /// The material that is used when changing scenes + /// + public static Material SceneTransitionMaterial { get; private set; } + + /// + /// The material that draws point lights + /// + public static Material PointLightMaterial { get; private set; } + + /// + /// The material that combines the various off screen textures + /// + public static Material DeferredCompositeMaterial { get; private set; } + + /// + /// A set of grayscale gradient textures to use as transition guides + /// + public static List SceneTransitionTextures { get; private set; } + + /// + /// The current transition between scenes + /// + public static SceneTransition SceneTransition { get; protected set; } = SceneTransition.Open(1000); + + /// + /// 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 a runtime generated 1x1 pixel texture. + /// + public static Texture2D Pixel { get; private set; } + + /// + /// Gets the ImGui renderer used for debug UIs. + /// + public static ImGuiRenderer ImGuiRenderer { get; private set; } + + /// + /// Gets the content manager used to load global assets. + /// + public static new ContentManager Content { get; private set; } + + /// + /// Gets the content manager that can load global assets from the SharedContent folder. + /// + public static ContentManager SharedContent { 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"; + + // Set the core's shared content manager, pointing to the SharedContent folder. + SharedContent = new ContentManager(Services, "SharedContent"); + + // 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 the ImGui renderer. + ImGuiRenderer = new ImGuiRenderer(this); + ImGuiRenderer.RebuildFontAtlas(); + + // Create a new input manager + Input = new InputManager(); + + // Create a new audio controller. + Audio = new AudioController(); + + // Create a 1x1 white pixel texture for drawing quads. + Pixel = new Texture2D(GraphicsDevice, 1, 1); + Pixel.SetData(new Color[]{ Color.White }); + } + + protected override void LoadContent() + { + base.LoadContent(); + + DeferredCompositeMaterial = SharedContent.WatchMaterial("effects/deferredCompositeEffect"); + + PointLightMaterial = SharedContent.WatchMaterial("effects/pointLightEffect"); + PointLightMaterial.SetParameter("LightBrightness", .25f); + PointLightMaterial.SetParameter("LightSharpness", .1f); + PointLightMaterial.IsDebugVisible = true; + + SceneTransitionMaterial = SharedContent.WatchMaterial("effects/sceneTransitionEffect"); + SceneTransitionMaterial.SetParameter("EdgeWidth", .05f); + + SceneTransitionTextures = new List(); + SceneTransitionTextures.Add(SharedContent.Load("images/angled")); + SceneTransitionTextures.Add(SharedContent.Load("images/concave")); + SceneTransitionTextures.Add(SharedContent.Load("images/radial")); + SceneTransitionTextures.Add(SharedContent.Load("images/ripple")); + } + + 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 && SceneTransition.IsComplete) + { + TransitionScene(); + } + + // If there is an active scene, update it. + if (s_activeScene != null) + { + s_activeScene.Update(gameTime); + } + + // Check if the scene transition material needs to be reloaded. + SceneTransitionMaterial.SetParameter("Progress", SceneTransition.DirectionalRatio); + SceneTransitionMaterial.Update(); + + PointLightMaterial.Update(); + DeferredCompositeMaterial.Update(); + + 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); + } + + // Draw the scene transition quad + SpriteBatch.Begin(effect: SceneTransitionMaterial.Effect); + SpriteBatch.Draw(SceneTransitionTextures[SceneTransition.TextureIndex % SceneTransitionTextures.Count], GraphicsDevice.Viewport.Bounds, Color.White); + SpriteBatch.End(); + + Material.DrawVisibleDebugUi(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; + SceneTransition = SceneTransition.Close(250); + } + } + + private static void TransitionScene() + { + SceneTransition = SceneTransition.Open(500); + + // 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/2dShaders/src/08-Light-Effect/MonoGameLibrary/Graphics/AnimatedSprite.cs b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/Graphics/AnimatedSprite.cs new file mode 100644 index 00000000..a1a3594e --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/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/2dShaders/src/08-Light-Effect/MonoGameLibrary/Graphics/Animation.cs b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/Graphics/Animation.cs new file mode 100644 index 00000000..44d61b65 --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/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/2dShaders/src/08-Light-Effect/MonoGameLibrary/Graphics/DeferredRenderer.cs b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/Graphics/DeferredRenderer.cs new file mode 100644 index 00000000..c5e269e9 --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/Graphics/DeferredRenderer.cs @@ -0,0 +1,155 @@ +using System.Collections.Generic; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace MonoGameLibrary.Graphics; + +public class DeferredRenderer +{ + /// + /// A texture that holds the unlit sprite drawings + /// + public RenderTarget2D ColorBuffer { get; set; } + + /// + /// A texture that holds the normal sprite drawins + /// + public RenderTarget2D NormalBuffer { get; set; } + + /// + /// A texture that holds the drawn lights + /// + public RenderTarget2D LightBuffer { get; set; } + + public DeferredRenderer() + { + var viewport = Core.GraphicsDevice.Viewport; + + ColorBuffer = new RenderTarget2D( + graphicsDevice: Core.GraphicsDevice, + width: viewport.Width, + height: viewport.Height, + mipMap: false, + preferredFormat: SurfaceFormat.Color, + preferredDepthFormat: DepthFormat.None); + + NormalBuffer = new RenderTarget2D( + graphicsDevice: Core.GraphicsDevice, + width: viewport.Width, + height: viewport.Height, + mipMap: false, + preferredFormat: SurfaceFormat.Color, + preferredDepthFormat: DepthFormat.None); + + LightBuffer = new RenderTarget2D( + graphicsDevice: Core.GraphicsDevice, + width: viewport.Width, + height: viewport.Height, + mipMap: false, + preferredFormat: SurfaceFormat.Color, + preferredDepthFormat: DepthFormat.None); + } + + public void StartColorPhase() + { + // all future draw calls will be drawn to the color buffer and normal buffer + Core.GraphicsDevice.SetRenderTargets(new RenderTargetBinding[] + { + // gets the results from shader semantic COLOR0 + new RenderTargetBinding(ColorBuffer), + + // gets the results from shader semantic COLOR1 + new RenderTargetBinding(NormalBuffer) + }); + Core.GraphicsDevice.Clear(Color.Transparent); + } + + public void StartLightPhase() + { + // all future draw calls will be drawn to the light buffer + Core.GraphicsDevice.SetRenderTarget(LightBuffer); + Core.GraphicsDevice.Clear(Color.Black); + } + + public void Finish() + { + // all future draw calls will be drawn to the screen + // note: 'null' means "the screen" in MonoGame + Core.GraphicsDevice.SetRenderTarget(null); + } + + public void DrawComposite(float ambient=.4f) + { + Core.DeferredCompositeMaterial.SetParameter("AmbientLight", ambient); + Core.DeferredCompositeMaterial.SetParameter("LightBuffer", LightBuffer); + var viewportBounds = Core.GraphicsDevice.Viewport.Bounds; + Core.SpriteBatch.Begin( + effect: Core.DeferredCompositeMaterial.Effect + ); + Core.SpriteBatch.Draw(ColorBuffer, viewportBounds, Color.White); + Core.SpriteBatch.End(); + } + + public void DebugDraw() + { + var viewportBounds = Core.GraphicsDevice.Viewport.Bounds; + + // the debug view for the color buffer lives in the top-left. + var colorBorderRect = new Rectangle( + x: viewportBounds.X, + y: viewportBounds.Y, + width: viewportBounds.Width / 2, + height: viewportBounds.Height / 2); + + // shrink the color rect by 8 pixels + var colorRect = colorBorderRect; + colorRect.Inflate(-8, -8); + + + // the debug view for the light buffer lives in the top-right. + var lightBorderRect = new Rectangle( + x: viewportBounds.Width / 2, + y: viewportBounds.Y, + width: viewportBounds.Width / 2, + height: viewportBounds.Height / 2); + + // shrink the light rect by 8 pixels + var lightRect = lightBorderRect; + lightRect.Inflate(-8, -8); + + // the debug view for the normal buffer lives in the top-right. + var normalBorderRect = new Rectangle( + x: viewportBounds.X, + y: viewportBounds.Height / 2, + width: viewportBounds.Width / 2, + height: viewportBounds.Height / 2); + + // shrink the normal rect by 8 pixels + var normalRect = normalBorderRect; + normalRect.Inflate(-8, -8); + + + Core.SpriteBatch.Begin(); + + // draw a debug border + Core.SpriteBatch.Draw(Core.Pixel, colorBorderRect, Color.MonoGameOrange); + + // draw the color buffer + Core.SpriteBatch.Draw(ColorBuffer, colorRect, Color.White); + + //draw a debug border + Core.SpriteBatch.Draw(Core.Pixel, lightBorderRect, Color.CornflowerBlue); + + // draw the light buffer + Core.SpriteBatch.Draw(LightBuffer, lightRect, Color.White); + + + // draw a debug border + Core.SpriteBatch.Draw(Core.Pixel, normalBorderRect, Color.MintCream); + + // draw the normal buffer + Core.SpriteBatch.Draw(NormalBuffer, normalRect, Color.White); + + Core.SpriteBatch.End(); + } +} \ No newline at end of file diff --git a/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/Graphics/Material.cs b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/Graphics/Material.cs new file mode 100644 index 00000000..f1a22a83 --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/Graphics/Material.cs @@ -0,0 +1,303 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using ImGuiNET; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using MonoGameLibrary.Content; +namespace MonoGameLibrary.Graphics; + +public class Material +{ + // materials that will be drawn during the standard debug UI pass. + private static HashSet s_debugMaterials = new HashSet(); + + /// + /// The hot-reloadable asset that this material is using + /// + public WatchedAsset Asset; + + /// + /// A cached version of the parameters available in the shader + /// + public Dictionary ParameterMap; + + /// + /// The currently loaded Effect that this material is using + /// + public Effect Effect => Asset.Asset; + + /// + /// Enable this variable to visualize the debugUI for the material + /// + public bool IsDebugVisible + { + get + { + return s_debugMaterials.Contains(this); + } + set + { + if (!value) + { + s_debugMaterials.Remove(this); + } + else + { + s_debugMaterials.Add(this); + } + } + } + + /// + /// When true, the debug UI will override parameters + /// + public bool DebugOverride; + + public Material(WatchedAsset asset) + { + Asset = asset; + UpdateParameterCache(); + } + + public void SetParameter(string name, float value) + { + if (DebugOverride) return; + + if (TryGetParameter(name, out var parameter)) + { + parameter.SetValue(value); + } + else + { + Console.WriteLine($"Warning: cannot set shader parameter=[{name}] because it does not exist in the compiled shader=[{Asset.AssetName}]"); + } + } + + public void SetParameter(string name, Matrix value) + { + if (DebugOverride) return; + + if (TryGetParameter(name, out var parameter)) + { + parameter.SetValue(value); + } + else + { + Console.WriteLine($"Warning: cannot set shader parameter=[{name}] because it does not exist in the compiled shader=[{Asset.AssetName}]"); + } + } + + public void SetParameter(string name, Vector2 value) + { + if (DebugOverride) return; + + if (TryGetParameter(name, out var parameter)) + { + parameter.SetValue(value); + } + else + { + Console.WriteLine($"Warning: cannot set shader parameter=[{name}] because it does not exist in the compiled shader=[{Asset.AssetName}]"); + } + } + + public void SetParameter(string name, Texture2D value) + { + if (DebugOverride) return; + + if (TryGetParameter(name, out var parameter)) + { + parameter.SetValue(value); + } + else + { + Console.WriteLine($"Warning: cannot set shader parameter=[{name}] because it does not exist in the compiled shader=[{Asset.AssetName}]"); + } + } + + /// + /// Check if the given parameter name is available in the compiled shader code. + /// Remember that a parameter will be optimized out of a shader if it is not being used + /// in the shader's return value. + /// + /// + /// + /// + public bool TryGetParameter(string name, out EffectParameter parameter) + { + return ParameterMap.TryGetValue(name, out parameter); + } + + /// + /// Rebuild the based on the current parameters available in the effect instance + /// + public void UpdateParameterCache() + { + ParameterMap = Effect.Parameters.ToDictionary(p => p.Name); + } + + [Conditional("DEBUG")] + public void Update() + { + if (Asset.TryRefresh(out var oldAsset)) + { + UpdateParameterCache(); + + foreach (var oldParam in oldAsset.Parameters) + { + if (!TryGetParameter(oldParam.Name, out var newParam)) + { + continue; + } + + switch (oldParam.ParameterClass) + { + case EffectParameterClass.Scalar: + newParam.SetValue(oldParam.GetValueSingle()); + break; + case EffectParameterClass.Matrix: + newParam.SetValue(oldParam.GetValueMatrix()); + break; + case EffectParameterClass.Vector when oldParam.ColumnCount == 2: // float2 + newParam.SetValue(oldParam.GetValueVector2()); + break; + case EffectParameterClass.Object: + newParam.SetValue(oldParam.GetValueTexture2D()); + break; + default: + Console.WriteLine("Warning: shader reload system was not able to re-apply property. " + + $"shader=[{Effect.Name}] " + + $"property=[{oldParam.Name}] " + + $"class=[{oldParam.ParameterClass}]"); + break; + } + } + } + } + + + + [Conditional("DEBUG")] + public void DrawDebug() + { + ImGui.Begin(Effect.Name); + + var currentSize = ImGui.GetWindowSize(); + ImGui.SetWindowSize(Effect.Name, new System.Numerics.Vector2(MathHelper.Max(100, currentSize.X), MathHelper.Max(100, currentSize.Y))); + + ImGui.AlignTextToFramePadding(); + ImGui.Text("Last Updated"); + ImGui.SameLine(); + ImGui.LabelText("##last-updated", Asset.UpdatedAt.ToString() + $" ({(DateTimeOffset.Now - Asset.UpdatedAt).ToString(@"h\:mm\:ss")} ago)"); + + ImGui.AlignTextToFramePadding(); + ImGui.Text("Override Values"); + ImGui.SameLine(); + ImGui.Checkbox("##override-values", ref DebugOverride); + + ImGui.NewLine(); + + bool ScalarSlider(string key, ref float value) + { + float min = 0; + float max = 1; + + return ImGui.SliderFloat($"##_prop{key}", ref value, min, max); + } + + foreach (var prop in ParameterMap) + { + switch (prop.Value.ParameterType, prop.Value.ParameterClass) + { + case (EffectParameterType.Single, EffectParameterClass.Scalar): + ImGui.AlignTextToFramePadding(); + ImGui.Text(prop.Key); + ImGui.SameLine(); + + var value = prop.Value.GetValueSingle(); + if (ScalarSlider(prop.Key, ref value)) + { + prop.Value.SetValue(value); + } + break; + + case (EffectParameterType.Single, EffectParameterClass.Vector): + ImGui.AlignTextToFramePadding(); + ImGui.Text(prop.Key); + + var vec2Value = prop.Value.GetValueVector2(); + ImGui.Indent(); + + ImGui.Text("X"); + ImGui.SameLine(); + + if (ScalarSlider(prop.Key + ".x", ref vec2Value.X)) + { + prop.Value.SetValue(vec2Value); + } + + ImGui.Text("Y"); + ImGui.SameLine(); + if (ScalarSlider(prop.Key + ".y", ref vec2Value.Y)) + { + prop.Value.SetValue(vec2Value); + } + ImGui.Unindent(); + break; + + case (EffectParameterType.Texture2D, EffectParameterClass.Object): + ImGui.AlignTextToFramePadding(); + ImGui.Text(prop.Key); + ImGui.SameLine(); + + var texture = prop.Value.GetValueTexture2D(); + if (texture != null) + { + var texturePtr = Core.ImGuiRenderer.BindTexture(texture); + ImGui.Image(texturePtr, new System.Numerics.Vector2(texture.Width, texture.Height)); + } + else + { + ImGui.Text("(null)"); + } + break; + + default: + ImGui.AlignTextToFramePadding(); + ImGui.Text(prop.Key); + ImGui.SameLine(); + ImGui.Text($"(unsupported {prop.Value.ParameterType}, {prop.Value.ParameterClass})"); + break; + } + } + ImGui.End(); + } + + [Conditional("DEBUG")] + public static void DrawVisibleDebugUi(GameTime gameTime) + { + // first, cull any materials that are not visible, or disposed. + var toRemove = new List(); + foreach (var material in s_debugMaterials) + { + if (material.Effect.IsDisposed) + { + toRemove.Add(material); + } + } + + foreach (var material in toRemove) + { + s_debugMaterials.Remove(material); + } + + Core.ImGuiRenderer.BeforeLayout(gameTime); + foreach (var material in s_debugMaterials) + { + material.DrawDebug(); + } + Core.ImGuiRenderer.AfterLayout(); + } +} \ No newline at end of file diff --git a/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/Graphics/PointLight.cs b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/Graphics/PointLight.cs new file mode 100644 index 00000000..cc539cc6 --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/Graphics/PointLight.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace MonoGameLibrary.Graphics; + +public class PointLight +{ + /// + /// The position of the light in world space + /// + public Vector2 Position { get; set; } + + /// + /// The color tint of the light + /// + public Color Color { get; set; } = Color.White; + + /// + /// The radius of the light in pixels + /// + public int Radius { get; set; } = 250; + + public static void Draw(SpriteBatch spriteBatch, List pointLights, Texture2D normalBuffer) + { + spriteBatch.Begin( + effect: Core.PointLightMaterial.Effect, + blendState: BlendState.Additive + ); + + foreach (var light in pointLights) + { + var diameter = light.Radius * 2; + var rect = new Rectangle((int)(light.Position.X - light.Radius), (int)(light.Position.Y - light.Radius), diameter, diameter); + spriteBatch.Draw(normalBuffer, rect, light.Color); + } + + spriteBatch.End(); + } +} \ No newline at end of file diff --git a/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/Graphics/RedColorMap.cs b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/Graphics/RedColorMap.cs new file mode 100644 index 00000000..d6e0bf3f --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/Graphics/RedColorMap.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace MonoGameLibrary.Graphics; + +public class RedColorMap +{ + public Texture2D ColorMap { get; set; } + + public RedColorMap() + { + ColorMap = new Texture2D(Core.GraphicsDevice, 256, 1, false, SurfaceFormat.Color); + } + + /// + /// Given a dictionary of red-color values (0 to 255) to swapColors, + /// Set the values of the so that it can be used + /// As the ColorMap parameter in the colorSwapEffect. + /// + public void SetColorsByRedValue(Dictionary map, bool overWrite = true) + { + var pixelData = new Color[ColorMap.Width]; + ColorMap.GetData(pixelData); + + for (var i = 0; i < pixelData.Length; i++) + { + // if the given color dictionary contains a color value for this red index, use it. + if (map.TryGetValue(i, out var swapColor)) + { + pixelData[i] = swapColor; + } + else if (overWrite) + { + // otherwise, default the pixel to transparent + pixelData[i] = Color.Transparent; + } + } + + ColorMap.SetData(pixelData); + } + + public void SetColorsByExistingColorMap(Texture2D existingColorMap) + { + var existingPixels = new Color[256]; + existingColorMap.GetData(existingPixels); + + var map = new Dictionary(); + for (var i = 0; i < existingPixels.Length; i++) + { + map[i] = existingPixels[i]; + } + + SetColorsByRedValue(map); + } +} \ No newline at end of file diff --git a/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/Graphics/Sprite.cs b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/Graphics/Sprite.cs new file mode 100644 index 00000000..20c44f0b --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/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/2dShaders/src/08-Light-Effect/MonoGameLibrary/Graphics/SpriteCamera3d.cs b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/Graphics/SpriteCamera3d.cs new file mode 100644 index 00000000..0602eb57 --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/Graphics/SpriteCamera3d.cs @@ -0,0 +1,51 @@ +using System; +using Microsoft.Xna.Framework; + +namespace MonoGameLibrary.Graphics; + +public class SpriteCamera3d +{ + /// + /// The field of view for the camera. + /// + public int Fov { get; set; } = 120; + + /// + /// By default, the camera is looking at the center of the screen. + /// This offset value can be used to "turn" the camera from the center towards the given vector value. + /// + public Vector2 LookOffset { get; set; } = Vector2.Zero; + + /// + /// Produce a matrix that will transform world-space coordinates into clip-space coordinates. + /// + /// + public Matrix CalculateMatrixTransform() + { + var viewport = Core.GraphicsDevice.Viewport; + + // start by creating the projection matrix + var projection = Matrix.CreatePerspectiveFieldOfView( + fieldOfView: MathHelper.ToRadians(Fov), + aspectRatio: Core.GraphicsDevice.Viewport.AspectRatio, + nearPlaneDistance: 0.0001f, + farPlaneDistance: 10000f + ); + + // position the camera far enough away to see the entire contents of the screen + var cameraZ = (viewport.Height * 0.5f) / (float)Math.Tan(MathHelper.ToRadians(Fov) * 0.5f); + + // create a view that is centered on the screen + var center = .5f * new Vector2(viewport.Width, viewport.Height); + var look = center + LookOffset; + var view = Matrix.CreateLookAt( + cameraPosition: new Vector3(center.X, center.Y, -cameraZ), + cameraTarget: new Vector3(look.X, look.Y, 0), + cameraUpVector: Vector3.Down + ); + + // the standard matrix format is world*view*projection, + // but given that we are skipping the world matrix, its just view*projection + return view * projection; + } +} \ No newline at end of file diff --git a/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/Graphics/TextureAtlas.cs b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/Graphics/TextureAtlas.cs new file mode 100644 index 00000000..e48c9abd --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/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/2dShaders/src/08-Light-Effect/MonoGameLibrary/Graphics/TextureRegion.cs b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/Graphics/TextureRegion.cs new file mode 100644 index 00000000..ecd69030 --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/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/2dShaders/src/08-Light-Effect/MonoGameLibrary/Graphics/Tilemap.cs b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/Graphics/Tilemap.cs new file mode 100644 index 00000000..96e1ee5e --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/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/2dShaders/src/08-Light-Effect/MonoGameLibrary/Graphics/Tileset.cs b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/Graphics/Tileset.cs new file mode 100644 index 00000000..80c2e65a --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/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/2dShaders/src/08-Light-Effect/MonoGameLibrary/ImGui/DrawVertDeclaration.cs b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/ImGui/DrawVertDeclaration.cs new file mode 100644 index 00000000..d846e7da --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/ImGui/DrawVertDeclaration.cs @@ -0,0 +1,29 @@ +using Microsoft.Xna.Framework.Graphics; + +namespace ImGuiNET.SampleProgram.XNA +{ + public static class DrawVertDeclaration + { + public static readonly VertexDeclaration Declaration; + + public static readonly int Size; + + static DrawVertDeclaration() + { + unsafe { Size = sizeof(ImDrawVert); } + + Declaration = new VertexDeclaration( + Size, + + // Position + new VertexElement(0, VertexElementFormat.Vector2, VertexElementUsage.Position, 0), + + // UV + new VertexElement(8, VertexElementFormat.Vector2, VertexElementUsage.TextureCoordinate, 0), + + // Color + new VertexElement(16, VertexElementFormat.Color, VertexElementUsage.Color, 0) + ); + } + } +} \ No newline at end of file diff --git a/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/ImGui/ImGuiRenderer.cs b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/ImGui/ImGuiRenderer.cs new file mode 100644 index 00000000..e2cc1a29 --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/ImGui/ImGuiRenderer.cs @@ -0,0 +1,436 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace ImGuiNET.SampleProgram.XNA +{ + /// + /// ImGui renderer for use with XNA-likes (FNA & MonoGame) + /// + public class ImGuiRenderer + { + private Game _game; + + // Graphics + private GraphicsDevice _graphicsDevice; + + private BasicEffect _effect; + private RasterizerState _rasterizerState; + + private byte[] _vertexData; + private VertexBuffer _vertexBuffer; + private int _vertexBufferSize; + + private byte[] _indexData; + private IndexBuffer _indexBuffer; + private int _indexBufferSize; + + // Textures + private Dictionary _loadedTextures; + + private int _textureId; + private IntPtr? _fontTextureId; + + // Input + private int _scrollWheelValue; + private int _horizontalScrollWheelValue; + private readonly float WHEEL_DELTA = 120; + private Keys[] _allKeys = Enum.GetValues(); + + public ImGuiRenderer(Game game) + { + var context = ImGui.CreateContext(); + ImGui.SetCurrentContext(context); + + _game = game ?? throw new ArgumentNullException(nameof(game)); + _graphicsDevice = game.GraphicsDevice; + + _loadedTextures = new Dictionary(); + + _rasterizerState = new RasterizerState() + { + CullMode = CullMode.None, + DepthBias = 0, + FillMode = FillMode.Solid, + MultiSampleAntiAlias = false, + ScissorTestEnable = true, + SlopeScaleDepthBias = 0 + }; + + SetupInput(); + } + + #region ImGuiRenderer + + /// + /// Creates a texture and loads the font data from ImGui. Should be called when the is initialized but before any rendering is done + /// + public virtual unsafe void RebuildFontAtlas() + { + // Get font texture from ImGui + var io = ImGui.GetIO(); + io.Fonts.GetTexDataAsRGBA32(out byte* pixelData, out int width, out int height, out int bytesPerPixel); + + // Copy the data to a managed array + var pixels = new byte[width * height * bytesPerPixel]; + unsafe { Marshal.Copy(new IntPtr(pixelData), pixels, 0, pixels.Length); } + + // Create and register the texture as an XNA texture + var tex2d = new Texture2D(_graphicsDevice, width, height, false, SurfaceFormat.Color); + tex2d.SetData(pixels); + + // Should a texture already have been build previously, unbind it first so it can be deallocated + if (_fontTextureId.HasValue) UnbindTexture(_fontTextureId.Value); + + // Bind the new texture to an ImGui-friendly id + _fontTextureId = BindTexture(tex2d); + + // Let ImGui know where to find the texture + io.Fonts.SetTexID(_fontTextureId.Value); + io.Fonts.ClearTexData(); // Clears CPU side texture data + } + + /// + /// Creates a pointer to a texture, which can be passed through ImGui calls such as . That pointer is then used by ImGui to let us know what texture to draw + /// + public virtual IntPtr BindTexture(Texture2D texture) + { + var id = new IntPtr(_textureId++); + + _loadedTextures.Add(id, texture); + + return id; + } + + /// + /// Removes a previously created texture pointer, releasing its reference and allowing it to be deallocated + /// + public virtual void UnbindTexture(IntPtr textureId) + { + _loadedTextures.Remove(textureId); + } + + /// + /// Sets up ImGui for a new frame, should be called at frame start + /// + public virtual void BeforeLayout(GameTime gameTime) + { + ImGui.GetIO().DeltaTime = (float)gameTime.ElapsedGameTime.TotalSeconds; + + UpdateInput(); + + ImGui.NewFrame(); + } + + /// + /// Asks ImGui for the generated geometry data and sends it to the graphics pipeline, should be called after the UI is drawn using ImGui.** calls + /// + public virtual void AfterLayout() + { + ImGui.Render(); + + unsafe { RenderDrawData(ImGui.GetDrawData()); } + } + + #endregion ImGuiRenderer + + #region Setup & Update + + /// + /// Setup key input event handler. + /// + protected virtual void SetupInput() + { + var io = ImGui.GetIO(); + + // MonoGame-specific ////////////////////// + _game.Window.TextInput += (s, a) => + { + if (a.Character == '\t') return; + io.AddInputCharacter(a.Character); + }; + + /////////////////////////////////////////// + + // FNA-specific /////////////////////////// + //TextInputEXT.TextInput += c => + //{ + // if (c == '\t') return; + + // ImGui.GetIO().AddInputCharacter(c); + //}; + /////////////////////////////////////////// + } + + /// + /// Updates the to the current matrices and texture + /// + protected virtual Effect UpdateEffect(Texture2D texture) + { + _effect = _effect ?? new BasicEffect(_graphicsDevice); + + var io = ImGui.GetIO(); + + _effect.World = Matrix.Identity; + _effect.View = Matrix.Identity; + _effect.Projection = Matrix.CreateOrthographicOffCenter(0f, io.DisplaySize.X, io.DisplaySize.Y, 0f, -1f, 1f); + _effect.TextureEnabled = true; + _effect.Texture = texture; + _effect.VertexColorEnabled = true; + + return _effect; + } + + /// + /// Sends XNA input state to ImGui + /// + protected virtual void UpdateInput() + { + if (!_game.IsActive) return; + + var io = ImGui.GetIO(); + + var mouse = Mouse.GetState(); + var keyboard = Keyboard.GetState(); + io.AddMousePosEvent(mouse.X, mouse.Y); + io.AddMouseButtonEvent(0, mouse.LeftButton == ButtonState.Pressed); + io.AddMouseButtonEvent(1, mouse.RightButton == ButtonState.Pressed); + io.AddMouseButtonEvent(2, mouse.MiddleButton == ButtonState.Pressed); + io.AddMouseButtonEvent(3, mouse.XButton1 == ButtonState.Pressed); + io.AddMouseButtonEvent(4, mouse.XButton2 == ButtonState.Pressed); + + io.AddMouseWheelEvent( + (mouse.HorizontalScrollWheelValue - _horizontalScrollWheelValue) / WHEEL_DELTA, + (mouse.ScrollWheelValue - _scrollWheelValue) / WHEEL_DELTA); + _scrollWheelValue = mouse.ScrollWheelValue; + _horizontalScrollWheelValue = mouse.HorizontalScrollWheelValue; + + foreach (var key in _allKeys) + { + if (TryMapKeys(key, out ImGuiKey imguikey)) + { + io.AddKeyEvent(imguikey, keyboard.IsKeyDown(key)); + } + } + + io.DisplaySize = new System.Numerics.Vector2(_graphicsDevice.PresentationParameters.BackBufferWidth, _graphicsDevice.PresentationParameters.BackBufferHeight); + io.DisplayFramebufferScale = new System.Numerics.Vector2(1f, 1f); + } + + private bool TryMapKeys(Keys key, out ImGuiKey imguikey) + { + //Special case not handed in the switch... + //If the actual key we put in is "None", return none and true. + //otherwise, return none and false. + if (key == Keys.None) + { + imguikey = ImGuiKey.None; + return true; + } + + imguikey = key switch + { + Keys.Back => ImGuiKey.Backspace, + Keys.Tab => ImGuiKey.Tab, + Keys.Enter => ImGuiKey.Enter, + Keys.CapsLock => ImGuiKey.CapsLock, + Keys.Escape => ImGuiKey.Escape, + Keys.Space => ImGuiKey.Space, + Keys.PageUp => ImGuiKey.PageUp, + Keys.PageDown => ImGuiKey.PageDown, + Keys.End => ImGuiKey.End, + Keys.Home => ImGuiKey.Home, + Keys.Left => ImGuiKey.LeftArrow, + Keys.Right => ImGuiKey.RightArrow, + Keys.Up => ImGuiKey.UpArrow, + Keys.Down => ImGuiKey.DownArrow, + Keys.PrintScreen => ImGuiKey.PrintScreen, + Keys.Insert => ImGuiKey.Insert, + Keys.Delete => ImGuiKey.Delete, + >= Keys.D0 and <= Keys.D9 => ImGuiKey._0 + (key - Keys.D0), + >= Keys.A and <= Keys.Z => ImGuiKey.A + (key - Keys.A), + >= Keys.NumPad0 and <= Keys.NumPad9 => ImGuiKey.Keypad0 + (key - Keys.NumPad0), + Keys.Multiply => ImGuiKey.KeypadMultiply, + Keys.Add => ImGuiKey.KeypadAdd, + Keys.Subtract => ImGuiKey.KeypadSubtract, + Keys.Decimal => ImGuiKey.KeypadDecimal, + Keys.Divide => ImGuiKey.KeypadDivide, + >= Keys.F1 and <= Keys.F24 => ImGuiKey.F1 + (key - Keys.F1), + Keys.NumLock => ImGuiKey.NumLock, + Keys.Scroll => ImGuiKey.ScrollLock, + Keys.LeftShift => ImGuiKey.ModShift, + Keys.LeftControl => ImGuiKey.ModCtrl, + Keys.LeftAlt => ImGuiKey.ModAlt, + Keys.OemSemicolon => ImGuiKey.Semicolon, + Keys.OemPlus => ImGuiKey.Equal, + Keys.OemComma => ImGuiKey.Comma, + Keys.OemMinus => ImGuiKey.Minus, + Keys.OemPeriod => ImGuiKey.Period, + Keys.OemQuestion => ImGuiKey.Slash, + Keys.OemTilde => ImGuiKey.GraveAccent, + Keys.OemOpenBrackets => ImGuiKey.LeftBracket, + Keys.OemCloseBrackets => ImGuiKey.RightBracket, + Keys.OemPipe => ImGuiKey.Backslash, + Keys.OemQuotes => ImGuiKey.Apostrophe, + Keys.BrowserBack => ImGuiKey.AppBack, + Keys.BrowserForward => ImGuiKey.AppForward, + _ => ImGuiKey.None, + }; + + return imguikey != ImGuiKey.None; + } + + #endregion Setup & Update + + #region Internals + + /// + /// Gets the geometry as set up by ImGui and sends it to the graphics device + /// + private void RenderDrawData(ImDrawDataPtr drawData) + { + // Setup render state: alpha-blending enabled, no face culling, no depth testing, scissor enabled, vertex/texcoord/color pointers + var lastViewport = _graphicsDevice.Viewport; + var lastScissorBox = _graphicsDevice.ScissorRectangle; + var lastRasterizer = _graphicsDevice.RasterizerState; + var lastDepthStencil = _graphicsDevice.DepthStencilState; + var lastBlendFactor = _graphicsDevice.BlendFactor; + var lastBlendState = _graphicsDevice.BlendState; + + _graphicsDevice.BlendFactor = Color.White; + _graphicsDevice.BlendState = BlendState.NonPremultiplied; + _graphicsDevice.RasterizerState = _rasterizerState; + _graphicsDevice.DepthStencilState = DepthStencilState.DepthRead; + + // Handle cases of screen coordinates != from framebuffer coordinates (e.g. retina displays) + drawData.ScaleClipRects(ImGui.GetIO().DisplayFramebufferScale); + + // Setup projection + _graphicsDevice.Viewport = new Viewport(0, 0, _graphicsDevice.PresentationParameters.BackBufferWidth, _graphicsDevice.PresentationParameters.BackBufferHeight); + + UpdateBuffers(drawData); + + RenderCommandLists(drawData); + + // Restore modified state + _graphicsDevice.Viewport = lastViewport; + _graphicsDevice.ScissorRectangle = lastScissorBox; + _graphicsDevice.RasterizerState = lastRasterizer; + _graphicsDevice.DepthStencilState = lastDepthStencil; + _graphicsDevice.BlendState = lastBlendState; + _graphicsDevice.BlendFactor = lastBlendFactor; + } + + private unsafe void UpdateBuffers(ImDrawDataPtr drawData) + { + if (drawData.TotalVtxCount == 0) + { + return; + } + + // Expand buffers if we need more room + if (drawData.TotalVtxCount > _vertexBufferSize) + { + _vertexBuffer?.Dispose(); + + _vertexBufferSize = (int)(drawData.TotalVtxCount * 1.5f); + _vertexBuffer = new VertexBuffer(_graphicsDevice, DrawVertDeclaration.Declaration, _vertexBufferSize, BufferUsage.None); + _vertexData = new byte[_vertexBufferSize * DrawVertDeclaration.Size]; + } + + if (drawData.TotalIdxCount > _indexBufferSize) + { + _indexBuffer?.Dispose(); + + _indexBufferSize = (int)(drawData.TotalIdxCount * 1.5f); + _indexBuffer = new IndexBuffer(_graphicsDevice, IndexElementSize.SixteenBits, _indexBufferSize, BufferUsage.None); + _indexData = new byte[_indexBufferSize * sizeof(ushort)]; + } + + // Copy ImGui's vertices and indices to a set of managed byte arrays + int vtxOffset = 0; + int idxOffset = 0; + + for (int n = 0; n < drawData.CmdListsCount; n++) + { + ImDrawListPtr cmdList = drawData.CmdLists[n]; + + fixed (void* vtxDstPtr = &_vertexData[vtxOffset * DrawVertDeclaration.Size]) + fixed (void* idxDstPtr = &_indexData[idxOffset * sizeof(ushort)]) + { + Buffer.MemoryCopy((void*)cmdList.VtxBuffer.Data, vtxDstPtr, _vertexData.Length, cmdList.VtxBuffer.Size * DrawVertDeclaration.Size); + Buffer.MemoryCopy((void*)cmdList.IdxBuffer.Data, idxDstPtr, _indexData.Length, cmdList.IdxBuffer.Size * sizeof(ushort)); + } + + vtxOffset += cmdList.VtxBuffer.Size; + idxOffset += cmdList.IdxBuffer.Size; + } + + // Copy the managed byte arrays to the gpu vertex- and index buffers + _vertexBuffer.SetData(_vertexData, 0, drawData.TotalVtxCount * DrawVertDeclaration.Size); + _indexBuffer.SetData(_indexData, 0, drawData.TotalIdxCount * sizeof(ushort)); + } + + private unsafe void RenderCommandLists(ImDrawDataPtr drawData) + { + _graphicsDevice.SetVertexBuffer(_vertexBuffer); + _graphicsDevice.Indices = _indexBuffer; + + int vtxOffset = 0; + int idxOffset = 0; + + for (int n = 0; n < drawData.CmdListsCount; n++) + { + ImDrawListPtr cmdList = drawData.CmdLists[n]; + + for (int cmdi = 0; cmdi < cmdList.CmdBuffer.Size; cmdi++) + { + ImDrawCmdPtr drawCmd = cmdList.CmdBuffer[cmdi]; + + if (drawCmd.ElemCount == 0) + { + continue; + } + + if (!_loadedTextures.ContainsKey(drawCmd.TextureId)) + { + throw new InvalidOperationException($"Could not find a texture with id '{drawCmd.TextureId}', please check your bindings"); + } + + _graphicsDevice.ScissorRectangle = new Rectangle( + (int)drawCmd.ClipRect.X, + (int)drawCmd.ClipRect.Y, + (int)(drawCmd.ClipRect.Z - drawCmd.ClipRect.X), + (int)(drawCmd.ClipRect.W - drawCmd.ClipRect.Y) + ); + + var effect = UpdateEffect(_loadedTextures[drawCmd.TextureId]); + + foreach (var pass in effect.CurrentTechnique.Passes) + { + pass.Apply(); + +#pragma warning disable CS0618 // // FNA does not expose an alternative method. + _graphicsDevice.DrawIndexedPrimitives( + primitiveType: PrimitiveType.TriangleList, + baseVertex: (int)drawCmd.VtxOffset + vtxOffset, + minVertexIndex: 0, + numVertices: cmdList.VtxBuffer.Size, + startIndex: (int)drawCmd.IdxOffset + idxOffset, + primitiveCount: (int)drawCmd.ElemCount / 3 + ); +#pragma warning restore CS0618 + } + } + + vtxOffset += cmdList.VtxBuffer.Size; + idxOffset += cmdList.IdxBuffer.Size; + } + } + + #endregion Internals + } +} \ No newline at end of file diff --git a/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/Input/GamePadInfo.cs b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/Input/GamePadInfo.cs new file mode 100644 index 00000000..7fd16126 --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/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/2dShaders/src/08-Light-Effect/MonoGameLibrary/Input/InputManager.cs b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/Input/InputManager.cs new file mode 100644 index 00000000..1790eb70 --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/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/2dShaders/src/08-Light-Effect/MonoGameLibrary/Input/KeyboardInfo.cs b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/Input/KeyboardInfo.cs new file mode 100644 index 00000000..c6770cb0 --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/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/2dShaders/src/08-Light-Effect/MonoGameLibrary/Input/MouseButton.cs b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/Input/MouseButton.cs new file mode 100644 index 00000000..5b041f80 --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/Input/MouseButton.cs @@ -0,0 +1,10 @@ +namespace MonoGameLibrary.Input; + +public enum MouseButton +{ + Left, + Middle, + Right, + XButton1, + XButton2 +} diff --git a/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/Input/MouseInfo.cs b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/Input/MouseInfo.cs new file mode 100644 index 00000000..09d6207c --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/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/2dShaders/src/08-Light-Effect/MonoGameLibrary/MonoGameLibrary.csproj b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/MonoGameLibrary.csproj new file mode 100644 index 00000000..69adcc21 --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/MonoGameLibrary.csproj @@ -0,0 +1,12 @@ + + + net8.0 + true + + + + + All + + + \ No newline at end of file diff --git a/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/Scenes/Scene.cs b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/Scenes/Scene.cs new file mode 100644 index 00000000..627d220f --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/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/2dShaders/src/08-Light-Effect/MonoGameLibrary/Scenes/SceneTransition.cs b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/Scenes/SceneTransition.cs new file mode 100644 index 00000000..bbd1f7d5 --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/Scenes/SceneTransition.cs @@ -0,0 +1,56 @@ +using System; +using Microsoft.Xna.Framework; + +namespace MonoGameLibrary.Scenes; + +public class SceneTransition +{ + public DateTimeOffset StartTime; + public TimeSpan Duration; + + /// + /// true when the transition is progressing from 0 to 1. + /// false when the transition is progressing from 1 to 0. + /// + public bool IsForwards; + + /// + /// The index into the + /// + public int TextureIndex; + + /// + /// The 0 to 1 value representing the progress of the transition. + /// + public float ProgressRatio => MathHelper.Clamp((float)(EndTime - DateTimeOffset.Now).TotalMilliseconds / (float)Duration.TotalMilliseconds, 0, 1); + + public float DirectionalRatio => IsForwards ? 1 - ProgressRatio : ProgressRatio; + + public DateTimeOffset EndTime => StartTime + Duration; + public bool IsComplete => DateTimeOffset.Now >= EndTime; + + + /// + /// Create a new transition + /// + /// + /// how long will the transition last in milliseconds? + /// + /// + /// should the transition be animating the Progress parameter from 0 to 1, or 1 to 0? + /// + /// + public static SceneTransition Create(int durationMs, bool isForwards) + { + return new SceneTransition + { + Duration = TimeSpan.FromMilliseconds(durationMs), + StartTime = DateTimeOffset.Now, + TextureIndex = Random.Shared.Next(), + IsForwards = isForwards + }; + } + + public static SceneTransition Open(int durationMs) => Create(durationMs, true); + public static SceneTransition Close(int durationMs) => Create(durationMs, false); +} \ No newline at end of file diff --git a/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/SharedContent/SharedContent.mgcb b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/SharedContent/SharedContent.mgcb new file mode 100644 index 00000000..2088274d --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/SharedContent/SharedContent.mgcb @@ -0,0 +1,93 @@ + +#----------------------------- Global Properties ----------------------------# + +/outputDir:bin/$(Platform) +/intermediateDir:obj/$(Platform) +/platform:DesktopGL +/config: +/profile:Reach +/compress:False + +#-------------------------------- References --------------------------------# + + +#---------------------------------- Content ---------------------------------# + +#begin effects/3dEffect.fx +/importer:EffectImporter +/processor:EffectProcessor +/processorParam:DebugMode=Auto +/build:effects/3dEffect.fx + +#begin effects/colorSwapEffect.fx +/importer:EffectImporter +/processor:EffectProcessor +/processorParam:DebugMode=Auto +/build:effects/colorSwapEffect.fx + +#begin effects/deferredCompositeEffect.fx +/importer:EffectImporter +/processor:EffectProcessor +/processorParam:DebugMode=Auto +/build:effects/deferredCompositeEffect.fx + +#begin effects/pointLightEffect.fx +/importer:EffectImporter +/processor:EffectProcessor +/processorParam:DebugMode=Auto +/build:effects/pointLightEffect.fx + +#begin effects/sceneTransitionEffect.fx +/importer:EffectImporter +/processor:EffectProcessor +/processorParam:DebugMode=Auto +/build:effects/sceneTransitionEffect.fx + +#begin images/angled.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/angled.png + +#begin images/concave.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/concave.png + +#begin images/radial.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/radial.png + +#begin images/ripple.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/ripple.png + diff --git a/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/SharedContent/effects/3dEffect.fx b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/SharedContent/effects/3dEffect.fx new file mode 100644 index 00000000..454e0b37 --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/SharedContent/effects/3dEffect.fx @@ -0,0 +1,31 @@ +#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; + +sampler2D SpriteTextureSampler = sampler_state +{ + Texture = ; +}; + +#include "3dEffect.fxh" + +float4 MainPS(VertexShaderOutput input) : COLOR +{ + return tex2D(SpriteTextureSampler,input.TextureCoordinates) * input.Color; +} + +technique SpriteDrawing +{ + pass P0 + { + PixelShader = compile PS_SHADERMODEL MainPS(); + VertexShader = compile VS_SHADERMODEL MainVS(); + } +}; \ No newline at end of file diff --git a/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/SharedContent/effects/3dEffect.fxh b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/SharedContent/effects/3dEffect.fxh new file mode 100644 index 00000000..304503fe --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/SharedContent/effects/3dEffect.fxh @@ -0,0 +1,45 @@ +#ifndef EFFECT_3DEFFECT +#define EFFECT_3DEFFECT +#include "common.fxh" + +float4x4 MatrixTransform; +float2 ScreenSize; +float SpinAmount; + +VertexShaderOutput MainVS(VertexShaderInput input) +{ + VertexShaderOutput output; + + float4 pos = input.Position; + + // create the center of rotation + float2 centerXZ = float2(ScreenSize.x * .5, 0); + + // convert the debug variable into an angle from 0 to 2 pi. + // shaders use radians for angles, so 2 pi = 360 degrees + float angle = SpinAmount * 6.28; + + // pre-compute the cos and sin of the angle + float cosA = cos(angle); + float sinA = sin(angle); + + // shift the position to the center of rotation + pos.xz -= centerXZ; + + // compute the rotation + float nextX = pos.x * cosA - pos.z * sinA; + float nextZ = pos.x * sinA + pos.z * cosA; + + // apply the rotation + pos.x = nextX; + pos.z = nextZ; + + // shift the position away from the center of rotation + pos.xz += centerXZ; + + output.Position = mul(pos, MatrixTransform); + output.Color = input.Color; + output.TextureCoordinates = input.TexCoord; + return output; +} +#endif \ No newline at end of file diff --git a/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/SharedContent/effects/colorSwapEffect.fx b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/SharedContent/effects/colorSwapEffect.fx new file mode 100644 index 00000000..8e4d4f2f --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/SharedContent/effects/colorSwapEffect.fx @@ -0,0 +1,25 @@ +#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 + +// the main Sprite texture passed to SpriteBatch.Draw() +Texture2D SpriteTexture; +sampler2D SpriteTextureSampler = sampler_state +{ + Texture = ; +}; + +#include "colors.fxh" + +technique SpriteDrawing +{ + pass P0 + { + PixelShader = compile PS_SHADERMODEL ColorSwapPS(); + } +}; \ No newline at end of file diff --git a/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/SharedContent/effects/colors.fxh b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/SharedContent/effects/colors.fxh new file mode 100644 index 00000000..81e3c79e --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/SharedContent/effects/colors.fxh @@ -0,0 +1,68 @@ +#ifndef COLORS +#define COLORS + +#include "common.fxh" + +// the custom color map passed to the Material.SetParameter() +Texture2D ColorMap; +sampler2D ColorMapSampler = sampler_state +{ + Texture = ; + MinFilter = Point; + MagFilter = Point; + MipFilter = Point; + AddressU = Clamp; + AddressV = Clamp; +}; + +// a control variable to lerp between original color and swapped color +float OriginalAmount; +float Saturation; + +float4 Grayscale(float4 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); +} + +float4 SwapColors(float4 color) +{ + // produce the key location + float2 keyUv = float2(color.r , 0); + + // read the swap color value + float4 swappedColor = tex2D(ColorMapSampler, keyUv) * color.a; + + // ignore the swap if the map does not have a value + bool hasSwapColor = swappedColor.a > 0; + if (!hasSwapColor) + { + return color; + } + + // return the result color + return lerp(swappedColor, color, OriginalAmount); +} + +float4 ColorSwapPS(VertexShaderOutput input) : COLOR +{ + // read the original color value + float4 originalColor = tex2D(SpriteTextureSampler,input.TextureCoordinates); + + float4 swapped = SwapColors(originalColor); + float4 saturated = Grayscale(swapped); + + return saturated; +} + +#endif \ No newline at end of file diff --git a/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/SharedContent/effects/common.fxh b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/SharedContent/effects/common.fxh new file mode 100644 index 00000000..e0e849c7 --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/SharedContent/effects/common.fxh @@ -0,0 +1,17 @@ +#ifndef COMMON +#define COMMON + +struct VertexShaderInput +{ + float4 Position : POSITION0; + float4 Color : COLOR0; + float2 TexCoord : TEXCOORD0; +}; + +struct VertexShaderOutput +{ + float4 Position : SV_POSITION; + float4 Color : COLOR0; + float2 TextureCoordinates : TEXCOORD0; +}; +#endif \ No newline at end of file diff --git a/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/SharedContent/effects/deferredCompositeEffect.fx b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/SharedContent/effects/deferredCompositeEffect.fx new file mode 100644 index 00000000..7bffc231 --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/SharedContent/effects/deferredCompositeEffect.fx @@ -0,0 +1,54 @@ +#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; + +sampler2D SpriteTextureSampler = sampler_state +{ + Texture = ; +}; + + +Texture2D LightBuffer; +sampler2D LightBufferSampler = sampler_state +{ + Texture = ; +}; + +struct VertexShaderOutput +{ + float4 Position : SV_POSITION; + float4 Color : COLOR0; + float2 TextureCoordinates : TEXCOORD0; +}; + + + +float AmbientLight; + +float4 MainPS(VertexShaderOutput input) : COLOR +{ + float4 color = tex2D(SpriteTextureSampler,input.TextureCoordinates) * input.Color; + float4 light = tex2D(LightBufferSampler,input.TextureCoordinates) * input.Color; + + float3 toneMapped = light.xyz / (.5 + dot(light.xyz, float3(0.299, 0.587, 0.114))); + light.xyz = toneMapped; + + light = saturate(light + AmbientLight); + + return color * light; +} + +technique SpriteDrawing +{ + pass P0 + { + PixelShader = compile PS_SHADERMODEL MainPS(); + } +}; \ No newline at end of file diff --git a/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/SharedContent/effects/pointLightEffect.fx b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/SharedContent/effects/pointLightEffect.fx new file mode 100644 index 00000000..949ee180 --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/SharedContent/effects/pointLightEffect.fx @@ -0,0 +1,88 @@ +#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 NormalBuffer; +sampler2D NormalBufferSampler = sampler_state +{ + Texture = ; +}; + +#include "3dEffect.fxh" +struct LightVertexShaderOutput +{ + float4 Position : POSITION0; + float4 Color : COLOR0; + float2 TextureCoordinates : TEXCOORD0; + float3 ScreenData : TEXCOORD1; +}; + +float LightBrightness; +float LightSharpness; + + +LightVertexShaderOutput LightVS(VertexShaderInput input) +{ + LightVertexShaderOutput output; + + VertexShaderOutput mainVsOutput = MainVS(input); + + // forward along the existing values from the MainVS's output + output.Position = mainVsOutput.Position;// / mainVsOutput.Position.w; + output.Color = mainVsOutput.Color; + output.TextureCoordinates = mainVsOutput.TextureCoordinates; + + // pack the required position variables, x, y, and w, into the ScreenData + output.ScreenData.xy = output.Position.xy; + output.ScreenData.z = output.Position.w; + + return output; +} + + +float4 MainPS(LightVertexShaderOutput input) : COLOR { + float dist = length(input.TextureCoordinates - .5); + float range = 5; // arbitrary maximum. + + float falloff = saturate(.5 - dist) * (LightBrightness * range + 1); + falloff = pow(abs(falloff), LightSharpness * range + 1); + + // correct the perspective divide. + input.ScreenData /= input.ScreenData.z; + + // put the clip-space coordinates into screen space. + float2 screenCoords = .5*(input.ScreenData.xy + 1); + screenCoords.y = 1 - screenCoords.y; + + float4 normal = tex2D(NormalBufferSampler,screenCoords); + // flip the y of the normals, because the art assets have them backwards. + normal.y = 1 - normal.y; + + // convert from [0,1] to [-1,1] + float3 normalDir = (normal.xyz-.5)*2; + + // find the direction the light is travelling at the current pixel + float3 lightDir = float3(normalize(.5 - input.TextureCoordinates), 1); + + // how much is the normal direction pointing towards the light direction? + float lightAmount = (dot(normalDir, lightDir)); + + float4 color = input.Color; + color.a *= falloff * lightAmount; + return color; +} + +technique SpriteDrawing +{ + pass P0 + { + VertexShader = compile VS_SHADERMODEL LightVS(); + PixelShader = compile PS_SHADERMODEL MainPS(); + } +}; \ No newline at end of file diff --git a/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/SharedContent/effects/sceneTransitionEffect.fx b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/SharedContent/effects/sceneTransitionEffect.fx new file mode 100644 index 00000000..0d87d021 --- /dev/null +++ b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/SharedContent/effects/sceneTransitionEffect.fx @@ -0,0 +1,42 @@ +#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; + +float Progress; +float EdgeWidth; + +sampler2D SpriteTextureSampler = sampler_state +{ + Texture = ; +}; + +struct VertexShaderOutput +{ + float4 Position : SV_POSITION; + float4 Color : COLOR0; + float2 TextureCoordinates : TEXCOORD0; +}; + +float4 MainPS(VertexShaderOutput input) : COLOR +{ + float2 uv = input.TextureCoordinates; + float value = tex2D(SpriteTextureSampler, uv).r; + float transitioned = smoothstep(Progress, Progress + EdgeWidth, value); + return float4(0, 0, 0, transitioned); +} + + +technique SpriteDrawing +{ + pass P0 + { + PixelShader = compile PS_SHADERMODEL MainPS(); + } +}; \ No newline at end of file diff --git a/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/SharedContent/images/angled.png b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/SharedContent/images/angled.png new file mode 100644 index 00000000..de0160f2 Binary files /dev/null and b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/SharedContent/images/angled.png differ diff --git a/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/SharedContent/images/concave.png b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/SharedContent/images/concave.png new file mode 100644 index 00000000..826e2207 Binary files /dev/null and b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/SharedContent/images/concave.png differ diff --git a/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/SharedContent/images/radial.png b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/SharedContent/images/radial.png new file mode 100644 index 00000000..bd1207cf Binary files /dev/null and b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/SharedContent/images/radial.png differ diff --git a/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/SharedContent/images/ripple.png b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/SharedContent/images/ripple.png new file mode 100644 index 00000000..e137653a Binary files /dev/null and b/Tutorials/2dShaders/src/08-Light-Effect/MonoGameLibrary/SharedContent/images/ripple.png differ diff --git a/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime.sln b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime.sln new file mode 100644 index 00000000..077462d5 --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DungeonSlime", "DungeonSlime\DungeonSlime.csproj", "{88BD3FF9-D3D6-4680-A274-F866E0DFCAC4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MonoGameLibrary", "MonoGameLibrary\MonoGameLibrary.csproj", "{AB85CEEE-6D97-4438-AEC4-797D2806F44A}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {88BD3FF9-D3D6-4680-A274-F866E0DFCAC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {88BD3FF9-D3D6-4680-A274-F866E0DFCAC4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {88BD3FF9-D3D6-4680-A274-F866E0DFCAC4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {88BD3FF9-D3D6-4680-A274-F866E0DFCAC4}.Release|Any CPU.Build.0 = Release|Any CPU + {AB85CEEE-6D97-4438-AEC4-797D2806F44A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AB85CEEE-6D97-4438-AEC4-797D2806F44A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AB85CEEE-6D97-4438-AEC4-797D2806F44A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AB85CEEE-6D97-4438-AEC4-797D2806F44A}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/.config/dotnet-tools.json b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/.config/dotnet-tools.json new file mode 100644 index 00000000..afd4e2c4 --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/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/2dShaders/src/09-Shadows-Effect/DungeonSlime/.mgstats b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/.mgstats new file mode 100644 index 00000000..eab26b31 --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/.mgstats @@ -0,0 +1 @@ +Source File,Dest File,Processor Type,Content Type,Source File Size,Dest File Size,Build Seconds diff --git a/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/Content.mgcb b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/Content.mgcb new file mode 100644 index 00000000..24b5916f --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/Content.mgcb @@ -0,0 +1,176 @@ + +#----------------------------- 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/gameEffect.fx +/importer:EffectImporter +/processor:EffectProcessor +/processorParam:DebugMode=Auto +/build:effects/gameEffect.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-normal.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-normal.png + +#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/color-map-1.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/color-map-1.png + +#begin images/color-map-2.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/color-map-2.png + +#begin images/color-map-dark-purple.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/color-map-dark-purple.png + +#begin images/color-map-green.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/color-map-green.png + +#begin images/color-map-pink.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/color-map-pink.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/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/audio/bounce.wav b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/audio/bounce.wav new file mode 100644 index 00000000..baa7a47b Binary files /dev/null and b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/audio/bounce.wav differ diff --git a/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/audio/collect.wav b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/audio/collect.wav new file mode 100644 index 00000000..506220de Binary files /dev/null and b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/audio/collect.wav differ diff --git a/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/audio/theme.ogg b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/audio/theme.ogg new file mode 100644 index 00000000..72e1fd3b Binary files /dev/null and b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/audio/theme.ogg differ diff --git a/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/audio/ui.wav b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/audio/ui.wav new file mode 100644 index 00000000..63e8941e Binary files /dev/null and b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/audio/ui.wav differ diff --git a/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/effects/gameEffect.fx b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/effects/gameEffect.fx new file mode 100644 index 00000000..ab3c7fdd --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/effects/gameEffect.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; + +sampler2D SpriteTextureSampler = sampler_state +{ + Texture = ; +}; + +Texture2D NormalMap; +sampler2D NormalMapSampler = sampler_state +{ + Texture = ; +}; + +#include "../../../MonoGameLibrary/SharedContent/effects/3dEffect.fxh" +#include "../../../MonoGameLibrary/SharedContent/effects/colors.fxh" + +struct PixelShaderOutput { + float4 color: COLOR0; + float4 normal: COLOR1; +}; + +PixelShaderOutput MainPS(VertexShaderOutput input) +{ + PixelShaderOutput output; + output.color = ColorSwapPS(input); + + // read the normal data from the NormalMap + float4 normal = tex2D(NormalMapSampler,input.TextureCoordinates); + output.normal = normal; + + if (output.color.a <= 0) clip(-1); + + return output; +} + + +technique SpriteDrawing +{ + pass P0 + { + VertexShader = compile VS_SHADERMODEL MainVS(); + PixelShader = compile PS_SHADERMODEL MainPS(); + } +}; \ No newline at end of file diff --git a/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/fonts/04B_30.spritefont b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/fonts/04B_30.spritefont new file mode 100644 index 00000000..63d4728c --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/fonts/04B_30.spritefont @@ -0,0 +1,16 @@ + + + + 04B_30.ttf + 17.5 + 0 + true + + + + + ~ + + + + diff --git a/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/fonts/04B_30.ttf b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/fonts/04B_30.ttf new file mode 100644 index 00000000..4b93740c Binary files /dev/null and b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/fonts/04B_30.ttf differ diff --git a/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/fonts/04B_30_5x.spritefont b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/fonts/04B_30_5x.spritefont new file mode 100644 index 00000000..dd239a53 --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/fonts/04B_30_5x.spritefont @@ -0,0 +1,16 @@ + + + + 04B_30.ttf + 87.5 + 0 + true + + + + + ~ + + + + diff --git a/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/fonts/04b_30.fnt b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/fonts/04b_30.fnt new file mode 100644 index 00000000..772f8c54 --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/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/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/images/atlas-definition.xml b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/images/atlas-definition.xml new file mode 100644 index 00000000..21772022 --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/images/atlas-definition.xml @@ -0,0 +1,34 @@ + + + images/atlas + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/images/atlas-normal.png b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/images/atlas-normal.png new file mode 100644 index 00000000..ae3ae78a Binary files /dev/null and b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/images/atlas-normal.png differ diff --git a/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/images/atlas.png b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/images/atlas.png new file mode 100644 index 00000000..f7def20f Binary files /dev/null and b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/images/atlas.png differ diff --git a/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/images/background-pattern.png b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/images/background-pattern.png new file mode 100644 index 00000000..2d8d878e Binary files /dev/null and b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/images/background-pattern.png differ diff --git a/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/images/color-map-1.png b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/images/color-map-1.png new file mode 100644 index 00000000..b5e3dc5a Binary files /dev/null and b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/images/color-map-1.png differ diff --git a/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/images/color-map-2.png b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/images/color-map-2.png new file mode 100644 index 00000000..2789bee8 Binary files /dev/null and b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/images/color-map-2.png differ diff --git a/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/images/color-map-dark-purple.png b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/images/color-map-dark-purple.png new file mode 100644 index 00000000..ffe9516e Binary files /dev/null and b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/images/color-map-dark-purple.png differ diff --git a/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/images/color-map-green.png b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/images/color-map-green.png new file mode 100644 index 00000000..87656c81 Binary files /dev/null and b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/images/color-map-green.png differ diff --git a/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/images/color-map-pink.png b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/images/color-map-pink.png new file mode 100644 index 00000000..e8910ded Binary files /dev/null and b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/images/color-map-pink.png differ diff --git a/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/images/logo.png b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/images/logo.png new file mode 100644 index 00000000..1509036c Binary files /dev/null and b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/images/logo.png differ diff --git a/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/images/tilemap-definition.xml b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Content/images/tilemap-definition.xml new file mode 100644 index 00000000..d36e9e52 --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/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 05 05 05 05 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/2dShaders/src/09-Shadows-Effect/DungeonSlime/DungeonSlime.csproj b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/DungeonSlime.csproj new file mode 100644 index 00000000..ab01c538 --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/DungeonSlime.csproj @@ -0,0 +1,77 @@ + + + WinExe + net8.0 + Major + false + false + + + app.manifest + Icon.ico + + + bin/$(Configuration)/$(TargetFramework) + + + + + + + + Icon.ico + + + Icon.bmp + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Game1.cs b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Game1.cs new file mode 100644 index 00000000..981a4c55 --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Game1.cs @@ -0,0 +1,75 @@ +using DungeonSlime.Scenes; +using Microsoft.Xna.Framework; +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() + { + // Allow the Core class to load any content. + base.LoadContent(); + + // Load the background theme music + _themeSong = Content.Load("audio/theme"); + } +} diff --git a/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/GameController.cs b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/GameController.cs new file mode 100644 index 00000000..a85df08f --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/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/2dShaders/src/09-Shadows-Effect/DungeonSlime/GameObjects/Bat.cs b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/GameObjects/Bat.cs new file mode 100644 index 00000000..a9c624e6 --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/GameObjects/Bat.cs @@ -0,0 +1,134 @@ +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; } + + /// + /// The shadow caster for this bat + /// + public ShadowCaster ShadowCaster { get; private 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; + + ShadowCaster = ShadowCaster.SimplePolygon(Point.Zero, radius: 10, sides: 12); + } + + /// + /// 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; + + // Update the position of the shadow caster. Move it up a bit due to the bat's artwork. + var size = new Vector2(_sprite.Width, _sprite.Height); + ShadowCaster.Position = Position - Vector2.UnitY * 10 + size * .5f; + } + + /// + /// Draws the bat. + /// + public void Draw() + { + _sprite.Draw(Core.SpriteBatch, Position); + } +} diff --git a/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/GameObjects/Slime.cs b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/GameObjects/Slime.cs new file mode 100644 index 00000000..b89d1a77 --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/GameObjects/Slime.cs @@ -0,0 +1,295 @@ +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 size of the slime + public int Size => _segments.Count; + + // 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; + + /// + /// A list of shadow casters for all of the slime segments + /// + public List ShadowCasters { get; private set; } = new List(); + + /// + /// 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); + + // Update the shadow casters + if (ShadowCasters.Count != _segments.Count) + { + ShadowCasters = new List(_segments.Count); + for (var i = 0; i < _segments.Count; i++) + { + ShadowCasters.Add(ShadowCaster.SimplePolygon(Point.Zero, radius: 30, sides: 12)); + } + } + + for (var i = 0; i < _segments.Count; i++) + { + var segment = _segments[i]; + Vector2 pos = Vector2.Lerp(segment.At, segment.To, _movementProgress); + var size = new Vector2(_sprite.Width, _sprite.Height); + ShadowCasters[i].Position = pos + size * .5f; + } + } + + /// + /// Draws the slime. + /// + public void Draw(Action configureSpriteBatch) + { + // Iterate through each segment and draw it + for (var i = 0 ; i < _segments.Count; i ++) + { + var segment = _segments[i]; + // 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); + + // Allow the sprite batch to be configured before each call. + configureSpriteBatch(i); + + // 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/2dShaders/src/09-Shadows-Effect/DungeonSlime/GameObjects/SlimeSegment.cs b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/GameObjects/SlimeSegment.cs new file mode 100644 index 00000000..b00189eb --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/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/2dShaders/src/09-Shadows-Effect/DungeonSlime/Icon.bmp b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Icon.bmp new file mode 100644 index 00000000..2b481653 Binary files /dev/null and b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Icon.bmp differ diff --git a/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Icon.ico b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Icon.ico new file mode 100644 index 00000000..7d9dec18 Binary files /dev/null and b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Icon.ico differ diff --git a/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Program.cs b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Program.cs new file mode 100644 index 00000000..4d9be314 --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Program.cs @@ -0,0 +1,3 @@ +MonoGameLibrary.Content.ContentManagerExtensions.StartContentWatcherTask(); +using var game = new DungeonSlime.Game1(); +game.Run(); \ No newline at end of file diff --git a/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Scenes/GameScene.cs b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Scenes/GameScene.cs new file mode 100644 index 00000000..1212ba8c --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Scenes/GameScene.cs @@ -0,0 +1,568 @@ +using System; +using System.Collections.Generic; +using DungeonSlime.GameObjects; +using DungeonSlime.UI; +using ImGuiNET; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Audio; +using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; +using MonoGameGum; +using MonoGameLibrary; +using MonoGameLibrary.Content; +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; + + // The normal texture atlas + private Texture2D _normalAtlas; + + // 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 amount of saturation to provide the grayscale shader effect + private float _saturation = 1.0f; + private Texture2D _colorMap; + private RedColorMap _slimeColorMap; + private TimeSpan _lastGrowTime; + + // The uber material for the game objects + private Material _gameMaterial; + private SpriteCamera3d _camera; + + // The deferred rendering resources + private DeferredRenderer _deferredRenderer; + + // A list of point lights to be rendered + private List _lights = new List(); + + // A list of shadow casters for all the lights + private List _shadowCasters = new List(); + + // 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(); + + // Create the deferred rendering resources + _deferredRenderer = new DeferredRenderer(); + InitializeLights(); + } + + private void InitializeLights() + { + // torch 1 + _lights.Add(new PointLight + { + Position = new Vector2(260, 100), + Color = Color.CornflowerBlue, + Radius = 600 + }); + + // torch 2 + _lights.Add(new PointLight + { + Position = new Vector2(1000, 100), + Color = Color.CornflowerBlue, + Radius = 600 + }); + + // underlight + _lights.Add(new PointLight + { + Position = new Vector2(600, 660), + Color = Color.MonoGameOrange, + Radius = 1200 + }); + } + + 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 colorSwap map + _colorMap = Content.Load("images/color-map-dark-purple"); + _slimeColorMap = new RedColorMap(); + _slimeColorMap.SetColorsByExistingColorMap(_colorMap); + _slimeColorMap.SetColorsByRedValue(new Dictionary + { + // main color + [32] = Color.LightSteelBlue, + }, false); + + // Load the normal maps + _normalAtlas = Content.Load("images/atlas-normal"); + + // Load the game material + _gameMaterial = Content.WatchMaterial("effects/gameEffect"); + _gameMaterial.SetParameter("ColorMap", _colorMap); + _camera = new SpriteCamera3d(); + _gameMaterial.SetParameter("MatrixTransform", _camera.CalculateMatrixTransform()); + _gameMaterial.SetParameter("ScreenSize", new Vector2(Core.GraphicsDevice.Viewport.Width, Core.GraphicsDevice.Viewport.Height)); + _gameMaterial.SetParameter("NormalMap", _normalAtlas); + } + + private bool _debugPause = false; + public override void Update(GameTime gameTime) + { + if (Core.Input.Keyboard.WasKeyJustPressed(Keys.P)) + { + _debugPause = !_debugPause; + } + + if (_debugPause) return; + + // Ensure the UI is always updated + _ui.Update(gameTime); + + // Set the camera view to look at the player slime + var viewport = Core.GraphicsDevice.Viewport; + var center = .5f * new Vector2(viewport.Width, viewport.Height); + var slimePosition = new Vector2(_slime?.GetBounds().X ?? center.X, _slime?.GetBounds().Y ?? center.Y); + var offset = .01f * (slimePosition - center); + _camera.LookOffset = offset; + + var matrixTransform = _camera.CalculateMatrixTransform(); + _gameMaterial.SetParameter("MatrixTransform", matrixTransform); + Core.PointLightMaterial.SetParameter("MatrixTransform", matrixTransform); + Core.PointLightMaterial.SetParameter("ScreenSize", new Vector2(Core.GraphicsDevice.Viewport.Width, Core.GraphicsDevice.Viewport.Height)); + Core.ShadowHullMaterial.SetParameter("MatrixTransform", matrixTransform); + Core.ShadowHullMaterial.SetParameter("ScreenSize", new Vector2(Core.GraphicsDevice.Viewport.Width, Core.GraphicsDevice.Viewport.Height)); + + // Update the colorSwap material if it was changed + _gameMaterial.Update(); + + 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; + } + } + else + { + _saturation = 1; + } + + // 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(gameTime); + + } + + private void CollisionChecks(GameTime gameTime) + { + // 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(); + + // Remember when the last time the slime grew + _lastGrowTime = gameTime.TotalGameTime; + + // 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(new Color(32, 16, 20)); + + _gameMaterial.SetParameter("Saturation", _saturation); + + // Start rendering to the deferred renderer + _deferredRenderer.StartColorPhase(); + Core.SpriteBatch.Begin( + samplerState: SamplerState.PointClamp, + sortMode: SpriteSortMode.Immediate, + rasterizerState: RasterizerState.CullNone, + effect: _gameMaterial.Effect); + + // Update the colorMap + _gameMaterial.SetParameter("ColorMap", _colorMap); + + // Draw the tilemap + _tilemap.Draw(Core.SpriteBatch); + + // Draw the bat. + _bat.Draw(); + + // Draw the slime. + _slime.Draw(segmentIndex => + { + const int flashTimeMs = 125; + var map = _colorMap; + var elapsedMs = (gameTime.TotalGameTime.TotalMilliseconds - _lastGrowTime.TotalMilliseconds); + var intervalsAgo = (int)(elapsedMs / flashTimeMs); + + if (intervalsAgo < _slime.Size && (intervalsAgo - segmentIndex) % _slime.Size == 0) + { + map = _slimeColorMap.ColorMap; + } + + _gameMaterial.SetParameter("ColorMap", map); + }); + + // Always end the sprite batch when finished. + Core.SpriteBatch.End(); + + // render the shadow buffers + var casters = new List(); + casters.AddRange(_shadowCasters); + casters.AddRange(_slime.ShadowCasters); + casters.Add(_bat.ShadowCaster); + + // start rendering the lights + _deferredRenderer.DrawLights(_lights, casters, (blend, stencil) => + { + Core.SpriteBatch.Begin( + effect: _gameMaterial.Effect, + depthStencilState: stencil, + blendState: blend); + _slime.Draw(_ => {}); + Core.SpriteBatch.End(); + }); + + // finish the deferred rendering + _deferredRenderer.Finish(); + + _deferredRenderer.DrawComposite(); + + // Draw the UI + _ui.Draw(); + + // Render the debug view for the game + //_deferredRenderer.DebugDraw(); + + } +} diff --git a/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Scenes/TitleScene.cs b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Scenes/TitleScene.cs new file mode 100644 index 00000000..b0f4d794 --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/Scenes/TitleScene.cs @@ -0,0 +1,368 @@ +using System; +using DungeonSlime.UI; +using Gum.Forms.Controls; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Audio; +using Microsoft.Xna.Framework.Graphics; +using MonoGameGum; +using MonoGameGum.GueDeriving; +using MonoGameLibrary; +using MonoGameLibrary.Content; +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; + + // The 3d material + private Material _3dMaterial; + + 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"); + + // Load the 3d effect + _3dMaterial = Core.SharedContent.WatchMaterial("effects/3dEffect"); + + var camera = new SpriteCamera3d + { + Fov = 40 + }; + _3dMaterial.SetParameter("MatrixTransform", camera.CalculateMatrixTransform()); + _3dMaterial.SetParameter("ScreenSize", new Vector2(Core.GraphicsDevice.Viewport.Width, Core.GraphicsDevice.Viewport.Height)); + } + + 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) + { + _3dMaterial.Update(); + + var spinAmount = Core.Input.Mouse.X / (float)Core.GraphicsDevice.Viewport.Width; + spinAmount = MathHelper.SmoothStep(-.1f, .1f, spinAmount); + _3dMaterial.SetParameter("SpinAmount", spinAmount); + + // 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, + rasterizerState: RasterizerState.CullNone, + effect: _3dMaterial.Effect); + + // 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/2dShaders/src/09-Shadows-Effect/DungeonSlime/UI/AnimatedButton.cs b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/UI/AnimatedButton.cs new file mode 100644 index 00000000..4cce6ee5 --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/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 by AnimationChains 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/2dShaders/src/09-Shadows-Effect/DungeonSlime/UI/GameSceneUI.cs b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/UI/GameSceneUI.cs new file mode 100644 index 00000000..498655c2 --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/UI/GameSceneUI.cs @@ -0,0 +1,340 @@ +using System; +using Gum.DataTypes; +using Gum.Forms.Controls; +using Gum.Managers; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Audio; +using Microsoft.Xna.Framework.Content; +using MonoGameGum; +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/2dShaders/src/09-Shadows-Effect/DungeonSlime/UI/OptionsSlider.cs b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/UI/OptionsSlider.cs new file mode 100644 index 00000000..53d6ee94 --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/UI/OptionsSlider.cs @@ -0,0 +1,253 @@ +using System; +using Gum.DataTypes; +using Gum.DataTypes.Variables; +using Gum.Forms.Controls; +using Gum.Managers; +using Microsoft.Xna.Framework; +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/2dShaders/src/09-Shadows-Effect/DungeonSlime/app.manifest b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/app.manifest new file mode 100644 index 00000000..caf45166 --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/DungeonSlime/app.manifest @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true/pm + permonitorv2,permonitor + + + + diff --git a/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Audio/AudioController.cs b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Audio/AudioController.cs new file mode 100644 index 00000000..1bffd636 --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/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/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Circle.cs b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Circle.cs new file mode 100644 index 00000000..0bb691bc --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/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/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Content/ContentManagerExtensions.cs b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Content/ContentManagerExtensions.cs new file mode 100644 index 00000000..e012836c --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Content/ContentManagerExtensions.cs @@ -0,0 +1,168 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; +using MonoGameLibrary.Graphics; + +namespace MonoGameLibrary.Content; + +public static class ContentManagerExtensions +{ + /// + /// Check if the given xnb file has a newer write-time than the last loaded version of the asset. + /// If the local file has been updated, reload the asset and return true. + /// + /// The that loaded the asset originally + /// The asset that will be reloaded if the xnb file is newer + /// If the asset has been reloaded, this out parameter will be set to the previous version of the asset before the newer version was loaded. + /// + /// true when asset was reloaded; false otherwise. + /// + public static bool TryRefresh(this ContentManager manager, WatchedAsset watchedAsset, out T oldAsset) + { + oldAsset = default; + + if (manager != watchedAsset.Owner) + throw new ArgumentException($"Used the wrong ContentManager to refresh {watchedAsset.AssetName}"); + + var path = Path.Combine(manager.RootDirectory, watchedAsset.AssetName) + ".xnb"; + var lastWriteTime = File.GetLastWriteTime(path); + + if (lastWriteTime <= watchedAsset.UpdatedAt) + { + return false; + } + + if (IsFileLocked(path)) return false; // wait for the file to not be locked. + + manager.UnloadAsset(watchedAsset.AssetName); + oldAsset = watchedAsset.Asset; + watchedAsset.Asset = manager.Load(watchedAsset.AssetName); + watchedAsset.UpdatedAt = lastWriteTime; + + return true; + } + + private static bool IsFileLocked(string path) + { + try + { + using FileStream _ = new FileStream(path, FileMode.Open, FileAccess.ReadWrite, FileShare.None); + // File is not locked + return false; + } + catch (IOException) + { + // File is locked or inaccessible + return true; + } + } + + /// + /// Load an asset and wrap it with the metadata required to refresh it later using the function + /// + /// + /// + /// + /// + public static WatchedAsset Watch(this ContentManager manager, string assetName) + { + var asset = manager.Load(assetName); + return new WatchedAsset + { + AssetName = assetName, + Asset = asset, + UpdatedAt = DateTimeOffset.Now, + Owner = manager + }; + } + + /// + /// Load an Effect into the wrapper class + /// + /// + /// + /// + public static Material WatchMaterial(this ContentManager manager, string assetName) + { + return new Material(manager.Watch(assetName)); + } + + + [Conditional("DEBUG")] + public static void StartContentWatcherTask() + { + var args = Environment.GetCommandLineArgs(); + foreach (var arg in args) + { + // if the application was started with the --no-reload option, then do not start the watcher. + if (arg == "--no-reload") return; + } + + // identify the project directory + var projectFile = Assembly.GetEntryAssembly().GetName().Name + ".csproj"; + var current = Directory.GetCurrentDirectory(); + string projectDirectory = null; + + while (current != null && projectDirectory == null) + { + if (File.Exists(Path.Combine(current, projectFile))) + { + // the valid project csproj exists in the directory + projectDirectory = current; + } + else + { + // try looking in the parent directory. + // When there is no parent directory, the variable becomes 'null' + current = Path.GetDirectoryName(current); + } + } + + // if no valid project was identified, then it is impossible to start the watcher + if (string.IsNullOrEmpty(projectDirectory)) return; + + // start the watcher process + var process = Process.Start(new ProcessStartInfo + { + FileName = "dotnet", + Arguments = "build -t:WatchContent --tl:off", + WorkingDirectory = projectDirectory, + WindowStyle = ProcessWindowStyle.Normal, + UseShellExecute = false, + CreateNoWindow = false + }); + + // when this program exits, make sure to emit a kill signal to the watcher process + AppDomain.CurrentDomain.ProcessExit += (_, __) => + { + try + { + if (!process.HasExited) + { + process.Kill(entireProcessTree: true); + } + } + catch + { + /* ignore */ + } + }; + AppDomain.CurrentDomain.UnhandledException += (sender, e) => + { + try + { + if (!process.HasExited) + { + process.Kill(entireProcessTree: true); + } + } + catch + { + /* ignore */ + } + }; + } +} \ No newline at end of file diff --git a/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Content/WatchedAsset.cs b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Content/WatchedAsset.cs new file mode 100644 index 00000000..39008666 --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Content/WatchedAsset.cs @@ -0,0 +1,33 @@ +using System; +using Microsoft.Xna.Framework.Content; + +namespace MonoGameLibrary.Content; + +public class WatchedAsset +{ + /// + /// The latest version of the asset. + /// + public T Asset { get; set; } + + /// + /// The last time the was loaded into memory. + /// + public DateTimeOffset UpdatedAt { get; set; } + + /// + /// The name of the . This is the name used to load the asset from disk. + /// + public string AssetName { get; init; } + + /// + /// The instance that loaded the asset. + /// + public ContentManager Owner { get; init; } + + + public bool TryRefresh(out T oldAsset) + { + return Owner.TryRefresh(this, out oldAsset); + } +} \ No newline at end of file diff --git a/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Core.cs b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Core.cs new file mode 100644 index 00000000..aed8c1d5 --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Core.cs @@ -0,0 +1,312 @@ +using System; +using System.Collections.Generic; +using ImGuiNET.SampleProgram.XNA; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; +using MonoGameLibrary.Audio; +using MonoGameLibrary.Content; +using MonoGameLibrary.Graphics; +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; + + /// + /// The material that is used when changing scenes + /// + public static Material SceneTransitionMaterial { get; private set; } + + /// + /// The material that draws point lights + /// + public static Material PointLightMaterial { get; private set; } + + /// + /// The material that draws shadow hulls + /// + public static Material ShadowHullMaterial { get; private set; } + + /// + /// The material that combines the various off screen textures + /// + public static Material DeferredCompositeMaterial { get; private set; } + + /// + /// A set of grayscale gradient textures to use as transition guides + /// + public static List SceneTransitionTextures { get; private set; } + + /// + /// The current transition between scenes + /// + public static SceneTransition SceneTransition { get; protected set; } = SceneTransition.Open(1000); + + /// + /// 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 a runtime generated 1x1 pixel texture. + /// + public static Texture2D Pixel { get; private set; } + + /// + /// Gets the ImGui renderer used for debug UIs. + /// + public static ImGuiRenderer ImGuiRenderer { get; private set; } + + /// + /// Gets the content manager used to load global assets. + /// + public static new ContentManager Content { get; private set; } + + /// + /// Gets the content manager that can load global assets from the SharedContent folder. + /// + public static ContentManager SharedContent { 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"; + + // Set the core's shared content manager, pointing to the SharedContent folder. + SharedContent = new ContentManager(Services, "SharedContent"); + + // 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 the ImGui renderer. + ImGuiRenderer = new ImGuiRenderer(this); + ImGuiRenderer.RebuildFontAtlas(); + + // Create a new input manager + Input = new InputManager(); + + // Create a new audio controller. + Audio = new AudioController(); + + // Create a 1x1 white pixel texture for drawing quads. + Pixel = new Texture2D(GraphicsDevice, 1, 1); + Pixel.SetData(new Color[]{ Color.White }); + } + + protected override void LoadContent() + { + base.LoadContent(); + + DeferredCompositeMaterial = SharedContent.WatchMaterial("effects/deferredCompositeEffect"); + + PointLightMaterial = SharedContent.WatchMaterial("effects/pointLightEffect"); + PointLightMaterial.SetParameter("LightBrightness", .25f); + PointLightMaterial.SetParameter("LightSharpness", .1f); + + ShadowHullMaterial = SharedContent.WatchMaterial("effects/shadowHullEffect"); + ShadowHullMaterial.SetParameter("ShadowFadeStartDistance", .013f); + ShadowHullMaterial.SetParameter("ShadowFadeEndDistance", .13f); + ShadowHullMaterial.SetParameter("ShadowIntensity", .85f); + + SceneTransitionMaterial = SharedContent.WatchMaterial("effects/sceneTransitionEffect"); + SceneTransitionMaterial.SetParameter("EdgeWidth", .05f); + + SceneTransitionTextures = new List(); + SceneTransitionTextures.Add(SharedContent.Load("images/angled")); + SceneTransitionTextures.Add(SharedContent.Load("images/concave")); + SceneTransitionTextures.Add(SharedContent.Load("images/radial")); + SceneTransitionTextures.Add(SharedContent.Load("images/ripple")); + } + + 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 && SceneTransition.IsComplete) + { + TransitionScene(); + } + + // If there is an active scene, update it. + if (s_activeScene != null) + { + s_activeScene.Update(gameTime); + } + + // Check if the scene transition material needs to be reloaded. + SceneTransitionMaterial.SetParameter("Progress", SceneTransition.DirectionalRatio); + SceneTransitionMaterial.Update(); + + PointLightMaterial.Update(); + ShadowHullMaterial.Update(); + + DeferredCompositeMaterial.SetParameter("ScreenSize", new Vector2(GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height)); + DeferredCompositeMaterial.SetParameter("BoxBlurStride", .18f); + DeferredCompositeMaterial.Update(); + + 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); + } + + // Draw the scene transition quad + SpriteBatch.Begin(effect: SceneTransitionMaterial.Effect); + SpriteBatch.Draw(SceneTransitionTextures[SceneTransition.TextureIndex % SceneTransitionTextures.Count], GraphicsDevice.Viewport.Bounds, Color.White); + SpriteBatch.End(); + + Material.DrawVisibleDebugUi(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; + SceneTransition = SceneTransition.Close(250); + } + } + + private static void TransitionScene() + { + SceneTransition = SceneTransition.Open(500); + + // 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/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Graphics/AnimatedSprite.cs b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Graphics/AnimatedSprite.cs new file mode 100644 index 00000000..a1a3594e --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/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/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Graphics/Animation.cs b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Graphics/Animation.cs new file mode 100644 index 00000000..44d61b65 --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/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/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Graphics/DeferredRenderer.cs b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Graphics/DeferredRenderer.cs new file mode 100644 index 00000000..68ab5fa8 --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Graphics/DeferredRenderer.cs @@ -0,0 +1,303 @@ +using System; +using System.Collections.Generic; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace MonoGameLibrary.Graphics; + +public class DeferredRenderer +{ + + /// + /// A texture that holds the unlit sprite drawings + /// + public RenderTarget2D ColorBuffer { get; set; } + + /// + /// A texture that holds the normal sprite drawins + /// + public RenderTarget2D NormalBuffer { get; set; } + + /// + /// A texture that holds the drawn lights + /// + public RenderTarget2D LightBuffer { get; set; } + + /// + /// The state used when writing shadow hulls + /// + private DepthStencilState _stencilWrite; + + /// + /// The state used when drawing point lights + /// + private DepthStencilState _stencilTest; + + /// + /// The state that will be ignored from shadows + /// + private DepthStencilState _stencilShadowExclude; + + /// + /// A custom blend state that wont write any color data + /// + private BlendState _shadowBlendState; + + /// + /// A custom rasterizer state that masks content to the light + /// + private RasterizerState _lightRasterizerState; + + public DeferredRenderer() + { + var viewport = Core.GraphicsDevice.Viewport; + + ColorBuffer = new RenderTarget2D( + graphicsDevice: Core.GraphicsDevice, + width: viewport.Width, + height: viewport.Height, + mipMap: false, + preferredFormat: SurfaceFormat.Color, + preferredDepthFormat: DepthFormat.None); + + NormalBuffer = new RenderTarget2D( + graphicsDevice: Core.GraphicsDevice, + width: viewport.Width, + height: viewport.Height, + mipMap: false, + preferredFormat: SurfaceFormat.Color, + preferredDepthFormat: DepthFormat.None); + + LightBuffer = new RenderTarget2D( + graphicsDevice: Core.GraphicsDevice, + width: viewport.Width, + height: viewport.Height, + mipMap: false, + preferredFormat: SurfaceFormat.Color, + preferredDepthFormat: DepthFormat.Depth24Stencil8); + + _stencilWrite = new DepthStencilState + { + // instruct MonoGame to use the stencil buffer + StencilEnable = true, + + // instruct every fragment to interact with the stencil buffer + StencilFunction = CompareFunction.LessEqual, + + // every operation will increase the shadow value (up to the max of 255), but only when the original + // stencil value was greater or equal to '1'. ('1' is the default clear value) + StencilPass = StencilOperation.IncrementSaturation, + + // this is the value that will be written into the stencil buffer + ReferenceStencil = 1, + + // ignore depth from the stencil buffer write/reads + DepthBufferEnable = false + }; + _stencilTest = new DepthStencilState + { + // instruct MonoGame to use the stencil buffer + StencilEnable = true, + + // instruct only fragments that have a current value greater or equal to the + // ReferenceStencil value to interact + StencilFunction = CompareFunction.GreaterEqual, + + // '1' and `0` are the "non shadow" values + ReferenceStencil = 1, + + // don't change the value of the stencil buffer. KEEP the current value. + StencilPass = StencilOperation.Keep, + + // ignore depth from the stencil buffer write/reads + DepthBufferEnable = false + }; + + _stencilShadowExclude = new DepthStencilState + { + // instruct MonoGame to use the stencil buffer + StencilEnable = true, + + // in the setup, always set the pixel to '0' + StencilFunction = CompareFunction.Always, + + // Write a '0' anywhere we don't want a shadow to appear + ReferenceStencil = 0, + + // Overwrite the current value + StencilPass = StencilOperation.Replace, + + // ignore depth from the stencil buffer write/reads + DepthBufferEnable = false + }; + + + _shadowBlendState = new BlendState + { + // no color channels will be written into the render target + ColorWriteChannels = ColorWriteChannels.None + }; + + _lightRasterizerState = new RasterizerState() + { + CullMode = CullMode.None, + ScissorTestEnable = true + }; + } + + public void StartColorPhase() + { + // all future draw calls will be drawn to the color buffer and normal buffer + Core.GraphicsDevice.SetRenderTargets(new RenderTargetBinding[] + { + // gets the results from shader semantic COLOR0 + new RenderTargetBinding(ColorBuffer), + + // gets the results from shader semantic COLOR1 + new RenderTargetBinding(NormalBuffer) + }); + Core.GraphicsDevice.Clear(Color.Transparent); + } + + public void DrawLights(List lights, List shadowCasters, Action prepareStencil) + { + Core.GraphicsDevice.SetRenderTarget(LightBuffer); + Core.GraphicsDevice.Clear(Color.Black); + foreach (var light in lights) + { + var diameter = light.Radius * 2; + var rect = new Rectangle( + (int)(light.Position.X - light.Radius), + (int)(light.Position.Y - light.Radius), + diameter, diameter); + + // initialize the stencil to '1'. + Core.GraphicsDevice.Clear(ClearOptions.Stencil, Color.Black, 0, 1); + + // set scissor rect so that only things around the current light are drawn + Core.GraphicsDevice.ScissorRectangle = rect; + + // Anything that draws in this setup will set the stencil back to '0'. This '0' acts as a "don't draw a shadow here". + prepareStencil?.Invoke(_shadowBlendState, _stencilShadowExclude); + + Core.ShadowHullMaterial.SetParameter("LightPosition", light.Position); + Core.SpriteBatch.Begin( + depthStencilState: _stencilWrite, + effect: Core.ShadowHullMaterial.Effect, + blendState: _shadowBlendState, + rasterizerState: _lightRasterizerState + ); + foreach (var caster in shadowCasters) + { + for (var i = 0; i < caster.Points.Count; i++) + { + var a = caster.Position + caster.Points[i]; + var b = caster.Position + caster.Points[(i + 1) % caster.Points.Count]; + + var screenSize = new Vector2(LightBuffer.Width, LightBuffer.Height); + var aToB = (b - a) / screenSize; + var packed = PointLight.PackVector2_SNorm(aToB); + Core.SpriteBatch.Draw(Core.Pixel, a, packed); + } + } + + Core.SpriteBatch.End(); + + + Core.SpriteBatch.Begin( + depthStencilState: _stencilTest, + effect: Core.PointLightMaterial.Effect, + blendState: BlendState.Additive + ); + + Core.SpriteBatch.Draw(NormalBuffer, rect, light.Color); + Core.SpriteBatch.End(); + + } + + Core.GraphicsDevice.Clear(ClearOptions.Stencil, Color.Black, 0, 0); + } + + + public void Finish() + { + // all future draw calls will be drawn to the screen + // note: 'null' means "the screen" in MonoGame + Core.GraphicsDevice.SetRenderTarget(null); + } + + public void DrawComposite(float ambient=.4f) + { + Core.DeferredCompositeMaterial.SetParameter("AmbientLight", ambient); + Core.DeferredCompositeMaterial.SetParameter("LightBuffer", LightBuffer); + var viewportBounds = Core.GraphicsDevice.Viewport.Bounds; + Core.SpriteBatch.Begin( + effect: Core.DeferredCompositeMaterial.Effect + ); + Core.SpriteBatch.Draw(ColorBuffer, viewportBounds, Color.White); + Core.SpriteBatch.End(); + } + + public void DebugDraw() + { + var viewportBounds = Core.GraphicsDevice.Viewport.Bounds; + + // the debug view for the color buffer lives in the top-left. + var colorBorderRect = new Rectangle( + x: viewportBounds.X, + y: viewportBounds.Y, + width: viewportBounds.Width / 2, + height: viewportBounds.Height / 2); + + // shrink the color rect by 8 pixels + var colorRect = colorBorderRect; + colorRect.Inflate(-8, -8); + + + // the debug view for the light buffer lives in the top-right. + var lightBorderRect = new Rectangle( + x: viewportBounds.Width / 2, + y: viewportBounds.Y, + width: viewportBounds.Width / 2, + height: viewportBounds.Height / 2); + + // shrink the light rect by 8 pixels + var lightRect = lightBorderRect; + lightRect.Inflate(-8, -8); + + // the debug view for the normal buffer lives in the top-right. + var normalBorderRect = new Rectangle( + x: viewportBounds.X, + y: viewportBounds.Height / 2, + width: viewportBounds.Width / 2, + height: viewportBounds.Height / 2); + + // shrink the normal rect by 8 pixels + var normalRect = normalBorderRect; + normalRect.Inflate(-8, -8); + + + Core.SpriteBatch.Begin(); + + // draw a debug border + Core.SpriteBatch.Draw(Core.Pixel, colorBorderRect, Color.MonoGameOrange); + + // draw the color buffer + Core.SpriteBatch.Draw(ColorBuffer, colorRect, Color.White); + + //draw a debug border + Core.SpriteBatch.Draw(Core.Pixel, lightBorderRect, Color.CornflowerBlue); + + // draw the light buffer + Core.SpriteBatch.Draw(LightBuffer, lightRect, Color.White); + + + // draw a debug border + Core.SpriteBatch.Draw(Core.Pixel, normalBorderRect, Color.MintCream); + + // draw the light buffer + Core.SpriteBatch.Draw(NormalBuffer, normalRect, Color.White); + + Core.SpriteBatch.End(); + } +} \ No newline at end of file diff --git a/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Graphics/Material.cs b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Graphics/Material.cs new file mode 100644 index 00000000..f1a22a83 --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Graphics/Material.cs @@ -0,0 +1,303 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using ImGuiNET; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using MonoGameLibrary.Content; +namespace MonoGameLibrary.Graphics; + +public class Material +{ + // materials that will be drawn during the standard debug UI pass. + private static HashSet s_debugMaterials = new HashSet(); + + /// + /// The hot-reloadable asset that this material is using + /// + public WatchedAsset Asset; + + /// + /// A cached version of the parameters available in the shader + /// + public Dictionary ParameterMap; + + /// + /// The currently loaded Effect that this material is using + /// + public Effect Effect => Asset.Asset; + + /// + /// Enable this variable to visualize the debugUI for the material + /// + public bool IsDebugVisible + { + get + { + return s_debugMaterials.Contains(this); + } + set + { + if (!value) + { + s_debugMaterials.Remove(this); + } + else + { + s_debugMaterials.Add(this); + } + } + } + + /// + /// When true, the debug UI will override parameters + /// + public bool DebugOverride; + + public Material(WatchedAsset asset) + { + Asset = asset; + UpdateParameterCache(); + } + + public void SetParameter(string name, float value) + { + if (DebugOverride) return; + + if (TryGetParameter(name, out var parameter)) + { + parameter.SetValue(value); + } + else + { + Console.WriteLine($"Warning: cannot set shader parameter=[{name}] because it does not exist in the compiled shader=[{Asset.AssetName}]"); + } + } + + public void SetParameter(string name, Matrix value) + { + if (DebugOverride) return; + + if (TryGetParameter(name, out var parameter)) + { + parameter.SetValue(value); + } + else + { + Console.WriteLine($"Warning: cannot set shader parameter=[{name}] because it does not exist in the compiled shader=[{Asset.AssetName}]"); + } + } + + public void SetParameter(string name, Vector2 value) + { + if (DebugOverride) return; + + if (TryGetParameter(name, out var parameter)) + { + parameter.SetValue(value); + } + else + { + Console.WriteLine($"Warning: cannot set shader parameter=[{name}] because it does not exist in the compiled shader=[{Asset.AssetName}]"); + } + } + + public void SetParameter(string name, Texture2D value) + { + if (DebugOverride) return; + + if (TryGetParameter(name, out var parameter)) + { + parameter.SetValue(value); + } + else + { + Console.WriteLine($"Warning: cannot set shader parameter=[{name}] because it does not exist in the compiled shader=[{Asset.AssetName}]"); + } + } + + /// + /// Check if the given parameter name is available in the compiled shader code. + /// Remember that a parameter will be optimized out of a shader if it is not being used + /// in the shader's return value. + /// + /// + /// + /// + public bool TryGetParameter(string name, out EffectParameter parameter) + { + return ParameterMap.TryGetValue(name, out parameter); + } + + /// + /// Rebuild the based on the current parameters available in the effect instance + /// + public void UpdateParameterCache() + { + ParameterMap = Effect.Parameters.ToDictionary(p => p.Name); + } + + [Conditional("DEBUG")] + public void Update() + { + if (Asset.TryRefresh(out var oldAsset)) + { + UpdateParameterCache(); + + foreach (var oldParam in oldAsset.Parameters) + { + if (!TryGetParameter(oldParam.Name, out var newParam)) + { + continue; + } + + switch (oldParam.ParameterClass) + { + case EffectParameterClass.Scalar: + newParam.SetValue(oldParam.GetValueSingle()); + break; + case EffectParameterClass.Matrix: + newParam.SetValue(oldParam.GetValueMatrix()); + break; + case EffectParameterClass.Vector when oldParam.ColumnCount == 2: // float2 + newParam.SetValue(oldParam.GetValueVector2()); + break; + case EffectParameterClass.Object: + newParam.SetValue(oldParam.GetValueTexture2D()); + break; + default: + Console.WriteLine("Warning: shader reload system was not able to re-apply property. " + + $"shader=[{Effect.Name}] " + + $"property=[{oldParam.Name}] " + + $"class=[{oldParam.ParameterClass}]"); + break; + } + } + } + } + + + + [Conditional("DEBUG")] + public void DrawDebug() + { + ImGui.Begin(Effect.Name); + + var currentSize = ImGui.GetWindowSize(); + ImGui.SetWindowSize(Effect.Name, new System.Numerics.Vector2(MathHelper.Max(100, currentSize.X), MathHelper.Max(100, currentSize.Y))); + + ImGui.AlignTextToFramePadding(); + ImGui.Text("Last Updated"); + ImGui.SameLine(); + ImGui.LabelText("##last-updated", Asset.UpdatedAt.ToString() + $" ({(DateTimeOffset.Now - Asset.UpdatedAt).ToString(@"h\:mm\:ss")} ago)"); + + ImGui.AlignTextToFramePadding(); + ImGui.Text("Override Values"); + ImGui.SameLine(); + ImGui.Checkbox("##override-values", ref DebugOverride); + + ImGui.NewLine(); + + bool ScalarSlider(string key, ref float value) + { + float min = 0; + float max = 1; + + return ImGui.SliderFloat($"##_prop{key}", ref value, min, max); + } + + foreach (var prop in ParameterMap) + { + switch (prop.Value.ParameterType, prop.Value.ParameterClass) + { + case (EffectParameterType.Single, EffectParameterClass.Scalar): + ImGui.AlignTextToFramePadding(); + ImGui.Text(prop.Key); + ImGui.SameLine(); + + var value = prop.Value.GetValueSingle(); + if (ScalarSlider(prop.Key, ref value)) + { + prop.Value.SetValue(value); + } + break; + + case (EffectParameterType.Single, EffectParameterClass.Vector): + ImGui.AlignTextToFramePadding(); + ImGui.Text(prop.Key); + + var vec2Value = prop.Value.GetValueVector2(); + ImGui.Indent(); + + ImGui.Text("X"); + ImGui.SameLine(); + + if (ScalarSlider(prop.Key + ".x", ref vec2Value.X)) + { + prop.Value.SetValue(vec2Value); + } + + ImGui.Text("Y"); + ImGui.SameLine(); + if (ScalarSlider(prop.Key + ".y", ref vec2Value.Y)) + { + prop.Value.SetValue(vec2Value); + } + ImGui.Unindent(); + break; + + case (EffectParameterType.Texture2D, EffectParameterClass.Object): + ImGui.AlignTextToFramePadding(); + ImGui.Text(prop.Key); + ImGui.SameLine(); + + var texture = prop.Value.GetValueTexture2D(); + if (texture != null) + { + var texturePtr = Core.ImGuiRenderer.BindTexture(texture); + ImGui.Image(texturePtr, new System.Numerics.Vector2(texture.Width, texture.Height)); + } + else + { + ImGui.Text("(null)"); + } + break; + + default: + ImGui.AlignTextToFramePadding(); + ImGui.Text(prop.Key); + ImGui.SameLine(); + ImGui.Text($"(unsupported {prop.Value.ParameterType}, {prop.Value.ParameterClass})"); + break; + } + } + ImGui.End(); + } + + [Conditional("DEBUG")] + public static void DrawVisibleDebugUi(GameTime gameTime) + { + // first, cull any materials that are not visible, or disposed. + var toRemove = new List(); + foreach (var material in s_debugMaterials) + { + if (material.Effect.IsDisposed) + { + toRemove.Add(material); + } + } + + foreach (var material in toRemove) + { + s_debugMaterials.Remove(material); + } + + Core.ImGuiRenderer.BeforeLayout(gameTime); + foreach (var material in s_debugMaterials) + { + material.DrawDebug(); + } + Core.ImGuiRenderer.AfterLayout(); + } +} \ No newline at end of file diff --git a/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Graphics/PointLight.cs b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Graphics/PointLight.cs new file mode 100644 index 00000000..df2fc648 --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Graphics/PointLight.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace MonoGameLibrary.Graphics; + +public class PointLight +{ + /// + /// The position of the light in world space + /// + public Vector2 Position { get; set; } + + /// + /// The color tint of the light + /// + public Color Color { get; set; } = Color.White; + + /// + /// The radius of the light in pixels + /// + public int Radius { get; set; } = 250; + + public static Color PackVector2_SNorm(Vector2 vec) + { + // Clamp to [-1, 1) + vec = Vector2.Clamp(vec, new Vector2(-1f), new Vector2(1f - 1f / 32768f)); + + short xInt = (short)(vec.X * 32767f); // signed 16-bit + short yInt = (short)(vec.Y * 32767f); + + byte r = (byte)((xInt >> 8) & 0xFF); + byte g = (byte)(xInt & 0xFF); + byte b = (byte)((yInt >> 8) & 0xFF); + byte a = (byte)(yInt & 0xFF); + + return new Color(r, g, b, a); + } +} \ No newline at end of file diff --git a/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Graphics/RedColorMap.cs b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Graphics/RedColorMap.cs new file mode 100644 index 00000000..d6e0bf3f --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Graphics/RedColorMap.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace MonoGameLibrary.Graphics; + +public class RedColorMap +{ + public Texture2D ColorMap { get; set; } + + public RedColorMap() + { + ColorMap = new Texture2D(Core.GraphicsDevice, 256, 1, false, SurfaceFormat.Color); + } + + /// + /// Given a dictionary of red-color values (0 to 255) to swapColors, + /// Set the values of the so that it can be used + /// As the ColorMap parameter in the colorSwapEffect. + /// + public void SetColorsByRedValue(Dictionary map, bool overWrite = true) + { + var pixelData = new Color[ColorMap.Width]; + ColorMap.GetData(pixelData); + + for (var i = 0; i < pixelData.Length; i++) + { + // if the given color dictionary contains a color value for this red index, use it. + if (map.TryGetValue(i, out var swapColor)) + { + pixelData[i] = swapColor; + } + else if (overWrite) + { + // otherwise, default the pixel to transparent + pixelData[i] = Color.Transparent; + } + } + + ColorMap.SetData(pixelData); + } + + public void SetColorsByExistingColorMap(Texture2D existingColorMap) + { + var existingPixels = new Color[256]; + existingColorMap.GetData(existingPixels); + + var map = new Dictionary(); + for (var i = 0; i < existingPixels.Length; i++) + { + map[i] = existingPixels[i]; + } + + SetColorsByRedValue(map); + } +} \ No newline at end of file diff --git a/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Graphics/ShadowCaster.cs b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Graphics/ShadowCaster.cs new file mode 100644 index 00000000..eefc4fb5 --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Graphics/ShadowCaster.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using Microsoft.Xna.Framework; + +namespace MonoGameLibrary.Graphics; + +public class ShadowCaster +{ + /// + /// The position of the shadow caster + /// + public Vector2 Position; + + /// + /// A list of at least 2 points that will be used to create a closed loop shape. + /// The points are relative to the position. + /// + public List Points; + + public static ShadowCaster SimplePolygon(Point position, float radius, int sides) + { + var anglePerSide = MathHelper.TwoPi / sides; + var caster = new ShadowCaster + { + Position = position.ToVector2(), + Points = new List(sides) + }; + for (var angle = 0f; angle < MathHelper.TwoPi; angle += anglePerSide) + { + var pt = radius * new Vector2(MathF.Cos(angle), MathF.Sin(angle)); + caster.Points.Add(pt); + } + + return caster; + } +} \ No newline at end of file diff --git a/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Graphics/Sprite.cs b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Graphics/Sprite.cs new file mode 100644 index 00000000..20c44f0b --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/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/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Graphics/SpriteCamera3d.cs b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Graphics/SpriteCamera3d.cs new file mode 100644 index 00000000..0602eb57 --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Graphics/SpriteCamera3d.cs @@ -0,0 +1,51 @@ +using System; +using Microsoft.Xna.Framework; + +namespace MonoGameLibrary.Graphics; + +public class SpriteCamera3d +{ + /// + /// The field of view for the camera. + /// + public int Fov { get; set; } = 120; + + /// + /// By default, the camera is looking at the center of the screen. + /// This offset value can be used to "turn" the camera from the center towards the given vector value. + /// + public Vector2 LookOffset { get; set; } = Vector2.Zero; + + /// + /// Produce a matrix that will transform world-space coordinates into clip-space coordinates. + /// + /// + public Matrix CalculateMatrixTransform() + { + var viewport = Core.GraphicsDevice.Viewport; + + // start by creating the projection matrix + var projection = Matrix.CreatePerspectiveFieldOfView( + fieldOfView: MathHelper.ToRadians(Fov), + aspectRatio: Core.GraphicsDevice.Viewport.AspectRatio, + nearPlaneDistance: 0.0001f, + farPlaneDistance: 10000f + ); + + // position the camera far enough away to see the entire contents of the screen + var cameraZ = (viewport.Height * 0.5f) / (float)Math.Tan(MathHelper.ToRadians(Fov) * 0.5f); + + // create a view that is centered on the screen + var center = .5f * new Vector2(viewport.Width, viewport.Height); + var look = center + LookOffset; + var view = Matrix.CreateLookAt( + cameraPosition: new Vector3(center.X, center.Y, -cameraZ), + cameraTarget: new Vector3(look.X, look.Y, 0), + cameraUpVector: Vector3.Down + ); + + // the standard matrix format is world*view*projection, + // but given that we are skipping the world matrix, its just view*projection + return view * projection; + } +} \ No newline at end of file diff --git a/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Graphics/TextureAtlas.cs b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Graphics/TextureAtlas.cs new file mode 100644 index 00000000..e48c9abd --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/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/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Graphics/TextureRegion.cs b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Graphics/TextureRegion.cs new file mode 100644 index 00000000..ecd69030 --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/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/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Graphics/Tilemap.cs b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Graphics/Tilemap.cs new file mode 100644 index 00000000..96e1ee5e --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/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/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Graphics/Tileset.cs b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Graphics/Tileset.cs new file mode 100644 index 00000000..80c2e65a --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/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/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/ImGui/DrawVertDeclaration.cs b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/ImGui/DrawVertDeclaration.cs new file mode 100644 index 00000000..d846e7da --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/ImGui/DrawVertDeclaration.cs @@ -0,0 +1,29 @@ +using Microsoft.Xna.Framework.Graphics; + +namespace ImGuiNET.SampleProgram.XNA +{ + public static class DrawVertDeclaration + { + public static readonly VertexDeclaration Declaration; + + public static readonly int Size; + + static DrawVertDeclaration() + { + unsafe { Size = sizeof(ImDrawVert); } + + Declaration = new VertexDeclaration( + Size, + + // Position + new VertexElement(0, VertexElementFormat.Vector2, VertexElementUsage.Position, 0), + + // UV + new VertexElement(8, VertexElementFormat.Vector2, VertexElementUsage.TextureCoordinate, 0), + + // Color + new VertexElement(16, VertexElementFormat.Color, VertexElementUsage.Color, 0) + ); + } + } +} \ No newline at end of file diff --git a/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/ImGui/ImGuiRenderer.cs b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/ImGui/ImGuiRenderer.cs new file mode 100644 index 00000000..e2cc1a29 --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/ImGui/ImGuiRenderer.cs @@ -0,0 +1,436 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace ImGuiNET.SampleProgram.XNA +{ + /// + /// ImGui renderer for use with XNA-likes (FNA & MonoGame) + /// + public class ImGuiRenderer + { + private Game _game; + + // Graphics + private GraphicsDevice _graphicsDevice; + + private BasicEffect _effect; + private RasterizerState _rasterizerState; + + private byte[] _vertexData; + private VertexBuffer _vertexBuffer; + private int _vertexBufferSize; + + private byte[] _indexData; + private IndexBuffer _indexBuffer; + private int _indexBufferSize; + + // Textures + private Dictionary _loadedTextures; + + private int _textureId; + private IntPtr? _fontTextureId; + + // Input + private int _scrollWheelValue; + private int _horizontalScrollWheelValue; + private readonly float WHEEL_DELTA = 120; + private Keys[] _allKeys = Enum.GetValues(); + + public ImGuiRenderer(Game game) + { + var context = ImGui.CreateContext(); + ImGui.SetCurrentContext(context); + + _game = game ?? throw new ArgumentNullException(nameof(game)); + _graphicsDevice = game.GraphicsDevice; + + _loadedTextures = new Dictionary(); + + _rasterizerState = new RasterizerState() + { + CullMode = CullMode.None, + DepthBias = 0, + FillMode = FillMode.Solid, + MultiSampleAntiAlias = false, + ScissorTestEnable = true, + SlopeScaleDepthBias = 0 + }; + + SetupInput(); + } + + #region ImGuiRenderer + + /// + /// Creates a texture and loads the font data from ImGui. Should be called when the is initialized but before any rendering is done + /// + public virtual unsafe void RebuildFontAtlas() + { + // Get font texture from ImGui + var io = ImGui.GetIO(); + io.Fonts.GetTexDataAsRGBA32(out byte* pixelData, out int width, out int height, out int bytesPerPixel); + + // Copy the data to a managed array + var pixels = new byte[width * height * bytesPerPixel]; + unsafe { Marshal.Copy(new IntPtr(pixelData), pixels, 0, pixels.Length); } + + // Create and register the texture as an XNA texture + var tex2d = new Texture2D(_graphicsDevice, width, height, false, SurfaceFormat.Color); + tex2d.SetData(pixels); + + // Should a texture already have been build previously, unbind it first so it can be deallocated + if (_fontTextureId.HasValue) UnbindTexture(_fontTextureId.Value); + + // Bind the new texture to an ImGui-friendly id + _fontTextureId = BindTexture(tex2d); + + // Let ImGui know where to find the texture + io.Fonts.SetTexID(_fontTextureId.Value); + io.Fonts.ClearTexData(); // Clears CPU side texture data + } + + /// + /// Creates a pointer to a texture, which can be passed through ImGui calls such as . That pointer is then used by ImGui to let us know what texture to draw + /// + public virtual IntPtr BindTexture(Texture2D texture) + { + var id = new IntPtr(_textureId++); + + _loadedTextures.Add(id, texture); + + return id; + } + + /// + /// Removes a previously created texture pointer, releasing its reference and allowing it to be deallocated + /// + public virtual void UnbindTexture(IntPtr textureId) + { + _loadedTextures.Remove(textureId); + } + + /// + /// Sets up ImGui for a new frame, should be called at frame start + /// + public virtual void BeforeLayout(GameTime gameTime) + { + ImGui.GetIO().DeltaTime = (float)gameTime.ElapsedGameTime.TotalSeconds; + + UpdateInput(); + + ImGui.NewFrame(); + } + + /// + /// Asks ImGui for the generated geometry data and sends it to the graphics pipeline, should be called after the UI is drawn using ImGui.** calls + /// + public virtual void AfterLayout() + { + ImGui.Render(); + + unsafe { RenderDrawData(ImGui.GetDrawData()); } + } + + #endregion ImGuiRenderer + + #region Setup & Update + + /// + /// Setup key input event handler. + /// + protected virtual void SetupInput() + { + var io = ImGui.GetIO(); + + // MonoGame-specific ////////////////////// + _game.Window.TextInput += (s, a) => + { + if (a.Character == '\t') return; + io.AddInputCharacter(a.Character); + }; + + /////////////////////////////////////////// + + // FNA-specific /////////////////////////// + //TextInputEXT.TextInput += c => + //{ + // if (c == '\t') return; + + // ImGui.GetIO().AddInputCharacter(c); + //}; + /////////////////////////////////////////// + } + + /// + /// Updates the to the current matrices and texture + /// + protected virtual Effect UpdateEffect(Texture2D texture) + { + _effect = _effect ?? new BasicEffect(_graphicsDevice); + + var io = ImGui.GetIO(); + + _effect.World = Matrix.Identity; + _effect.View = Matrix.Identity; + _effect.Projection = Matrix.CreateOrthographicOffCenter(0f, io.DisplaySize.X, io.DisplaySize.Y, 0f, -1f, 1f); + _effect.TextureEnabled = true; + _effect.Texture = texture; + _effect.VertexColorEnabled = true; + + return _effect; + } + + /// + /// Sends XNA input state to ImGui + /// + protected virtual void UpdateInput() + { + if (!_game.IsActive) return; + + var io = ImGui.GetIO(); + + var mouse = Mouse.GetState(); + var keyboard = Keyboard.GetState(); + io.AddMousePosEvent(mouse.X, mouse.Y); + io.AddMouseButtonEvent(0, mouse.LeftButton == ButtonState.Pressed); + io.AddMouseButtonEvent(1, mouse.RightButton == ButtonState.Pressed); + io.AddMouseButtonEvent(2, mouse.MiddleButton == ButtonState.Pressed); + io.AddMouseButtonEvent(3, mouse.XButton1 == ButtonState.Pressed); + io.AddMouseButtonEvent(4, mouse.XButton2 == ButtonState.Pressed); + + io.AddMouseWheelEvent( + (mouse.HorizontalScrollWheelValue - _horizontalScrollWheelValue) / WHEEL_DELTA, + (mouse.ScrollWheelValue - _scrollWheelValue) / WHEEL_DELTA); + _scrollWheelValue = mouse.ScrollWheelValue; + _horizontalScrollWheelValue = mouse.HorizontalScrollWheelValue; + + foreach (var key in _allKeys) + { + if (TryMapKeys(key, out ImGuiKey imguikey)) + { + io.AddKeyEvent(imguikey, keyboard.IsKeyDown(key)); + } + } + + io.DisplaySize = new System.Numerics.Vector2(_graphicsDevice.PresentationParameters.BackBufferWidth, _graphicsDevice.PresentationParameters.BackBufferHeight); + io.DisplayFramebufferScale = new System.Numerics.Vector2(1f, 1f); + } + + private bool TryMapKeys(Keys key, out ImGuiKey imguikey) + { + //Special case not handed in the switch... + //If the actual key we put in is "None", return none and true. + //otherwise, return none and false. + if (key == Keys.None) + { + imguikey = ImGuiKey.None; + return true; + } + + imguikey = key switch + { + Keys.Back => ImGuiKey.Backspace, + Keys.Tab => ImGuiKey.Tab, + Keys.Enter => ImGuiKey.Enter, + Keys.CapsLock => ImGuiKey.CapsLock, + Keys.Escape => ImGuiKey.Escape, + Keys.Space => ImGuiKey.Space, + Keys.PageUp => ImGuiKey.PageUp, + Keys.PageDown => ImGuiKey.PageDown, + Keys.End => ImGuiKey.End, + Keys.Home => ImGuiKey.Home, + Keys.Left => ImGuiKey.LeftArrow, + Keys.Right => ImGuiKey.RightArrow, + Keys.Up => ImGuiKey.UpArrow, + Keys.Down => ImGuiKey.DownArrow, + Keys.PrintScreen => ImGuiKey.PrintScreen, + Keys.Insert => ImGuiKey.Insert, + Keys.Delete => ImGuiKey.Delete, + >= Keys.D0 and <= Keys.D9 => ImGuiKey._0 + (key - Keys.D0), + >= Keys.A and <= Keys.Z => ImGuiKey.A + (key - Keys.A), + >= Keys.NumPad0 and <= Keys.NumPad9 => ImGuiKey.Keypad0 + (key - Keys.NumPad0), + Keys.Multiply => ImGuiKey.KeypadMultiply, + Keys.Add => ImGuiKey.KeypadAdd, + Keys.Subtract => ImGuiKey.KeypadSubtract, + Keys.Decimal => ImGuiKey.KeypadDecimal, + Keys.Divide => ImGuiKey.KeypadDivide, + >= Keys.F1 and <= Keys.F24 => ImGuiKey.F1 + (key - Keys.F1), + Keys.NumLock => ImGuiKey.NumLock, + Keys.Scroll => ImGuiKey.ScrollLock, + Keys.LeftShift => ImGuiKey.ModShift, + Keys.LeftControl => ImGuiKey.ModCtrl, + Keys.LeftAlt => ImGuiKey.ModAlt, + Keys.OemSemicolon => ImGuiKey.Semicolon, + Keys.OemPlus => ImGuiKey.Equal, + Keys.OemComma => ImGuiKey.Comma, + Keys.OemMinus => ImGuiKey.Minus, + Keys.OemPeriod => ImGuiKey.Period, + Keys.OemQuestion => ImGuiKey.Slash, + Keys.OemTilde => ImGuiKey.GraveAccent, + Keys.OemOpenBrackets => ImGuiKey.LeftBracket, + Keys.OemCloseBrackets => ImGuiKey.RightBracket, + Keys.OemPipe => ImGuiKey.Backslash, + Keys.OemQuotes => ImGuiKey.Apostrophe, + Keys.BrowserBack => ImGuiKey.AppBack, + Keys.BrowserForward => ImGuiKey.AppForward, + _ => ImGuiKey.None, + }; + + return imguikey != ImGuiKey.None; + } + + #endregion Setup & Update + + #region Internals + + /// + /// Gets the geometry as set up by ImGui and sends it to the graphics device + /// + private void RenderDrawData(ImDrawDataPtr drawData) + { + // Setup render state: alpha-blending enabled, no face culling, no depth testing, scissor enabled, vertex/texcoord/color pointers + var lastViewport = _graphicsDevice.Viewport; + var lastScissorBox = _graphicsDevice.ScissorRectangle; + var lastRasterizer = _graphicsDevice.RasterizerState; + var lastDepthStencil = _graphicsDevice.DepthStencilState; + var lastBlendFactor = _graphicsDevice.BlendFactor; + var lastBlendState = _graphicsDevice.BlendState; + + _graphicsDevice.BlendFactor = Color.White; + _graphicsDevice.BlendState = BlendState.NonPremultiplied; + _graphicsDevice.RasterizerState = _rasterizerState; + _graphicsDevice.DepthStencilState = DepthStencilState.DepthRead; + + // Handle cases of screen coordinates != from framebuffer coordinates (e.g. retina displays) + drawData.ScaleClipRects(ImGui.GetIO().DisplayFramebufferScale); + + // Setup projection + _graphicsDevice.Viewport = new Viewport(0, 0, _graphicsDevice.PresentationParameters.BackBufferWidth, _graphicsDevice.PresentationParameters.BackBufferHeight); + + UpdateBuffers(drawData); + + RenderCommandLists(drawData); + + // Restore modified state + _graphicsDevice.Viewport = lastViewport; + _graphicsDevice.ScissorRectangle = lastScissorBox; + _graphicsDevice.RasterizerState = lastRasterizer; + _graphicsDevice.DepthStencilState = lastDepthStencil; + _graphicsDevice.BlendState = lastBlendState; + _graphicsDevice.BlendFactor = lastBlendFactor; + } + + private unsafe void UpdateBuffers(ImDrawDataPtr drawData) + { + if (drawData.TotalVtxCount == 0) + { + return; + } + + // Expand buffers if we need more room + if (drawData.TotalVtxCount > _vertexBufferSize) + { + _vertexBuffer?.Dispose(); + + _vertexBufferSize = (int)(drawData.TotalVtxCount * 1.5f); + _vertexBuffer = new VertexBuffer(_graphicsDevice, DrawVertDeclaration.Declaration, _vertexBufferSize, BufferUsage.None); + _vertexData = new byte[_vertexBufferSize * DrawVertDeclaration.Size]; + } + + if (drawData.TotalIdxCount > _indexBufferSize) + { + _indexBuffer?.Dispose(); + + _indexBufferSize = (int)(drawData.TotalIdxCount * 1.5f); + _indexBuffer = new IndexBuffer(_graphicsDevice, IndexElementSize.SixteenBits, _indexBufferSize, BufferUsage.None); + _indexData = new byte[_indexBufferSize * sizeof(ushort)]; + } + + // Copy ImGui's vertices and indices to a set of managed byte arrays + int vtxOffset = 0; + int idxOffset = 0; + + for (int n = 0; n < drawData.CmdListsCount; n++) + { + ImDrawListPtr cmdList = drawData.CmdLists[n]; + + fixed (void* vtxDstPtr = &_vertexData[vtxOffset * DrawVertDeclaration.Size]) + fixed (void* idxDstPtr = &_indexData[idxOffset * sizeof(ushort)]) + { + Buffer.MemoryCopy((void*)cmdList.VtxBuffer.Data, vtxDstPtr, _vertexData.Length, cmdList.VtxBuffer.Size * DrawVertDeclaration.Size); + Buffer.MemoryCopy((void*)cmdList.IdxBuffer.Data, idxDstPtr, _indexData.Length, cmdList.IdxBuffer.Size * sizeof(ushort)); + } + + vtxOffset += cmdList.VtxBuffer.Size; + idxOffset += cmdList.IdxBuffer.Size; + } + + // Copy the managed byte arrays to the gpu vertex- and index buffers + _vertexBuffer.SetData(_vertexData, 0, drawData.TotalVtxCount * DrawVertDeclaration.Size); + _indexBuffer.SetData(_indexData, 0, drawData.TotalIdxCount * sizeof(ushort)); + } + + private unsafe void RenderCommandLists(ImDrawDataPtr drawData) + { + _graphicsDevice.SetVertexBuffer(_vertexBuffer); + _graphicsDevice.Indices = _indexBuffer; + + int vtxOffset = 0; + int idxOffset = 0; + + for (int n = 0; n < drawData.CmdListsCount; n++) + { + ImDrawListPtr cmdList = drawData.CmdLists[n]; + + for (int cmdi = 0; cmdi < cmdList.CmdBuffer.Size; cmdi++) + { + ImDrawCmdPtr drawCmd = cmdList.CmdBuffer[cmdi]; + + if (drawCmd.ElemCount == 0) + { + continue; + } + + if (!_loadedTextures.ContainsKey(drawCmd.TextureId)) + { + throw new InvalidOperationException($"Could not find a texture with id '{drawCmd.TextureId}', please check your bindings"); + } + + _graphicsDevice.ScissorRectangle = new Rectangle( + (int)drawCmd.ClipRect.X, + (int)drawCmd.ClipRect.Y, + (int)(drawCmd.ClipRect.Z - drawCmd.ClipRect.X), + (int)(drawCmd.ClipRect.W - drawCmd.ClipRect.Y) + ); + + var effect = UpdateEffect(_loadedTextures[drawCmd.TextureId]); + + foreach (var pass in effect.CurrentTechnique.Passes) + { + pass.Apply(); + +#pragma warning disable CS0618 // // FNA does not expose an alternative method. + _graphicsDevice.DrawIndexedPrimitives( + primitiveType: PrimitiveType.TriangleList, + baseVertex: (int)drawCmd.VtxOffset + vtxOffset, + minVertexIndex: 0, + numVertices: cmdList.VtxBuffer.Size, + startIndex: (int)drawCmd.IdxOffset + idxOffset, + primitiveCount: (int)drawCmd.ElemCount / 3 + ); +#pragma warning restore CS0618 + } + } + + vtxOffset += cmdList.VtxBuffer.Size; + idxOffset += cmdList.IdxBuffer.Size; + } + } + + #endregion Internals + } +} \ No newline at end of file diff --git a/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Input/GamePadInfo.cs b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Input/GamePadInfo.cs new file mode 100644 index 00000000..7fd16126 --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/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/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Input/InputManager.cs b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Input/InputManager.cs new file mode 100644 index 00000000..1790eb70 --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/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/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Input/KeyboardInfo.cs b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Input/KeyboardInfo.cs new file mode 100644 index 00000000..c6770cb0 --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/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/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Input/MouseButton.cs b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Input/MouseButton.cs new file mode 100644 index 00000000..5b041f80 --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Input/MouseButton.cs @@ -0,0 +1,10 @@ +namespace MonoGameLibrary.Input; + +public enum MouseButton +{ + Left, + Middle, + Right, + XButton1, + XButton2 +} diff --git a/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Input/MouseInfo.cs b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Input/MouseInfo.cs new file mode 100644 index 00000000..09d6207c --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/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/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/MonoGameLibrary.csproj b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/MonoGameLibrary.csproj new file mode 100644 index 00000000..69adcc21 --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/MonoGameLibrary.csproj @@ -0,0 +1,12 @@ + + + net8.0 + true + + + + + All + + + \ No newline at end of file diff --git a/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Scenes/Scene.cs b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Scenes/Scene.cs new file mode 100644 index 00000000..627d220f --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/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/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Scenes/SceneTransition.cs b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Scenes/SceneTransition.cs new file mode 100644 index 00000000..bbd1f7d5 --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/Scenes/SceneTransition.cs @@ -0,0 +1,56 @@ +using System; +using Microsoft.Xna.Framework; + +namespace MonoGameLibrary.Scenes; + +public class SceneTransition +{ + public DateTimeOffset StartTime; + public TimeSpan Duration; + + /// + /// true when the transition is progressing from 0 to 1. + /// false when the transition is progressing from 1 to 0. + /// + public bool IsForwards; + + /// + /// The index into the + /// + public int TextureIndex; + + /// + /// The 0 to 1 value representing the progress of the transition. + /// + public float ProgressRatio => MathHelper.Clamp((float)(EndTime - DateTimeOffset.Now).TotalMilliseconds / (float)Duration.TotalMilliseconds, 0, 1); + + public float DirectionalRatio => IsForwards ? 1 - ProgressRatio : ProgressRatio; + + public DateTimeOffset EndTime => StartTime + Duration; + public bool IsComplete => DateTimeOffset.Now >= EndTime; + + + /// + /// Create a new transition + /// + /// + /// how long will the transition last in milliseconds? + /// + /// + /// should the transition be animating the Progress parameter from 0 to 1, or 1 to 0? + /// + /// + public static SceneTransition Create(int durationMs, bool isForwards) + { + return new SceneTransition + { + Duration = TimeSpan.FromMilliseconds(durationMs), + StartTime = DateTimeOffset.Now, + TextureIndex = Random.Shared.Next(), + IsForwards = isForwards + }; + } + + public static SceneTransition Open(int durationMs) => Create(durationMs, true); + public static SceneTransition Close(int durationMs) => Create(durationMs, false); +} \ No newline at end of file diff --git a/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/SharedContent/SharedContent.mgcb b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/SharedContent/SharedContent.mgcb new file mode 100644 index 00000000..0defe70b --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/SharedContent/SharedContent.mgcb @@ -0,0 +1,99 @@ + +#----------------------------- Global Properties ----------------------------# + +/outputDir:bin/$(Platform) +/intermediateDir:obj/$(Platform) +/platform:DesktopGL +/config: +/profile:Reach +/compress:False + +#-------------------------------- References --------------------------------# + + +#---------------------------------- Content ---------------------------------# + +#begin effects/3dEffect.fx +/importer:EffectImporter +/processor:EffectProcessor +/processorParam:DebugMode=Auto +/build:effects/3dEffect.fx + +#begin effects/colorSwapEffect.fx +/importer:EffectImporter +/processor:EffectProcessor +/processorParam:DebugMode=Auto +/build:effects/colorSwapEffect.fx + +#begin effects/deferredCompositeEffect.fx +/importer:EffectImporter +/processor:EffectProcessor +/processorParam:DebugMode=Auto +/build:effects/deferredCompositeEffect.fx + +#begin effects/pointLightEffect.fx +/importer:EffectImporter +/processor:EffectProcessor +/processorParam:DebugMode=Auto +/build:effects/pointLightEffect.fx + +#begin effects/sceneTransitionEffect.fx +/importer:EffectImporter +/processor:EffectProcessor +/processorParam:DebugMode=Auto +/build:effects/sceneTransitionEffect.fx + +#begin effects/shadowHullEffect.fx +/importer:EffectImporter +/processor:EffectProcessor +/processorParam:DebugMode=Auto +/build:effects/shadowHullEffect.fx + +#begin images/angled.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/angled.png + +#begin images/concave.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/concave.png + +#begin images/radial.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/radial.png + +#begin images/ripple.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/ripple.png + diff --git a/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/SharedContent/effects/3dEffect.fx b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/SharedContent/effects/3dEffect.fx new file mode 100644 index 00000000..454e0b37 --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/SharedContent/effects/3dEffect.fx @@ -0,0 +1,31 @@ +#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; + +sampler2D SpriteTextureSampler = sampler_state +{ + Texture = ; +}; + +#include "3dEffect.fxh" + +float4 MainPS(VertexShaderOutput input) : COLOR +{ + return tex2D(SpriteTextureSampler,input.TextureCoordinates) * input.Color; +} + +technique SpriteDrawing +{ + pass P0 + { + PixelShader = compile PS_SHADERMODEL MainPS(); + VertexShader = compile VS_SHADERMODEL MainVS(); + } +}; \ No newline at end of file diff --git a/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/SharedContent/effects/3dEffect.fxh b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/SharedContent/effects/3dEffect.fxh new file mode 100644 index 00000000..304503fe --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/SharedContent/effects/3dEffect.fxh @@ -0,0 +1,45 @@ +#ifndef EFFECT_3DEFFECT +#define EFFECT_3DEFFECT +#include "common.fxh" + +float4x4 MatrixTransform; +float2 ScreenSize; +float SpinAmount; + +VertexShaderOutput MainVS(VertexShaderInput input) +{ + VertexShaderOutput output; + + float4 pos = input.Position; + + // create the center of rotation + float2 centerXZ = float2(ScreenSize.x * .5, 0); + + // convert the debug variable into an angle from 0 to 2 pi. + // shaders use radians for angles, so 2 pi = 360 degrees + float angle = SpinAmount * 6.28; + + // pre-compute the cos and sin of the angle + float cosA = cos(angle); + float sinA = sin(angle); + + // shift the position to the center of rotation + pos.xz -= centerXZ; + + // compute the rotation + float nextX = pos.x * cosA - pos.z * sinA; + float nextZ = pos.x * sinA + pos.z * cosA; + + // apply the rotation + pos.x = nextX; + pos.z = nextZ; + + // shift the position away from the center of rotation + pos.xz += centerXZ; + + output.Position = mul(pos, MatrixTransform); + output.Color = input.Color; + output.TextureCoordinates = input.TexCoord; + return output; +} +#endif \ No newline at end of file diff --git a/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/SharedContent/effects/colorSwapEffect.fx b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/SharedContent/effects/colorSwapEffect.fx new file mode 100644 index 00000000..8e4d4f2f --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/SharedContent/effects/colorSwapEffect.fx @@ -0,0 +1,25 @@ +#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 + +// the main Sprite texture passed to SpriteBatch.Draw() +Texture2D SpriteTexture; +sampler2D SpriteTextureSampler = sampler_state +{ + Texture = ; +}; + +#include "colors.fxh" + +technique SpriteDrawing +{ + pass P0 + { + PixelShader = compile PS_SHADERMODEL ColorSwapPS(); + } +}; \ No newline at end of file diff --git a/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/SharedContent/effects/colors.fxh b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/SharedContent/effects/colors.fxh new file mode 100644 index 00000000..81e3c79e --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/SharedContent/effects/colors.fxh @@ -0,0 +1,68 @@ +#ifndef COLORS +#define COLORS + +#include "common.fxh" + +// the custom color map passed to the Material.SetParameter() +Texture2D ColorMap; +sampler2D ColorMapSampler = sampler_state +{ + Texture = ; + MinFilter = Point; + MagFilter = Point; + MipFilter = Point; + AddressU = Clamp; + AddressV = Clamp; +}; + +// a control variable to lerp between original color and swapped color +float OriginalAmount; +float Saturation; + +float4 Grayscale(float4 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); +} + +float4 SwapColors(float4 color) +{ + // produce the key location + float2 keyUv = float2(color.r , 0); + + // read the swap color value + float4 swappedColor = tex2D(ColorMapSampler, keyUv) * color.a; + + // ignore the swap if the map does not have a value + bool hasSwapColor = swappedColor.a > 0; + if (!hasSwapColor) + { + return color; + } + + // return the result color + return lerp(swappedColor, color, OriginalAmount); +} + +float4 ColorSwapPS(VertexShaderOutput input) : COLOR +{ + // read the original color value + float4 originalColor = tex2D(SpriteTextureSampler,input.TextureCoordinates); + + float4 swapped = SwapColors(originalColor); + float4 saturated = Grayscale(swapped); + + return saturated; +} + +#endif \ No newline at end of file diff --git a/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/SharedContent/effects/common.fxh b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/SharedContent/effects/common.fxh new file mode 100644 index 00000000..e0e849c7 --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/SharedContent/effects/common.fxh @@ -0,0 +1,17 @@ +#ifndef COMMON +#define COMMON + +struct VertexShaderInput +{ + float4 Position : POSITION0; + float4 Color : COLOR0; + float2 TexCoord : TEXCOORD0; +}; + +struct VertexShaderOutput +{ + float4 Position : SV_POSITION; + float4 Color : COLOR0; + float2 TextureCoordinates : TEXCOORD0; +}; +#endif \ No newline at end of file diff --git a/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/SharedContent/effects/deferredCompositeEffect.fx b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/SharedContent/effects/deferredCompositeEffect.fx new file mode 100644 index 00000000..221b00f9 --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/SharedContent/effects/deferredCompositeEffect.fx @@ -0,0 +1,78 @@ +#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; + +sampler2D SpriteTextureSampler = sampler_state +{ + Texture = ; +}; + + +Texture2D LightBuffer; +sampler2D LightBufferSampler = sampler_state +{ + Texture = ; +}; + +struct VertexShaderOutput +{ + float4 Position : SV_POSITION; + float4 Color : COLOR0; + float2 TextureCoordinates : TEXCOORD0; +}; + + +float AmbientLight; +float2 ScreenSize; +float BoxBlurStride; + +float4 Blur(float2 texCoord) +{ + float4 color = float4(0, 0, 0, 0); + + float2 texelSize = 1 / ScreenSize; + int kernalSize = 1; + float stride = BoxBlurStride * 30; // allow the stride to range up a size of 30 + for (int x = -kernalSize; x <= kernalSize; x++) + { + for (int y = -kernalSize; y <= kernalSize; y++) + { + + float2 offset = float2(x, y) * texelSize * stride; + color += tex2D(LightBufferSampler, texCoord + offset); + } + } + + int totalSamples = pow(kernalSize*2+1, 2); + color /= totalSamples; + color.a = 1; + return color; +} + + +float4 MainPS(VertexShaderOutput input) : COLOR +{ + float4 color = tex2D(SpriteTextureSampler,input.TextureCoordinates) * input.Color; + float4 light = Blur(input.TextureCoordinates) * input.Color; + + float3 toneMapped = light.xyz / (.5 + dot(light.xyz, float3(0.299, 0.587, 0.114))); + light.xyz = toneMapped; + + light = saturate(light + AmbientLight); + return color * light; +} + +technique SpriteDrawing +{ + pass P0 + { + PixelShader = compile PS_SHADERMODEL MainPS(); + } +}; \ No newline at end of file diff --git a/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/SharedContent/effects/pointLightEffect.fx b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/SharedContent/effects/pointLightEffect.fx new file mode 100644 index 00000000..949ee180 --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/SharedContent/effects/pointLightEffect.fx @@ -0,0 +1,88 @@ +#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 NormalBuffer; +sampler2D NormalBufferSampler = sampler_state +{ + Texture = ; +}; + +#include "3dEffect.fxh" +struct LightVertexShaderOutput +{ + float4 Position : POSITION0; + float4 Color : COLOR0; + float2 TextureCoordinates : TEXCOORD0; + float3 ScreenData : TEXCOORD1; +}; + +float LightBrightness; +float LightSharpness; + + +LightVertexShaderOutput LightVS(VertexShaderInput input) +{ + LightVertexShaderOutput output; + + VertexShaderOutput mainVsOutput = MainVS(input); + + // forward along the existing values from the MainVS's output + output.Position = mainVsOutput.Position;// / mainVsOutput.Position.w; + output.Color = mainVsOutput.Color; + output.TextureCoordinates = mainVsOutput.TextureCoordinates; + + // pack the required position variables, x, y, and w, into the ScreenData + output.ScreenData.xy = output.Position.xy; + output.ScreenData.z = output.Position.w; + + return output; +} + + +float4 MainPS(LightVertexShaderOutput input) : COLOR { + float dist = length(input.TextureCoordinates - .5); + float range = 5; // arbitrary maximum. + + float falloff = saturate(.5 - dist) * (LightBrightness * range + 1); + falloff = pow(abs(falloff), LightSharpness * range + 1); + + // correct the perspective divide. + input.ScreenData /= input.ScreenData.z; + + // put the clip-space coordinates into screen space. + float2 screenCoords = .5*(input.ScreenData.xy + 1); + screenCoords.y = 1 - screenCoords.y; + + float4 normal = tex2D(NormalBufferSampler,screenCoords); + // flip the y of the normals, because the art assets have them backwards. + normal.y = 1 - normal.y; + + // convert from [0,1] to [-1,1] + float3 normalDir = (normal.xyz-.5)*2; + + // find the direction the light is travelling at the current pixel + float3 lightDir = float3(normalize(.5 - input.TextureCoordinates), 1); + + // how much is the normal direction pointing towards the light direction? + float lightAmount = (dot(normalDir, lightDir)); + + float4 color = input.Color; + color.a *= falloff * lightAmount; + return color; +} + +technique SpriteDrawing +{ + pass P0 + { + VertexShader = compile VS_SHADERMODEL LightVS(); + PixelShader = compile PS_SHADERMODEL MainPS(); + } +}; \ No newline at end of file diff --git a/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/SharedContent/effects/sceneTransitionEffect.fx b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/SharedContent/effects/sceneTransitionEffect.fx new file mode 100644 index 00000000..0d87d021 --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/SharedContent/effects/sceneTransitionEffect.fx @@ -0,0 +1,42 @@ +#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; + +float Progress; +float EdgeWidth; + +sampler2D SpriteTextureSampler = sampler_state +{ + Texture = ; +}; + +struct VertexShaderOutput +{ + float4 Position : SV_POSITION; + float4 Color : COLOR0; + float2 TextureCoordinates : TEXCOORD0; +}; + +float4 MainPS(VertexShaderOutput input) : COLOR +{ + float2 uv = input.TextureCoordinates; + float value = tex2D(SpriteTextureSampler, uv).r; + float transitioned = smoothstep(Progress, Progress + EdgeWidth, value); + return float4(0, 0, 0, transitioned); +} + + +technique SpriteDrawing +{ + pass P0 + { + PixelShader = compile PS_SHADERMODEL MainPS(); + } +}; \ No newline at end of file diff --git a/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/SharedContent/effects/shadowHullEffect.fx b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/SharedContent/effects/shadowHullEffect.fx new file mode 100644 index 00000000..557224e2 --- /dev/null +++ b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/SharedContent/effects/shadowHullEffect.fx @@ -0,0 +1,128 @@ +#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; + +sampler2D SpriteTextureSampler = sampler_state +{ + Texture = ; +}; + +#include "3dEffect.fxh" + +float2 UnpackVector2FromColor_SNorm(float4 color) +{ + // Convert [0,1] to byte range [0,255] + float4 bytes = color * 255.0; + + // Reconstruct 16-bit unsigned ints (x and y) + float xInt = bytes.r * 256.0 + bytes.g; + float yInt = bytes.b * 256.0 + bytes.a; + + // Convert from unsigned to signed short range [-32768, 32767] + if (xInt >= 32768.0) xInt -= 65536.0; + if (yInt >= 32768.0) yInt -= 65536.0; + + // Convert from signed 16-bit to float in [-1, 1] + float x = xInt / 32767.0; + float y = yInt / 32767.0; + + return float2(x, y); +} + +float2 LightPosition; +VertexShaderOutput ShadowHullVS(VertexShaderInput input) +{ + VertexShaderInput modified = input; + float distance = ScreenSize.x + ScreenSize.y; + float2 pos = input.Position.xy; + + float2 P = pos - (.5 * input.TexCoord) / ScreenSize; + float2 A = P; + + float2 aToB = UnpackVector2FromColor_SNorm(input.Color) * ScreenSize; + float2 B = A + aToB; + + // expand the segment by 1 unit in each direction + float2 direction = normalize(aToB); + A -= direction*1; + B += direction*1; + + // cull faces + float2 normal = float2(-direction.y, direction.x); + float alignment = dot(normal, (LightPosition - A)); + if (alignment < 0){ + modified.Color.a = -1; + } + + float2 lightRayA = normalize(A - LightPosition); + float2 a = A + distance * lightRayA; + float2 lightRayB = normalize(B - LightPosition); + float2 b = B + distance * lightRayB; + + int id = input.TexCoord.x + input.TexCoord.y * 2; + if (id == 0) { // S --> A + pos = A; + } else if (id == 1) { // D --> a + pos = a; + } else if (id == 3) { // F --> b + pos = b; + } else if (id == 2) { // G --> B + pos = B; + } + + modified.Position.xy = pos; + VertexShaderOutput output = MainVS(modified); + + return output; +} + +// Bayer 4x4 values normalized +static const float bayer4x4[16] = { + 0.0/16.0, 8.0/16.0, 2.0/16.0, 10.0/16.0, + 12.0/16.0, 4.0/16.0, 14.0/16.0, 6.0/16.0, + 3.0/16.0, 11.0/16.0, 1.0/16.0, 9.0/16.0, + 15.0/16.0, 7.0/16.0, 13.0/16.0, 5.0/16.0 +}; + +float ShadowFadeStartDistance; +float ShadowFadeEndDistance; +float ShadowIntensity; + +float4 MainPS(VertexShaderOutput input) : COLOR +{ + + // get an ordered dither value + int2 pixel = int2(input.TextureCoordinates * ScreenSize); + int idx = (pixel.x % 4) + (pixel.y % 4) * 4; + float ditherValue = bayer4x4[idx]; + + // produce the fade-out gradient + float maxDistance = ScreenSize.x + ScreenSize.y; + float endDistance = ShadowFadeEndDistance; + float startDistance = ShadowFadeStartDistance; + float fade = saturate((input.TextureCoordinates.x - endDistance) / (startDistance - endDistance)); + fade = min(fade, ShadowIntensity); + + if (ditherValue > fade){ + clip(-1); + } + + clip(input.Color.a); + return float4(0,0,0,1); // return black +} + +technique SpriteDrawing +{ + pass P0 + { + PixelShader = compile PS_SHADERMODEL MainPS(); + VertexShader = compile VS_SHADERMODEL ShadowHullVS(); + } +}; \ No newline at end of file diff --git a/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/SharedContent/images/angled.png b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/SharedContent/images/angled.png new file mode 100644 index 00000000..de0160f2 Binary files /dev/null and b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/SharedContent/images/angled.png differ diff --git a/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/SharedContent/images/concave.png b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/SharedContent/images/concave.png new file mode 100644 index 00000000..826e2207 Binary files /dev/null and b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/SharedContent/images/concave.png differ diff --git a/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/SharedContent/images/radial.png b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/SharedContent/images/radial.png new file mode 100644 index 00000000..bd1207cf Binary files /dev/null and b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/SharedContent/images/radial.png differ diff --git a/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/SharedContent/images/ripple.png b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/SharedContent/images/ripple.png new file mode 100644 index 00000000..e137653a Binary files /dev/null and b/Tutorials/2dShaders/src/09-Shadows-Effect/MonoGameLibrary/SharedContent/images/ripple.png differ diff --git a/Tutorials/global.json b/Tutorials/global.json new file mode 100644 index 00000000..4e08f958 --- /dev/null +++ b/Tutorials/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "8.0.300", + "rollForward": "latestFeature" + } +} \ No newline at end of file