diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml
index e511562c..b8bf20c9 100644
--- a/.github/workflows/dotnet.yml
+++ b/.github/workflows/dotnet.yml
@@ -63,7 +63,7 @@ jobs:
path: artifacts/NeonShooter-Windows/**
- name: Build Windows Binary for Platformer2D
- run: dotnet publish Platformer2D/Platformer2D.WindowsDX/Platformer2D.WindowsDX.csproj -c Release -r win-x64 --self-contained true -o ./artifacts/Platformer2D-Windows
+ run: dotnet publish Platformer2D/Windows/Platformer2D.csproj -c Release -r win-x64 --self-contained true -o ./artifacts/Platformer2D-Windows
- name: Archive Platformer2D
uses: actions/upload-artifact@v6
@@ -187,8 +187,8 @@ jobs:
- name: Build and Package Platformer2D
run: |
- dotnet build Platformer2D/Platformer2D.DesktopGL/Platformer2D.DesktopGL.csproj -c Release
- monopack -p Platformer2D/Platformer2D.DesktopGL/Platformer2D.DesktopGL.csproj -o ./artifacts/Platformer2D -rids win-x64,linux-x64,osx-x64,osx-arm64 -i Platformer2D/Platformer2D.DesktopGL/Info.plist -c Platformer2D/Platformer2D.DesktopGL/Platformer2D.DesktopGL.icns -v --macos-universal
+ dotnet build Platformer2D/Desktop/Platformer2D.csproj -c Release
+ monopack -p Platformer2D/Desktop/Platformer2D.csproj -o ./artifacts/Platformer2D -rids win-x64,linux-x64,osx-x64,osx-arm64 -i Platformer2D/Desktop/Info.plist -c Platformer2D/Desktop/Platformer2D.icns -v --macos-universal
- name: Archive Platformer2D Windows
uses: actions/upload-artifact@v6
@@ -305,13 +305,13 @@ jobs:
path: NeonShooter/NeonShooter.Android/bin/Release/net9.0-android/**/*-Signed.apk
- name: Build Android Binary for Platformer2D
- run: dotnet build Platformer2D/Platformer2D.Android/Platformer2D.Android.csproj -c Release
+ run: dotnet build Platformer2D/Android/Platformer2D.csproj -c Release
- name: Archive Platformer2D Android
uses: actions/upload-artifact@v6
with:
name: Android-Platformer2D
- path: Platformer2D/Platformer2D.Android/bin/Release/net9.0-android/**/*-Signed.apk
+ path: Platformer2D/Android/bin/Release/net9.0-android/**/*-Signed.apk
- name: Build Android Binary for DungeonSlime
run: dotnet build Tutorials/learn-monogame-2d/src/27-Conclusion/DungeonSlime/Platforms/Android/DungeonSlime.csproj -c Release
diff --git a/Platformer2D/Android/AndroidManifest.xml b/Platformer2D/Android/AndroidManifest.xml
new file mode 100644
index 00000000..d7be7d84
--- /dev/null
+++ b/Platformer2D/Android/AndroidManifest.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Platformer2D/Android/MainActivity.cs b/Platformer2D/Android/MainActivity.cs
new file mode 100644
index 00000000..4be04f2b
--- /dev/null
+++ b/Platformer2D/Android/MainActivity.cs
@@ -0,0 +1,52 @@
+using Android.App;
+using Android.Content.PM;
+using Android.OS;
+using Android.Views;
+
+using Microsoft.Xna.Framework;
+
+using Platformer2D.Core;
+
+namespace Platformer2D.Android
+{
+ ///
+ /// The main activity for the Android application. It initializes the game instance,
+ /// sets up the rendering view, and starts the game loop.
+ ///
+ ///
+ /// This class is responsible for managing the Android activity lifecycle and integrating
+ /// with the MonoGame framework.
+ ///
+ [Activity(
+ Label = "Platformer2D",
+ MainLauncher = true,
+ Icon = "@drawable/icon",
+ Theme = "@style/Theme.Splash",
+ AlwaysRetainTaskState = true,
+ LaunchMode = LaunchMode.SingleInstance,
+ ScreenOrientation = ScreenOrientation.SensorLandscape,
+ ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.Keyboard | ConfigChanges.KeyboardHidden
+ )]
+ public class MainActivity : AndroidGameActivity
+ {
+ private Platformer2DGame _game;
+ private View _view;
+
+ ///
+ /// Called when the activity is first created. Initializes the game instance,
+ /// retrieves its rendering view, and sets it as the content view of the activity.
+ /// Finally, starts the game loop.
+ ///
+ /// A Bundle containing the activity's previously saved state, if any.
+ protected override void OnCreate(Bundle bundle)
+ {
+ base.OnCreate(bundle);
+
+ _game = new Platformer2DGame();
+ _view = _game.Services.GetService(typeof(View)) as View;
+
+ SetContentView(_view);
+ _game.Run();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Platformer2D/Android/Platformer2D.csproj b/Platformer2D/Android/Platformer2D.csproj
new file mode 100644
index 00000000..4026fa3c
--- /dev/null
+++ b/Platformer2D/Android/Platformer2D.csproj
@@ -0,0 +1,24 @@
+
+
+ Exe
+ net9.0-android
+ 21
+ com.companyname.Platformer2D
+ 1
+ 1.0
+ Platformer2D
+ Platformer2D
+
+
+
+ Content\Platformer2D.mgcb
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Platformer2D/Android/Resources/drawable-hdpi/icon.png b/Platformer2D/Android/Resources/drawable-hdpi/icon.png
new file mode 100644
index 00000000..749d561d
Binary files /dev/null and b/Platformer2D/Android/Resources/drawable-hdpi/icon.png differ
diff --git a/Platformer2D/Android/Resources/drawable-hdpi/splash.png b/Platformer2D/Android/Resources/drawable-hdpi/splash.png
new file mode 100644
index 00000000..30929455
Binary files /dev/null and b/Platformer2D/Android/Resources/drawable-hdpi/splash.png differ
diff --git a/Platformer2D/Android/Resources/drawable-mdpi/icon.png b/Platformer2D/Android/Resources/drawable-mdpi/icon.png
new file mode 100644
index 00000000..fb6c912d
Binary files /dev/null and b/Platformer2D/Android/Resources/drawable-mdpi/icon.png differ
diff --git a/Platformer2D/Android/Resources/drawable-mdpi/splash.png b/Platformer2D/Android/Resources/drawable-mdpi/splash.png
new file mode 100644
index 00000000..617e4fbf
Binary files /dev/null and b/Platformer2D/Android/Resources/drawable-mdpi/splash.png differ
diff --git a/Platformer2D/Android/Resources/drawable-xhdpi/icon.png b/Platformer2D/Android/Resources/drawable-xhdpi/icon.png
new file mode 100644
index 00000000..48dfa4ed
Binary files /dev/null and b/Platformer2D/Android/Resources/drawable-xhdpi/icon.png differ
diff --git a/Platformer2D/Android/Resources/drawable-xhdpi/splash.png b/Platformer2D/Android/Resources/drawable-xhdpi/splash.png
new file mode 100644
index 00000000..c416ec97
Binary files /dev/null and b/Platformer2D/Android/Resources/drawable-xhdpi/splash.png differ
diff --git a/Platformer2D/Android/Resources/drawable-xxhdpi/icon.png b/Platformer2D/Android/Resources/drawable-xxhdpi/icon.png
new file mode 100644
index 00000000..b09bb777
Binary files /dev/null and b/Platformer2D/Android/Resources/drawable-xxhdpi/icon.png differ
diff --git a/Platformer2D/Android/Resources/drawable-xxhdpi/splash.png b/Platformer2D/Android/Resources/drawable-xxhdpi/splash.png
new file mode 100644
index 00000000..e0dd401d
Binary files /dev/null and b/Platformer2D/Android/Resources/drawable-xxhdpi/splash.png differ
diff --git a/Platformer2D/Android/Resources/drawable-xxxhdpi/icon.png b/Platformer2D/Android/Resources/drawable-xxxhdpi/icon.png
new file mode 100644
index 00000000..d5c1c3db
Binary files /dev/null and b/Platformer2D/Android/Resources/drawable-xxxhdpi/icon.png differ
diff --git a/Platformer2D/Android/Resources/drawable-xxxhdpi/splash.png b/Platformer2D/Android/Resources/drawable-xxxhdpi/splash.png
new file mode 100644
index 00000000..7273776f
Binary files /dev/null and b/Platformer2D/Android/Resources/drawable-xxxhdpi/splash.png differ
diff --git a/Platformer2D/Android/Resources/values/Styles.xml b/Platformer2D/Android/Resources/values/Styles.xml
new file mode 100644
index 00000000..c0989a05
--- /dev/null
+++ b/Platformer2D/Android/Resources/values/Styles.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/Platformer2D/Android/Resources/values/ic_launcher_background.xml b/Platformer2D/Android/Resources/values/ic_launcher_background.xml
new file mode 100644
index 00000000..49ccebea
--- /dev/null
+++ b/Platformer2D/Android/Resources/values/ic_launcher_background.xml
@@ -0,0 +1,4 @@
+
+
+ #9ACEEB
+
\ No newline at end of file
diff --git a/Platformer2D/Platformer2D.Android/Resources/Values/Strings.xml b/Platformer2D/Android/Resources/values/strings.xml
similarity index 100%
rename from Platformer2D/Platformer2D.Android/Resources/Values/Strings.xml
rename to Platformer2D/Android/Resources/values/strings.xml
diff --git a/Platformer2D/Platformer2D.Core/Content/Backgrounds/Layer0_0.png b/Platformer2D/Core/Content/Backgrounds/Layer0_0.png
similarity index 100%
rename from Platformer2D/Platformer2D.Core/Content/Backgrounds/Layer0_0.png
rename to Platformer2D/Core/Content/Backgrounds/Layer0_0.png
diff --git a/Platformer2D/Platformer2D.Core/Content/Backgrounds/Layer0_1.png b/Platformer2D/Core/Content/Backgrounds/Layer0_1.png
similarity index 100%
rename from Platformer2D/Platformer2D.Core/Content/Backgrounds/Layer0_1.png
rename to Platformer2D/Core/Content/Backgrounds/Layer0_1.png
diff --git a/Platformer2D/Platformer2D.Core/Content/Backgrounds/Layer0_2.png b/Platformer2D/Core/Content/Backgrounds/Layer0_2.png
similarity index 100%
rename from Platformer2D/Platformer2D.Core/Content/Backgrounds/Layer0_2.png
rename to Platformer2D/Core/Content/Backgrounds/Layer0_2.png
diff --git a/Platformer2D/Platformer2D.Core/Content/Backgrounds/Layer1_0.png b/Platformer2D/Core/Content/Backgrounds/Layer1_0.png
similarity index 100%
rename from Platformer2D/Platformer2D.Core/Content/Backgrounds/Layer1_0.png
rename to Platformer2D/Core/Content/Backgrounds/Layer1_0.png
diff --git a/Platformer2D/Platformer2D.Core/Content/Backgrounds/Layer1_1.png b/Platformer2D/Core/Content/Backgrounds/Layer1_1.png
similarity index 100%
rename from Platformer2D/Platformer2D.Core/Content/Backgrounds/Layer1_1.png
rename to Platformer2D/Core/Content/Backgrounds/Layer1_1.png
diff --git a/Platformer2D/Platformer2D.Core/Content/Backgrounds/Layer1_2.png b/Platformer2D/Core/Content/Backgrounds/Layer1_2.png
similarity index 100%
rename from Platformer2D/Platformer2D.Core/Content/Backgrounds/Layer1_2.png
rename to Platformer2D/Core/Content/Backgrounds/Layer1_2.png
diff --git a/Platformer2D/Platformer2D.Core/Content/Backgrounds/Layer2_0.png b/Platformer2D/Core/Content/Backgrounds/Layer2_0.png
similarity index 100%
rename from Platformer2D/Platformer2D.Core/Content/Backgrounds/Layer2_0.png
rename to Platformer2D/Core/Content/Backgrounds/Layer2_0.png
diff --git a/Platformer2D/Platformer2D.Core/Content/Backgrounds/Layer2_1.png b/Platformer2D/Core/Content/Backgrounds/Layer2_1.png
similarity index 100%
rename from Platformer2D/Platformer2D.Core/Content/Backgrounds/Layer2_1.png
rename to Platformer2D/Core/Content/Backgrounds/Layer2_1.png
diff --git a/Platformer2D/Platformer2D.Core/Content/Backgrounds/Layer2_2.png b/Platformer2D/Core/Content/Backgrounds/Layer2_2.png
similarity index 100%
rename from Platformer2D/Platformer2D.Core/Content/Backgrounds/Layer2_2.png
rename to Platformer2D/Core/Content/Backgrounds/Layer2_2.png
diff --git a/Platformer2D/Platformer2D.Core/Content/Fonts/Hud.spritefont b/Platformer2D/Core/Content/Fonts/Hud.spritefont
similarity index 66%
rename from Platformer2D/Platformer2D.Core/Content/Fonts/Hud.spritefont
rename to Platformer2D/Core/Content/Fonts/Hud.spritefont
index aec8567b..5830b928 100644
--- a/Platformer2D/Platformer2D.Core/Content/Fonts/Hud.spritefont
+++ b/Platformer2D/Core/Content/Fonts/Hud.spritefont
@@ -46,14 +46,40 @@ with.
+
- ~
+ þ
+
+
+
+
+
+ ゟ
+
+
+
+
+ ゠
+ ヿ
+
+
+
+
+ 一
+ 龯
+
+
+
+
+ А
+ я
diff --git a/Platformer2D/Platformer2D.Core/Content/Fonts/Roboto-Bold.ttf b/Platformer2D/Core/Content/Fonts/Roboto-Bold.ttf
similarity index 100%
rename from Platformer2D/Platformer2D.Core/Content/Fonts/Roboto-Bold.ttf
rename to Platformer2D/Core/Content/Fonts/Roboto-Bold.ttf
diff --git a/Platformer2D/Core/Content/Icon.bmp b/Platformer2D/Core/Content/Icon.bmp
new file mode 100644
index 00000000..63007cfd
Binary files /dev/null and b/Platformer2D/Core/Content/Icon.bmp differ
diff --git a/Platformer2D/Core/Content/Icon.ico b/Platformer2D/Core/Content/Icon.ico
new file mode 100644
index 00000000..46f8863a
Binary files /dev/null and b/Platformer2D/Core/Content/Icon.ico differ
diff --git a/Platformer2D/Platformer2D.Core/Content/Levels/0.txt b/Platformer2D/Core/Content/Levels/00.txt
similarity index 66%
rename from Platformer2D/Platformer2D.Core/Content/Levels/0.txt
rename to Platformer2D/Core/Content/Levels/00.txt
index 5b60a2ff..05f2f6af 100644
--- a/Platformer2D/Platformer2D.Core/Content/Levels/0.txt
+++ b/Platformer2D/Core/Content/Levels/00.txt
@@ -2,14 +2,14 @@
....................
....................
....................
+...............2....
+..............###...
....................
....................
-....................
-.........GGG........
.........###........
....................
-....GGG.......GGG...
-....###.......###...
+.....1..............
+....#;#.............
....................
-.1................X.
+........P.......X...
####################
diff --git a/Platformer2D/Core/Content/Levels/01.txt b/Platformer2D/Core/Content/Levels/01.txt
new file mode 100644
index 00000000..9cc95495
--- /dev/null
+++ b/Platformer2D/Core/Content/Levels/01.txt
@@ -0,0 +1,15 @@
+....................
+....................
+............X.......
+......########......
+..2.................
+####............####
+....................
+....................
+.......------.......
+...--...........--..
+.1................1.
+#;##............####
+....................
+.P..................
+####################
diff --git a/Platformer2D/Core/Content/Levels/02.txt b/Platformer2D/Core/Content/Levels/02.txt
new file mode 100644
index 00000000..0d6faca3
--- /dev/null
+++ b/Platformer2D/Core/Content/Levels/02.txt
@@ -0,0 +1,15 @@
+........................................
+#..................#....................
+...##1........1##.........########......
+...###2......2###.......##........##....
+..#####3....3#####....##............--..
+...##.##....##.##....#...2..............
+...##..#3.DX#..##....#...#..............
+#..##...####...##..#.#........#######-..
+P..##..........##...#.##..........##...#
+...##..........##.......##1....A##....#.
+..###....121...###........###;###....#..
+........#;##........................#...
+...................................#....
+...4....A.......B.......................
+###########..############..######..#####
diff --git a/Platformer2D/Core/Content/Levels/03.txt b/Platformer2D/Core/Content/Levels/03.txt
new file mode 100644
index 00000000..275c3fa7
--- /dev/null
+++ b/Platformer2D/Core/Content/Levels/03.txt
@@ -0,0 +1,15 @@
+....................
+P...................
+......########......
+....##........##....
+..##............--..
+.#...2..............
+.#...#....X.........
+.#........#######-..
+#.##..........##...#
+....##1....A##....#.
+......###;###....#..
+................#...
+....................
+....................
+####################
diff --git a/Platformer2D/Core/Content/Levels/04.txt b/Platformer2D/Core/Content/Levels/04.txt
new file mode 100644
index 00000000..a2e8fa58
--- /dev/null
+++ b/Platformer2D/Core/Content/Levels/04.txt
@@ -0,0 +1,15 @@
+....................
+......P.....A3......
+.....#########......
+....#.........#2....
+...#.....3.....#1...
+..#.....---.....#...
+..#..B......C...#...
+#.#######;#######..#
+..#.............#...
+..#.............#...
+.-#........X....#-..
+.......#####........
+#.................##
+....B........A......
+####################
diff --git a/Platformer2D/Core/Content/Levels/05.txt b/Platformer2D/Core/Content/Levels/05.txt
new file mode 100644
index 00000000..9d19d3a4
--- /dev/null
+++ b/Platformer2D/Core/Content/Levels/05.txt
@@ -0,0 +1,15 @@
+....................
+#..................#
+...##1........1##...
+...###2......2###...
+..#####3....3#####..
+...##.##....##.##...
+...##..#3.DX#..##...
+#..##...####...##..#
+P..##..........##...
+...##..........##...
+..###....121...###..
+........####........
+....................
+........A.......B...
+####################
diff --git a/Platformer2D/Core/Content/Levels/06.txt b/Platformer2D/Core/Content/Levels/06.txt
new file mode 100644
index 00000000..f0fbf62b
--- /dev/null
+++ b/Platformer2D/Core/Content/Levels/06.txt
@@ -0,0 +1,15 @@
+....................
+........12221.......
+#..##############...
+...##...............
+...##...............
+P..##A..B..3.C..D...
+..###############...
+...##...............
+...##...............
+...##.1..1..1..1....
+#..##############...
+...................X
+...................#
+....B.........C.....
+#########.##########
diff --git a/Platformer2D/Platformer2D.Core/Content/Overlays/you_died.png b/Platformer2D/Core/Content/Overlays/you_died.png
similarity index 100%
rename from Platformer2D/Platformer2D.Core/Content/Overlays/you_died.png
rename to Platformer2D/Core/Content/Overlays/you_died.png
diff --git a/Platformer2D/Platformer2D.Core/Content/Overlays/you_lose.png b/Platformer2D/Core/Content/Overlays/you_lose.png
similarity index 100%
rename from Platformer2D/Platformer2D.Core/Content/Overlays/you_lose.png
rename to Platformer2D/Core/Content/Overlays/you_lose.png
diff --git a/Platformer2D/Platformer2D.Core/Content/Overlays/you_win.png b/Platformer2D/Core/Content/Overlays/you_win.png
similarity index 100%
rename from Platformer2D/Platformer2D.Core/Content/Overlays/you_win.png
rename to Platformer2D/Core/Content/Overlays/you_win.png
diff --git a/Platformer2D/Platformer2D.Core/Content/Platformer2D.mgcb b/Platformer2D/Core/Content/Platformer2D.mgcb
similarity index 83%
rename from Platformer2D/Platformer2D.Core/Content/Platformer2D.mgcb
rename to Platformer2D/Core/Content/Platformer2D.mgcb
index 3a32c404..f8f10d45 100644
--- a/Platformer2D/Platformer2D.Core/Content/Platformer2D.mgcb
+++ b/Platformer2D/Core/Content/Platformer2D.mgcb
@@ -4,7 +4,7 @@
/outputDir:bin/$(Platform)
/intermediateDir:obj/$(Platform)
/platform:DesktopGL
-/config:$(Configuration)
+/config:
/profile:Reach
/compress:False
@@ -128,14 +128,26 @@
/processorParam:TextureFormat=Compressed
/build:Fonts/Hud.spritefont
-#begin Levels/0.txt
-/copy:Levels/0.txt
+#begin Levels/00.txt
+/copy:Levels/00.txt
-#begin Levels/1.txt
-/copy:Levels/1.txt
+#begin Levels/01.txt
+/copy:Levels/01.txt
-#begin Levels/2.txt
-/copy:Levels/2.txt
+#begin Levels/02.txt
+/copy:Levels/02.txt
+
+#begin Levels/03.txt
+/copy:Levels/03.txt
+
+#begin Levels/04.txt
+/copy:Levels/04.txt
+
+#begin Levels/05.txt
+/copy:Levels/05.txt
+
+#begin Levels/06.txt
+/copy:Levels/06.txt
#begin Overlays/you_died.png
/importer:TextureImporter
@@ -173,17 +185,17 @@
/processorParam:TextureFormat=Color
/build:Overlays/you_win.png
-#begin Sounds/ExitReached.wav
+#begin Sounds/PlayerExitReached.wav
/importer:WavImporter
/processor:SoundEffectProcessor
/processorParam:Quality=Best
-/build:Sounds/ExitReached.wav
+/build:Sounds/PlayerExitReached.wav
-#begin Sounds/GemCollected.wav
+#begin Sounds/PlayerGemCollected.wav
/importer:WavImporter
/processor:SoundEffectProcessor
/processorParam:Quality=Best
-/build:Sounds/GemCollected.wav
+/build:Sounds/PlayerGemCollected.wav
#begin Sounds/MonsterKilled.wav
/importer:WavImporter
@@ -191,11 +203,11 @@
/processorParam:Quality=Best
/build:Sounds/MonsterKilled.wav
-#begin Sounds/Music.wma
-/importer:WmaImporter
+#begin Sounds/Music.mp3
+/importer:Mp3Importer
/processor:SongProcessor
/processorParam:Quality=Best
-/build:Sounds/Music.wma
+/build:Sounds/Music.mp3
#begin Sounds/PlayerFall.wav
/importer:WavImporter
@@ -215,11 +227,11 @@
/processorParam:Quality=Best
/build:Sounds/PlayerKilled.wav
-#begin Sounds/Powerup.wav
+#begin Sounds/PlayerPowerUp.wav
/importer:WavImporter
/processor:SoundEffectProcessor
/processorParam:Quality=Best
-/build:Sounds/Powerup.wav
+/build:Sounds/PlayerPowerUp.wav
#begin Sprites/Gem.png
/importer:TextureImporter
@@ -233,6 +245,18 @@
/processorParam:TextureFormat=Color
/build:Sprites/Gem.png
+#begin Sprites/MonsterA/Die.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:Sprites/MonsterA/Die.png
+
#begin Sprites/MonsterA/Idle.png
/importer:TextureImporter
/processor:TextureProcessor
@@ -257,6 +281,18 @@
/processorParam:TextureFormat=Color
/build:Sprites/MonsterA/Run.png
+#begin Sprites/MonsterB/Die.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:Sprites/MonsterB/Die.png
+
#begin Sprites/MonsterB/Idle.png
/importer:TextureImporter
/processor:TextureProcessor
@@ -281,6 +317,18 @@
/processorParam:TextureFormat=Color
/build:Sprites/MonsterB/Run.png
+#begin Sprites/MonsterC/Die.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:Sprites/MonsterC/Die.png
+
#begin Sprites/MonsterC/Idle.png
/importer:TextureImporter
/processor:TextureProcessor
@@ -305,6 +353,18 @@
/processorParam:TextureFormat=Color
/build:Sprites/MonsterC/Run.png
+#begin Sprites/MonsterD/Die.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:Sprites/MonsterD/Die.png
+
#begin Sprites/MonsterD/Idle.png
/importer:TextureImporter
/processor:TextureProcessor
@@ -401,6 +461,42 @@
/processorParam:TextureFormat=Color
/build:Sprites/VirtualControlArrow.png
+#begin Sprites/blank.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:Sprites/blank.png
+
+#begin Sprites/gradient.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:Sprites/gradient.png
+
+#begin Sprites/backpack.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:Sprites/backpack.png
+
#begin Tiles/BlockA0.png
/importer:TextureImporter
/processor:TextureProcessor
diff --git a/Platformer2D/Platformer2D.Core/Content/Sounds/MonsterKilled.wav b/Platformer2D/Core/Content/Sounds/MonsterKilled.wav
similarity index 100%
rename from Platformer2D/Platformer2D.Core/Content/Sounds/MonsterKilled.wav
rename to Platformer2D/Core/Content/Sounds/MonsterKilled.wav
diff --git a/Platformer2D/Core/Content/Sounds/Music.mp3 b/Platformer2D/Core/Content/Sounds/Music.mp3
new file mode 100644
index 00000000..a210bbff
Binary files /dev/null and b/Platformer2D/Core/Content/Sounds/Music.mp3 differ
diff --git a/Platformer2D/Platformer2D.Core/Content/Sounds/ExitReached.wav b/Platformer2D/Core/Content/Sounds/PlayerExitReached.wav
similarity index 100%
rename from Platformer2D/Platformer2D.Core/Content/Sounds/ExitReached.wav
rename to Platformer2D/Core/Content/Sounds/PlayerExitReached.wav
diff --git a/Platformer2D/Platformer2D.Core/Content/Sounds/PlayerFall.wav b/Platformer2D/Core/Content/Sounds/PlayerFall.wav
similarity index 100%
rename from Platformer2D/Platformer2D.Core/Content/Sounds/PlayerFall.wav
rename to Platformer2D/Core/Content/Sounds/PlayerFall.wav
diff --git a/Platformer2D/Platformer2D.Core/Content/Sounds/GemCollected.wav b/Platformer2D/Core/Content/Sounds/PlayerGemCollected.wav
similarity index 100%
rename from Platformer2D/Platformer2D.Core/Content/Sounds/GemCollected.wav
rename to Platformer2D/Core/Content/Sounds/PlayerGemCollected.wav
diff --git a/Platformer2D/Platformer2D.Core/Content/Sounds/PlayerJump.wav b/Platformer2D/Core/Content/Sounds/PlayerJump.wav
similarity index 100%
rename from Platformer2D/Platformer2D.Core/Content/Sounds/PlayerJump.wav
rename to Platformer2D/Core/Content/Sounds/PlayerJump.wav
diff --git a/Platformer2D/Platformer2D.Core/Content/Sounds/PlayerKilled.wav b/Platformer2D/Core/Content/Sounds/PlayerKilled.wav
similarity index 100%
rename from Platformer2D/Platformer2D.Core/Content/Sounds/PlayerKilled.wav
rename to Platformer2D/Core/Content/Sounds/PlayerKilled.wav
diff --git a/Platformer2D/Platformer2D.Core/Content/Sounds/Powerup.wav b/Platformer2D/Core/Content/Sounds/PlayerPowerUp.wav
similarity index 100%
rename from Platformer2D/Platformer2D.Core/Content/Sounds/Powerup.wav
rename to Platformer2D/Core/Content/Sounds/PlayerPowerUp.wav
diff --git a/Platformer2D/Platformer2D.Core/Content/Sprites/Gem.png b/Platformer2D/Core/Content/Sprites/Gem.png
similarity index 100%
rename from Platformer2D/Platformer2D.Core/Content/Sprites/Gem.png
rename to Platformer2D/Core/Content/Sprites/Gem.png
diff --git a/Platformer2D/Core/Content/Sprites/MonsterA/Die.png b/Platformer2D/Core/Content/Sprites/MonsterA/Die.png
new file mode 100644
index 00000000..0beaab95
Binary files /dev/null and b/Platformer2D/Core/Content/Sprites/MonsterA/Die.png differ
diff --git a/Platformer2D/Platformer2D.Core/Content/Sprites/MonsterA/Idle.png b/Platformer2D/Core/Content/Sprites/MonsterA/Idle.png
similarity index 100%
rename from Platformer2D/Platformer2D.Core/Content/Sprites/MonsterA/Idle.png
rename to Platformer2D/Core/Content/Sprites/MonsterA/Idle.png
diff --git a/Platformer2D/Platformer2D.Core/Content/Sprites/MonsterA/Run.png b/Platformer2D/Core/Content/Sprites/MonsterA/Run.png
similarity index 100%
rename from Platformer2D/Platformer2D.Core/Content/Sprites/MonsterA/Run.png
rename to Platformer2D/Core/Content/Sprites/MonsterA/Run.png
diff --git a/Platformer2D/Core/Content/Sprites/MonsterB/Die.png b/Platformer2D/Core/Content/Sprites/MonsterB/Die.png
new file mode 100644
index 00000000..a20411f4
Binary files /dev/null and b/Platformer2D/Core/Content/Sprites/MonsterB/Die.png differ
diff --git a/Platformer2D/Platformer2D.Core/Content/Sprites/MonsterB/Idle.png b/Platformer2D/Core/Content/Sprites/MonsterB/Idle.png
similarity index 100%
rename from Platformer2D/Platformer2D.Core/Content/Sprites/MonsterB/Idle.png
rename to Platformer2D/Core/Content/Sprites/MonsterB/Idle.png
diff --git a/Platformer2D/Platformer2D.Core/Content/Sprites/MonsterB/Run.png b/Platformer2D/Core/Content/Sprites/MonsterB/Run.png
similarity index 100%
rename from Platformer2D/Platformer2D.Core/Content/Sprites/MonsterB/Run.png
rename to Platformer2D/Core/Content/Sprites/MonsterB/Run.png
diff --git a/Platformer2D/Core/Content/Sprites/MonsterC/Die.png b/Platformer2D/Core/Content/Sprites/MonsterC/Die.png
new file mode 100644
index 00000000..1ef04514
Binary files /dev/null and b/Platformer2D/Core/Content/Sprites/MonsterC/Die.png differ
diff --git a/Platformer2D/Platformer2D.Core/Content/Sprites/MonsterC/Idle.png b/Platformer2D/Core/Content/Sprites/MonsterC/Idle.png
similarity index 100%
rename from Platformer2D/Platformer2D.Core/Content/Sprites/MonsterC/Idle.png
rename to Platformer2D/Core/Content/Sprites/MonsterC/Idle.png
diff --git a/Platformer2D/Platformer2D.Core/Content/Sprites/MonsterC/Run.png b/Platformer2D/Core/Content/Sprites/MonsterC/Run.png
similarity index 100%
rename from Platformer2D/Platformer2D.Core/Content/Sprites/MonsterC/Run.png
rename to Platformer2D/Core/Content/Sprites/MonsterC/Run.png
diff --git a/Platformer2D/Core/Content/Sprites/MonsterD/Die.png b/Platformer2D/Core/Content/Sprites/MonsterD/Die.png
new file mode 100644
index 00000000..e69a866c
Binary files /dev/null and b/Platformer2D/Core/Content/Sprites/MonsterD/Die.png differ
diff --git a/Platformer2D/Platformer2D.Core/Content/Sprites/MonsterD/Idle.png b/Platformer2D/Core/Content/Sprites/MonsterD/Idle.png
similarity index 100%
rename from Platformer2D/Platformer2D.Core/Content/Sprites/MonsterD/Idle.png
rename to Platformer2D/Core/Content/Sprites/MonsterD/Idle.png
diff --git a/Platformer2D/Platformer2D.Core/Content/Sprites/MonsterD/Run.png b/Platformer2D/Core/Content/Sprites/MonsterD/Run.png
similarity index 100%
rename from Platformer2D/Platformer2D.Core/Content/Sprites/MonsterD/Run.png
rename to Platformer2D/Core/Content/Sprites/MonsterD/Run.png
diff --git a/Platformer2D/Platformer2D.Core/Content/Sprites/Player/Celebrate.png b/Platformer2D/Core/Content/Sprites/Player/Celebrate.png
similarity index 100%
rename from Platformer2D/Platformer2D.Core/Content/Sprites/Player/Celebrate.png
rename to Platformer2D/Core/Content/Sprites/Player/Celebrate.png
diff --git a/Platformer2D/Platformer2D.Core/Content/Sprites/Player/Die.png b/Platformer2D/Core/Content/Sprites/Player/Die.png
similarity index 100%
rename from Platformer2D/Platformer2D.Core/Content/Sprites/Player/Die.png
rename to Platformer2D/Core/Content/Sprites/Player/Die.png
diff --git a/Platformer2D/Core/Content/Sprites/Player/Idle.png b/Platformer2D/Core/Content/Sprites/Player/Idle.png
new file mode 100644
index 00000000..561473ff
Binary files /dev/null and b/Platformer2D/Core/Content/Sprites/Player/Idle.png differ
diff --git a/Platformer2D/Platformer2D.Core/Content/Sprites/Player/Jump.png b/Platformer2D/Core/Content/Sprites/Player/Jump.png
similarity index 100%
rename from Platformer2D/Platformer2D.Core/Content/Sprites/Player/Jump.png
rename to Platformer2D/Core/Content/Sprites/Player/Jump.png
diff --git a/Platformer2D/Platformer2D.Core/Content/Sprites/Player/Run.png b/Platformer2D/Core/Content/Sprites/Player/Run.png
similarity index 100%
rename from Platformer2D/Platformer2D.Core/Content/Sprites/Player/Run.png
rename to Platformer2D/Core/Content/Sprites/Player/Run.png
diff --git a/Platformer2D/Platformer2D.Core/Content/Sprites/VirtualControlArrow.png b/Platformer2D/Core/Content/Sprites/VirtualControlArrow.png
similarity index 100%
rename from Platformer2D/Platformer2D.Core/Content/Sprites/VirtualControlArrow.png
rename to Platformer2D/Core/Content/Sprites/VirtualControlArrow.png
diff --git a/Platformer2D/Core/Content/Sprites/backpack.png b/Platformer2D/Core/Content/Sprites/backpack.png
new file mode 100644
index 00000000..f2c5a39b
Binary files /dev/null and b/Platformer2D/Core/Content/Sprites/backpack.png differ
diff --git a/Platformer2D/Core/Content/Sprites/blank.png b/Platformer2D/Core/Content/Sprites/blank.png
new file mode 100644
index 00000000..3165c372
Binary files /dev/null and b/Platformer2D/Core/Content/Sprites/blank.png differ
diff --git a/Platformer2D/Core/Content/Sprites/gradient.png b/Platformer2D/Core/Content/Sprites/gradient.png
new file mode 100644
index 00000000..1a5a0f76
Binary files /dev/null and b/Platformer2D/Core/Content/Sprites/gradient.png differ
diff --git a/Platformer2D/Platformer2D.Core/Content/Tiles/BlockA0.png b/Platformer2D/Core/Content/Tiles/BlockA0.png
similarity index 100%
rename from Platformer2D/Platformer2D.Core/Content/Tiles/BlockA0.png
rename to Platformer2D/Core/Content/Tiles/BlockA0.png
diff --git a/Platformer2D/Platformer2D.Core/Content/Tiles/BlockA1.png b/Platformer2D/Core/Content/Tiles/BlockA1.png
similarity index 100%
rename from Platformer2D/Platformer2D.Core/Content/Tiles/BlockA1.png
rename to Platformer2D/Core/Content/Tiles/BlockA1.png
diff --git a/Platformer2D/Platformer2D.Core/Content/Tiles/BlockA2.png b/Platformer2D/Core/Content/Tiles/BlockA2.png
similarity index 100%
rename from Platformer2D/Platformer2D.Core/Content/Tiles/BlockA2.png
rename to Platformer2D/Core/Content/Tiles/BlockA2.png
diff --git a/Platformer2D/Platformer2D.Core/Content/Tiles/BlockA3.png b/Platformer2D/Core/Content/Tiles/BlockA3.png
similarity index 100%
rename from Platformer2D/Platformer2D.Core/Content/Tiles/BlockA3.png
rename to Platformer2D/Core/Content/Tiles/BlockA3.png
diff --git a/Platformer2D/Platformer2D.Core/Content/Tiles/BlockA4.png b/Platformer2D/Core/Content/Tiles/BlockA4.png
similarity index 100%
rename from Platformer2D/Platformer2D.Core/Content/Tiles/BlockA4.png
rename to Platformer2D/Core/Content/Tiles/BlockA4.png
diff --git a/Platformer2D/Platformer2D.Core/Content/Tiles/BlockA5.png b/Platformer2D/Core/Content/Tiles/BlockA5.png
similarity index 100%
rename from Platformer2D/Platformer2D.Core/Content/Tiles/BlockA5.png
rename to Platformer2D/Core/Content/Tiles/BlockA5.png
diff --git a/Platformer2D/Platformer2D.Core/Content/Tiles/BlockA6.png b/Platformer2D/Core/Content/Tiles/BlockA6.png
similarity index 100%
rename from Platformer2D/Platformer2D.Core/Content/Tiles/BlockA6.png
rename to Platformer2D/Core/Content/Tiles/BlockA6.png
diff --git a/Platformer2D/Platformer2D.Core/Content/Tiles/BlockB0.png b/Platformer2D/Core/Content/Tiles/BlockB0.png
similarity index 100%
rename from Platformer2D/Platformer2D.Core/Content/Tiles/BlockB0.png
rename to Platformer2D/Core/Content/Tiles/BlockB0.png
diff --git a/Platformer2D/Platformer2D.Core/Content/Tiles/BlockB1.png b/Platformer2D/Core/Content/Tiles/BlockB1.png
similarity index 100%
rename from Platformer2D/Platformer2D.Core/Content/Tiles/BlockB1.png
rename to Platformer2D/Core/Content/Tiles/BlockB1.png
diff --git a/Platformer2D/Platformer2D.Core/Content/Tiles/Exit.png b/Platformer2D/Core/Content/Tiles/Exit.png
similarity index 100%
rename from Platformer2D/Platformer2D.Core/Content/Tiles/Exit.png
rename to Platformer2D/Core/Content/Tiles/Exit.png
diff --git a/Platformer2D/Platformer2D.Core/Content/Tiles/Platform.png b/Platformer2D/Core/Content/Tiles/Platform.png
similarity index 100%
rename from Platformer2D/Platformer2D.Core/Content/Tiles/Platform.png
rename to Platformer2D/Core/Content/Tiles/Platform.png
diff --git a/Platformer2D/Core/Content/android-icons-generator.sh b/Platformer2D/Core/Content/android-icons-generator.sh
new file mode 100644
index 00000000..8fb09134
--- /dev/null
+++ b/Platformer2D/Core/Content/android-icons-generator.sh
@@ -0,0 +1,33 @@
+#!/bin/zsh
+
+# Declare a constant variables
+readonly resources_base_path="../../Platformer2D.Android/Resources"
+readonly drawable_resources_path="$resources_base_path/drawable-"
+readonly mdpi="mdpi"
+readonly hdpi="hdpi"
+readonly xhdpi="xhdpi"
+readonly xxhdpi="xxhdpi"
+readonly xxxhdpi="xxxhdpi"
+
+echo "Generating Android icons"
+
+echo "Generating Android splash screens"
+mkdir -p "$drawable_resources_path$mdpi"
+mkdir -p "$drawable_resources_path$hdpi"
+mkdir -p "$drawable_resources_path$xhdpi"
+mkdir -p "$drawable_resources_path$xxhdpi"
+mkdir -p "$drawable_resources_path$xxxhdpi"
+
+sips -Z 48 icon-1024.png -o "$drawable_resources_path$mdpi/icon.png"
+sips -Z 72 icon-1024.png -o "$drawable_resources_path$hdpi/icon.png"
+sips -Z 96 icon-1024.png -o "$drawable_resources_path$xhdpi/icon.png"
+sips -Z 144 icon-1024.png -o "$drawable_resources_path$xxhdpi/icon.png"
+sips -Z 192 icon-1024.png -o "$drawable_resources_path$xxxhdpi/icon.png"
+
+sips -Z 470 splash.png -o "$drawable_resources_path$mdpi/splash.png"
+sips -Z 640 splash.png -o "$drawable_resources_path$hdpi/splash.png"
+sips -Z 960 splash.png -o "$drawable_resources_path$xhdpi/splash.png"
+sips -Z 1440 splash.png -o "$drawable_resources_path$xxhdpi/splash.png"
+sips -Z 1920 splash.png -o "$drawable_resources_path$xxxhdpi/splash.png"
+
+echo "Android Generation Complete!"
\ No newline at end of file
diff --git a/Platformer2D/Core/Content/icon-1024.png b/Platformer2D/Core/Content/icon-1024.png
new file mode 100644
index 00000000..99f7aee2
Binary files /dev/null and b/Platformer2D/Core/Content/icon-1024.png differ
diff --git a/Platformer2D/Core/Content/ios-icons-generator.sh b/Platformer2D/Core/Content/ios-icons-generator.sh
new file mode 100644
index 00000000..7c1ec1c4
--- /dev/null
+++ b/Platformer2D/Core/Content/ios-icons-generator.sh
@@ -0,0 +1,174 @@
+#!/bin/zsh
+
+# Declare a constant variables
+readonly top_level_path="../../Platformer2D.iOS/AppIcon.xcassets"
+readonly xcassets_path="$top_level_path/AppIcon.appiconset"
+
+while true; do
+ # Prompt for user confirmation
+ echo -n "This script will delete your '$xcassets_path' directory and recreate all the assets again. Are you sure you wish to proceed? [(y)es/(n)o]: "
+ read confirm
+
+ # Check the user's response
+ if [[ "${confirm:l}" == "y" || "${confirm:l}" == "yes" ]]; then
+ # Check if the directory exists
+ if [[ -d "$top_level_path" ]]; then
+ # Top level directory exists, delete it
+ rm -rf "$top_level_path"
+ echo "'$top_level_path' directory deleted successfully."
+ else
+ echo "'$top_level_path' directory does not exist. Continuing."
+ fi
+ break
+ elif [[ "${confirm:l}" == "n" || "${confirm:l}" == "no" ]]; then
+ echo "Deletion canceled."
+ exit 0
+ else
+ echo "Invalid input. Please enter 'y'/'yes' or 'n'/'no'."
+ fi
+done
+
+echo "iOS Icon Generation Started!"
+
+echo "Creating $xcassets_path directory"
+mkdir -p "$xcassets_path"
+
+# Generate the required icon sizes
+echo "Generating iOS icons"
+sips -Z 20 icon-1024.png -o "$xcassets_path/icon_20x20.png"
+sips -Z 29 icon-1024.png -o "$xcassets_path/icon_29x29.png"
+sips -Z 40 icon-1024.png -o "$xcassets_path/icon_40x40.png"
+sips -Z 58 icon-1024.png -o "$xcassets_path/icon_58x58.png"
+sips -Z 60 icon-1024.png -o "$xcassets_path/icon_60x60.png"
+sips -Z 76 icon-1024.png -o "$xcassets_path/icon_76x76.png"
+sips -Z 80 icon-1024.png -o "$xcassets_path/icon_80x80.png"
+sips -Z 87 icon-1024.png -o "$xcassets_path/icon_87x87.png"
+sips -Z 120 icon-1024.png -o "$xcassets_path/icon_120x120.png"
+sips -Z 152 icon-1024.png -o "$xcassets_path/icon_152x152.png"
+sips -Z 167 icon-1024.png -o "$xcassets_path/icon_167x167.png"
+sips -Z 180 icon-1024.png -o "$xcassets_path/icon_180x180.png"
+# yes I know it's the same size
+sips -Z 1024 icon-1024.png -o "$xcassets_path/icon_1024x1024.png"
+
+# Create the Contents.json file
+echo "Generating Contents.json file"
+cat > "$xcassets_path/Contents.json" < "$xcassets_path/Contents.json" <
+
+ net9.0
+ AnyCPU
+ Platformer2D.Core
+
+
+
+ All
+
+
+
\ No newline at end of file
diff --git a/Platformer2D/Core/Effects/Particle.cs b/Platformer2D/Core/Effects/Particle.cs
new file mode 100644
index 00000000..ae63b353
--- /dev/null
+++ b/Platformer2D/Core/Effects/Particle.cs
@@ -0,0 +1,133 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace Platformer2D.Core.Effects;
+
+//
+/// The data for a single particle in this game's particle systems.
+///
+public class Particle
+{
+ ///
+ /// The amount of drag applied to velocity per second,
+ /// Help simulate some air resistance or even gravity
+ ///
+ public float DragPerSecond = 0.9f;
+
+ ///
+ /// The current colour of the particle.
+ ///
+ public Color Color;
+
+ ///
+ /// The direction that this particle is moving in.
+ ///
+ public Vector2 Direction;
+
+ ///
+ /// The total lifetime of the particle.
+ ///
+ public float LifeTime;
+
+ ///
+ /// As LifeTime itself changes over time
+ ///
+ public float InitialLifeTime;
+
+ ///
+ /// The position of the particle.
+ ///
+ public Vector2 Position;
+
+ ///
+ /// The previous position of the particle.
+ ///
+ public Vector2 PreviousPosition;
+
+ ///
+ /// How much to scale the particle.
+ ///
+ public float Scale;
+
+ ///
+ /// The length of the lines drawn for each particle.
+ ///
+ public float TailLength;
+
+ ///
+ /// The velocity vector of particle.
+ ///
+ Vector2 Velocity;
+
+
+ ///
+ /// Check if the particle is still alive
+ ///
+ public bool IsAlive => LifeTime > 0;
+
+
+ ///
+ /// Triggered when the particle "dies"
+ /// Be careful or circular referencing emitters or you'll have endless particles.
+ ///
+ public event Action OnDeath;
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public Particle(Vector2 position, Vector2 direction, float speed, float lifeTime, Color color, float scale, float tailLength = 0f)
+ {
+ Position = position;
+ PreviousPosition = position;
+ Velocity = direction * speed;
+ LifeTime = lifeTime;
+ InitialLifeTime = lifeTime;
+ Color = color;
+ Scale = scale;
+ TailLength = tailLength;
+ }
+
+ ///
+ /// Updates the particle's state based on elapsed game time.
+ /// Handles position updates, velocity adjustments, and lifetime reduction.
+ ///
+ /// Provides timing values from the game loop.
+ public void Update(GameTime gameTime)
+ {
+ // Get elapsed time in seconds
+ var elapsedTime = (float)gameTime.ElapsedGameTime.TotalSeconds;
+
+ // Avoid further processing if time is frozen (e.g., game paused)
+ if (elapsedTime <= 0)
+ return;
+
+ // Store the previous position before updating
+ PreviousPosition = Position;
+
+ // Update particle's position based on velocity and elapsed time
+ Position += Velocity * elapsedTime;
+
+ // Apply drag to reduce velocity over time (simulates air resistance)
+ float dragFactor = Math.Max(1 - (elapsedTime * DragPerSecond), 0);
+ Velocity *= dragFactor;
+
+ // Reduce the particle's remaining lifespan
+ LifeTime -= elapsedTime;
+
+ // Gradually fade the particle's color based on remaining life
+ Color.A = (byte)(255f * LifeTime / InitialLifeTime);
+
+ // Trigger death event if the particle's lifespan has expired
+ if (!IsAlive)
+ {
+ OnDeath?.Invoke(Position);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Platformer2D/Core/Effects/ParticleEffectType.cs b/Platformer2D/Core/Effects/ParticleEffectType.cs
new file mode 100644
index 00000000..24f55eb9
--- /dev/null
+++ b/Platformer2D/Core/Effects/ParticleEffectType.cs
@@ -0,0 +1,12 @@
+namespace Platformer2D.Core.Effects;
+
+///
+/// Enum describes the type of particle effects we support.
+///
+public enum ParticleEffectType
+{
+ Confetti,
+ Explosions,
+ Fireworks,
+ Sparkles,
+}
\ No newline at end of file
diff --git a/Platformer2D/Core/Effects/ParticleManager.cs b/Platformer2D/Core/Effects/ParticleManager.cs
new file mode 100644
index 00000000..7cfb35af
--- /dev/null
+++ b/Platformer2D/Core/Effects/ParticleManager.cs
@@ -0,0 +1,337 @@
+using System;
+using System.Collections.Generic;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace Platformer2D.Core.Effects;
+
+public class ParticleManager
+{
+ private Random random;
+
+ private Vector2 position;
+ ///
+ /// Position where these particles eminate from
+ ///
+ public Vector2 Position
+ {
+ get => position;
+ set => position = value;
+ }
+
+ private Vector2 textureOrigin;
+ private Texture2D texture;
+ ///
+ /// Texture to be used for this set of particles
+ ///
+ public Texture2D Texture
+ {
+ get => texture;
+ set => texture = value;
+ }
+
+ private List particles;
+ ///
+ /// How many particles still left to be shown
+ ///
+ public int ParticleCount => particles != null ? particles.Count : 0;
+
+ private bool hasFinishedEmitting;
+ ///
+ /// Indicates whether all particles have finished
+ ///
+ public bool Finished => hasFinishedEmitting && ParticleCount == 0;
+
+ ///
+ /// ParticleManager constructor
+ ///
+ ///
+ ///
+ public ParticleManager(Texture2D texture, Vector2 position)
+ {
+ this.particles = new List();
+ this.random = new Random();
+ this.texture = texture;
+ this.textureOrigin = new Vector2(texture.Width / 2, texture.Height / 2);
+ this.position = position;
+ }
+
+ ///
+ /// Emit built-in particles based on the effect type
+ ///
+ ///
+ ///
+ ///
+ public void Emit(int numberOfParticles, ParticleEffectType effectType, Color? color = null)
+ {
+ hasFinishedEmitting = false;
+
+ switch (effectType)
+ {
+ case ParticleEffectType.Confetti:
+ EmitConfetti(numberOfParticles, position, color);
+ break;
+ case ParticleEffectType.Explosions:
+ EmitExplosions(numberOfParticles, position, color);
+ break;
+ case ParticleEffectType.Fireworks:
+ EmitFireworks(numberOfParticles, position, color);
+ break;
+ case ParticleEffectType.Sparkles:
+ EmitSparkles(numberOfParticles, position, color);
+ break;
+ }
+
+ // Assume no more particles will be emitted unless explicitly called again
+ hasFinishedEmitting = true;
+ }
+
+ ///
+ /// Emit particles for Confetti effect
+ ///
+ ///
+ ///
+ ///
+ private void EmitConfetti(int numberOfParticles, Vector2 emitPosition, Color? color = null)
+ {
+ for (int i = 0; i < numberOfParticles; i++)
+ {
+ // Generate a random direction vector
+ Vector2 randomDirection = new Vector2(
+ (float)(random.NextDouble() * 2 - 1), // X component in range [-1, 1]
+ (float)random.NextDouble() // Y component in range [0, 1]
+ );
+
+ // Normalize the direction vector
+ randomDirection.Normalize();
+
+ // Generate a random speed in a reasonable range
+ float speed = (float)random.NextDouble() * 200 + 50; // Speed between 50 and 250
+
+ Vector2 velocity = new Vector2((float)(random.NextDouble() * 2 - 1), (float)random.NextDouble()) * 200;
+ float lifetime = (float)random.NextDouble() * 3f + 1f;
+
+ // Determine the particle's color
+ Color actualParticleColor = color ?? new Color(random.Next(256), random.Next(256), random.Next(256)); // Bright colors for confetti
+
+ float scale = (float)random.NextDouble() * 0.5f + 0.3f;
+
+ var particle = new Particle(emitPosition, randomDirection, speed, lifetime, actualParticleColor, scale);
+ particles.Add(particle);
+ }
+ }
+
+ ///
+ /// Emit particles for Explosions effect
+ ///
+ ///
+ ///
+ ///
+ private void EmitExplosions(int numberOfParticles, Vector2 emitPosition, Color? color = null)
+ {
+ for (int i = 0; i < numberOfParticles; i++)
+ {
+ // Calculate a random direction for the explosion
+ float angle = (float)(random.NextDouble() * Math.PI * 2); // Random angle in radians
+ Vector2 direction = new Vector2(
+ (float)Math.Cos(angle),
+ (float)Math.Sin(angle)
+ );
+
+ // Generate a random speed for explosive velocity
+ float speed = (float)(random.NextDouble() * 300 + 100); // Speed between 100 and 400
+
+ // Generate a random lifetime
+ float lifetime = (float)random.NextDouble() * 1.5f + 0.5f; // Lifetime between 0.5 and 2 seconds
+
+ // Determine the particle's color
+ Color actualParticleColor = color ?? new Color(
+ random.Next(200, 256), // High red
+ random.Next(100, 200), // Medium green
+ random.Next(0, 100) // Low blue
+ );
+
+ // Generate a random scale for the particle
+ float scale = (float)random.NextDouble() * 0.5f + 0.2f;
+
+ // Create the particle give it a tail
+ var particle = new Particle(emitPosition, direction, speed, lifetime, actualParticleColor, scale, 10);
+
+ // Add the particle to the collection
+ particles.Add(particle);
+ }
+ }
+
+ ///
+ /// Emit particles for Fireworks effect
+ ///
+ ///
+ ///
+ ///
+ private void EmitFireworks(int numberOfParticles, Vector2 emitPosition, Color? color = null)
+ {
+ for (int i = 0; i < numberOfParticles; i++)
+ {
+ // Generate a random angle for each particle
+ float angle = (float)(random.NextDouble() * Math.PI * 2); // Full 360 degrees in radians
+
+ // Create a unit direction vector based on the angle
+ Vector2 direction = new Vector2(
+ (float)Math.Cos(angle),
+ (float)Math.Sin(angle)
+ );
+
+ // Assign a random speed for explosive effect
+ float speed = (float)random.NextDouble() * 300 + 100; // Speed between 100 and 400
+
+ // Generate a random lifetime for the particle
+ float lifetime = (float)random.NextDouble() * 2f + 1f; // Lifetime between 1 and 3 seconds
+
+ // Assign a color to the particle
+ Color actualParticleColor = color ?? new Color(
+ random.Next(256), // Random red component
+ random.Next(256), // Random green component
+ random.Next(256) // Random blue component
+ );
+
+ // Assign a random scale for each particle
+ float scale = (float)random.NextDouble() * 0.5f + 0.5f;
+
+ // Create the particle with the direction and speed
+ var particle = new Particle(emitPosition, direction, speed, lifetime, actualParticleColor, scale);
+
+ // Attach an event to trigger additional effects on particle death
+ particle.OnDeath += FireworkParticle_OnDeath;
+
+ // Add the particle to the collection
+ particles.Add(particle);
+ }
+ }
+
+ ///
+ /// Emit particles for Sparkles effect
+ ///
+ ///
+ ///
+ ///
+ private void EmitSparkles(int numberOfParticles, Vector2 emitPosition, Color? color = null)
+ {
+ for (int i = 0; i < numberOfParticles; i++)
+ {
+ // Calculate a random direction for the sparkles
+ float angle = (float)(random.NextDouble() * Math.PI * 2); // Random angle in radians
+ Vector2 direction = new Vector2(
+ (float)Math.Cos(angle),
+ (float)Math.Sin(angle)
+ );
+
+ // Generate a random speed for the sparkle
+ float speed = (float)(random.NextDouble() * 300); // Speed between 0 and 300
+
+ // Generate a random lifetime
+ float lifetime = (float)random.NextDouble() * 1f + 0.5f; // Lifetime between 0.5 and 1.5 seconds
+
+ // Determine the particle's color
+ Color actualParticleColor = color ?? Color.White * ((float)random.NextDouble() * 0.5f + 0.5f); // Light sparkly effect
+
+ // Generate a random scale for the particle
+ float scale = (float)random.NextDouble() * 0.5f + 0.2f;
+
+ // Create the particle using the new constructor
+ var particle = new Particle(emitPosition, direction, speed, lifetime, actualParticleColor, scale);
+
+ // Add the particle to the collection
+ particles.Add(particle);
+ }
+ }
+
+ ///
+ /// Event fireed when the Fireworks particle dies
+ ///
+ ///
+ private void FireworkParticle_OnDeath(Vector2 particlePosition)
+ {
+ EmitExplosions(5, particlePosition);
+ }
+
+ ///
+ /// Update each Particle that is still alive
+ ///
+ ///
+ public void Update(GameTime gameTime)
+ {
+ for (int i = particles.Count - 1; i >= 0; i--)
+ {
+ particles[i].Update(gameTime);
+
+ if (!particles[i].IsAlive)
+ {
+ particles.RemoveAt(i);
+ }
+ }
+ }
+
+ ///
+ /// Controls the "density" of the tail
+ /// Dense Tail (t += 1f): A continuous, almost solid-looking trail.Ideal for effects like glowing streaks.
+ /// Sparse Tail (t += 10f): A dotted, fragmented appearance.Useful for effects like spark trails or light debris.
+ ///
+ const float tailDensity = 5f;
+
+ ///
+ /// Draws all active particles and their corresponding tails.
+ ///
+ /// The SpriteBatch used to draw the particles.
+ public void Draw(SpriteBatch spriteBatch)
+ {
+ foreach (Particle particle in particles)
+ {
+ // Only draw particles that are still active
+ if (particle.IsAlive)
+ {
+ // Calculate the direction and length of the particle's tail
+ Vector2 tailDirection = particle.Position - particle.PreviousPosition;
+ float tailLength = particle.TailLength * tailDirection.Length();
+
+ // Normalize the tail direction vector to ensure consistent movement scaling
+ if (tailDirection != Vector2.Zero)
+ tailDirection.Normalize();
+
+ // Draw the main particle
+ spriteBatch.Draw(
+ texture, // Particle texture
+ particle.Position, // Particle position
+ null, // No source rectangle (draw full texture)
+ particle.Color, // Particle's color
+ 0.0f, // No rotation
+ textureOrigin, // Origin for positioning
+ particle.Scale, // Scale factor for particle size
+ SpriteEffects.None, // No flipping
+ 0f); // Draw layer depth
+
+ // Draw the particle's tail in segments to simulate a fading trail
+ for (float t = 0; t < tailLength; t += tailDensity)
+ {
+ // Calculate the position of the tail segment
+ Vector2 tailPosition = particle.Position - tailDirection * t;
+
+ // Fade each tail segment from fully opaque to fully transparent
+ float alpha = MathHelper.Clamp(1f - (t / tailLength), 0f, 1f);
+ Color tailColor = particle.Color * alpha;
+
+ // Draw the tail segment with a slightly smaller scale
+ spriteBatch.Draw(
+ texture,
+ tailPosition,
+ null,
+ tailColor,
+ 0f,
+ textureOrigin,
+ particle.Scale * 0.8f, // Tail segments are slightly smaller than the main particle
+ SpriteEffects.None,
+ 0f);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Platformer2D/Platformer2D.Core/Game/Animation.cs b/Platformer2D/Core/Game/Animation.cs
similarity index 79%
rename from Platformer2D/Platformer2D.Core/Game/Animation.cs
rename to Platformer2D/Core/Game/Animation.cs
index 342d4577..d4e62619 100644
--- a/Platformer2D/Platformer2D.Core/Game/Animation.cs
+++ b/Platformer2D/Core/Game/Animation.cs
@@ -1,16 +1,6 @@
-#region File Description
-//-----------------------------------------------------------------------------
-// Animation.cs
-//
-// Microsoft XNA Community Game Platform
-// Copyright (C) Microsoft Corporation. All rights reserved.
-//-----------------------------------------------------------------------------
-#endregion
+using Microsoft.Xna.Framework.Graphics;
-using System;
-using Microsoft.Xna.Framework.Graphics;
-
-namespace Platformer2D
+namespace Platformer2D.Core
{
///
/// Represents an animated texture.
@@ -77,8 +67,11 @@ public int FrameHeight
}
///
- /// Constructors a new animation.
- ///
+ /// Constructs a new animation with the specified texture, frame duration, and looping behavior.
+ ///
+ /// The texture containing the animation frames.
+ /// The duration (in seconds) each frame should be displayed.
+ /// Indicates whether the animation should loop continuously.
public Animation(Texture2D texture, float frameTime, bool isLooping)
{
this.texture = texture;
diff --git a/Platformer2D/Core/Game/AnimationPlayer.cs b/Platformer2D/Core/Game/AnimationPlayer.cs
new file mode 100644
index 00000000..7a34c832
--- /dev/null
+++ b/Platformer2D/Core/Game/AnimationPlayer.cs
@@ -0,0 +1,118 @@
+using System;
+using Platformer2D.Core.Localization;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace Platformer2D.Core
+{
+ ///
+ /// Controls playback of an Animation.
+ ///
+ struct AnimationPlayer
+ {
+ ///
+ /// Gets the animation which is currently playing.
+ ///
+ public Animation Animation
+ {
+ get { return animation; }
+ }
+ Animation animation;
+
+ ///
+ /// Gets the index of the current frame in the animation.
+ ///
+ public int FrameIndex
+ {
+ get { return frameIndex; }
+ }
+ int frameIndex;
+
+ ///
+ /// The amount of time in seconds that the current frame has been shown for.
+ ///
+ private float time;
+
+ ///
+ /// Gets a texture origin at the bottom center of each frame.
+ ///
+ public Vector2 Origin
+ {
+ get { return new Vector2(Animation.FrameWidth / 2.0f, Animation.FrameHeight); }
+ }
+
+ ///
+ /// Begins or continues playback of an animation.
+ ///
+ public void PlayAnimation(Animation animation)
+ {
+ // If this animation is already running, do not restart it.
+ if (Animation == animation)
+ return;
+
+ // Start the new animation.
+ this.animation = animation;
+ this.frameIndex = 0;
+ this.time = 0.0f;
+ }
+
+ ///
+ /// Draws the current frame of the animation at the specified position using the default color (white).
+ ///
+ /// Provides the elapsed game time for animation timing.
+ /// The SpriteBatch used to draw the animation.
+ /// The screen position where the animation should be drawn.
+ /// Specifies effects to apply (e.g., flip horizontally).
+ public void Draw(GameTime gameTime, SpriteBatch spriteBatch, Vector2 position, SpriteEffects spriteEffects)
+ {
+ Draw(gameTime, spriteBatch, position, spriteEffects, Color.White);
+ }
+
+ ///
+ /// Advances the animation's time position and draws the current frame at the specified position.
+ ///
+ /// Provides the elapsed game time for animation timing.
+ /// The SpriteBatch used to draw the animation.
+ /// The screen position where the animation should be drawn.
+ /// Specifies effects to apply (e.g., flip horizontally).
+ /// The color to tint the animation. Use for no tint.
+ ///
+ /// Thrown if no animation is set in the Animation property.
+ ///
+ public void Draw(GameTime gameTime, SpriteBatch spriteBatch, Vector2 position, SpriteEffects spriteEffects, Color color)
+ {
+ if (Animation == null)
+ throw new NotSupportedException(Resources.ErrorNoAnimation);
+
+ // Process the elapsed time to advance the animation
+ time += (float)gameTime.ElapsedGameTime.TotalSeconds;
+
+ // Advance the frame if enough time has passed
+ while (time > Animation.FrameTime)
+ {
+ time -= Animation.FrameTime;
+
+ // Loop the animation or clamp to the final frame
+ if (Animation.IsLooping)
+ {
+ frameIndex = (frameIndex + 1) % Animation.FrameCount;
+ }
+ else
+ {
+ frameIndex = Math.Min(frameIndex + 1, Animation.FrameCount - 1);
+ }
+ }
+
+ // Determine the portion of the texture representing the current frame
+ Rectangle source = new Rectangle(
+ frameIndex * Animation.Texture.Height, // Horizontal offset per frame
+ 0, // Top edge of the frame
+ Animation.Texture.Height, // Frame width (assuming square frames)
+ Animation.Texture.Height // Frame height
+ );
+
+ // Draw the current frame at the specified position
+ spriteBatch.Draw(Animation.Texture, position, source, color, 0.0f, Origin, 1.0f, spriteEffects, 0.0f);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Platformer2D/Core/Game/Circle.cs b/Platformer2D/Core/Game/Circle.cs
new file mode 100644
index 00000000..487bf714
--- /dev/null
+++ b/Platformer2D/Core/Game/Circle.cs
@@ -0,0 +1,57 @@
+using Microsoft.Xna.Framework;
+
+namespace Platformer2D.Core
+{
+ ///
+ /// Represents a 2D circle.
+ ///
+ struct Circle
+ {
+ ///
+ /// Center position of the circle.
+ ///
+ public Vector2 Center;
+
+ ///
+ /// Radius of the circle.
+ ///
+ public float Radius;
+
+ ///
+ /// Constructs a new circle.
+ ///
+ /// The Vector2 position which will the center of the circle.
+ /// The radius of the circle.
+ public Circle(Vector2 position, float radius)
+ {
+ Center = position;
+ Radius = radius;
+ }
+
+ ///
+ /// Determines if this circle intersects with the specified rectangle.
+ ///
+ /// The rectangle to test for intersection.
+ ///
+ /// true if the circle and rectangle overlap; otherwise, false.
+ ///
+ public bool Intersects(Rectangle rectangle)
+ {
+ // Find the closest point on the rectangle to the circle's center.
+ // This ensures the point lies within the rectangle's bounds.
+ Vector2 closestPoint = new Vector2(
+ MathHelper.Clamp(Center.X, rectangle.Left, rectangle.Right),
+ MathHelper.Clamp(Center.Y, rectangle.Top, rectangle.Bottom)
+ );
+
+ // Calculate the vector from the circle's center to the closest point
+ Vector2 direction = Center - closestPoint;
+
+ // Calculate the squared distance (avoids the costlier square root operation)
+ float distanceSquared = direction.LengthSquared();
+
+ // The circle and rectangle intersect if this distance is less than the circle's radius squared.
+ return ((distanceSquared > 0) && (distanceSquared < Radius * Radius));
+ }
+ }
+}
\ No newline at end of file
diff --git a/Platformer2D/Platformer2D.Core/Game/Enemy.cs b/Platformer2D/Core/Game/Enemy.cs
similarity index 71%
rename from Platformer2D/Platformer2D.Core/Game/Enemy.cs
rename to Platformer2D/Core/Game/Enemy.cs
index 590c58a0..2e726978 100644
--- a/Platformer2D/Platformer2D.Core/Game/Enemy.cs
+++ b/Platformer2D/Core/Game/Enemy.cs
@@ -1,38 +1,31 @@
-#region File Description
-//-----------------------------------------------------------------------------
-// Enemy.cs
-//
-// Microsoft XNA Community Game Platform
-// Copyright (C) Microsoft Corporation. All rights reserved.
-//-----------------------------------------------------------------------------
-#endregion
-
-using System;
+using System;
using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Graphics;
-namespace Platformer2D
+namespace Platformer2D.Core
{
///
- /// Facing direction along the X axis.
- ///
- enum FaceDirection
- {
- Left = -1,
- Right = 1,
- }
-
- ///
- /// A monster who is impeding the progress of our fearless adventurer.
+ /// An enemy who is impeding the progress of our fearless adventurer.
///
class Enemy
{
+ bool isAlive = true;
+ ///
+ /// Gets a value indicating whether the enemy is currently alive.
+ ///
+ public bool IsAlive => isAlive;
+
+ Level level;
+ ///
+ /// Gets the level instance to which this enemy belongs.
+ ///
public Level Level
{
get { return level; }
}
- Level level;
+ Vector2 position;
///
/// Position in world space of the bottom center of this enemy.
///
@@ -40,7 +33,6 @@ public Vector2 Position
{
get { return position; }
}
- Vector2 position;
private Rectangle localBounds;
///
@@ -60,8 +52,12 @@ public Rectangle BoundingRectangle
// Animations
private Animation runAnimation;
private Animation idleAnimation;
+ private Animation dieAnimation;
private AnimationPlayer sprite;
+ // Sounds
+ private SoundEffect killedSound;
+
///
/// The direction this enemy is facing and moving along the X axis.
///
@@ -85,6 +81,9 @@ public Rectangle BoundingRectangle
///
/// Constructs a new Enemy.
///
+ /// The level instance to which this enemy belongs.
+ /// The initial position of the enemy in world space.
+ /// The name of the sprite set to load for this enemy.
public Enemy(Level level, Vector2 position, string spriteSet)
{
this.level = level;
@@ -96,14 +95,19 @@ public Enemy(Level level, Vector2 position, string spriteSet)
///
/// Loads a particular enemy sprite sheet and sounds.
///
+ /// The name of the sprite set to load for this enemy.
public void LoadContent(string spriteSet)
{
// Load animations.
spriteSet = "Sprites/" + spriteSet + "/";
runAnimation = new Animation(Level.Content.Load(spriteSet + "Run"), 0.1f, true);
idleAnimation = new Animation(Level.Content.Load(spriteSet + "Idle"), 0.15f, true);
+ dieAnimation = new Animation(Level.Content.Load(spriteSet + "Die"), 0.07f, false);
sprite.PlayAnimation(idleAnimation);
+ // Load sounds.
+ killedSound = Level.Content.Load("Sounds/MonsterKilled");
+
// Calculate bounds within texture size.
int width = (int)(idleAnimation.FrameWidth * 0.35);
int left = (idleAnimation.FrameWidth - width) / 2;
@@ -112,12 +116,15 @@ public void LoadContent(string spriteSet)
localBounds = new Rectangle(left, top, width, height);
}
-
///
/// Paces back and forth along a platform, waiting at either end.
///
+ /// Provides a snapshot of timing values.
public void Update(GameTime gameTime)
{
+ if (!isAlive)
+ return;
+
float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds;
// Calculate tile position based on the side we are walking towards.
@@ -128,7 +135,7 @@ public void Update(GameTime gameTime)
if (waitTime > 0)
{
// Wait for some amount of time.
- waitTime = Math.Max(0.0f, waitTime - (float)gameTime.ElapsedGameTime.TotalSeconds);
+ waitTime = Math.Max(0.0f, waitTime - elapsed);
if (waitTime <= 0.0f)
{
// Then turn around.
@@ -143,7 +150,7 @@ public void Update(GameTime gameTime)
{
waitTime = MaxWaitTime;
}
- else
+ else if (!Level.Paused)
{
// Move in the current direction.
Vector2 velocity = new Vector2((int)direction * MoveSpeed * elapsed, 0.0f);
@@ -155,12 +162,19 @@ public void Update(GameTime gameTime)
///
/// Draws the animated enemy.
///
+ /// Provides a snapshot of timing values.
+ /// The SpriteBatch used to draw the enemy.
public void Draw(GameTime gameTime, SpriteBatch spriteBatch)
{
+ if (!isAlive)
+ {
+ sprite.PlayAnimation(dieAnimation);
+ }
// Stop running when the game is paused or before turning around.
- if (!Level.Player.IsAlive ||
+ else if (!Level.Player.IsAlive ||
Level.ReachedExit ||
- Level.TimeRemaining == TimeSpan.Zero ||
+ Level.TimeTaken == Level.MaximumTimeToCompleteLevel ||
+ Level.Paused ||
waitTime > 0)
{
sprite.PlayAnimation(idleAnimation);
@@ -170,10 +184,19 @@ public void Draw(GameTime gameTime, SpriteBatch spriteBatch)
sprite.PlayAnimation(runAnimation);
}
-
// Draw facing the way the enemy is moving.
SpriteEffects flip = direction > 0 ? SpriteEffects.FlipHorizontally : SpriteEffects.None;
sprite.Draw(gameTime, spriteBatch, Position, flip);
}
+
+ ///
+ /// Handles the enemy being killed by the player.
+ ///
+ /// The player who killed the enemy.
+ public void OnKilled(Player killedBy)
+ {
+ isAlive = false;
+ killedSound.Play();
+ }
}
}
\ No newline at end of file
diff --git a/Platformer2D/Core/Game/FaceDirection.cs b/Platformer2D/Core/Game/FaceDirection.cs
new file mode 100644
index 00000000..f7cc32be
--- /dev/null
+++ b/Platformer2D/Core/Game/FaceDirection.cs
@@ -0,0 +1,11 @@
+namespace Platformer2D.Core
+{
+ ///
+ /// Facing direction along the X axis.
+ ///
+ enum FaceDirection
+ {
+ Left = -1,
+ Right = 1,
+ }
+}
\ No newline at end of file
diff --git a/Platformer2D/Core/Game/Gem.cs b/Platformer2D/Core/Game/Gem.cs
new file mode 100644
index 00000000..4a7f333f
--- /dev/null
+++ b/Platformer2D/Core/Game/Gem.cs
@@ -0,0 +1,206 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Audio;
+using Microsoft.Xna.Framework.Graphics;
+using System;
+
+namespace Platformer2D.Core
+{
+ ///
+ /// A valuable item the player can collect.
+ ///
+ class Gem
+ {
+ private Texture2D texture;
+ private Vector2 origin;
+ private SoundEffect collectedSound;
+
+ ///
+ /// The point value of this gem when collected by the player.
+ ///
+ public int Value = 10;
+
+ ///
+ /// The color of this gem, which can be used for visual distinction.
+ ///
+ public readonly Color Color = Color.Green;
+
+ // The gem is animated from a base position along the Y axis.
+ private Vector2 basePosition;
+ private Vector2 levelDimensions;
+ private float bounce;
+
+ // Bounce control constants
+ const float BounceHeight = 0.18f;
+ const float BounceRate = 3.0f;
+ const float BounceSync = -0.75f;
+
+ ///
+ /// Gets the level instance to which this gem belongs.
+ ///
+ public Level Level
+ {
+ get { return level; }
+ }
+ Level level;
+
+ ///
+ /// Gets the current position of this gem in world space.
+ ///
+ public Vector2 Position
+ {
+ get
+ {
+ return basePosition + new Vector2(0.0f, bounce);
+ }
+ }
+
+ ///
+ /// Gets a circle which bounds this gem in world space.
+ ///
+ public Circle BoundingCircle
+ {
+ get
+ {
+ return new Circle(Position, Tile.Width / 3.0f);
+ }
+ }
+
+ Vector2 scale;
+
+ ///
+ /// Gets or sets the scale of the gem, used for visual effects like shrinking during collection.
+ ///
+ public Vector2 Scale { get => scale; set => scale = value; }
+
+ Vector2 collectedPosition;
+
+ ///
+ /// Gets or sets the current state of the gem (e.g., Waiting, Collecting, Collected).
+ ///
+ public GemState State { get; set; } = GemState.Waiting;
+
+ bool isPowerUp = false;
+
+ ///
+ /// Gets or sets whether this gem is a power-up gem, providing special abilities to the player.
+ ///
+ public bool IsPowerUp { get => isPowerUp; set => isPowerUp = value; }
+
+ ///
+ /// Constructs a new gem.
+ ///
+ /// The level instance to which this gem belongs.
+ /// The initial position of the gem in world space.
+ /// The type of gem, which determines its value, color, and behavior.
+ /// The dimensions of the level, used for positioning and bounds checking.
+ public Gem(Level level, Vector2 position, char gemType, Vector2 levelDimensions)
+ {
+ this.level = level;
+ this.basePosition = position;
+ this.levelDimensions = levelDimensions;
+
+ switch (gemType)
+ {
+ case '1':
+ Value = 10;
+ Color = Color.Green;
+ break;
+
+ case '2':
+ Value = 30;
+ Color = Color.Yellow;
+ break;
+
+ case '3':
+ Value = 50;
+ Color = Color.Red;
+ break;
+
+ case '4':
+ Value = 100;
+ Color = Color.Blue; // Only because blue it is my favourite colour
+ isPowerUp = true;
+ break;
+ }
+
+ LoadContent();
+ }
+
+ ///
+ /// Loads the gem texture and collected sound.
+ ///
+ public void LoadContent()
+ {
+ texture = Level.Content.Load("Sprites/Gem");
+ origin = new Vector2(texture.Width / 2.0f, texture.Height / 2.0f);
+ collectedSound = Level.Content.Load("Sounds/PlayerGemCollected");
+ }
+
+ ///
+ /// Bounces up and down in the air to entice players to collect them.
+ ///
+ /// Provides a snapshot of timing values.
+ /// The point towards which the gem moves when collected.
+ public void Update(GameTime gameTime, Vector2 collectionPoint)
+ {
+ collectedPosition = collectionPoint;
+
+ switch (State)
+ {
+ case GemState.Collected:
+ break;
+
+ case GemState.Collecting:
+ if (basePosition.Y > collectedPosition.Y)
+ {
+ // Move towards top centre of the screen.
+ Vector2 direction = collectedPosition - basePosition;
+ direction.Normalize();
+ basePosition += direction * 256 * (float)gameTime.ElapsedGameTime.TotalSeconds;
+ scale /= 1.010f;
+ }
+
+ if (basePosition.Y <= collectedPosition.Y)
+ {
+ State = GemState.Collected;
+ }
+ break;
+
+ case GemState.Waiting:
+ // Bounce along a sine curve over time.
+ // Include the X coordinate so that neighboring gems bounce in a nice wave pattern.
+ double t = gameTime.TotalGameTime.TotalSeconds * BounceRate + Position.X * BounceSync;
+ bounce = (float)Math.Sin(t) * BounceHeight * texture.Height;
+ scale = new Vector2(1.0f, 1.0f);
+ break;
+ default:
+ break;
+ }
+ }
+
+ ///
+ /// Called when this gem has been collected by a player and removed from the level.
+ ///
+ ///
+ /// The player who collected this gem. Although currently not used, this parameter would be
+ /// useful for creating special power-up gems. For example, a gem could make the player invincible.
+ ///
+ public void OnCollected(Player collectedBy)
+ {
+ collectedSound.Play();
+
+ if (isPowerUp)
+ collectedBy.PowerUp();
+ }
+
+ ///
+ /// Draws a gem in the appropriate color.
+ ///
+ /// Provides a snapshot of timing values.
+ /// The SpriteBatch used to draw the gem.
+ public void Draw(GameTime gameTime, SpriteBatch spriteBatch)
+ {
+ spriteBatch.Draw(texture, Position, null, Color, 0.0f, origin, scale, SpriteEffects.None, 0.0f);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Platformer2D/Core/Game/GemState.cs b/Platformer2D/Core/Game/GemState.cs
new file mode 100644
index 00000000..888d0512
--- /dev/null
+++ b/Platformer2D/Core/Game/GemState.cs
@@ -0,0 +1,12 @@
+namespace Platformer2D.Core
+{
+ ///
+ /// The various states the gem could be in.
+ ///
+ internal enum GemState
+ {
+ Collected,
+ Collecting,
+ Waiting,
+ }
+}
\ No newline at end of file
diff --git a/Platformer2D/Core/Game/Layer.cs b/Platformer2D/Core/Game/Layer.cs
new file mode 100644
index 00000000..630d378d
--- /dev/null
+++ b/Platformer2D/Core/Game/Layer.cs
@@ -0,0 +1,47 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace Platformer2D.Core
+{
+ ///
+ /// Represents a parallax scrolling layer in the game, typically used for background elements.
+ ///
+ internal class Layer
+ {
+ private Texture2D[] textures;
+ private float scrollSpeed;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// An array of textures representing the segments of the layer.
+ /// The speed at which this layer scrolls relative to the camera.
+ public Layer(Texture2D[] textures, float scrollSpeed)
+ {
+ this.textures = textures;
+ this.scrollSpeed = scrollSpeed;
+ }
+
+ ///
+ /// Draws the layer, scrolling it based on the camera's position.
+ ///
+ /// Provides a snapshot of timing values. This parameter is not currently used.
+ /// The SpriteBatch used to draw the layer.
+ /// The current position of the camera in world space.
+ internal void Draw(GameTime gameTime, SpriteBatch spriteBatch, float cameraPosition)
+ {
+ // Assume each segment is the same width.
+ int segmentWidth = textures[0].Width;
+
+ // Calculate which segments to draw and how much to offset them.
+ float x = cameraPosition * scrollSpeed;
+ int leftSegment = (int)Math.Floor(x / segmentWidth);
+ int rightSegment = leftSegment + 1;
+ x = (x / segmentWidth - leftSegment) * -segmentWidth;
+
+ spriteBatch.Draw(textures[leftSegment % textures.Length], new Vector2(x, 0.0f), Color.White);
+ spriteBatch.Draw(textures[rightSegment % textures.Length], new Vector2(x + segmentWidth, 0.0f), Color.White);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Platformer2D/Core/Game/Level.cs b/Platformer2D/Core/Game/Level.cs
new file mode 100644
index 00000000..9bd142db
--- /dev/null
+++ b/Platformer2D/Core/Game/Level.cs
@@ -0,0 +1,968 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using Platformer2D.Core.Effects;
+using Platformer2D.Core.Inputs;
+using Platformer2D.Core.Localization;
+using Platformer2D.Core.Settings;
+using Platformer2D.ScreenManagers;
+using Platformer2D.Screens;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Audio;
+using Microsoft.Xna.Framework.Content;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace Platformer2D.Core
+{
+ ///
+ /// A uniform grid of tiles with collections of gems and enemies.
+ /// The level owns the player and controls the game's win and lose
+ /// conditions as well as scoring.
+ ///
+ class Level : IDisposable
+ {
+ // Physical structure of the level.
+ private Tile[,] tiles;
+ private Layer[] layers;
+
+ // The layer which entities are drawn on top of.
+ private const int EntityLayer = 2;
+
+ Player player;
+
+ ///
+ /// Gets the player instance in the level.
+ ///
+ public Player Player
+ {
+ get { return player; }
+ }
+
+ private List gems = new List();
+
+ ///
+ /// Gets or sets the list of gems in the level.
+ ///
+ internal List Gems { get => gems; set => gems = value; }
+
+ private List enemies = new List();
+
+ // Key locations in the level.
+ private Vector2 start;
+ private Point exit = InvalidPosition;
+
+ ///
+ /// Gets or sets the exit position of the level.
+ ///
+ internal Point Exit { get => exit; set => exit = value; }
+
+ private static readonly Point InvalidPosition = new Point(-1, -1);
+
+ // Level game state.
+ private Random random = new Random(354668); // Arbitrary, but constant seed
+
+ ///
+ /// Gets the current score of the level.
+ ///
+ public int Score => score;
+ int score;
+
+ bool reachedExit;
+
+ ///
+ /// Gets whether the player has reached the exit.
+ ///
+ public bool ReachedExit => reachedExit;
+
+ TimeSpan timeTaken;
+
+ ///
+ /// Gets the time taken to complete the level.
+ ///
+ public TimeSpan TimeTaken => timeTaken;
+
+ private string levelPath;
+ private bool onMainMenu;
+ private TimeSpan maximumTimeToCompleteLevel = TimeSpan.FromMinutes(2.0);
+
+ ///
+ /// Gets the maximum time allowed to complete the level.
+ ///
+ public TimeSpan MaximumTimeToCompleteLevel { get => maximumTimeToCompleteLevel; }
+
+ private const int PointsPerSecond = 5;
+
+ int gemsCollected;
+
+ ///
+ /// Gets the number of gems collected by the player.
+ ///
+ public int GemsCollected => gemsCollected;
+
+ int gemsCount;
+
+ ///
+ /// Gets the total number of gems in the level.
+ ///
+ public int GemsCount => gemsCount;
+
+ bool newHighScore;
+
+ ///
+ /// Gets whether a new high score has been achieved.
+ ///
+ public bool NewHighScore => newHighScore;
+
+ private ScreenManager screenManager;
+ ContentManager content;
+
+ ///
+ /// Gets the content manager for the level.
+ ///
+ public ContentManager Content
+ {
+ get { return content; }
+ }
+
+ private SoundEffect exitReachedSound;
+
+ private SpriteFont hudFont;
+
+ // When the time remaining is less than the warning time, it blinks on the hud
+ private static readonly TimeSpan WarningTime = TimeSpan.FromSeconds(30);
+
+ ///
+ /// Gets the width of the level measured in tiles.
+ ///
+ public int Width => tiles.GetLength(0);
+
+ ///
+ /// Gets the height of the level measured in tiles.
+ ///
+ public int Height => tiles.GetLength(1);
+
+ private ParticleManager particleManager;
+ private bool particlesExploding;
+
+ ///
+ /// Gets or sets the particle manager for the level.
+ ///
+ public ParticleManager ParticleManager { get => particleManager; set => particleManager = value; }
+
+ private SettingsManager settingsManager;
+ private bool saved;
+ private bool readyToPlay;
+
+ // Backpack related variables
+ private Texture2D backpack;
+ private Vector2 backpackPosition;
+
+ ///
+ /// Gets the position of the backpack in the level.
+ ///
+ public Vector2 BackpackPosition => backpackPosition;
+
+ private float cameraPosition;
+ private Vector2 collectionPoint = new Vector2();
+
+ ///
+ /// Gets or sets the leaderboard manager for the level.
+ ///
+ public SettingsManager LeaderboardManager
+ {
+ get => settingsManager;
+
+ set
+ {
+ if (value != null
+ && settingsManager != value)
+ {
+ settingsManager = value;
+ settingsManager.Load();
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets whether the level is paused.
+ ///
+ public bool Paused { get; internal set; }
+
+ // The number of levels in the Levels directory of our content. We assume that
+ // levels in our content are 0-based and that all numbers under this constant
+ // have a level file present. This allows us to not need to check for the file
+ // or handle exceptions, both of which can add unnecessary time to level loading.
+ public const int NUMBER_OF_LEVELS = 5;
+ private const int NUMBER_OF_LAYERS = 3;
+
+ ///
+ /// Event triggered when a gem is collected by the player.
+ ///
+ public event EventHandler<(Gem, Player)> GemCollected;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The screen manager for the game.
+ /// The path to the level file.
+ /// The index of the level.
+ public Level(ScreenManager screenManager, string levelPath, int levelIndex)
+ {
+ this.screenManager = screenManager;
+
+ // Create a new content manager to load content used just by this level.
+ content = new ContentManager(this.screenManager.Game.Services, "Content");
+
+ timeTaken = TimeSpan.Zero;
+ this.levelPath = levelPath;
+
+ // If it's the MainMenu/Tutorial level, ignore stats and giving it a score.
+ onMainMenu = levelPath.Contains("00.txt");
+
+ using (Stream fileStream = TitleContainer.OpenStream(levelPath))
+ {
+ LoadTiles(fileStream);
+ }
+
+ // Load background layer textures.
+ layers = new Layer[3];
+
+ var textures0 = new Texture2D[3];
+ for (int i = 0; i < 3; ++i)
+ {
+ textures0[i] = Content.Load("Backgrounds/Layer0" + "_" + i);
+ }
+ layers[0] = new Layer(textures0, 0.2f);
+
+ var textures1 = new Texture2D[3];
+ for (int i = 0; i < 3; ++i)
+ {
+ textures1[i] = Content.Load("Backgrounds/Layer1" + "_" + i);
+ }
+ layers[1] = new Layer(textures1, 0.5f);
+
+ var textures2 = new Texture2D[3];
+ for (int i = 0; i < 3; ++i)
+ {
+ textures2[i] = Content.Load("Backgrounds/Layer2" + "_" + i);
+ }
+ layers[2] = new Layer(textures2, 0.8f);
+
+ // Load sounds.
+ exitReachedSound = Content.Load("Sounds/PlayerExitReached");
+
+ gemsCount = gems.Count;
+
+ // Load font
+ hudFont = content.Load("Fonts/Hud");
+
+ // Our backpack to store the collected gems :)
+ backpack = content.Load("Sprites/backpack");
+
+ // Hook into the GemCollected event
+ GemCollected += Level_GemCollected;
+ }
+
+ private void Level_GemCollected(object sender, (Gem gem, Player collectedBy) e)
+ {
+ score += e.gem.Value;
+
+ e.gem.OnCollected(e.collectedBy);
+ }
+
+ ///
+ /// Iterates over every tile in the structure file and loads its
+ /// appearance and behavior. This method also validates that the
+ /// file is well-formed with a player start point, exit, etc.
+ ///
+ ///
+ /// A stream containing the tile data.
+ ///
+ private void LoadTiles(Stream fileStream)
+ {
+ // Load the level and ensure all of the lines are the same length.
+ int width;
+ List lines = new List();
+ using (StreamReader reader = new StreamReader(fileStream))
+ {
+ string line = reader.ReadLine();
+ width = line.Length;
+ while (line != null)
+ {
+ lines.Add(line);
+ if (line.Length != width)
+ throw new Exception(String.Format(Resources.ErrorLevelLineLength, lines.Count));
+ line = reader.ReadLine();
+ }
+ }
+
+ // Allocate the tile grid.
+ tiles = new Tile[width, lines.Count];
+
+ // Loop over every tile position,
+ for (int y = 0; y < Height; ++y)
+ {
+ for (int x = 0; x < Width; ++x)
+ {
+ // to load each tile.
+ char tileType = lines[y][x];
+ tiles[x, y] = LoadTile(tileType, x, y);
+ }
+ }
+
+ // Verify that the level has a beginning and an end.
+ if (Player == null)
+ throw new NotSupportedException(Resources.ErrorLevelStartingPoint);
+ if (exit == InvalidPosition)
+ throw new NotSupportedException(Resources.ErrorLevelExit);
+ }
+
+ ///
+ /// Loads an individual tile's appearance and behavior.
+ ///
+ ///
+ /// The character loaded from the structure file which
+ /// indicates what should be loaded.
+ ///
+ ///
+ /// The X location of this tile in tile space.
+ ///
+ ///
+ /// The Y location of this tile in tile space.
+ ///
+ /// The loaded tile.
+ private Tile LoadTile(char tileType, int x, int y)
+ {
+ switch (tileType)
+ {
+ // Blank space
+ case '.':
+ return new Tile(null, TileCollision.Passable);
+
+ // Exit
+ case 'X':
+ return LoadExitTile(x, y);
+
+ // Minimal value Gem
+ case '1':
+ return LoadGemTile(x, y, tileType);
+ // Medium value Gem
+ case '2':
+ return LoadGemTile(x, y, tileType);
+ // Maximum value Gem
+ case '3':
+ return LoadGemTile(x, y, tileType);
+ // PowerUp Gem
+ case '4':
+ return LoadGemTile(x, y, tileType);
+
+ // Floating platform
+ case '-':
+ return LoadTile("Platform", TileCollision.Platform);
+
+ // Various enemy types
+ case 'A':
+ case 'B':
+ case 'C':
+ case 'D':
+ return LoadEnemyTile(x, y, tileType);
+
+ // Platform block
+ case '~':
+ return LoadVarietyTile("BlockB", 2, TileCollision.Platform);
+
+ // Passable block
+ case ':':
+ return LoadVarietyTile("BlockB", 2, TileCollision.Passable);
+
+ // Impassable block
+ case '#':
+ return LoadVarietyTile("BlockA", 7, TileCollision.Impassable);
+
+ // Breakable block
+ case ';':
+ return LoadVarietyTile("BlockB", 2, TileCollision.Breakable);
+
+ // Player 1 start point
+ case 'P':
+ return LoadStartTile(x, y);
+
+ // Unknown tile type character
+ default:
+ throw new NotSupportedException(String.Format(Resources.ErrorUnsupportedTileType, tileType, x, y));
+ }
+ }
+
+ ///
+ /// Creates a new tile. The other tile loading methods typically chain to this
+ /// method after performing their special logic.
+ ///
+ ///
+ /// Path to a tile texture relative to the Content/Tiles directory.
+ ///
+ ///
+ /// The tile collision type for the new tile.
+ ///
+ /// The new tile.
+ private Tile LoadTile(string name, TileCollision collision)
+ {
+ return new Tile(Content.Load("Tiles/" + name), collision);
+ }
+
+ ///
+ /// Loads a tile with a random appearance.
+ ///
+ ///
+ /// The content name prefix for this group of tile variations. Tile groups are
+ /// name LikeThis0.png and LikeThis1.png and LikeThis2.png.
+ ///
+ ///
+ /// The number of variations in this group.
+ ///
+ private Tile LoadVarietyTile(string baseName, int variationCount, TileCollision collision)
+ {
+ int index = random.Next(variationCount);
+ return LoadTile(baseName + index, collision);
+ }
+
+ ///
+ /// Instantiates a player, puts him in the level, and remembers where to put him when he is resurrected.
+ ///
+ private Tile LoadStartTile(int x, int y)
+ {
+ if (Player != null)
+ throw new NotSupportedException(Resources.ErrorLevelOneStartingPoint);
+
+ start = RectangleExtensions.GetBottomCenter(GetBounds(x, y));
+ player = new Player(this, start);
+ player.Mode = PlayerMode.Playing;
+
+ return new Tile(null, TileCollision.Passable);
+ }
+
+ ///
+ /// Remembers the location of the level's exit.
+ ///
+ private Tile LoadExitTile(int x, int y)
+ {
+ if (exit != InvalidPosition)
+ throw new NotSupportedException(Resources.ErrorLevelOneExit);
+
+ exit = GetBounds(x, y).Center;
+
+ return LoadTile("Exit", TileCollision.Passable);
+ }
+
+ ///
+ /// Instantiates an enemy and puts him in the level.
+ ///
+ private Tile LoadEnemyTile(int x, int y, char monsterType)
+ {
+ Vector2 position = RectangleExtensions.GetBottomCenter(GetBounds(x, y));
+ enemies.Add(new Enemy(this, position, "Monster" + monsterType));
+
+ return new Tile(null, TileCollision.Passable);
+ }
+
+ ///
+ /// Instantiates a gem and puts it in the level.
+ ///
+ private Tile LoadGemTile(int x, int y, char gemType)
+ {
+ Point position = GetBounds(x, y).Center;
+ gems.Add(new Gem(this, new Vector2(position.X, position.Y), gemType, new Vector2(Width * Tile.Width, Height * Tile.Height)));
+
+ return new Tile(null, TileCollision.Passable);
+ }
+
+ ///
+ /// Unloads the level content.
+ ///
+ public void Dispose()
+ {
+ Content.Unload();
+ }
+
+ ///
+ /// Gets the collision mode of the tile at a particular location.
+ /// This method handles tiles outside of the levels boundaries by making it
+ /// impossible to escape past the left or right edges, but allowing things
+ /// to jump beyond the top of the level and fall off the bottom.
+ ///
+ public TileCollision GetCollision(int x, int y)
+ {
+ // Prevent escaping past the level ends.
+ if (x < 0 || x >= Width)
+ return TileCollision.Impassable;
+ // Allow jumping past the level top and falling through the bottom.
+ if (y < 0 || y >= Height)
+ return TileCollision.Passable;
+
+ return tiles[x, y].Collision;
+ }
+
+ ///
+ /// Gets the bounding rectangle of a tile in world space.
+ ///
+ public Rectangle GetBounds(int x, int y)
+ {
+ return new Rectangle(x * Tile.Width, y * Tile.Height, Tile.Width, Tile.Height);
+ }
+
+ ///
+ /// Updates all objects in the world, performs collision between them,
+ /// and handles the time limit with scoring.
+ ///
+ /// Provides a snapshot of timing values.
+ /// Provides a snapshot of input states.
+ /// Provides the current display orientation.
+ /// Indicates whether the level is ready to be played.
+ public void Update(
+ GameTime gameTime,
+ InputState inputState,
+ DisplayOrientation displayOrientation,
+ bool readyToPlay = true)
+ {
+ if (gameTime == null)
+ throw new ArgumentNullException(nameof(gameTime));
+ if (inputState == null)
+ throw new ArgumentNullException(nameof(inputState));
+
+ this.readyToPlay = readyToPlay;
+ particleManager.Update(gameTime);
+
+ if (ReachedExit
+ && !particlesExploding)
+ {
+ particleManager.Position = Player.Position;
+ particleManager.Emit(100, SettingsScreen.CurrentParticleEffect);
+ particlesExploding = true;
+ }
+
+ if (ReachedExit)
+ {
+ if (onMainMenu)
+ return;
+
+ if (!saved)
+ {
+ // We only flag a high score, if it's a faster time and all gems were collected.
+ if (timeTaken < settingsManager.Settings.FastestTime
+ && gemsCollected == gemsCount)
+ {
+ newHighScore = true;
+ }
+
+ if (newHighScore)
+ {
+ // If it already exists update it, otherwise add it
+ if (settingsManager.Settings.FastestTime != timeTaken)
+ {
+ settingsManager.Settings.FastestTime = timeTaken;
+ }
+
+ if (settingsManager.Settings.GemsCollected < gemsCollected)
+ {
+ settingsManager.Settings.GemsCollected = gemsCollected;
+ }
+
+ if (!saved)
+ {
+ settingsManager.Save();
+ saved = true;
+ }
+ }
+ }
+ // Animate the time being converted into points.
+ int seconds = (int)Math.Round(gameTime.ElapsedGameTime.TotalSeconds * 100.0f);
+ seconds = Math.Min(seconds, (int)Math.Ceiling(TimeTaken.TotalSeconds));
+ timeTaken += TimeSpan.FromSeconds(seconds);
+ score += seconds * PointsPerSecond;
+ }
+ else
+ {
+
+ UpdateGems(gameTime);
+
+ if (readyToPlay)
+ {
+ timeTaken += gameTime.ElapsedGameTime;
+
+ Player.Update(gameTime, inputState, displayOrientation);
+
+ // Parallax Scroll if necessary
+ UpdateCamera(screenManager.BaseScreenSize);
+
+ UpdateEnemies(gameTime);
+
+ // The player has reached the exit if they are standing on the ground and
+ // his bounding rectangle contains the center of the exit tile. They can only
+ // exit when they have collected all of the gems.
+ if (Player.IsAlive &&
+ Player.IsOnGround &&
+ Player.BoundingRectangle.Contains(exit))
+ {
+ OnExitReached();
+ }
+ }
+ }
+
+ if (timeTaken > maximumTimeToCompleteLevel)
+ {
+ timeTaken = maximumTimeToCompleteLevel;
+ }
+ }
+
+ ///
+ /// Animates each gem and checks to allows the player to collect them.
+ ///
+ private void UpdateGems(GameTime gameTime)
+ {
+ // We don't recreate a new Vector2 object each frame, we just update it
+ // Calculate the collectionPoint relative to the current camera view
+ // This will help the gems track the backpack, as the camera moves.
+ // Like a homing missile :)
+ collectionPoint.X = cameraPosition + backpackPosition.X + (backpack.Width / 2);
+ collectionPoint.Y = backpackPosition.Y + (backpack.Height / 2);
+
+ for (int i = 0; i < gems.Count; ++i)
+ {
+ Gem gem = gems[i];
+
+ gem.Update(gameTime, collectionPoint);
+
+ switch (gem.State)
+ {
+ case GemState.Collected:
+ gems.RemoveAt(i--);
+ break;
+
+ case GemState.Collecting:
+ break;
+
+ case GemState.Waiting:
+ if (gem.BoundingCircle.Intersects(Player.BoundingRectangle))
+ {
+ gemsCollected++;
+ gem.Scale = new Vector2(1.5f, 1.5f);
+ gem.State = GemState.Collecting;
+ OnGemCollected(gem, Player);
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ ///
+ /// Animates each enemy and allow them to kill the player.
+ ///
+ private void UpdateEnemies(GameTime gameTime)
+ {
+ foreach (Enemy enemy in enemies)
+ {
+ enemy.Update(gameTime);
+
+ if (enemy.IsAlive && enemy.BoundingRectangle.Intersects(Player.BoundingRectangle))
+ {
+ // Touching an enemy while having the power-up kills the enemy
+ if (Player.IsPoweredUp)
+ {
+ OnEnemyKilled(enemy, Player);
+ }
+ // Touching an enemy instantly kills the player
+ else
+ {
+ OnPlayerKilled(enemy);
+ }
+ }
+ }
+ }
+
+ ///
+ /// Called when a gem is collected.
+ ///
+ /// The gem that was collected.
+ /// The player who collected this gem.
+ private void OnGemCollected(Gem gem, Player collectedBy)
+ {
+ // Call any associated events
+ GemCollected?.Invoke(this, new(gem, collectedBy));
+ }
+
+ ///
+ /// Called when the player is killed.
+ ///
+ ///
+ /// The enemy who killed the player. This is null if the player was not killed by an
+ /// enemy, such as when a player falls into a hole.
+ ///
+ private void OnPlayerKilled(Enemy killedBy)
+ {
+ Player.OnKilled(killedBy);
+ }
+
+ ///
+ /// Called when the enemy is killed.
+ ///
+ ///
+ /// The enemy who died.
+ ///
+ ///
+ /// The player who killed the enemy. Could be used when we have extra players
+ ///
+ private void OnEnemyKilled(Enemy enemy, Player killedBy)
+ {
+ enemy.OnKilled(killedBy);
+ }
+
+ ///
+ /// Called when the player reaches the level's exit.
+ ///
+ private void OnExitReached()
+ {
+ Player.OnReachedExit();
+ exitReachedSound.Play();
+ reachedExit = true;
+ }
+
+ ///
+ /// Restores the player to the starting point to try the level again.
+ ///
+ public void StartNewLife()
+ {
+ Player.Reset(start);
+ }
+
+ ///
+ /// Draws everything in the level, including background layers, tiles, entities (gems, player, enemies),
+ /// foreground layers, and the HUD. This method ensures that all elements are rendered in the correct order
+ /// and with the appropriate transformations (e.g., parallax scrolling for background layers).
+ ///
+ /// Provides a snapshot of timing values, used for animations and time-based effects.
+ /// The SpriteBatch used to draw the level elements.
+ public void Draw(GameTime gameTime, SpriteBatch spriteBatch)
+ {
+ // Create a camera transformation matrix to simulate parallax scrolling.
+ Matrix cameraTransform = Matrix.CreateTranslation(-cameraPosition, 0.0f, 0.0f);
+
+ // Get the global transformation scale for consistent rendering across resolutions.
+ float transformScale = screenManager.GlobalTransformation.M11;
+
+ // Draw background layers (layers behind entities).
+ for (int i = 0; i <= EntityLayer; ++i)
+ {
+ spriteBatch.Begin(SpriteSortMode.Deferred, null, null, null, null, null, screenManager.GlobalTransformation);
+ layers[i].Draw(gameTime, spriteBatch, cameraPosition / transformScale);
+ spriteBatch.End();
+ }
+
+ // Draw main game elements (tiles, gems, player, enemies) with camera transformation.
+ spriteBatch.Begin(SpriteSortMode.Deferred, null, null, null, null, null, cameraTransform * screenManager.GlobalTransformation);
+
+ DrawTiles(spriteBatch);
+
+ float cameraRight = cameraPosition + screenManager.BaseScreenSize.X;
+
+ // Draw visible gems.
+ foreach (Gem gem in gems)
+ {
+ if (IsInView(gem.Position.X, cameraPosition, cameraRight))
+ {
+ gem.Draw(gameTime, spriteBatch);
+ }
+ }
+
+ // Draw the player.
+ Player.Draw(gameTime, spriteBatch);
+
+ // Draw visible enemies.
+ foreach (Enemy enemy in enemies)
+ {
+ if (IsInView(enemy.Position.X, cameraPosition, cameraRight))
+ {
+ enemy.Draw(gameTime, spriteBatch);
+ }
+ }
+
+ spriteBatch.End();
+
+ // Draw foreground layers (layers in front of entities).
+ for (int i = EntityLayer + 1; i < layers.Length; ++i)
+ {
+ spriteBatch.Begin(SpriteSortMode.Deferred, null, null, null, null, null, screenManager.GlobalTransformation);
+ layers[i].Draw(gameTime, spriteBatch, cameraPosition / transformScale);
+ spriteBatch.End();
+ }
+
+ // Draw the HUD (time, score, backpack, etc.).
+ spriteBatch.Begin(SpriteSortMode.Deferred, null, null, null, null, null, screenManager.GlobalTransformation);
+
+ particleManager.Draw(spriteBatch);
+ DrawHud(spriteBatch);
+
+ spriteBatch.End();
+ }
+
+ ///
+ /// Determines whether a given position is within the visible area of the camera.
+ ///
+ /// The X-coordinate of the position to check.
+ /// The left edge of the camera's view.
+ /// The right edge of the camera's view.
+ /// True if the position is within the camera's view, otherwise false.
+ private bool IsInView(float positionX, float cameraLeft, float cameraRight)
+ {
+ return positionX >= cameraLeft - Tile.Width
+ && positionX <= cameraRight + Tile.Width;
+ }
+
+ ///
+ /// Draws all visible tiles in the level. This method calculates the range of tiles currently
+ /// visible within the camera's view and renders them.
+ ///
+ /// The SpriteBatch used to draw the tiles.
+ private void DrawTiles(SpriteBatch spriteBatch)
+ {
+ // Calculate the visible range of tiles based on the camera's position.
+ int left = (int)Math.Floor(cameraPosition / Tile.Width);
+ int right = (int)(left + screenManager.BaseScreenSize.X / Tile.Width);
+ right = Math.Min(right, Width - 1);
+
+ // Reuse a single Vector2 object for tile positions to reduce memory allocations.
+ var position = new Vector2();
+
+ // Loop through each tile position within the visible range.
+ for (int y = 0; y < Height; ++y)
+ {
+ for (int x = left; x <= right; ++x)
+ {
+ // If the tile has a texture, draw it at its calculated screen position.
+ Texture2D texture = tiles[x, y].Texture;
+ if (texture != null)
+ {
+ position.X = x * Tile.Size.X;
+ position.Y = y * Tile.Size.Y;
+
+ spriteBatch.Draw(texture, position, Color.White);
+ }
+ }
+ }
+ }
+
+ ///
+ /// Breaks a tile at the specified position, removing it from the level and triggering
+ /// a particle effect to simulate its destruction.
+ ///
+ /// The X-coordinate of the tile in tile space.
+ /// The Y-coordinate of the tile in tile space.
+ internal void BreakTile(int x, int y)
+ {
+ RemoveTile(x, y);
+
+ // Use Particle effect to explode the removed tile, above the player's head
+ particleManager.Position = new Vector2(Player.Position.X, Player.Position.Y - 20);
+ particleManager.Emit(50, ParticleEffectType.Confetti, Color.SandyBrown);
+ }
+
+ ///
+ /// Removes a tile from the level by making it passable and removing its texture.
+ /// This effectively makes the tile "disappear" from the game world.
+ /// Thus making the level layout appear dynamic.
+ ///
+ /// The X-coordinate of the tile in tile space.
+ /// The Y-coordinate of the tile in tile space.
+ internal void RemoveTile(int x, int y)
+ {
+ // Replace the tile with a passable, textureless tile.
+ tiles[x, y] = new Tile(null, TileCollision.Passable);
+ }
+
+ ///
+ /// Draws the Heads-Up Display (HUD), including the time remaining, score, and backpack.
+ /// The HUD is drawn in screen space and is not affected by the camera's position.
+ ///
+ /// The SpriteBatch used to draw the HUD elements.
+ private void DrawHud(SpriteBatch spriteBatch)
+ {
+ // Only draw the full HUD if the level is ready to play.
+ if (readyToPlay)
+ {
+ // Draw the time taken in the format "MM:SS".
+ string drawableString = Resources.Time +
+ TimeTaken.Minutes.ToString("00") + ":" +
+ TimeTaken.Seconds.ToString("00");
+ Color timeColor = TimeTaken < MaximumTimeToCompleteLevel - WarningTime
+ || ReachedExit
+ || (int)TimeTaken.TotalSeconds % 2 == 0 ? Color.Yellow : Color.Red;
+
+ DrawShadowedString(spriteBatch, hudFont, drawableString,
+ new Vector2(20, 20),
+ timeColor);
+
+ // Draw the score in the top-right corner of the screen.
+ drawableString = Resources.Score + Score.ToString();
+ Vector2 scoreDimensions = hudFont.MeasureString(drawableString);
+ Vector2 scorePosition = new Vector2(
+ screenManager.BaseScreenSize.X - scoreDimensions.X - 20,
+ 20
+ );
+
+ DrawShadowedString(spriteBatch, hudFont, drawableString, scorePosition, Color.Yellow);
+ }
+
+ // Draw the backpack in the center-top of the screen.
+ backpackPosition = new Vector2(
+ (screenManager.BaseScreenSize.X - backpack.Width) / 2,
+ 20
+ );
+
+ spriteBatch.Draw(backpack, backpackPosition, Color.White);
+ }
+
+ ///
+ /// Draws a string with a shadow effect, making it more readable against varying backgrounds.
+ ///
+ /// The SpriteBatch used to draw the string.
+ /// The font to use for rendering the string.
+ /// The string to draw.
+ /// The position at which to draw the string.
+ /// The color of the string.
+ private void DrawShadowedString(SpriteBatch spriteBatch, SpriteFont font, string value, Vector2 position, Color color)
+ {
+ // Draw the shadow slightly offset from the main text.
+ spriteBatch.DrawString(font, value, position + new Vector2(1.0f, 1.0f), Color.Black);
+ // Draw the main text.
+ spriteBatch.DrawString(font, value, position, color);
+ }
+
+ const float ViewMargin = 0.35f;
+ ///
+ /// Updates the camera's position based on the player's movement, ensuring the camera
+ /// stays centered on the player while preventing it from scrolling outside the level bounds.
+ ///
+ /// The dimensions of the BaseScreenSize.
+ private void UpdateCamera(Vector2 screenSize)
+ {
+ if (!readyToPlay || Player == null)
+ return;
+
+ // Calculate the edges of the screen based on the view margin.
+ float marginWidth = screenSize.X * ViewMargin;
+ float marginLeft = cameraPosition + marginWidth;
+ float marginRight = cameraPosition + screenSize.X - marginWidth;
+
+ // Calculate how far to scroll the camera when the player approaches the screen edges.
+ float cameraMovement = 0.0f;
+ if (Player.Position.X < marginLeft)
+ cameraMovement = Player.Position.X - marginLeft;
+ else if (Player.Position.X > marginRight)
+ cameraMovement = Player.Position.X - marginRight;
+
+ // Update the camera position, ensuring it stays within the level bounds.
+ float maxCameraPosition = Tile.Width * Width - screenSize.X;
+ cameraPosition = MathHelper.Clamp(cameraPosition + cameraMovement, 0.0f, maxCameraPosition);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Platformer2D/Core/Game/Player.cs b/Platformer2D/Core/Game/Player.cs
new file mode 100644
index 00000000..cb25bcdb
--- /dev/null
+++ b/Platformer2D/Core/Game/Player.cs
@@ -0,0 +1,652 @@
+using System;
+using Platformer2D.Core.Inputs;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Audio;
+using Microsoft.Xna.Framework.Graphics;
+using Microsoft.Xna.Framework.Input;
+
+namespace Platformer2D.Core
+{
+ ///
+ /// Our fearless adventurer!
+ /// Handles movement, physics, collisions, animations, and player states.
+ ///
+ class Player
+ {
+ // ==================== Animation Properties ====================
+ private Animation idleAnimation;
+ private Animation runAnimation;
+ private Animation jumpAnimation;
+ private Animation celebrateAnimation;
+ private Animation dieAnimation;
+
+
+ // Determines if the sprite is flipped horizontally based on movement direction
+ private SpriteEffects flip = SpriteEffects.None;
+
+ // Manages the current animation being played
+ private AnimationPlayer sprite;
+
+ // ==================== Sound Effects ====================
+ private SoundEffect killedSound;
+ private SoundEffect jumpSound;
+ private SoundEffect fallSound;
+ private SoundEffect powerUpSound;
+
+ private Level level;
+ ///
+ /// Gets the level that contains this player.
+ ///
+ public Level Level
+ {
+ get { return level; }
+ }
+
+ private bool isAlive;
+ ///
+ /// Gets whether the player is currently alive.
+ ///
+ public bool IsAlive
+ {
+ get { return isAlive; }
+ }
+
+ Vector2 position;
+ ///
+ /// Gets or sets the player's position in the world.
+ ///
+ public Vector2 Position
+ {
+ get { return position; }
+ set { position = value; }
+ }
+
+ // Stores the bottom position from the previous frame for platform collision detection
+ private float previousBottom;
+
+ ///
+ /// Gets or sets the player's velocity vector.
+ ///
+ public Vector2 Velocity
+ {
+ get { return velocity; }
+ set { velocity = value; }
+ }
+ Vector2 velocity;
+
+ // ==================== Movement Constants ====================
+ private const float MoveAcceleration = 13000.0f;
+ private const float MaxMoveSpeed = 1750.0f;
+ private const float GroundDragFactor = 0.48f;
+ private const float AirDragFactor = 0.58f;
+
+ // ==================== Jump Constants ====================
+ private const float MaxJumpTime = 0.35f;
+ private const float JumpLaunchVelocity = -3500.0f;
+ private const float GravityAcceleration = 3400.0f;
+ private const float MaxFallSpeed = 550.0f;
+ private const float JumpControlPower = 0.14f;
+
+ // ==================== Input Configuration ====================
+ private const float MoveStickScale = 1.0f;
+ private const float AccelerometerScale = 1.5f;
+ private const Buttons JumpButton = Buttons.A;
+
+ private bool isOnGround;
+ ///
+ /// Gets whether the player is currently standing on ground.
+ /// Used to determine if player can jump and which animations to play.
+ ///
+ public bool IsOnGround
+ {
+ get { return isOnGround; }
+ }
+
+ ///
+ /// Horizontal movement input value. -1.0 for left, 1.0 for right, 0.0 for no movement.
+ ///
+ private float movement;
+ public float Movement
+ {
+ get
+ {
+ return movement;
+ }
+ set
+ {
+ movement = value;
+ }
+ }
+
+ // ==================== Jump State ====================
+ ///
+ /// Indicates if the player is attempting to jump in the current frame
+ ///
+ private bool isJumping;
+ public bool IsJumping
+ {
+ get
+ {
+ return isJumping;
+ }
+ set
+ {
+ isJumping = value;
+ }
+ }
+
+ private bool wasJumping;
+ private float initialFallYPosition;
+ private bool isFalling;
+ private float jumpTime;
+ private const float MaxSafeFallDistance = -250f;
+
+ // ==================== Collision Detection ====================
+ // Local bounds of the player sprite relative to the sprite origin
+ private Rectangle localBounds;
+ ///
+ /// Gets a rectangle which bounds the player in world space,
+ /// used for collision detection with the level.
+ ///
+ public Rectangle BoundingRectangle
+ {
+ get
+ {
+ int left = (int)Math.Round(Position.X - sprite.Origin.X) + localBounds.X;
+ int top = (int)Math.Round(Position.Y - sprite.Origin.Y) + localBounds.Y;
+
+ return new Rectangle(left, top, localBounds.Width, localBounds.Height);
+ }
+ }
+
+ // ==================== PowerUp State ====================
+ private const float MaxPowerUpTime = 6.0f;
+
+ private float powerUpTime;
+ ///
+ /// Gets whether the player currently has an active power-up
+ ///
+ public bool IsPoweredUp
+ {
+ get { return powerUpTime > 0.0f; }
+ }
+
+ // Current player mode (e.g., Playing, Scripted movement)
+ public PlayerMode Mode { get; internal set; }
+
+ // Colors used for the power-up visual effect, cycling through these creates a flashing effect
+ // Could be stored in a file and read-in
+ private readonly Color[] poweredUpColors = {
+ Color.Red,
+ Color.Blue,
+ Color.Orange,
+ Color.Yellow,
+ };
+
+ ///
+ /// Constructs a new player character in the specified level at the given position.
+ ///
+ /// The level the player belongs to
+ /// The initial position in the level
+ public Player(Level level, Vector2 position)
+ {
+ this.level = level;
+
+ LoadContent();
+
+ Reset(position);
+ }
+
+ ///
+ /// Loads all player-related content: sprites, animations, and sound effects.
+ ///
+ public void LoadContent()
+ {
+ // Load animated textures.
+ idleAnimation = new Animation(Level.Content.Load("Sprites/Player/Idle"), 0.6f, true);
+ runAnimation = new Animation(Level.Content.Load("Sprites/Player/Run"), 0.1f, true);
+ jumpAnimation = new Animation(Level.Content.Load("Sprites/Player/Jump"), 0.1f, false);
+ celebrateAnimation = new Animation(Level.Content.Load("Sprites/Player/Celebrate"), 0.1f, false);
+ dieAnimation = new Animation(Level.Content.Load("Sprites/Player/Die"), 0.1f, false);
+
+ // Create collision bounds - smaller than the sprite for better gameplay feel
+ int width = (int)(idleAnimation.FrameWidth * 0.4);
+ int left = (idleAnimation.FrameWidth - width) / 2;
+ int height = (int)(idleAnimation.FrameHeight * 0.8);
+ int top = idleAnimation.FrameHeight - height;
+ localBounds = new Rectangle(left, top, width, height);
+
+ // Load sounds.
+ killedSound = Level.Content.Load("Sounds/PlayerKilled");
+ jumpSound = Level.Content.Load("Sounds/PlayerJump");
+ fallSound = Level.Content.Load("Sounds/PlayerFall");
+ powerUpSound = Level.Content.Load("Sounds/PlayerPowerUp");
+ }
+
+ ///
+ /// Resets the player to life at the specified position.
+ /// Called at the beginning of a level and when respawning after death.
+ ///
+ /// The position to respawn at
+ public void Reset(Vector2 position)
+ {
+ Position = position;
+ Velocity = Vector2.Zero;
+ isAlive = true;
+ sprite.PlayAnimation(idleAnimation);
+ }
+
+ ///
+ /// Updates the player's state based on input, physics, and animations.
+ /// Main update method called each frame.
+ ///
+ /// Provides timing information
+ /// Current state of all input devices
+ /// Orientation of the display for accelerometer adjustments
+ public void Update(
+ GameTime gameTime,
+ InputState inputState,
+ DisplayOrientation displayOrientation)
+ {
+ // Only process input if the player is in playing mode
+ if (Mode == PlayerMode.Playing)
+ HandleInput(inputState, displayOrientation);
+
+ Move(gameTime);
+ }
+
+ ///
+ /// Updates player's position, applies physics, and updates animations.
+ /// Called each frame after input is processed.
+ ///
+ /// Provides timing information
+ public void Move(GameTime gameTime)
+ {
+ if (IsAlive)
+ {
+ ApplyPhysics(gameTime);
+
+ // Update power-up timer
+ if (IsPoweredUp)
+ powerUpTime = Math.Max(0.0f, powerUpTime - (float)gameTime.ElapsedGameTime.TotalSeconds);
+
+ // Play the appropriate animation based on player state
+ if (IsOnGround)
+ {
+ if (Math.Abs(Velocity.X) - 0.02f > 0)
+ {
+ sprite.PlayAnimation(runAnimation);
+ }
+ else
+ {
+ sprite.PlayAnimation(idleAnimation);
+ }
+ }
+ }
+
+ // Reset input state for next frame
+ movement = 0.0f;
+ isJumping = false;
+ }
+
+ ///
+ /// Processes player input from keyboard, gamepad, accelerometer, and touch/mouse.
+ /// Sets movement direction and jump state based on input.
+ ///
+ /// Current state of all input devices
+ /// Orientation of the display for accelerometer adjustments
+ private void HandleInput(
+ InputState inputState,
+ DisplayOrientation displayOrientation)
+ {
+ // Get analog horizontal movement from gamepad
+ movement = inputState.CurrentGamePadStates[0].ThumbSticks.Left.X * MoveStickScale;
+
+ // Ignore small movements to prevent subtle drifting
+ if (Math.Abs(movement) < 0.5f)
+ movement = 0.0f;
+
+ // Process keyboard and D-pad input for movement
+ if (inputState.CurrentGamePadStates[0].IsButtonDown(Buttons.DPadLeft) ||
+ inputState.CurrentKeyboardStates[0].IsKeyDown(Keys.Left) ||
+ inputState.CurrentKeyboardStates[0].IsKeyDown(Keys.A))
+ {
+ movement = -1.0f;
+ }
+ else if (inputState.CurrentGamePadStates[0].IsButtonDown(Buttons.DPadRight) ||
+ inputState.CurrentKeyboardStates[0].IsKeyDown(Keys.Right) ||
+ inputState.CurrentKeyboardStates[0].IsKeyDown(Keys.D))
+ {
+ movement = 1.0f;
+ }
+
+ // Check for jump input from gamepad or keyboard
+ isJumping =
+ inputState.CurrentGamePadStates[0].IsButtonDown(JumpButton) ||
+ inputState.CurrentKeyboardStates[0].IsKeyDown(Keys.Space) ||
+ inputState.CurrentKeyboardStates[0].IsKeyDown(Keys.Up) ||
+ inputState.CurrentKeyboardStates[0].IsKeyDown(Keys.W);
+
+ // Handle touch/mouse input if activated
+ if (inputState.CurrentTouchState.Count > 0 || inputState.CurrentMouseState.LeftButton == ButtonState.Pressed)
+ {
+ HandleClickInput(inputState.CurrentCursorLocation);
+ }
+ }
+
+ ///
+ /// Processes touch/mouse input for movement and jumping.
+ /// Click/tap above player to jump, to the side to move.
+ ///
+ /// Screen coordinates of touch/click
+ private void HandleClickInput(Vector2 clickPosition)
+ {
+ // Get current player position for reference
+ Vector2 playerPosition = Position;
+
+ // Define thresholds for different input zones
+ float jumpThresholdY = playerPosition.Y - 50; // Area above player for jumping
+ float moveThresholdX = 20f; // Minimum distance for horizontal movement
+
+ // Check if click is in the "jump zone" (above player)
+ bool shouldJump = clickPosition.Y < jumpThresholdY;
+
+ // Check if click is in "move right" or "move left" zones
+ bool shouldMoveRight = clickPosition.X > playerPosition.X + moveThresholdX;
+ bool shouldMoveLeft = clickPosition.X < playerPosition.X - moveThresholdX;
+
+ // Apply the appropriate action based on zone
+ if (shouldJump)
+ {
+ // Trigger jump
+ isJumping = true;
+ }
+ else if (shouldMoveRight)
+ {
+ // Move right
+ movement = 1.0f;
+ }
+ else if (shouldMoveLeft)
+ {
+ // Move left
+ movement = -1.0f;
+ }
+ else
+ {
+ // No movement if clicked too close to player
+ movement = 0.0f;
+ }
+ }
+
+ ///
+ /// Applies physics to update player's velocity and position.
+ /// Handles gravity, jump physics, drag, and maximum velocity caps.
+ ///
+ /// Provides timing information
+ public void ApplyPhysics(GameTime gameTime)
+ {
+ float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds;
+
+ // Store position before physics update for collision resolution
+ Vector2 previousPosition = Position;
+
+ // Apply horizontal acceleration based on input and vertical acceleration due to gravity
+ velocity.X += movement * MoveAcceleration * elapsed;
+ velocity.Y = MathHelper.Clamp(velocity.Y + GravityAcceleration * elapsed, -MaxFallSpeed, MaxFallSpeed);
+
+ // Calculate jump physics if jumping
+ velocity.Y = DoJump(velocity.Y, gameTime);
+
+ // Apply drag to slow the player down (different values for ground and air)
+ if (IsOnGround)
+ velocity.X *= GroundDragFactor;
+ else
+ velocity.X *= AirDragFactor;
+
+ // Cap horizontal speed to prevent excessive velocity
+ velocity.X = MathHelper.Clamp(velocity.X, -MaxMoveSpeed, MaxMoveSpeed);
+
+ // Update position based on velocity
+ Position += velocity * elapsed;
+ Position = new Vector2((float)Math.Round(Position.X), (float)Math.Round(Position.Y));
+
+ // Check and resolve collisions with the level
+ HandleCollisions();
+
+ // If collision prevented movement, reset velocity component to zero
+ if (Position.X == previousPosition.X)
+ velocity.X = 0;
+
+ if (Position.Y == previousPosition.Y)
+ velocity.Y = 0;
+ }
+
+ ///
+ /// Calculates the Y velocity based on jump state and timing.
+ /// Implements a variable-height jump with more control at the apex.
+ /// Also handles fall detection and damage from excessive falls.
+ ///
+ ///
+ /// During the accent of a jump, the Y velocity is completely
+ /// overridden by a power curve. During the decent, gravity takes
+ /// over. The jump velocity is controlled by the jumpTime field
+ /// which measures time into the accent of the current jump.
+ ///
+ /// Current vertical velocity
+ /// Provides timing information
+ /// Updated vertical velocity
+ private float DoJump(float velocityY, GameTime gameTime)
+ {
+ // If the player wants to jump
+ if (isJumping)
+ {
+ // Begin or continue a jump - either just pressed jump on ground or holding jump in mid-jump
+ if ((!wasJumping && IsOnGround) || jumpTime > 0.0f)
+ {
+ // Play jump sound when starting a new jump
+ if (jumpTime == 0.0f)
+ jumpSound.Play();
+
+ // Track jump duration and play jump animation
+ jumpTime += (float)gameTime.ElapsedGameTime.TotalSeconds;
+ sprite.PlayAnimation(jumpAnimation);
+ }
+
+ // During the ascent phase of the jump (controlled by jump button duration)
+ if (0.0f < jumpTime && jumpTime <= MaxJumpTime)
+ {
+ // Apply a power curve that gives more control at the apex of the jump
+ // The longer the button is held, the higher the jump (up to a maximum)
+ velocityY = JumpLaunchVelocity * (1.0f - (float)Math.Pow(jumpTime / MaxJumpTime, JumpControlPower));
+ }
+ else
+ {
+ // Jump button held too long or released, end the controlled jump phase
+ jumpTime = 0.0f;
+ }
+
+ // Reset fall tracking when jumping
+ isFalling = false;
+ }
+ else
+ {
+ // Not jumping or canceled jump - reset jump timer
+ jumpTime = 0.0f;
+
+ // Detect when player starts falling (not on ground, not jumping, not already in fall state)
+ if (!IsOnGround && !isJumping && !isFalling)
+ {
+ // Record starting height of fall for damage calculation
+ initialFallYPosition = position.Y;
+ isFalling = true;
+ }
+
+ // If player lands after falling
+ if (IsOnGround && isFalling)
+ {
+ // Calculate total fall distance
+ float fallDistance = initialFallYPosition - position.Y;
+
+ // Apply fall damage if fall was too far
+ if (fallDistance < MaxSafeFallDistance)
+ {
+ OnKilled(null);
+ }
+
+ // Reset fall state after landing
+ isFalling = false;
+ }
+ }
+ // Track jump button state for next frame
+ wasJumping = isJumping;
+
+ return velocityY;
+ }
+
+ ///
+ /// Detects and resolves collisions between the player and level tiles.
+ /// Handles different tile types (impassable, platform, breakable).
+ ///
+ private void HandleCollisions()
+ {
+ // Get player's collision rectangle and determine which tiles to check
+ Rectangle bounds = BoundingRectangle;
+ int leftTile = (int)Math.Floor((float)bounds.Left / Tile.Width);
+ int rightTile = (int)Math.Ceiling(((float)bounds.Right / Tile.Width)) - 1;
+ int topTile = (int)Math.Floor((float)bounds.Top / Tile.Height);
+ int bottomTile = (int)Math.Ceiling(((float)bounds.Bottom / Tile.Height)) - 1;
+
+ // Reset ground detection for this frame
+ isOnGround = false;
+
+ // Check each potentially colliding tile
+ for (int y = topTile; y <= bottomTile; ++y)
+ {
+ for (int x = leftTile; x <= rightTile; ++x)
+ {
+ // Skip non-collidable tiles
+ TileCollision collision = Level.GetCollision(x, y);
+ if (collision != TileCollision.Passable)
+ {
+ // Calculate overlap depth between player and tile
+ Rectangle tileBounds = Level.GetBounds(x, y);
+ Vector2 depth = RectangleExtensions.GetIntersectionDepth(bounds, tileBounds);
+
+ if (depth != Vector2.Zero)
+ {
+ float absDepthX = Math.Abs(depth.X);
+ float absDepthY = Math.Abs(depth.Y);
+
+ // Resolve collision along the shallowest axis (usually gives better results)
+ // Platforms are special cases that only collide from above
+ if (absDepthY < absDepthX || collision == TileCollision.Platform)
+ {
+ // Check if player is standing on this tile (previous bottom was above tile top)
+ if (previousBottom <= tileBounds.Top)
+ isOnGround = true;
+
+ // Only apply Y collision for impassable tiles or when on ground for platforms
+ if (collision == TileCollision.Impassable || IsOnGround)
+ {
+ // Push player out of collision along Y axis
+ Position = new Vector2(Position.X, Position.Y + depth.Y);
+ bounds = BoundingRectangle; // Update bounds for subsequent checks
+ }
+
+ // Special handling for breakable tiles when hit from below
+ if (collision == TileCollision.Breakable && depth.Y < 0 && previousBottom > tileBounds.Top)
+ {
+ level.BreakTile(x, y);
+ }
+ }
+ else if (collision == TileCollision.Impassable) // Not for platforms
+ {
+ // Push player out of collision along X axis
+ Position = new Vector2(Position.X + depth.X, Position.Y);
+ bounds = BoundingRectangle; // Update bounds for subsequent checks
+ }
+ }
+ }
+ }
+ }
+
+ // Kill player if they fall below the level
+ if (BoundingRectangle.Top >= level.Height * Tile.Height)
+ OnKilled(null);
+
+ // Store bottom position for next frame's platform detection
+ previousBottom = bounds.Bottom;
+ }
+
+ ///
+ /// Handles player death, either from enemies or environmental hazards.
+ /// Plays death animation and sound effect.
+ ///
+ ///
+ /// The enemy that killed the player, or null if killed by falling or other hazard.
+ ///
+ public void OnKilled(Enemy killedBy)
+ {
+ isAlive = false;
+
+ // Play appropriate death sound
+ if (killedBy != null)
+ killedSound.Play();
+ else
+ fallSound.Play();
+
+ // Play death animation
+ sprite.PlayAnimation(dieAnimation);
+ }
+
+ ///
+ /// Called when player reaches the level exit.
+ /// Plays celebration animation.
+ ///
+ public void OnReachedExit()
+ {
+ sprite.PlayAnimation(celebrateAnimation);
+ }
+
+ ///
+ /// Draws the player with appropriate animation, facing direction, and color effects.
+ ///
+ /// Provides timing information
+ /// SpriteBatch used for drawing
+ public void Draw(GameTime gameTime, SpriteBatch spriteBatch)
+ {
+ // Flip sprite based on movement direction
+ if (Velocity.X > 0)
+ flip = SpriteEffects.FlipHorizontally;
+ else if (Velocity.X < 0)
+ flip = SpriteEffects.None;
+
+ // Apply color effects for power-up state
+ Color color;
+ if (IsPoweredUp)
+ {
+ // Cycle through power-up colors for flashing effect
+ float t = ((float)gameTime.TotalGameTime.TotalSeconds + powerUpTime / MaxPowerUpTime) * 20.0f;
+ int colorIndex = (int)t % poweredUpColors.Length;
+ color = poweredUpColors[colorIndex];
+ }
+ else
+ {
+ color = Color.White; // Normal color when not powered up
+ }
+
+ // Draw the player sprite with current animation, position, and effects
+ sprite.Draw(gameTime, spriteBatch, Position, flip, color);
+ }
+
+ ///
+ /// Activates power-up state for the player.
+ /// Sets power-up timer and plays power-up sound effect.
+ ///
+ internal void PowerUp()
+ {
+ powerUpTime = MaxPowerUpTime;
+ powerUpSound.Play();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Platformer2D/Core/Game/PlayerMode.cs b/Platformer2D/Core/Game/PlayerMode.cs
new file mode 100644
index 00000000..679a7da4
--- /dev/null
+++ b/Platformer2D/Core/Game/PlayerMode.cs
@@ -0,0 +1,11 @@
+namespace Platformer2D.Core
+{
+ ///
+ /// The various modes the player could be in.
+ ///
+ enum PlayerMode
+ {
+ Scripting,
+ Playing
+ }
+}
diff --git a/Platformer2D/Core/Game/RectangleExtensions.cs b/Platformer2D/Core/Game/RectangleExtensions.cs
new file mode 100644
index 00000000..e1275607
--- /dev/null
+++ b/Platformer2D/Core/Game/RectangleExtensions.cs
@@ -0,0 +1,70 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace Platformer2D.Core
+{
+ ///
+ /// A set of helpful methods for working with rectangles.
+ ///
+ public static class RectangleExtensions
+ {
+ ///
+ /// Calculates the signed depth of intersection between two rectangles.
+ ///
+ /// The first rectangle.
+ /// The second rectangle.
+ ///
+ /// A representing the depth of the intersection.
+ /// Positive values indicate that is to the right or below .
+ /// Negative values indicate that is to the left or above.
+ /// If the rectangles are not intersecting, returns .
+ ///
+ public static Vector2 GetIntersectionDepth(this Rectangle rectA, Rectangle rectB)
+ {
+ // Calculate half sizes for both rectangles (helps with center-based calculations).
+ float halfWidthA = rectA.Width / 2.0f;
+ float halfHeightA = rectA.Height / 2.0f;
+ float halfWidthB = rectB.Width / 2.0f;
+ float halfHeightB = rectB.Height / 2.0f;
+
+ // Calculate the centers of each rectangle.
+ Vector2 centerA = new Vector2(rectA.Left + halfWidthA, rectA.Top + halfHeightA);
+ Vector2 centerB = new Vector2(rectB.Left + halfWidthB, rectB.Top + halfHeightB);
+
+ // Calculate the current distance between the rectangle centers.
+ float distanceX = centerA.X - centerB.X;
+ float distanceY = centerA.Y - centerB.Y;
+
+ // Calculate the minimum distance required for the rectangles to *not* be intersecting.
+ float minDistanceX = halfWidthA + halfWidthB;
+ float minDistanceY = halfHeightA + halfHeightB;
+
+ // If the rectangles are not overlapping, return zero depth.
+ if (Math.Abs(distanceX) >= minDistanceX || Math.Abs(distanceY) >= minDistanceY)
+ return Vector2.Zero;
+
+ // Calculate intersection depths.
+ float depthX = distanceX > 0
+ ? minDistanceX - distanceX // Positive depth (rectA is on the right side)
+ : -minDistanceX - distanceX; // Negative depth (rectA is on the left side)
+
+ float depthY = distanceY > 0
+ ? minDistanceY - distanceY // Positive depth (rectA is below)
+ : -minDistanceY - distanceY; // Negative depth (rectA is above)
+
+ return new Vector2(depthX, depthY);
+ }
+
+ ///
+ /// Gets the position of the center of the bottom edge of the rectangle.
+ ///
+ /// The rectangle to calculate from.
+ ///
+ /// A representing the bottom-center point of the rectangle.
+ ///
+ public static Vector2 GetBottomCenter(this Rectangle rect)
+ {
+ return new Vector2(rect.X + rect.Width / 2.0f, rect.Bottom);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Platformer2D/Core/Game/Tile.cs b/Platformer2D/Core/Game/Tile.cs
new file mode 100644
index 00000000..3af3b95d
--- /dev/null
+++ b/Platformer2D/Core/Game/Tile.cs
@@ -0,0 +1,47 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace Platformer2D.Core
+{
+ ///
+ /// Represents the visual appearance and collision behavior of a tile.
+ ///
+ struct Tile
+ {
+ ///
+ /// The texture that represents the tile's visual appearance.
+ ///
+ public Texture2D Texture;
+
+ ///
+ /// The type of collision behavior this tile exhibits.
+ ///
+ public TileCollision Collision;
+
+ ///
+ /// The standard width of a tile, measured in pixels.
+ ///
+ public const int Width = 40;
+
+ ///
+ /// The standard height of a tile, measured in pixels.
+ ///
+ public const int Height = 32;
+
+ ///
+ /// The size of a tile as a for convenience.
+ ///
+ public static readonly Vector2 Size = new Vector2(Width, Height);
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The texture representing the tile's appearance.
+ /// The collision type that defines the tile's behavior.
+ public Tile(Texture2D texture, TileCollision collision)
+ {
+ Texture = texture;
+ Collision = collision;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Platformer2D/Core/Game/TileCollision.cs b/Platformer2D/Core/Game/TileCollision.cs
new file mode 100644
index 00000000..0039b74e
--- /dev/null
+++ b/Platformer2D/Core/Game/TileCollision.cs
@@ -0,0 +1,33 @@
+namespace Platformer2D.Core
+{
+ ///
+ /// Controls the collision detection and response behavior of a tile.
+ ///
+ enum TileCollision
+ {
+ ///
+ /// A passable tile is one which does not hinder player motion at all.
+ ///
+ Passable = 0,
+
+ ///
+ /// An impassable tile is one which does not allow the player to move through
+ /// it at all. It is completely solid.
+ ///
+ Impassable = 1,
+
+ ///
+ /// A platform tile is one which behaves like a passable tile except when the
+ /// player is above it. A player can jump up through a platform as well as move
+ /// past it to the left and right, but can not fall down through the top of it.
+ ///
+ Platform = 2,
+
+ ///
+ /// A breakable tile is one which behaves like a platform tile except when the
+ /// player is below it, the player jumps up the tile breaks/disappears.
+ /// Our version of Mario :).
+ ///
+ Breakable = 3,
+ }
+}
\ No newline at end of file
diff --git a/Platformer2D/Core/Inputs/InputState.cs b/Platformer2D/Core/Inputs/InputState.cs
new file mode 100644
index 00000000..79b8a129
--- /dev/null
+++ b/Platformer2D/Core/Inputs/InputState.cs
@@ -0,0 +1,464 @@
+using System;
+using System.Collections.Generic;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+using Microsoft.Xna.Framework.Input;
+using Microsoft.Xna.Framework.Input.Touch;
+
+namespace Platformer2D.Core.Inputs;
+
+///
+/// Helper for reading input from keyboard, gamepad, and touch input. This class
+/// tracks both the current and previous state of the input devices, and implements
+/// query methods for high level input actions such as "move up through the menu"
+/// or "pause the game".
+///
+public class InputState
+{
+ public const int MaxInputs = 4; // Maximum number of supported input devices (e.g., players)
+
+ // Current Inputstates - Tracks the latest state of all input devices
+ public readonly GamePadState[] CurrentGamePadStates;
+ public readonly KeyboardState[] CurrentKeyboardStates;
+ public MouseState CurrentMouseState;
+ private int touchCount; // Number of active touch inputs
+ public TouchCollection CurrentTouchState;
+
+ // Last Inputstates - Stores the previous frame's input states for detecting changes
+ public readonly GamePadState[] LastGamePadStates;
+ public readonly KeyboardState[] LastKeyboardStates;
+ public MouseState LastMouseState;
+ public TouchCollection LastTouchState;
+
+ public readonly List Gestures = new List(); // Stores touch gestures
+
+ ///
+ /// Cursor move speed in pixels per second
+ ///
+ private const float cursorMoveSpeed = 250.0f;
+
+ private Vector2 currentCursorLocation;
+ ///
+ /// Current location of our Cursor
+ ///
+ public Vector2 CurrentCursorLocation => currentCursorLocation;
+
+ private Vector2 lastCursorLocation;
+ ///
+ /// Current location of our Cursor
+ ///
+ public Vector2 LastCursorLocation => lastCursorLocation;
+
+ private bool isMouseWheelScrolledDown;
+ ///
+ /// Has the user scrolled the mouse wheel down?
+ ///
+ public bool IsMouseWheelScrolledDown => isMouseWheelScrolledDown;
+
+ private bool isMouseWheelScrolledUp;
+ private Matrix inputTransformation; // Used to transform input coordinates between screen and game space
+
+ ///
+ /// Has the user scrolled the mouse wheel up?
+ ///
+ public bool IsMouseWheelScrolledUp => isMouseWheelScrolledUp;
+
+ ///
+ /// Constructs a new input state.
+ ///
+ public InputState()
+ {
+ // Initialize arrays for multiple controller/keyboard states
+ CurrentKeyboardStates = new KeyboardState[MaxInputs];
+ CurrentGamePadStates = new GamePadState[MaxInputs];
+
+ LastKeyboardStates = new KeyboardState[MaxInputs];
+ LastGamePadStates = new GamePadState[MaxInputs];
+
+ // Configure platform-specific input options
+ if (Platformer2DGame.IsMobile)
+ {
+ TouchPanel.EnabledGestures = GestureType.Tap;
+ }
+ else if (Platformer2DGame.IsDesktop)
+ {
+ // No desktop-specific initialization needed
+ }
+ else
+ {
+ // For now, we'll throw an exception if we don't know the platform
+ throw new PlatformNotSupportedException();
+ }
+ }
+
+ ///
+ /// Reads the latest state of all the inputs.
+ ///
+ /// Provides a snapshot of timing values.
+ /// The base screen size to constrain cursor movement within.
+ public void Update(GameTime gameTime, Vector2 baseScreenSize)
+ {
+ // Update keyboard and gamepad states for all players
+ for (int i = 0; i < MaxInputs; i++)
+ {
+ LastKeyboardStates[i] = CurrentKeyboardStates[i];
+ LastGamePadStates[i] = CurrentGamePadStates[i];
+
+ CurrentKeyboardStates[i] = Keyboard.GetState();
+ CurrentGamePadStates[i] = GamePad.GetState((PlayerIndex)i);
+ }
+
+ // Update mouse state
+ LastMouseState = CurrentMouseState;
+ CurrentMouseState = Mouse.GetState();
+
+ // Update touch state
+ touchCount = 0;
+ LastTouchState = CurrentTouchState;
+ CurrentTouchState = TouchPanel.GetState();
+
+ // Process all available gestures
+ Gestures.Clear();
+ while (TouchPanel.IsGestureAvailable)
+ {
+ Gestures.Add(TouchPanel.ReadGesture());
+ }
+
+ // Process touch inputs
+ foreach (TouchLocation location in CurrentTouchState)
+ {
+ switch (location.State)
+ {
+ case TouchLocationState.Pressed:
+ touchCount++;
+ lastCursorLocation = currentCursorLocation;
+ // Transform touch position to game coordinates
+ currentCursorLocation = TransformCursorLocation(location.Position);
+ break;
+ case TouchLocationState.Moved:
+ break;
+ case TouchLocationState.Released:
+ break;
+ }
+ }
+
+ // Handle mouse clicks as touch equivalents
+ if (IsLeftMouseButtonClicked())
+ {
+ lastCursorLocation = currentCursorLocation;
+ // Transform mouse position to game coordinates
+ currentCursorLocation = TransformCursorLocation(new Vector2(CurrentMouseState.X, CurrentMouseState.Y));
+ touchCount = 1;
+ }
+
+ if (IsMiddleMouseButtonClicked())
+ {
+ touchCount = 2; // Treat middle mouse click as double touch
+ }
+
+ if (IsRightMoustButtonClicked())
+ {
+ touchCount = 3; // Treat right mouse click as triple touch
+ }
+
+ // Reset mouse wheel flags
+ isMouseWheelScrolledUp = false;
+ isMouseWheelScrolledDown = false;
+
+ // Detect mouse wheel scrolling
+ if (CurrentMouseState.ScrollWheelValue != LastMouseState.ScrollWheelValue)
+ {
+ int scrollWheelDelta = CurrentMouseState.ScrollWheelValue - LastMouseState.ScrollWheelValue;
+
+ // Handle the scroll wheel event based on the delta
+ if (scrollWheelDelta > 0)
+ {
+ // Mouse wheel scrolled up
+ isMouseWheelScrolledUp = true;
+ }
+ else if (scrollWheelDelta < 0)
+ {
+ // Mouse wheel scrolled down
+ isMouseWheelScrolledDown = true;
+ }
+ }
+
+ // Update the cursor location using gamepad and keyboard
+ float elapsedTime = (float)gameTime.ElapsedGameTime.TotalSeconds;
+
+ // Move cursor with gamepad thumbstick
+ if (CurrentGamePadStates[0].IsConnected)
+ {
+ lastCursorLocation = currentCursorLocation;
+
+ currentCursorLocation.X += CurrentGamePadStates[0].ThumbSticks.Left.X * elapsedTime * cursorMoveSpeed;
+ currentCursorLocation.Y -= CurrentGamePadStates[0].ThumbSticks.Left.Y * elapsedTime * cursorMoveSpeed;
+ }
+
+ // Move cursor with keyboard arrow keys
+ if (CurrentKeyboardStates[0].IsKeyDown(Keys.Up))
+ {
+ currentCursorLocation.Y -= elapsedTime * cursorMoveSpeed;
+ }
+ if (CurrentKeyboardStates[0].IsKeyDown(Keys.Down))
+ {
+ currentCursorLocation.Y += elapsedTime * cursorMoveSpeed;
+ }
+ if (CurrentKeyboardStates[0].IsKeyDown(Keys.Left))
+ {
+ currentCursorLocation.X -= elapsedTime * cursorMoveSpeed;
+ }
+ if (CurrentKeyboardStates[0].IsKeyDown(Keys.Right))
+ {
+ currentCursorLocation.X += elapsedTime * cursorMoveSpeed;
+ }
+
+ // Keep cursor within rendered screen bounds
+ currentCursorLocation.X = MathHelper.Clamp(currentCursorLocation.X, 0f, (int)baseScreenSize.X);
+ currentCursorLocation.Y = MathHelper.Clamp(currentCursorLocation.Y, 0f, (int)baseScreenSize.Y);
+ }
+
+ ///
+ /// Checks if left mouse button was clicked (pressed and then released)
+ ///
+ /// True if left mouse button was clicked, false otherwise.
+ internal bool IsLeftMouseButtonClicked()
+ {
+ return CurrentMouseState.LeftButton == ButtonState.Released && LastMouseState.LeftButton == ButtonState.Pressed;
+ }
+
+ ///
+ /// Checks if middle mouse button was clicked (pressed and then released)
+ ///
+ /// True if middle mouse button was clicked, false otherwise.
+ internal bool IsMiddleMouseButtonClicked()
+ {
+ return CurrentMouseState.MiddleButton == ButtonState.Released && LastMouseState.MiddleButton == ButtonState.Pressed;
+ }
+
+ ///
+ /// Checks if right mouse button was clicked (pressed and then released)
+ ///
+ /// True if right mouse button was clicked, false otherwise.
+ internal bool IsRightMoustButtonClicked()
+ {
+ return CurrentMouseState.RightButton == ButtonState.Released && LastMouseState.RightButton == ButtonState.Pressed;
+ }
+
+ ///
+ /// Helper for checking if a key was newly pressed during this update.
+ ///
+ /// The key to check.
+ /// The player to read input for, or null for any player.
+ /// Outputs which player pressed the key.
+ /// True if the key was newly pressed, false otherwise.
+ public bool IsNewKeyPress(Keys key, PlayerIndex? controllingPlayer,
+ out PlayerIndex playerIndex)
+ {
+ if (controllingPlayer.HasValue)
+ {
+ // Read input from the specified player.
+ playerIndex = controllingPlayer.Value;
+
+ int i = (int)playerIndex;
+
+ return (CurrentKeyboardStates[i].IsKeyDown(key) &&
+ LastKeyboardStates[i].IsKeyUp(key));
+ }
+ else
+ {
+ // Accept input from any player.
+ return (IsNewKeyPress(key, PlayerIndex.One, out playerIndex) ||
+ IsNewKeyPress(key, PlayerIndex.Two, out playerIndex) ||
+ IsNewKeyPress(key, PlayerIndex.Three, out playerIndex) ||
+ IsNewKeyPress(key, PlayerIndex.Four, out playerIndex));
+ }
+ }
+
+
+ ///
+ /// Helper for checking if a button was newly pressed during this update.
+ ///
+ /// The button to check.
+ /// The player to read input for, or null for any player.
+ /// Outputs which player pressed the button.
+ /// True if the button was newly pressed, false otherwise.
+ public bool IsNewButtonPress(Buttons button, PlayerIndex? controllingPlayer,
+ out PlayerIndex playerIndex)
+ {
+ if (controllingPlayer.HasValue)
+ {
+ // Read input from the specified player.
+ playerIndex = controllingPlayer.Value;
+
+ int i = (int)playerIndex;
+
+ return (CurrentGamePadStates[i].IsButtonDown(button) &&
+ LastGamePadStates[i].IsButtonUp(button));
+ }
+ else
+ {
+ // Accept input from any player.
+ return (IsNewButtonPress(button, PlayerIndex.One, out playerIndex) ||
+ IsNewButtonPress(button, PlayerIndex.Two, out playerIndex) ||
+ IsNewButtonPress(button, PlayerIndex.Three, out playerIndex) ||
+ IsNewButtonPress(button, PlayerIndex.Four, out playerIndex));
+ }
+ }
+
+
+ ///
+ /// Checks for a "menu select" input action.
+ ///
+ /// The player to read input for, or null for any player.
+ /// Outputs which player triggered the action.
+ /// True if menu select action occurred, false otherwise.
+ public bool IsMenuSelect(PlayerIndex? controllingPlayer,
+ out PlayerIndex playerIndex)
+ {
+ return IsNewKeyPress(Keys.Space, controllingPlayer, out playerIndex) ||
+ IsNewKeyPress(Keys.Enter, controllingPlayer, out playerIndex) ||
+ IsNewButtonPress(Buttons.A, controllingPlayer, out playerIndex) ||
+ IsNewButtonPress(Buttons.Start, controllingPlayer, out playerIndex);
+ }
+
+
+ ///
+ /// Checks for a "menu cancel" input action.
+ ///
+ /// The player to read input for, or null for any player.
+ /// Outputs which player triggered the action.
+ /// True if menu cancel action occurred, false otherwise.
+ public bool IsMenuCancel(PlayerIndex? controllingPlayer,
+ out PlayerIndex playerIndex)
+ {
+ return IsNewKeyPress(Keys.Escape, controllingPlayer, out playerIndex) ||
+ IsNewButtonPress(Buttons.B, controllingPlayer, out playerIndex) ||
+ IsNewButtonPress(Buttons.Back, controllingPlayer, out playerIndex);
+ }
+
+
+ ///
+ /// Checks for a "menu up" input action.
+ ///
+ /// The player to read input for, or null for any player.
+ /// True if menu up action occurred, false otherwise.
+ public bool IsMenuUp(PlayerIndex? controllingPlayer)
+ {
+ PlayerIndex playerIndex;
+
+ return IsNewKeyPress(Keys.Up, controllingPlayer, out playerIndex) ||
+ IsNewButtonPress(Buttons.DPadUp, controllingPlayer, out playerIndex) ||
+ IsNewButtonPress(Buttons.LeftThumbstickUp, controllingPlayer, out playerIndex) ||
+ IsMouseWheelScrolledUp;
+ }
+
+
+ ///
+ /// Checks for a "menu down" input action.
+ ///
+ /// The player to read input for, or null for any player.
+ /// True if menu down action occurred, false otherwise.
+ public bool IsMenuDown(PlayerIndex? controllingPlayer)
+ {
+ PlayerIndex playerIndex;
+
+ return IsNewKeyPress(Keys.Down, controllingPlayer, out playerIndex) ||
+ IsNewButtonPress(Buttons.DPadDown, controllingPlayer, out playerIndex) ||
+ IsNewButtonPress(Buttons.LeftThumbstickDown, controllingPlayer, out playerIndex) ||
+ IsMouseWheelScrolledDown;
+ }
+
+
+ ///
+ /// Checks for a "pause the game" input action.
+ ///
+ /// The player to read input for, or null for any player.
+ /// Optional rectangle to check for clicks within.
+ /// True if pause action occurred, false otherwise.
+ public bool IsPauseGame(PlayerIndex? controllingPlayer, Rectangle? rectangle = null)
+ {
+ PlayerIndex playerIndex;
+
+ bool pointInRect = false;
+
+ // Check if the cursor is in the provided rectangle and was clicked
+ if (rectangle.HasValue)
+ {
+ if (rectangle.Value.Contains(CurrentCursorLocation)
+ && (IsLeftMouseButtonClicked() || touchCount > 0))
+ {
+ pointInRect = true;
+ }
+ }
+
+ return IsNewKeyPress(Keys.Escape, controllingPlayer, out playerIndex)
+ || IsNewButtonPress(Buttons.Back, controllingPlayer, out playerIndex)
+ || IsNewButtonPress(Buttons.Start, controllingPlayer, out playerIndex)
+ || pointInRect;
+ }
+
+ ///
+ /// Checks if player has selected next on either keyboard or gamepad.
+ ///
+ /// The player to read input for, or null for any player.
+ /// True if select next action occurred, false otherwise.
+ public bool IsSelectNext(PlayerIndex? controllingPlayer)
+ {
+ PlayerIndex playerIndex;
+
+ return IsNewKeyPress(Keys.Right, controllingPlayer, out playerIndex) ||
+ IsNewButtonPress(Buttons.DPadRight, controllingPlayer, out playerIndex);
+ }
+
+ ///
+ /// Checks if player has selected previous on either keyboard or gamepad.
+ ///
+ /// The player to read input for, or null for any player.
+ /// True if select previous action occurred, false otherwise.
+ public bool IsSelectPrevious(PlayerIndex? controllingPlayer)
+ {
+ PlayerIndex playerIndex;
+
+ return IsNewKeyPress(Keys.Left, controllingPlayer, out playerIndex) ||
+ IsNewButtonPress(Buttons.DPadLeft, controllingPlayer, out playerIndex);
+ }
+
+ ///
+ /// Updates the matrix used to transform input coordinates.
+ ///
+ /// The transformation matrix to apply.
+ internal void UpdateInputTransformation(Matrix inputTransformation)
+ {
+ this.inputTransformation = inputTransformation;
+ }
+
+ ///
+ /// Transforms touch/mouse positions from screen space to game space.
+ ///
+ /// The screen-space position to transform.
+ /// The transformed position in game space.
+ public Vector2 TransformCursorLocation(Vector2 mousePosition)
+ {
+ // Transform back to cursor location
+ return Vector2.Transform(mousePosition, inputTransformation);
+ }
+
+ ///
+ /// Checks if a UI element was clicked, either by mouse or touch.
+ ///
+ /// The rectangle bounds of the UI element to check.
+ /// True if the UI element was clicked, false otherwise.
+ internal bool IsUIClicked(Rectangle rectangle)
+ {
+ bool pointInRect = false;
+
+ if (rectangle.Contains(CurrentCursorLocation)
+ && (IsLeftMouseButtonClicked() || touchCount > 0))
+ {
+ pointInRect = true;
+ }
+
+ return pointInRect;
+ }
+}
\ No newline at end of file
diff --git a/Platformer2D/Core/Inputs/TouchCollectionExtensions.cs b/Platformer2D/Core/Inputs/TouchCollectionExtensions.cs
new file mode 100644
index 00000000..b105cbe7
--- /dev/null
+++ b/Platformer2D/Core/Inputs/TouchCollectionExtensions.cs
@@ -0,0 +1,26 @@
+using Microsoft.Xna.Framework.Input.Touch;
+
+namespace Platformer2D.Core.Inputs;
+
+///
+/// Provides extension methods for the TouchCollection type.
+///
+public static class TouchCollectionExtensions
+{
+ ///
+ /// Determines if there are any touches on the screen.
+ ///
+ /// The current TouchCollection.
+ /// True if there are any touches in the Pressed or Moved state, false otherwise
+ public static bool AnyTouch(this TouchCollection touchState)
+ {
+ foreach (TouchLocation location in touchState)
+ {
+ if (location.State == TouchLocationState.Pressed || location.State == TouchLocationState.Moved)
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/Platformer2D/Core/Inputs/VirtualGamePad.cs b/Platformer2D/Core/Inputs/VirtualGamePad.cs
new file mode 100644
index 00000000..06566913
--- /dev/null
+++ b/Platformer2D/Core/Inputs/VirtualGamePad.cs
@@ -0,0 +1,135 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+using Microsoft.Xna.Framework.Input;
+using Microsoft.Xna.Framework.Input.Touch;
+
+namespace Platformer2D.Core.Inputs;
+
+///
+/// Provides an on-screen virtual gamepad interface for mobile or touch-based platforms.
+/// This class handles fading in/out visual controls, tracking touch input, and generating
+/// corresponding for seamless integration with existing game logic.
+///
+class VirtualGamePad
+{
+ private readonly Vector2 baseScreenSize;
+ private Matrix globalTransformation;
+ private readonly Texture2D texture;
+
+ private float secondsSinceLastInput;
+ private float opacity;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The base screen size used to scale touch input.
+ ///
+ /// A transformation matrix used to scale touch coordinates to match the base screen size.
+ ///
+ /// The texture representing the visual gamepad controls.
+ public VirtualGamePad(Vector2 baseScreenSize, Matrix globalTransformation, Texture2D texture)
+ {
+ this.baseScreenSize = baseScreenSize;
+ this.globalTransformation = Matrix.Invert(globalTransformation); // Inverted for touch-to-screen conversion
+ this.texture = texture;
+ secondsSinceLastInput = float.MaxValue; // Ensures controls are initially faded out
+ }
+
+ ///
+ /// Notifies the virtual gamepad that the player is actively moving.
+ /// Resets the input timer to trigger a fade-out effect for the controls.
+ ///
+ public void NotifyPlayerIsMoving()
+ {
+ secondsSinceLastInput = 0;
+ }
+
+ ///
+ /// Updates the opacity of the visual gamepad controls based on recent player activity.
+ ///
+ /// Provides timing values for the game loop.
+ public void Update(GameTime gameTime)
+ {
+ var secondsElapsed = (float)gameTime.ElapsedGameTime.TotalSeconds;
+ secondsSinceLastInput += secondsElapsed;
+
+ // Fade-out logic: If the player is moving, reduce opacity quickly
+ // Otherwise, after 4 seconds of inactivity, fade the controls back in
+ if (secondsSinceLastInput < 4)
+ opacity = Math.Max(0, opacity - secondsElapsed * 4);
+ else
+ opacity = Math.Min(1, opacity + secondsElapsed * 2);
+ }
+
+ ///
+ /// Draws the visual gamepad controls on the screen with the appropriate opacity.
+ ///
+ /// The sprite batch used to draw the textures.
+ public void Draw(SpriteBatch spriteBatch)
+ {
+ var spriteCenter = new Vector2(64, 64); // Texture's visual center for rotation pivot
+ var color = Color.Multiply(Color.White, opacity);
+
+ // Draw directional controls
+ spriteBatch.Draw(texture, new Vector2(64, baseScreenSize.Y - 64), null, color, -MathHelper.PiOver2, spriteCenter, 1, SpriteEffects.None, 0);
+ spriteBatch.Draw(texture, new Vector2(192, baseScreenSize.Y - 64), null, color, MathHelper.PiOver2, spriteCenter, 1, SpriteEffects.None, 0);
+
+ // Draw the primary action button (e.g., jump/attack)
+ spriteBatch.Draw(texture, new Vector2(baseScreenSize.X - 128, baseScreenSize.Y - 128), null, color, 0, Vector2.Zero, 1, SpriteEffects.None, 0);
+ }
+
+ ///
+ /// Generates a based on touch input and the current physical gamepad state.
+ ///
+ /// The touch collection representing active touch points.
+ /// The current state of the physical gamepad.
+ /// A combined reflecting both touch and physical input.
+ public GamePadState GetState(TouchCollection touchState, GamePadState gpState)
+ {
+ Buttons buttonsPressed = 0;
+
+ // Evaluate touch input to determine virtual button presses
+ foreach (var touch in touchState)
+ {
+ if (touch.State == TouchLocationState.Moved || touch.State == TouchLocationState.Pressed)
+ {
+ Vector2 pos = touch.Position;
+ Vector2.Transform(ref pos, ref globalTransformation, out pos);
+
+ if (pos.X < 128)
+ buttonsPressed |= Buttons.DPadLeft;
+ else if (pos.X < 256)
+ buttonsPressed |= Buttons.DPadRight;
+ else if (pos.X >= baseScreenSize.X - 128)
+ buttonsPressed |= Buttons.A;
+ }
+ }
+
+ // Combine real gamepad inputs with virtual gamepad inputs
+ var gpButtons = gpState.Buttons;
+ buttonsPressed |= gpButtons.A == ButtonState.Pressed ? Buttons.A : 0;
+ buttonsPressed |= gpButtons.B == ButtonState.Pressed ? Buttons.B : 0;
+ buttonsPressed |= gpButtons.X == ButtonState.Pressed ? Buttons.X : 0;
+ buttonsPressed |= gpButtons.Y == ButtonState.Pressed ? Buttons.Y : 0;
+
+ buttonsPressed |= gpButtons.Start == ButtonState.Pressed ? Buttons.Start : 0;
+ buttonsPressed |= gpButtons.Back == ButtonState.Pressed ? Buttons.Back : 0;
+
+ buttonsPressed |= gpState.IsButtonDown(Buttons.DPadDown) ? Buttons.DPadDown : 0;
+ buttonsPressed |= gpState.IsButtonDown(Buttons.DPadLeft) ? Buttons.DPadLeft : 0;
+ buttonsPressed |= gpState.IsButtonDown(Buttons.DPadRight) ? Buttons.DPadRight : 0;
+ buttonsPressed |= gpState.IsButtonDown(Buttons.DPadUp) ? Buttons.DPadUp : 0;
+
+ buttonsPressed |= gpButtons.BigButton == ButtonState.Pressed ? Buttons.BigButton : 0;
+ buttonsPressed |= gpButtons.LeftShoulder == ButtonState.Pressed ? Buttons.LeftShoulder : 0;
+ buttonsPressed |= gpButtons.RightShoulder == ButtonState.Pressed ? Buttons.RightShoulder : 0;
+
+ buttonsPressed |= gpButtons.LeftStick == ButtonState.Pressed ? Buttons.LeftStick : 0;
+ buttonsPressed |= gpButtons.RightStick == ButtonState.Pressed ? Buttons.RightStick : 0;
+
+ // Create a new GamePadState with the combined inputs
+ var buttons = new GamePadButtons(buttonsPressed);
+ return new GamePadState(gpState.ThumbSticks, gpState.Triggers, buttons, gpState.DPad);
+ }
+}
\ No newline at end of file
diff --git a/Platformer2D/Core/Localization/LocalizationManager.cs b/Platformer2D/Core/Localization/LocalizationManager.cs
new file mode 100644
index 00000000..cf15749b
--- /dev/null
+++ b/Platformer2D/Core/Localization/LocalizationManager.cs
@@ -0,0 +1,88 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Reflection;
+using System.Resources;
+using System.Threading;
+
+namespace Platformer2D.Core.Localization;
+
+///
+/// Manages localization settings for the game, including retrieving supported cultures and setting the current culture for localization.
+///
+internal class LocalizationManager
+{
+ ///
+ /// the culture code we default to
+ ///
+ public const string DEFAULT_CULTURE_CODE = "en-EN";
+
+ ///
+ /// Retrieves a list of supported cultures based on available language resources in the game.
+ /// This method checks the current culture settings and the satellite assemblies for available localized resources.
+ ///
+ /// A list of objects representing the cultures supported by the game.
+ ///
+ /// This method iterates through all specific cultures defined in the satellite assemblies and attempts to load the corresponding resource set.
+ /// If a resource set is found for a particular culture, that culture is added to the list of supported cultures. The invariant culture
+ /// is always included in the returned list as it represents the default (non-localized) resources.
+ ///
+ public static List GetSupportedCultures()
+ {
+ // Create a list to hold supported cultures
+ List supportedCultures = new List();
+
+ // Get the current assembly
+ Assembly assembly = Assembly.GetExecutingAssembly();
+
+ // Resource manager for your Resources.resx
+ ResourceManager resourceManager = new ResourceManager("Platformer2D.Core.Localization.Resources", assembly);
+
+ // Get all cultures defined in the satellite assemblies
+ CultureInfo[] cultures = CultureInfo.GetCultures(CultureTypes.SpecificCultures);
+
+ foreach (CultureInfo culture in cultures)
+ {
+ try
+ {
+ // Try to get the resource set for this culture
+ var resourceSet = resourceManager.GetResourceSet(culture, true, false);
+ if (resourceSet != null)
+ {
+ supportedCultures.Add(culture);
+ }
+ }
+ catch (MissingManifestResourceException)
+ {
+ // This exception is thrown when there is no .resx for the culture, ignore it
+ }
+ }
+
+ // Always add the default (invariant) culture - the base .resx file
+ supportedCultures.Add(CultureInfo.InvariantCulture);
+
+ return supportedCultures;
+ }
+
+ ///
+ /// Sets the current culture of the game based on the specified culture code.
+ /// This method updates both the current culture and UI culture for the current thread.
+ ///
+ /// The culture code (e.g., "en-US", "fr-FR") to set for the game.
+ ///
+ /// This method modifies the and properties,
+ /// which affect how dates, numbers, and other culture-specific values are formatted, as well as how localized resources are loaded.
+ ///
+ public static void SetCulture(string cultureCode)
+ {
+ if (string.IsNullOrEmpty(cultureCode))
+ cultureCode = DEFAULT_CULTURE_CODE;
+
+ // Create a CultureInfo object from the culture code
+ CultureInfo culture = new CultureInfo(cultureCode);
+
+ // Set the current culture and UI culture for the current thread
+ Thread.CurrentThread.CurrentCulture = culture;
+ Thread.CurrentThread.CurrentUICulture = culture;
+ }
+}
\ No newline at end of file
diff --git a/Platformer2D/Core/Localization/Resources.Designer.cs b/Platformer2D/Core/Localization/Resources.Designer.cs
new file mode 100644
index 00000000..fa8af729
--- /dev/null
+++ b/Platformer2D/Core/Localization/Resources.Designer.cs
@@ -0,0 +1,483 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace Platformer2D.Core.Localization {
+ using System;
+
+
+ ///
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// This class was generated by MSBuild using the GenerateResource task.
+ /// To add or remove a member, edit your .resx file then rerun MSBuild.
+ ///
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Build.Tasks.StronglyTypedResourceBuilder", "15.1.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Resources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Resources() {
+ }
+
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Platformer2D.Core.Localization.Resources", typeof(Resources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ ///
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to About.
+ ///
+ internal static string About {
+ get {
+ return ResourceManager.GetString("About", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Back.
+ ///
+ internal static string Back {
+ get {
+ return ResourceManager.GetString("Back", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Collect These!.
+ ///
+ internal static string CollectThese {
+ get {
+ return ResourceManager.GetString("CollectThese", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Display Mode : {0}.
+ ///
+ internal static string DisplayMode {
+ get {
+ return ResourceManager.GetString("DisplayMode", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Don't Die!.
+ ///
+ internal static string DontDie {
+ get {
+ return ResourceManager.GetString("DontDie", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to english (English).
+ ///
+ internal static string English {
+ get {
+ return ResourceManager.GetString("English", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Initialize can only be called once.
+ ///
+ internal static string ErrorAccelerometerInitializeOnce {
+ get {
+ return ResourceManager.GetString("ErrorAccelerometerInitializeOnce", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to You must Initialize before you can call GetState().
+ ///
+ internal static string ErrorAccelerometerMustInitialize {
+ get {
+ return ResourceManager.GetString("ErrorAccelerometerMustInitialize", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Acceleration: {0}, IsActive: {1}.
+ ///
+ internal static string ErrorAccelerometerToString {
+ get {
+ return ResourceManager.GetString("ErrorAccelerometerToString", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to A level must have an exit..
+ ///
+ internal static string ErrorLevelExit {
+ get {
+ return ResourceManager.GetString("ErrorLevelExit", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The length of line {0} is different from all preceeding lines..
+ ///
+ internal static string ErrorLevelLineLength {
+ get {
+ return ResourceManager.GetString("ErrorLevelLineLength", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to A level may only have one exit..
+ ///
+ internal static string ErrorLevelOneExit {
+ get {
+ return ResourceManager.GetString("ErrorLevelOneExit", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to A level may only have one starting point..
+ ///
+ internal static string ErrorLevelOneStartingPoint {
+ get {
+ return ResourceManager.GetString("ErrorLevelOneStartingPoint", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to A level must have a starting point..
+ ///
+ internal static string ErrorLevelStartingPoint {
+ get {
+ return ResourceManager.GetString("ErrorLevelStartingPoint", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to No animation is currently playing..
+ ///
+ internal static string ErrorNoAnimation {
+ get {
+ return ResourceManager.GetString("ErrorNoAnimation", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Unsupported tile type character '{0}' at position {1}, {2}..
+ ///
+ internal static string ErrorUnsupportedTileType {
+ get {
+ return ResourceManager.GetString("ErrorUnsupportedTileType", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Exit.
+ ///
+ internal static string Exit {
+ get {
+ return ResourceManager.GetString("Exit", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Are you sure you want to exit?.
+ ///
+ internal static string ExitQuestion {
+ get {
+ return ResourceManager.GetString("ExitQuestion", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Fullscreen.
+ ///
+ internal static string FullScreen {
+ get {
+ return ResourceManager.GetString("FullScreen", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Gems Collected.
+ ///
+ internal static string GemsCollected {
+ get {
+ return ResourceManager.GetString("GemsCollected", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Get To Here!.
+ ///
+ internal static string GetToHere {
+ get {
+ return ResourceManager.GetString("GetToHere", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Language: .
+ ///
+ internal static string Language {
+ get {
+ return ResourceManager.GetString("Language", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Let's GO!!!!.
+ ///
+ internal static string LetsGo {
+ get {
+ return ResourceManager.GetString("LetsGo", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Level Completed!.
+ ///
+ internal static string LevelCompleted {
+ get {
+ return ResourceManager.GetString("LevelCompleted", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Loading....
+ ///
+ internal static string Loading {
+ get {
+ return ResourceManager.GetString("Loading", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Main Menu.
+ ///
+ internal static string MainMenu {
+ get {
+ return ResourceManager.GetString("MainMenu", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to MonoGame Site.
+ ///
+ internal static string MonoGameSite {
+ get {
+ return ResourceManager.GetString("MonoGameSite", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to CONGRATULATIONS!! You set a new high score..
+ ///
+ internal static string NewHighScore {
+ get {
+ return ResourceManager.GetString("NewHighScore", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to No = B button, Esc.
+ ///
+ internal static string NoButtonHelp {
+ get {
+ return ResourceManager.GetString("NoButtonHelp", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to [ No ].
+ ///
+ internal static string NoButtonText {
+ get {
+ return ResourceManager.GetString("NoButtonText", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Particle Effect: .
+ ///
+ internal static string ParticleEffect {
+ get {
+ return ResourceManager.GetString("ParticleEffect", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Paused.
+ ///
+ internal static string Paused {
+ get {
+ return ResourceManager.GetString("Paused", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Play.
+ ///
+ internal static string Play {
+ get {
+ return ResourceManager.GetString("Play", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Quit.
+ ///
+ internal static string Quit {
+ get {
+ return ResourceManager.GetString("Quit", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Are you sure you want to quit?.
+ ///
+ internal static string QuitQuestion {
+ get {
+ return ResourceManager.GetString("QuitQuestion", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Resume.
+ ///
+ internal static string Resume {
+ get {
+ return ResourceManager.GetString("Resume", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to SCORE: .
+ ///
+ internal static string Score {
+ get {
+ return ResourceManager.GetString("Score", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Settings.
+ ///
+ internal static string Settings {
+ get {
+ return ResourceManager.GetString("Settings", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Show Virtual GamePad: .
+ ///
+ internal static string ShowVirtualGamePad {
+ get {
+ return ResourceManager.GetString("ShowVirtualGamePad", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Tap To Pause.
+ ///
+ internal static string TapToPause {
+ get {
+ return ResourceManager.GetString("TapToPause", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to TIME: .
+ ///
+ internal static string Time {
+ get {
+ return ResourceManager.GetString("Time", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Time Ran Out!.
+ ///
+ internal static string TimeRanOut {
+ get {
+ return ResourceManager.GetString("TimeRanOut", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Tutorial.
+ ///
+ internal static string Tutorial {
+ get {
+ return ResourceManager.GetString("Tutorial", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Windowed.
+ ///
+ internal static string Windowed {
+ get {
+ return ResourceManager.GetString("Windowed", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Yes = A Button, Space, Enter.
+ ///
+ internal static string YesButtonHelp {
+ get {
+ return ResourceManager.GetString("YesButtonHelp", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to [ Yes ].
+ ///
+ internal static string YesButtonText {
+ get {
+ return ResourceManager.GetString("YesButtonText", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to You DIED!.
+ ///
+ internal static string YouDied {
+ get {
+ return ResourceManager.GetString("YouDied", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/Platformer2D/Core/Localization/Resources.es-ES.resx b/Platformer2D/Core/Localization/Resources.es-ES.resx
new file mode 100644
index 00000000..7cccad08
--- /dev/null
+++ b/Platformer2D/Core/Localization/Resources.es-ES.resx
@@ -0,0 +1,261 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Jugar
+
+
+ Ajustes
+
+
+ Salir
+
+
+ Menú Principal
+
+
+ Volver
+
+
+ No se está reproduciendo ninguna animación.
+
+
+ Tipo de baldosa no compatible '{0}' en la posición {1}, {2}.
+
+
+ Un nivel debe tener una salida.
+
+
+ Un nivel debe tener un punto de inicio.
+
+
+ Un nivel solo puede tener un punto de inicio.
+
+
+ Un nivel solo puede tener una salida.
+
+
+ La longitud de la línea {0} es diferente de todas las líneas anteriores.
+
+
+ Debe inicializar antes de llamar a GetState()
+
+
+ Inicializar solo puede llamarse una vez
+
+
+ Aceleración: {0}, Activo: {1}
+
+
+ TIEMPO:
+
+
+ PUNTUACIÓN:
+
+
+ Cargando....
+
+
+ ¿Estás seguro de que deseas salir?
+
+
+ Reanudar
+
+
+ Abandonar
+
+
+ Pausado
+
+
+ ¿Estás seguro de que deseas abandonar?
+
+
+ Acerca de
+
+
+ [ No ]
+
+
+ [ Sí ]
+
+
+ Modo de Pantalla: {0}
+
+
+ Ventana
+
+
+ Pantalla Completa
+
+
+ Idioma:
+
+
+ Efecto de Partículas:
+
+
+ ¡Vamos!
+
+
+ Sitio de MonoGame
+
+
+ ingles (Ingles)
+
+
+ Tutorial
+
+
+ ¡Colecciona estos!
+
+
+ ¡Llegue hasta aquí!
+
+
+ ¡No mueras!
+
+
+ ¡Nivel Completado!
+
+
+ ¡Se Acabó el Tiempo!
+
+
+ ¡Has Muerto!
+
+
+ Gemas Recolectadas
+
+
+ ¡FELICITACIONES! Has conseguido un nuevo récord.
+
+
+ Mostrar GamePad virtual:
+
+
+ Sí = Botón A, Espacio, Enter
+
+
+ No = Botón B, Esc
+
+
+ Toca para pausar
+
+
diff --git a/Platformer2D/Core/Localization/Resources.fr-FR.resx b/Platformer2D/Core/Localization/Resources.fr-FR.resx
new file mode 100644
index 00000000..9d5dd810
--- /dev/null
+++ b/Platformer2D/Core/Localization/Resources.fr-FR.resx
@@ -0,0 +1,261 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Jouer
+
+
+ Paramètres
+
+
+ Sortie
+
+
+ Menu Principal
+
+
+ Retourner
+
+
+ Aucune animation n'est actuellement en cours de lecture.
+
+
+ Caractère de type de tuile non pris en charge '{0}' à la position {1}, {2}.
+
+
+ Un niveau doit avoir une sortie.
+
+
+ Un niveau doit avoir un point de départ.
+
+
+ Un niveau ne peut avoir qu'un seul point de départ.
+
+
+ Un niveau ne peut avoir qu'une seule sortie.
+
+
+ La longueur de la ligne {0} est différente de toutes les lignes précédentes.
+
+
+ Vous devez initialiser avant de pouvoir appeler GetState()
+
+
+ Initialize ne peut être appelé qu'une seule fois
+
+
+ Accélération : {0}, C'est Actif : {1}
+
+
+ TEMPS:
+
+
+ SCORE:
+
+
+ Chargement...
+
+
+ Etes-vous sûr de vouloir sortier?
+
+
+ Reprendre
+
+
+ Quitter
+
+
+ En Pause
+
+
+ Etes-vous sûr de vouloir quitter?
+
+
+ À Propos
+
+
+ [ Non ]
+
+
+ [ Oui ]
+
+
+ Mode D'affichage : {0}
+
+
+ Fenêtré
+
+
+ Plein écran
+
+
+ Langue:
+
+
+ Effet De Particules:
+
+
+ Allons-y!!!!
+
+
+ Site MonoGame
+
+
+ anglais (Anglais)
+
+
+ Tutorial
+
+
+ Collectionnez-les!
+
+
+ Arrive ici!
+
+
+ Ne meurs pas!
+
+
+ Niveau Terminé !
+
+
+ Le Temps est Écoulé !
+
+
+ Tu es MORT !
+
+
+ Gemmes Collectées
+
+
+ FÉLICITATIONS !! Vous avez établi un nouveau record.
+
+
+ Afficher le GamePad virtuel:
+
+
+ Non = Bouton V, Esc
+
+
+ Oui = Bouton A, Espace, Entrée
+
+
+ Appuyez pour mettre en pause
+
+
diff --git a/Platformer2D/Core/Localization/Resources.resx b/Platformer2D/Core/Localization/Resources.resx
new file mode 100644
index 00000000..6bf57969
--- /dev/null
+++ b/Platformer2D/Core/Localization/Resources.resx
@@ -0,0 +1,261 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Play
+
+
+ Settings
+
+
+ Exit
+
+
+ Main Menu
+
+
+ Back
+
+
+ No animation is currently playing.
+
+
+ Unsupported tile type character '{0}' at position {1}, {2}.
+
+
+ A level must have an exit.
+
+
+ A level must have a starting point.
+
+
+ A level may only have one starting point.
+
+
+ A level may only have one exit.
+
+
+ The length of line {0} is different from all preceeding lines.
+
+
+ You must Initialize before you can call GetState()
+
+
+ Initialize can only be called once
+
+
+ Acceleration: {0}, IsActive: {1}
+
+
+ TIME:
+
+
+ SCORE:
+
+
+ Loading...
+
+
+ Are you sure you want to exit?
+
+
+ Resume
+
+
+ Quit
+
+
+ Paused
+
+
+ Are you sure you want to quit?
+
+
+ About
+
+
+ [ No ]
+
+
+ [ Yes ]
+
+
+ Display Mode : {0}
+
+
+ Windowed
+
+
+ Fullscreen
+
+
+ Language:
+
+
+ Particle Effect:
+
+
+ Let's GO!!!!
+
+
+ MonoGame Site
+
+
+ english (English)
+
+
+ Tutorial
+
+
+ Collect These!
+
+
+ Get To Here!
+
+
+ Don't Die!
+
+
+ Level Completed!
+
+
+ Time Ran Out!
+
+
+ You DIED!
+
+
+ Gems Collected
+
+
+ CONGRATULATIONS!! You set a new high score.
+
+
+ Yes = A Button, Space, Enter
+
+
+ No = B button, Esc
+
+
+ Show Virtual GamePad:
+
+
+ Tap To Pause
+
+
\ No newline at end of file
diff --git a/Platformer2D/Core/Platformer2DGame.cs b/Platformer2D/Core/Platformer2DGame.cs
new file mode 100644
index 00000000..ece236b9
--- /dev/null
+++ b/Platformer2D/Core/Platformer2DGame.cs
@@ -0,0 +1,150 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using Platformer2D.Core.Effects;
+using Platformer2D.Core.Localization;
+using Platformer2D.Core.Settings;
+using Platformer2D.ScreenManagers;
+using Platformer2D.Screens;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace Platformer2D.Core
+{
+ ///
+ /// The main class for the game, responsible for managing game components, settings,
+ /// and platform-specific configurations.
+ ///
+ ///
+ /// This class is the entry point for the game and handles initialization, content loading,
+ /// and screen management.
+ /// }
+ public class Platformer2DGame : Game
+ {
+ // Resources for drawing.
+ private GraphicsDeviceManager graphicsDeviceManager;
+
+ // Manages the game's screen transitions and screens.
+ private ScreenManager screenManager;
+
+ // Manages game settings, such as preferences and configurations.
+ private SettingsManager settingsManager;
+
+ // Manages leaderboard data for tracking high scores and achievements.
+ private SettingsManager leaderboardManager;
+
+ // Texture for rendering particles.
+ private Texture2D particleTexture;
+
+ // Manages particle effects in the game.
+ private ParticleManager particleManager;
+
+ ///
+ /// Indicates if the game is running on a mobile platform.
+ ///
+ public readonly static bool IsMobile = OperatingSystem.IsAndroid() || OperatingSystem.IsIOS();
+
+ ///
+ /// Indicates if the game is running on a desktop platform.
+ ///
+ public readonly static bool IsDesktop = OperatingSystem.IsMacOS() || OperatingSystem.IsLinux() || OperatingSystem.IsWindows();
+
+ ///
+ /// Initializes a new instance of the game. Configures platform-specific settings,
+ /// initializes services like settings and leaderboard managers, and sets up the
+ /// screen manager for screen transitions.
+ ///
+ public Platformer2DGame()
+ {
+ graphicsDeviceManager = new GraphicsDeviceManager(this);
+
+ // Share GraphicsDeviceManager as a service.
+ Services.AddService(typeof(GraphicsDeviceManager), graphicsDeviceManager);
+
+ // Determine the appropriate settings storage based on the platform.
+ ISettingsStorage storage;
+ if (IsMobile)
+ {
+ storage = new MobileSettingsStorage();
+ graphicsDeviceManager.IsFullScreen = true;
+ IsMouseVisible = false;
+ }
+ else if (IsDesktop)
+ {
+ storage = new DesktopSettingsStorage();
+ graphicsDeviceManager.IsFullScreen = false;
+ graphicsDeviceManager.PreferredBackBufferWidth = 1280;
+ graphicsDeviceManager.PreferredBackBufferHeight = 720;
+ IsMouseVisible = true;
+ }
+ else
+ {
+ throw new PlatformNotSupportedException();
+ }
+
+ // Initialize settings and leaderboard managers.
+ settingsManager = new SettingsManager(storage);
+ Services.AddService(typeof(SettingsManager), settingsManager);
+
+ leaderboardManager = new SettingsManager(storage);
+ Services.AddService(typeof(SettingsManager), leaderboardManager);
+
+ Content.RootDirectory = "Content";
+
+ // Configure screen orientations.
+ graphicsDeviceManager.SupportedOrientations = DisplayOrientation.LandscapeLeft | DisplayOrientation.LandscapeRight;
+
+ // Initialize the screen manager.
+ screenManager = new ScreenManager(this);
+ Components.Add(screenManager);
+ }
+
+ ///
+ /// Initializes the game, including setting up localization and adding the
+ /// initial screens to the ScreenManager.
+ ///
+ protected override void Initialize()
+ {
+ base.Initialize();
+
+ // Load supported languages and set the default language.
+ List cultures = LocalizationManager.GetSupportedCultures();
+ var languages = new List();
+ for (int i = 0; i < cultures.Count; i++)
+ {
+ languages.Add(cultures[i]);
+ }
+
+ // Ensure the language index is valid, default to 0 if out of range
+ int languageIndex = settingsManager.Settings.Language;
+ if (languageIndex < 0 || languageIndex >= languages.Count)
+ {
+ languageIndex = 0;
+ settingsManager.Settings.Language = 0;
+ settingsManager.Save();
+ }
+
+ var selectedLanguage = languages[languageIndex].Name;
+ LocalizationManager.SetCulture(selectedLanguage);
+
+ // Add background and main menu screens.
+ screenManager.AddScreen(new BackgroundScreen(), null);
+ screenManager.AddScreen(new MainMenuScreen(), null);
+ }
+
+ ///
+ /// Loads game content, such as textures and particle systems.
+ ///
+ protected override void LoadContent()
+ {
+ base.LoadContent();
+
+ // Load a texture for particles and initialize the particle manager.
+ particleTexture = Content.Load("Sprites/blank");
+ particleManager = new ParticleManager(particleTexture, new Vector2(400, 200));
+
+ // Share the particle manager as a service.
+ Services.AddService(typeof(ParticleManager), particleManager);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Platformer2D/Core/ScreenManagers/ScreenManager.cs b/Platformer2D/Core/ScreenManagers/ScreenManager.cs
new file mode 100644
index 00000000..2186575a
--- /dev/null
+++ b/Platformer2D/Core/ScreenManagers/ScreenManager.cs
@@ -0,0 +1,339 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using Platformer2D.Core.Inputs;
+using Platformer2D.Screens;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content;
+using Microsoft.Xna.Framework.Graphics;
+using Microsoft.Xna.Framework.Input.Touch;
+
+namespace Platformer2D.ScreenManagers;
+
+///
+/// The ScreenManager is a component responsible for managing multiple instances.
+/// It maintains a stack of screens, invokes their Update and Draw methods, and automatically routes input
+/// to the topmost active screen.
+///
+public class ScreenManager : DrawableGameComponent
+{
+ // List of active screens and screens pending update.
+ private readonly List screens = new List();
+ private readonly List screensToUpdate = new List();
+
+ // Manages player input.
+ private readonly InputState inputState = new InputState();
+
+ // Shared resources for drawing and content management.
+ private SpriteBatch spriteBatch;
+ private SpriteFont font;
+ private Texture2D blankTexture;
+
+ private bool isInitialized;
+ private bool traceEnabled;
+
+ internal const int BASE_BUFFER_WIDTH = 800;
+ internal const int BASE_BUFFER_HEIGHT = 400;
+
+ private int backbufferWidth;
+ /// Gets or sets the current backbuffer width.
+ public int BackbufferWidth { get => backbufferWidth; set => backbufferWidth = value; }
+
+ private int backbufferHeight;
+ /// Gets or sets the current backbuffer height.
+ public int BackbufferHeight { get => backbufferHeight; set => backbufferHeight = value; }
+
+ private Vector2 baseScreenSize = new Vector2(BASE_BUFFER_WIDTH, BASE_BUFFER_HEIGHT);
+ /// Gets or sets the base screen size used for scaling calculations.
+ public Vector2 BaseScreenSize { get => baseScreenSize; set => baseScreenSize = value; }
+
+ private Matrix globalTransformation;
+ /// Gets or sets the global transformation matrix for scaling and positioning.
+ public Matrix GlobalTransformation { get => globalTransformation; set => globalTransformation = value; }
+
+ ///
+ /// Provides access to a shared SpriteBatch instance for drawing operations.
+ ///
+ public SpriteBatch SpriteBatch => spriteBatch;
+
+ ///
+ /// Provides access to a shared SpriteFont instance for text rendering.
+ ///
+ public SpriteFont Font => font;
+
+ ///
+ /// Enables or disables screen tracing for debugging purposes.
+ /// When enabled, the manager prints a list of active screens during updates.
+ ///
+ public bool TraceEnabled { get => traceEnabled; set => traceEnabled = value; }
+
+ Rectangle safeArea = new Rectangle(0, 0, BASE_BUFFER_WIDTH, BASE_BUFFER_HEIGHT);
+ ///
+ /// Returns the portion of the screen where drawing is safely allowed.
+ ///
+ public Rectangle SafeArea
+ {
+ get
+ {
+ return safeArea;
+ }
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The associated Game instance.
+ public ScreenManager(Game game) : base(game)
+ {
+ TouchPanel.EnabledGestures = GestureType.None;
+ }
+
+ ///
+ /// Initializes the ScreenManager and any required services.
+ ///
+ public override void Initialize()
+ {
+ base.Initialize();
+ isInitialized = true;
+ }
+
+ ///
+ /// Loads graphical content for the ScreenManager and all active screens.
+ ///
+ protected override void LoadContent()
+ {
+ ContentManager content = Game.Content;
+ spriteBatch = new SpriteBatch(GraphicsDevice);
+ font = content.Load("Fonts/Hud");
+ blankTexture = content.Load("Sprites/blank");
+
+ foreach (GameScreen screen in screens)
+ {
+ screen.LoadContent();
+ }
+ }
+
+ ///
+ /// Unloads graphical content for all screens.
+ ///
+ protected override void UnloadContent()
+ {
+ foreach (GameScreen screen in screens)
+ {
+ screen.UnloadContent();
+ }
+ }
+
+ ///
+ /// Updates the active screens and processes input.
+ ///
+ /// Provides a snapshot of the game's timing state.
+ public override void Update(GameTime gameTime)
+ {
+ inputState.Update(gameTime, BaseScreenSize);
+ screensToUpdate.Clear();
+ screensToUpdate.AddRange(screens);
+
+ bool otherScreenHasFocus = !Game.IsActive;
+ bool coveredByOtherScreen = false;
+
+ while (screensToUpdate.Count > 0)
+ {
+ GameScreen screen = screensToUpdate[^1];
+ screensToUpdate.RemoveAt(screensToUpdate.Count - 1);
+
+ screen.Update(gameTime, otherScreenHasFocus, coveredByOtherScreen);
+
+ if (screen.ScreenState == ScreenState.TransitionOn || screen.ScreenState == ScreenState.Active)
+ {
+ if (!otherScreenHasFocus)
+ {
+ screen.HandleInput(gameTime, inputState);
+ otherScreenHasFocus = true;
+ }
+
+ if (!screen.IsPopup)
+ coveredByOtherScreen = true;
+ }
+ }
+
+ if (traceEnabled)
+ TraceScreens();
+ }
+
+ ///
+ /// Prints active screen names to the debug console for diagnostic purposes.
+ ///
+ private void TraceScreens()
+ {
+ var screenNames = screens.Select(screen => screen.GetType().Name).ToList();
+ Debug.WriteLine(string.Join(", ", screenNames));
+ }
+
+ ///
+ /// Draws the active screens.
+ ///
+ /// Provides a snapshot of the game's timing state.
+ public override void Draw(GameTime gameTime)
+ {
+ foreach (var screen in screens)
+ {
+ if (screen.ScreenState != ScreenState.Hidden)
+ {
+ screen.Draw(gameTime);
+ }
+ }
+ }
+
+ ///
+ /// Releases resources used by the object.
+ ///
+ ///
+ /// True to release both managed and unmanaged resources; false to release only unmanaged resources.
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ try
+ {
+ if (disposing)
+ {
+ // Dispose of managed resources.
+ spriteBatch?.Dispose();
+ }
+ // No unmanaged resources to dispose in this example.
+ }
+ finally
+ {
+ // Call the base class's Dispose method to ensure proper cleanup.
+ base.Dispose(disposing);
+ }
+ }
+
+ ///
+ /// Adds a new screen to the ScreenManager.
+ ///
+ /// The screen to add.
+ /// The controlling player, if applicable.
+ public void AddScreen(GameScreen screen, PlayerIndex? controllingPlayer)
+ {
+ screen.ControllingPlayer = controllingPlayer;
+ screen.ScreenManager = this;
+ screen.IsExiting = false;
+
+ if (isInitialized)
+ {
+ screen.LoadContent();
+ }
+
+ screens.Add(screen);
+ TouchPanel.EnabledGestures = screen.EnabledGestures;
+ }
+
+ ///
+ /// Removes a screen from the ScreenManager.
+ ///
+ /// The screen to remove.
+ public void RemoveScreen(GameScreen screen)
+ {
+ if (isInitialized)
+ {
+ screen.UnloadContent();
+ }
+
+ screens.Remove(screen);
+ screensToUpdate.Remove(screen);
+
+ if (screens.Count > 0)
+ {
+ TouchPanel.EnabledGestures = screens[^1].EnabledGestures;
+ }
+ }
+
+ ///
+ /// Returns an array of all active screens managed by the ScreenManager.
+ ///
+ ///
+ /// An array containing all current GameScreen instances. This array is a copy
+ /// of the internal list to ensure screens are only added or removed using
+ /// and
+ /// .
+ ///
+ public GameScreen[] GetScreens()
+ {
+ return screens.ToArray();
+ }
+
+ ///
+ /// Draws a translucent black fullscreen sprite. This is used for fading
+ /// screens in and out, or for darkening the background behind popups.
+ ///
+ /// The opacity level of the fade (0 = fully transparent, 1 = fully opaque).
+ public void FadeBackBufferToBlack(float alpha)
+ {
+ // Draw without transformation to cover the entire backbuffer
+ spriteBatch.Begin(SpriteSortMode.Deferred, null, null, null, null, null, null);
+
+ spriteBatch.Draw(blankTexture,
+ new Rectangle(0, 0, backbufferWidth, backbufferHeight),
+ Color.Black * alpha);
+
+ spriteBatch.End();
+ }
+
+ ///
+ /// Scales the game presentation area to match the screen's aspect ratio.
+ ///
+ public void ScalePresentationArea()
+ {
+ // Validate parameters before calculation
+ if (GraphicsDevice == null || baseScreenSize.X <= 0 || baseScreenSize.Y <= 0)
+ {
+ throw new InvalidOperationException("Invalid graphics configuration");
+ }
+
+ // Fetch screen dimensions
+ backbufferWidth = GraphicsDevice.PresentationParameters.BackBufferWidth;
+ backbufferHeight = GraphicsDevice.PresentationParameters.BackBufferHeight;
+
+ // Prevent division by zero
+ if (backbufferHeight == 0 || baseScreenSize.Y == 0)
+ {
+ return;
+ }
+
+ // Calculate aspect ratios
+ float baseAspectRatio = baseScreenSize.X / baseScreenSize.Y;
+ float screenAspectRatio = backbufferWidth / (float)backbufferHeight;
+
+ // Determine uniform scaling factor
+ float scalingFactor;
+ float horizontalOffset = 0;
+ float verticalOffset = 0;
+
+ if (screenAspectRatio > baseAspectRatio)
+ {
+ // Wider screen: scale by height
+ scalingFactor = backbufferHeight / baseScreenSize.Y;
+
+ // Centre things horizontally.
+ horizontalOffset = (backbufferWidth - baseScreenSize.X * scalingFactor) / 2;
+ }
+ else
+ {
+ // Taller screen: scale by width
+ scalingFactor = backbufferWidth / baseScreenSize.X;
+
+ // Don't center vertically - align to top
+ verticalOffset = -30;
+ }
+
+ // Update the transformation matrix
+ globalTransformation = Matrix.CreateScale(scalingFactor) *
+ Matrix.CreateTranslation(horizontalOffset, verticalOffset, 0);
+
+ // Update the inputTransformation with the Inverted globalTransformation
+ inputState.UpdateInputTransformation(Matrix.Invert(globalTransformation));
+
+ }
+}
\ No newline at end of file
diff --git a/Platformer2D/Core/Screens/AboutScreen.cs b/Platformer2D/Core/Screens/AboutScreen.cs
new file mode 100644
index 00000000..2694787d
--- /dev/null
+++ b/Platformer2D/Core/Screens/AboutScreen.cs
@@ -0,0 +1,69 @@
+using Platformer2D.Core.Localization;
+
+namespace Platformer2D.Screens;
+
+///
+/// Represents the "About" screen, providing information about the game and its technology.
+/// This screen displays credits and links to the MonoGame website.
+///
+///
+/// This class extends , inheriting its menu management capabilities.
+///
+class AboutScreen : MenuScreen
+{
+ private MenuEntry builtWithMonoGameMenuEntry;
+ private MenuEntry monoGameWebsiteMenuEntry;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// This constructor sets the screen's title and creates the menu entries.
+ /// It also hooks up event handlers for menu entry selections.
+ ///
+ public AboutScreen()
+ : base(Resources.About) // Assumes Resources.About contains the screen title
+ {
+ // Create the static label entry. isabled as it's a label
+ builtWithMonoGameMenuEntry = new MenuEntry("#BuiltWithMonoGame", false);
+
+ // Create the clickable link entry.
+ monoGameWebsiteMenuEntry = new MenuEntry(Resources.MonoGameSite);
+
+ // Create the "Back" button entry.
+ MenuEntry back = new MenuEntry(Resources.Back);
+
+ // Attach event handlers for menu entry selections.
+ monoGameWebsiteMenuEntry.Selected += MonoGameWebsiteMenuSelected;
+ back.Selected += OnCancel;
+
+ // Add the menu entries to the screen.
+ MenuEntries.Add(builtWithMonoGameMenuEntry);
+ MenuEntries.Add(monoGameWebsiteMenuEntry);
+ MenuEntries.Add(back);
+ }
+
+ ///
+ /// Handles the selection event for the MonoGame website menu entry.
+ ///
+ /// The source of the event.
+ /// The instance containing the event data.
+ private void MonoGameWebsiteMenuSelected(object sender, PlayerIndexEventArgs e)
+ {
+ LaunchDefaultBrowser("https://www.monogame.net/");
+ }
+
+ ///
+ /// Launches the default web browser with the specified URL.
+ ///
+ /// The URL to open in the browser.
+ ///
+ /// This method uses to launch the browser.
+ /// Note: Platform-specific adjustments might be necessary for cross-platform compatibility.
+ ///
+ private static void LaunchDefaultBrowser(string url)
+ {
+ // UseShellExecute is crucial for launching the default browser.
+ System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(url) { UseShellExecute = true });
+ }
+}
\ No newline at end of file
diff --git a/Platformer2D/Core/Screens/BackgroundScreen.cs b/Platformer2D/Core/Screens/BackgroundScreen.cs
new file mode 100644
index 00000000..aaf8c594
--- /dev/null
+++ b/Platformer2D/Core/Screens/BackgroundScreen.cs
@@ -0,0 +1,80 @@
+using System;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace Platformer2D.Screens;
+
+///
+/// The BackgroundScreen renders a static background image behind all other menu screens.
+/// It remains fixed and unaffected by transitions on top of it.
+///
+class BackgroundScreen : GameScreen
+{
+ ContentManager content;
+ Texture2D backgroundTexture;
+
+ ///
+ /// Initializes a new instance of the BackgroundScreen class.
+ /// Sets the transition times for screen appearance and disappearance.
+ ///
+ public BackgroundScreen()
+ {
+ TransitionOnTime = TimeSpan.FromSeconds(0.5);
+ TransitionOffTime = TimeSpan.FromSeconds(0.5);
+ }
+
+ ///
+ /// Loads the background texture using a local ContentManager.
+ /// This allows for independent unloading of the background texture.
+ ///
+ public override void LoadContent()
+ {
+ if (content == null)
+ content = new ContentManager(ScreenManager.Game.Services, "Content");
+
+ backgroundTexture = content.Load("Backgrounds/Layer0_2");
+ }
+
+ ///
+ /// Unloads the background texture by unloading the local ContentManager.
+ ///
+ public override void UnloadContent()
+ {
+ content.Unload();
+ }
+
+ ///
+ /// Updates the background screen.
+ /// Forces the screen to remain active even when covered by other screens.
+ ///
+ /// Provides a snapshot of timing values.
+ /// Indicates whether another screen has focus.
+ /// Indicates whether the screen is covered by another screen.
+ public override void Update(GameTime gameTime, bool otherScreenHasFocus, bool coveredByOtherScreen)
+ {
+ // Force coveredByOtherScreen to false to prevent the screen from transitioning off.
+ base.Update(gameTime, otherScreenHasFocus, false);
+ }
+
+ ///
+ /// Draws the background screen.
+ /// Clears the screen and renders the background texture with transition alpha.
+ ///
+ /// Provides a snapshot of timing values.
+ public override void Draw(GameTime gameTime)
+ {
+ // Clear the screen to black to prevent visual artifacts.
+ ScreenManager.GraphicsDevice.Clear(ClearOptions.Target, Color.Black, 0, 0);
+
+ SpriteBatch spriteBatch = ScreenManager.SpriteBatch;
+ Rectangle fullscreen = new Rectangle(0, 0, (int)ScreenManager.BaseScreenSize.X, (int)ScreenManager.BaseScreenSize.Y);
+
+ spriteBatch.Begin(SpriteSortMode.Deferred, null, null, null, null, null, ScreenManager.GlobalTransformation);
+
+ // Draw the background texture with the current transition alpha.
+ spriteBatch.Draw(backgroundTexture, fullscreen, new Color(TransitionAlpha, TransitionAlpha, TransitionAlpha));
+
+ spriteBatch.End();
+ }
+}
\ No newline at end of file
diff --git a/Platformer2D/Core/Screens/EndOfLevelMessageState.cs b/Platformer2D/Core/Screens/EndOfLevelMessageState.cs
new file mode 100644
index 00000000..61173240
--- /dev/null
+++ b/Platformer2D/Core/Screens/EndOfLevelMessageState.cs
@@ -0,0 +1,11 @@
+namespace Platformer2D.Screens;
+
+///
+/// The various stage of the pop-up message after completing a level
+///
+internal enum EndOfLevelMessageState
+{
+ NotShowing,
+ Show,
+ Showing,
+}
\ No newline at end of file
diff --git a/Platformer2D/Core/Screens/GameScreen.cs b/Platformer2D/Core/Screens/GameScreen.cs
new file mode 100644
index 00000000..3ced7b10
--- /dev/null
+++ b/Platformer2D/Core/Screens/GameScreen.cs
@@ -0,0 +1,303 @@
+using System;
+using Platformer2D.Core.Inputs;
+using Platformer2D.ScreenManagers;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Input.Touch;
+
+namespace Platformer2D.Screens;
+
+///
+/// A screen is a single layer that has update and draw logic, and which
+/// can be combined with other layers to build up a complex menu system.
+/// For instance the main menu, the options menu, the "are you sure you
+/// want to quit" message box, and the main game itself are all implemented
+/// as screens.
+///
+public abstract class GameScreen
+{
+ private bool isPopup = false;
+ ///
+ /// Normally when one screen is brought up over the top of another,
+ /// the first screen will transition off to make room for the new
+ /// one. This property indicates whether the screen is only a small
+ /// popup, in which case screens underneath it do not need to bother
+ /// transitioning off.
+ ///
+ public bool IsPopup
+ {
+ get { return isPopup; }
+ protected set { isPopup = value; }
+ }
+
+ private TimeSpan transitionOnTime = TimeSpan.Zero;
+ ///
+ /// Indicates how long the screen takes to
+ /// transition on when it is activated.
+ ///
+ public TimeSpan TransitionOnTime
+ {
+ get { return transitionOnTime; }
+ protected set { transitionOnTime = value; }
+ }
+
+ private TimeSpan transitionOffTime = TimeSpan.Zero;
+ ///
+ /// Indicates how long the screen takes to
+ /// transition off when it is deactivated.
+ ///
+ public TimeSpan TransitionOffTime
+ {
+ get { return transitionOffTime; }
+ protected set { transitionOffTime = value; }
+ }
+
+ private float transitionPosition = 1;
+ ///
+ /// Gets the current position of the screen transition, ranging
+ /// from zero (fully active, no transition) to one (transitioned
+ /// fully off to nothing).
+ ///
+ public float TransitionPosition
+ {
+ get { return transitionPosition; }
+ protected set { transitionPosition = value; }
+ }
+
+ ///
+ /// Gets the current alpha of the screen transition, ranging
+ /// from 1 (fully active, no transition) to 0 (transitioned
+ /// fully off to nothing).
+ ///
+ public float TransitionAlpha
+ {
+ get { return 1f - TransitionPosition; }
+ }
+
+ private ScreenState screenState = ScreenState.TransitionOn;
+ ///
+ /// Gets the current screen transition state.
+ ///
+ public ScreenState ScreenState
+ {
+ get { return screenState; }
+ protected set { screenState = value; }
+ }
+
+ private bool isExiting = false;
+ ///
+ /// There are two possible reasons why a screen might be transitioning
+ /// off. It could be temporarily going away to make room for another
+ /// screen that is on top of it, or it could be going away for good.
+ /// This property indicates whether the screen is exiting for real:
+ /// if set, the screen will automatically remove itself as soon as the
+ /// transition finishes.
+ ///
+ public bool IsExiting
+ {
+ get { return isExiting; }
+ protected internal set { isExiting = value; }
+ }
+
+ private bool otherScreenHasFocus;
+ ///
+ /// Checks whether this screen is active and can respond to user input.
+ ///
+ public bool IsActive
+ {
+ get
+ {
+ return !otherScreenHasFocus &&
+ (screenState == ScreenState.TransitionOn ||
+ screenState == ScreenState.Active);
+ }
+ }
+
+ private ScreenManager screenManager;
+ ///
+ /// Gets the manager that this screen belongs to.
+ ///
+ public ScreenManager ScreenManager
+ {
+ get { return screenManager; }
+ internal set { screenManager = value; }
+ }
+
+ private PlayerIndex? controllingPlayer;
+ ///
+ /// Gets the index of the player who is currently controlling this screen,
+ /// or null if it is accepting input from any player. This is used to lock
+ /// the game to a specific player profile. The main menu responds to input
+ /// from any connected gamepad, but whichever player makes a selection from
+ /// this menu is given control over all subsequent screens, so other gamepads
+ /// are inactive until the controlling player returns to the main menu.
+ ///
+ public PlayerIndex? ControllingPlayer
+ {
+ get { return controllingPlayer; }
+ internal set { controllingPlayer = value; }
+ }
+
+ private GestureType enabledGestures = GestureType.None;
+ ///
+ /// Gets the gestures the screen is interested in. Screens should be as specific
+ /// as possible with gestures to increase the accuracy of the gesture engine.
+ /// For example, most menus only need Tap or perhaps Tap and VerticalDrag to operate.
+ /// These gestures are handled by the ScreenManager when screens change and
+ /// all gestures are placed in the InputState passed to the HandleInput method.
+ ///
+ public GestureType EnabledGestures
+ {
+ get { return enabledGestures; }
+ protected set
+ {
+ enabledGestures = value;
+
+ // the screen manager handles this during screen changes, but
+ // if this screen is active and the gesture types are changing,
+ // we have to update the TouchPanel ourself.
+ if (ScreenState == ScreenState.Active)
+ {
+ TouchPanel.EnabledGestures = value;
+ }
+ }
+ }
+
+ ///
+ /// Load graphics content for the screen, but 1st scale the presentation area.
+ ///
+ public virtual void LoadContent()
+ {
+ ScreenManager.ScalePresentationArea();
+ }
+
+
+ ///
+ /// Unload content for the screen.
+ ///
+ public virtual void UnloadContent() { }
+
+ ///
+ /// Allows the screen to run logic, such as updating the transition position.
+ /// Unlike HandleInput, this method is called regardless of whether the screen
+ /// is active, hidden, or in the middle of a transition.
+ ///
+ /// Provides a snapshot of timing values.
+ /// Indicates whether another screen has focus.
+ /// Indicates whether the screen is covered by another screen.
+ public virtual void Update(GameTime gameTime, bool otherScreenHasFocus, bool coveredByOtherScreen)
+ {
+ // Stores whether another screen has focus.
+ this.otherScreenHasFocus = otherScreenHasFocus;
+
+ if (isExiting)
+ {
+ // If the screen is marked for exit, initiate the transition off.
+ screenState = ScreenState.TransitionOff;
+
+ // Update the transition position towards the "off" state.
+ if (!UpdateTransition(gameTime, transitionOffTime, 1))
+ {
+ // If the transition is complete, remove the screen.
+ ScreenManager.RemoveScreen(this);
+ }
+ }
+ else if (coveredByOtherScreen)
+ {
+ // If another screen is covering this one, start the transition off.
+ if (UpdateTransition(gameTime, transitionOffTime, 1))
+ {
+ // Still transitioning off.
+ screenState = ScreenState.TransitionOff;
+ }
+ else
+ {
+ // Transition off complete, hide the screen.
+ screenState = ScreenState.Hidden;
+ }
+ }
+ else
+ {
+ // If no other screen is covering, start the transition on.
+ if (UpdateTransition(gameTime, transitionOnTime, -1))
+ {
+ // Still transitioning on.
+ screenState = ScreenState.TransitionOn;
+ }
+ else
+ {
+ // Transition on complete, activate the screen.
+ screenState = ScreenState.Active;
+ }
+ }
+
+ // Check if the back buffer size has changed (e.g., window resize).
+ if (ScreenManager.BackbufferHeight != ScreenManager.GraphicsDevice.PresentationParameters.BackBufferHeight
+ || ScreenManager.BackbufferWidth != ScreenManager.GraphicsDevice.PresentationParameters.BackBufferWidth)
+ {
+ // Adjust the presentation area to match the new back buffer size.
+ ScreenManager.ScalePresentationArea();
+ }
+ }
+
+ ///
+ /// Helper for updating the screen transition position.
+ ///
+ /// Provides a snapshot of timing values.
+ /// The total time for the transition.
+ /// The direction of the transition (-1 for on, 1 for off).
+ /// True if the transition is still in progress; otherwise, false.
+ bool UpdateTransition(GameTime gameTime, TimeSpan time, int direction)
+ {
+ // Calculate the amount to move the transition position.
+ float transitionDelta;
+
+ if (time == TimeSpan.Zero)
+ transitionDelta = 1;
+ else
+ transitionDelta = (float)(gameTime.ElapsedGameTime.TotalMilliseconds / time.TotalMilliseconds);
+
+ // Update the transition position.
+ transitionPosition += transitionDelta * direction;
+
+ // Check if the transition has reached its end.
+ if (((direction < 0) && (transitionPosition <= 0)) || ((direction > 0) && (transitionPosition >= 1)))
+ {
+ // Clamp the transition position to the valid range.
+ transitionPosition = MathHelper.Clamp(transitionPosition, 0, 1);
+ return false; // Transition finished.
+ }
+
+ // Transition is still in progress.
+ return true;
+ }
+
+ ///
+ /// Handles user input for the screen. Called only when the screen is active.
+ ///
+ /// Provides a snapshot of timing values.
+ /// The current input state.
+ public virtual void HandleInput(GameTime gameTime, InputState inputState) { }
+
+ ///
+ /// Draws the screen content.
+ ///
+ /// Provides a snapshot of timing values.
+ public virtual void Draw(GameTime gameTime) { }
+
+ ///
+ /// Initiates the screen's exit process, respecting transition timings.
+ ///
+ public void ExitScreen()
+ {
+ if (TransitionOffTime == TimeSpan.Zero)
+ {
+ // If no transition time, remove the screen immediately.
+ ScreenManager.RemoveScreen(this);
+ }
+ else
+ {
+ // Mark the screen for exiting, which triggers the transition off.
+ isExiting = true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Platformer2D/Core/Screens/GameplayScreen.cs b/Platformer2D/Core/Screens/GameplayScreen.cs
new file mode 100644
index 00000000..d83c6446
--- /dev/null
+++ b/Platformer2D/Core/Screens/GameplayScreen.cs
@@ -0,0 +1,363 @@
+using System;
+using System.IO;
+using Platformer2D.Core;
+using Platformer2D.Core.Effects;
+using Platformer2D.Core.Inputs;
+using Platformer2D.Core.Localization;
+using Platformer2D.Core.Settings;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content;
+using Microsoft.Xna.Framework.Graphics;
+using Microsoft.Xna.Framework.Input;
+using Microsoft.Xna.Framework.Input.Touch;
+using Microsoft.Xna.Framework.Media;
+
+namespace Platformer2D.Screens;
+
+///
+/// This screen implements the actual game logic and manages the gameplay experience.
+/// It controls level loading, player interaction, game state updates, and rendering
+/// for the active gameplay session.
+///
+partial class GameplayScreen : GameScreen
+{
+ ///
+ /// Content manager for loading and managing game assets.
+ ///
+ ContentManager content;
+
+ ///
+ /// Controls the opacity of the pause screen overlay when the game is paused.
+ ///
+ float pauseAlpha;
+
+ ///
+ /// SpriteBatch instance used for rendering 2D elements.
+ ///
+ private SpriteBatch spriteBatch;
+
+ ///
+ /// Current level index (zero-based) in the game progression.
+ ///
+ private int levelIndex = 0;
+
+ ///
+ /// Reference to the currently active Level object.
+ ///
+ private Level level;
+
+ ///
+ /// Flag indicating if the player has chosen to continue after level completion or failure.
+ ///
+ private bool wasContinuePressed;
+
+ ///
+ /// Current state of the gamepad input for the active player.
+ ///
+ private GamePadState currentGamePadState;
+
+ ///
+ /// Previous state of the gamepad input for the active player, used to detect button press events.
+ ///
+ private GamePadState previousGamePadState;
+
+ ///
+ /// Current state of the keyboard input for the active player.
+ ///
+ private KeyboardState currentKeyboardState;
+
+ ///
+ /// Current touch input state, used for mobile device controls.
+ ///
+ private TouchCollection currentTouchState;
+
+ ///
+ /// Manager for particle effects in the game.
+ ///
+ private ParticleManager particleManager;
+
+ ///
+ /// Manager for leaderboard data and high scores.
+ ///
+ private SettingsManager leaderboardManager;
+
+ ///
+ /// Text message displayed at the end of a level (completion, failure, etc.).
+ ///
+ private string endOfLevelMessage;
+
+ ///
+ /// Tracks the display state of the end-of-level message.
+ ///
+ private EndOfLevelMessageState endOfLevelMessgeState;
+
+ ///
+ /// Spacing in pixels between text elements and screen edges.
+ ///
+ private const int textEdgeSpacing = 10;
+
+ ///
+ /// Initializes a new instance of the GameplayScreen class.
+ /// Sets up transition times for smooth screen changes.
+ ///
+ public GameplayScreen()
+ {
+ TransitionOnTime = TimeSpan.FromSeconds(1.5);
+ TransitionOffTime = TimeSpan.FromSeconds(0.5);
+ }
+
+ ///
+ /// Loads all content required for the gameplay screen.
+ /// This includes loading the initial level, music, and acquiring necessary services.
+ ///
+ public override void LoadContent()
+ {
+ base.LoadContent();
+
+ if (content == null)
+ content = new ContentManager(ScreenManager.Game.Services, "Content");
+
+ spriteBatch = ScreenManager.SpriteBatch;
+
+ MediaPlayer.IsRepeating = true;
+ MediaPlayer.Play(content.Load("Sounds/Music"));
+
+ particleManager ??= ScreenManager.Game.Services.GetService();
+
+ leaderboardManager ??= ScreenManager.Game.Services.GetService>();
+
+ LoadNextLevel();
+
+ // once the load has finished, we use ResetElapsedTime to tell the game's
+ // timing mechanism that we have just finished a very long frame, and that
+ // it should not try to catch up.
+ ScreenManager.Game.ResetElapsedTime();
+ }
+
+ ///
+ /// Loads the next level in sequence, cycling back to the first level after the last one.
+ /// Handles level disposal, initialization, and leaderboard setup.
+ ///
+ private void LoadNextLevel()
+ {
+ // move to the next level
+ levelIndex = (levelIndex + 1) % Level.NUMBER_OF_LEVELS;
+
+ // Unloads the content for the current level before loading the next one.
+ if (level != null)
+ level.Dispose();
+
+ // Load the level.
+ var levelPath = string.Format("Content/Levels/{0:00}.txt", levelIndex);
+ level = new Level(ScreenManager, levelPath, levelIndex);
+ level.ParticleManager = particleManager;
+
+ var levelFileName = Path.GetFileName(levelPath);
+ var leaderboardFileName = Path.ChangeExtension(levelFileName, ".json");
+ leaderboardManager.Storage.SettingsFileName = leaderboardFileName;
+ level.LeaderboardManager = leaderboardManager;
+
+ endOfLevelMessgeState = EndOfLevelMessageState.NotShowing;
+ }
+
+ ///
+ /// Reloads the current level after a failure.
+ /// Decrements the level index and calls LoadNextLevel to reset the current level.
+ ///
+ private void ReloadCurrentLevel()
+ {
+ --levelIndex;
+ LoadNextLevel();
+ }
+
+ ///
+ /// Unloads all content used by the gameplay screen when it's no longer needed.
+ ///
+ public override void UnloadContent()
+ {
+ content.Unload();
+ }
+
+ ///
+ /// Updates the game state, including level logic, end-of-level conditions, and screen transitions.
+ ///
+ /// Provides a snapshot of timing values for frame-based updates.
+ /// Indicates if another screen has focus.
+ /// Indicates if this screen is covered by another screen.
+ public override void Update(GameTime gameTime, bool otherScreenHasFocus,
+ bool coveredByOtherScreen)
+ {
+ base.Update(gameTime, otherScreenHasFocus, false);
+
+ // Gradually fade in or out depending on whether we are covered by the pause screen.
+ if (coveredByOtherScreen)
+ pauseAlpha = Math.Min(pauseAlpha + 1f / 32, 1);
+ else
+ pauseAlpha = Math.Max(pauseAlpha - 1f / 32, 0);
+
+ level.Paused = !IsActive;
+
+ if (IsActive)
+ {
+ if (level.ParticleManager.Finished)
+ {
+ switch (endOfLevelMessgeState)
+ {
+ case EndOfLevelMessageState.NotShowing:
+ if (level.TimeTaken == level.MaximumTimeToCompleteLevel)
+ {
+ if (level.ReachedExit)
+ {
+ endOfLevelMessage = GetLevelStats(Resources.LevelCompleted);
+ }
+ else
+ {
+ endOfLevelMessage = GetLevelStats(Resources.TimeRanOut);
+ }
+
+ endOfLevelMessgeState = EndOfLevelMessageState.Show;
+ }
+ else if (!level.Player.IsAlive)
+ {
+ endOfLevelMessage = GetLevelStats(Resources.YouDied);
+ endOfLevelMessgeState = EndOfLevelMessageState.Show;
+ }
+ break;
+ case EndOfLevelMessageState.Showing:
+ break;
+ }
+ }
+ }
+ }
+
+ ///
+ /// Generates a formatted message with level statistics for display at the end of a level.
+ ///
+ /// The title message indicating completion or failure state.
+ /// A formatted string containing the level statistics.
+ private string GetLevelStats(string messageTitle)
+ {
+ var message = messageTitle + Environment.NewLine + Environment.NewLine;
+
+ if (level.NewHighScore)
+ message += Resources.NewHighScore + Environment.NewLine + Environment.NewLine;
+
+ message +=
+ Resources.Score + ": " + level.Score + Environment.NewLine +
+ Resources.Time + ": " + level.TimeTaken + Environment.NewLine +
+ Resources.GemsCollected + $": {level.GemsCollected:D2}/ {level.GemsCount:D2}";
+
+ return message;
+ }
+
+ ///
+ /// Processes player input and updates game state accordingly.
+ /// Handles pausing, level continuation, and player actions.
+ ///
+ /// Provides a snapshot of timing values.
+ /// Current input state for all input devices.
+ /// Thrown if inputState is null.
+ public override void HandleInput(GameTime gameTime, InputState inputState)
+ {
+ ArgumentNullException.ThrowIfNull(inputState);
+
+ base.HandleInput(gameTime, inputState);
+
+ // Get all of our input states for the active player profile.
+ int playerIndex = ControllingPlayer != null ? (int)ControllingPlayer.Value : (int)PlayerIndex.One;
+
+ // The game pauses either if the user presses the pause button, or if
+ // they unplug the active gamepad. This requires us to keep track of
+ // whether a gamepad was ever plugged in, because we don't want to pause
+ // on PC if they are playing with a keyboard and have no gamepad at all!
+ bool gamePadDisconnected = !currentGamePadState.IsConnected && previousGamePadState.IsConnected;
+
+ Rectangle? backpackTouched = null;
+ if (Platformer2DGame.IsMobile)
+ {
+ backpackTouched = new Rectangle((int)level.BackpackPosition.X,
+ (int)level.BackpackPosition.Y,
+ (int)level.BackpackPosition.X + 32,
+ (int)level.BackpackPosition.Y + 32);
+ }
+
+ if (inputState.IsPauseGame(ControllingPlayer, backpackTouched)
+ || gamePadDisconnected)
+ {
+ ScreenManager.AddScreen(new PauseScreen(), ControllingPlayer);
+ }
+ else
+ {
+ // update our level, passing down the GameTime along with all of our input states
+ level.Update(gameTime,
+ inputState,
+ ScreenManager.Game.Window.CurrentOrientation);
+
+ currentKeyboardState = inputState.CurrentKeyboardStates[playerIndex];
+ previousGamePadState = inputState.LastGamePadStates[playerIndex];
+ currentGamePadState = inputState.CurrentGamePadStates[playerIndex];
+
+ currentTouchState = inputState.CurrentTouchState;
+
+ // Exit the game when back is pressed.
+ if (currentGamePadState.Buttons.Back == ButtonState.Pressed)
+ ScreenManager.Game.Exit();
+
+ if (endOfLevelMessgeState == EndOfLevelMessageState.Show && IsActive)
+ {
+ var toastMessageBox = new MessageBoxScreen(endOfLevelMessage, false, new TimeSpan(0, 0, 5), true);
+ toastMessageBox.Accepted += (sender, e) =>
+ {
+ wasContinuePressed = true;
+ };
+ endOfLevelMessgeState = EndOfLevelMessageState.Showing;
+ ScreenManager.AddScreen(toastMessageBox, ControllingPlayer);
+ }
+
+ // Perform the appropriate action to advance the game and
+ // to get the player back to playing.
+ if (wasContinuePressed)
+ {
+ if (!level.Player.IsAlive)
+ {
+ level.StartNewLife();
+ }
+ else if (level.TimeTaken == level.MaximumTimeToCompleteLevel)
+ {
+ if (level.ReachedExit)
+ {
+ LoadNextLevel();
+ }
+ else
+ {
+ ReloadCurrentLevel();
+ }
+ }
+
+ wasContinuePressed = false;
+ }
+ }
+ }
+
+ ///
+ /// Renders the gameplay elements, including the level, UI, and transition effects.
+ ///
+ /// Provides a snapshot of timing values for frame-based rendering.
+ public override void Draw(GameTime gameTime)
+ {
+ // This game has a blue background. Why? Because!
+ ScreenManager.GraphicsDevice.Clear(ClearOptions.Target, Color.Black, 0, 0);
+
+ level.Draw(gameTime, spriteBatch);
+
+ base.Draw(gameTime);
+
+ // If the game is transitioning on or off, fade it out to black.
+ if (TransitionPosition > 0 || pauseAlpha > 0)
+ {
+ float alpha = MathHelper.Lerp(1f - TransitionAlpha, 1f, pauseAlpha / 2);
+
+ ScreenManager.FadeBackBufferToBlack(alpha);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Platformer2D/Core/Screens/LoadingScreen.cs b/Platformer2D/Core/Screens/LoadingScreen.cs
new file mode 100644
index 00000000..2b5d0acb
--- /dev/null
+++ b/Platformer2D/Core/Screens/LoadingScreen.cs
@@ -0,0 +1,136 @@
+using System;
+using Platformer2D.Core.Localization;
+using Platformer2D.ScreenManagers;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace Platformer2D.Screens;
+
+///
+/// The loading screen coordinates transitions between the menu system and the
+/// game itself. Normally one screen will transition off at the same time as
+/// the next screen is transitioning on, but for larger transitions that can
+/// take a longer time to load their data, we want the menu system to be entirely
+/// gone before we start loading the game. This is done as follows:
+///
+/// - Tell all the existing screens to transition off.
+/// - Activate a loading screen, which will transition on at the same time.
+/// - The loading screen watches the state of the previous screens.
+/// - When it sees they have finished transitioning off, it activates the real
+/// next screen, which may take a long time to load its data. The loading
+/// screen will be the only thing displayed while this load is taking place.
+///
+class LoadingScreen : GameScreen
+{
+ bool loadingIsSlow;
+ bool otherScreensAreGone;
+
+ GameScreen[] screensToLoad;
+
+ ///
+ /// The constructor is private: loading screens should
+ /// be activated via the static Load method instead.
+ ///
+ /// The screen manager.
+ /// Indicates whether the loading process is expected to be slow.
+ /// The array of screens to load.
+ private LoadingScreen(ScreenManager screenManager, bool loadingIsSlow, GameScreen[] screensToLoad)
+ {
+ this.loadingIsSlow = loadingIsSlow;
+ this.screensToLoad = screensToLoad;
+
+ TransitionOnTime = TimeSpan.FromSeconds(0.5);
+ }
+
+ ///
+ /// Activates the loading screen.
+ ///
+ /// The screen manager.
+ /// Indicates whether the loading process is expected to be slow.
+ /// The player index controlling the loading screen.
+ /// The array of screens to load.
+ public static void Load(ScreenManager screenManager, bool loadingIsSlow, PlayerIndex? controllingPlayer, params GameScreen[] screensToLoad)
+ {
+ // Tell all the current screens to transition off.
+ foreach (GameScreen screen in screenManager.GetScreens())
+ screen.ExitScreen();
+
+ // Create and activate the loading screen.
+ LoadingScreen loadingScreen = new LoadingScreen(screenManager, loadingIsSlow, screensToLoad);
+
+ screenManager.AddScreen(loadingScreen, controllingPlayer);
+ }
+
+ ///
+ /// Updates the loading screen.
+ ///
+ /// Provides a snapshot of timing values.
+ /// Indicates whether another screen has focus.
+ /// Indicates whether the screen is covered by another screen.
+ public override void Update(GameTime gameTime, bool otherScreenHasFocus, bool coveredByOtherScreen)
+ {
+ base.Update(gameTime, otherScreenHasFocus, coveredByOtherScreen);
+
+ // If all the previous screens have finished transitioning
+ // off, it is time to actually perform the load.
+ if (otherScreensAreGone)
+ {
+ ScreenManager.RemoveScreen(this);
+
+ foreach (GameScreen screen in screensToLoad)
+ {
+ if (screen != null)
+ {
+ ScreenManager.AddScreen(screen, ControllingPlayer);
+ }
+ }
+
+ // Once the load has finished, we use ResetElapsedTime to tell
+ // the game timing mechanism that we have just finished a very
+ // long frame, and that it should not try to catch up.
+ ScreenManager.Game.ResetElapsedTime();
+ }
+ }
+
+ ///
+ /// Draws the loading screen.
+ ///
+ /// Provides a snapshot of timing values.
+ public override void Draw(GameTime gameTime)
+ {
+ // If we are the only active screen, that means all the previous screens
+ // must have finished transitioning off. We check for this in the Draw
+ // method, rather than in Update, because it isn't enough just for the
+ // screens to be gone: in order for the transition to look good we must
+ // have actually drawn a frame without them before we perform the load.
+ if ((ScreenState == ScreenState.Active) && (ScreenManager.GetScreens().Length == 1))
+ {
+ otherScreensAreGone = true;
+ }
+
+ // The gameplay screen takes a while to load, so we display a loading
+ // message while that is going on, but the menus load very quickly, and
+ // it would look silly if we flashed this up for just a fraction of a
+ // second while returning from the game to the menus. This parameter
+ // tells us how long the loading is going to take, so we know whether
+ // to bother drawing the message.
+ if (loadingIsSlow)
+ {
+ SpriteBatch spriteBatch = ScreenManager.SpriteBatch;
+ SpriteFont font = ScreenManager.Font;
+
+ string message = Resources.Loading;
+
+ // Center the text in the BaseScreenSize, the GlobalTransformation will scale everything for us.
+ Vector2 textSize = font.MeasureString(message);
+ Vector2 textPosition = (ScreenManager.BaseScreenSize - textSize) / 2;
+
+ Color color = Color.White * TransitionAlpha;
+
+ // Draw the text.
+ spriteBatch.Begin(SpriteSortMode.Deferred, null, null, null, null, null, ScreenManager.GlobalTransformation);
+ spriteBatch.DrawString(font, message, textPosition, color);
+ spriteBatch.End();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Platformer2D/Core/Screens/MainMenuScreen.cs b/Platformer2D/Core/Screens/MainMenuScreen.cs
new file mode 100644
index 00000000..78224856
--- /dev/null
+++ b/Platformer2D/Core/Screens/MainMenuScreen.cs
@@ -0,0 +1,346 @@
+using System;
+using System.Reflection.Emit;
+using Platformer2D.Core;
+using Platformer2D.Core.Effects;
+using Platformer2D.Core.Inputs;
+using Platformer2D.Core.Localization;
+using Platformer2D.Core.Settings;
+using Platformer2D.ScreenManagers;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace Platformer2D.Screens;
+
+///
+/// The main menu screen is the first thing displayed when the game starts up.
+///
+class MainMenuScreen : MenuScreen
+{
+ private ContentManager content;
+ private Level level;
+ private bool readyToPlay;
+ private PlayerIndex playerIndex;
+ private ParticleManager particleManager;
+ private SettingsManager settingsManager;
+ private MenuEntry playMenuEntry;
+ private MenuEntry tutorialMenuEntry;
+ private MenuEntry settingsMenuEntry;
+ private MenuEntry aboutMenuEntry;
+ private MenuEntry exitMenuEntry;
+ private Texture2D gradientTexture;
+ private bool showTutorial;
+ private int tutorialStep = -1;
+ private TimeSpan timeSinceLastMessage;
+
+ ///
+ /// Constructor fills in the menu contents.
+ ///
+ public MainMenuScreen()
+ : base(Resources.MainMenu)
+ {
+ // Create our menu entries.
+ playMenuEntry = new MenuEntry(Resources.Play);
+ tutorialMenuEntry = new MenuEntry(Resources.Tutorial);
+ settingsMenuEntry = new MenuEntry(Resources.Settings);
+ aboutMenuEntry = new MenuEntry(Resources.About);
+ exitMenuEntry = new MenuEntry(Resources.Exit);
+
+ // Hook up menu event handlers.
+ playMenuEntry.Selected += PlayMenuEntrySelected;
+ tutorialMenuEntry.Selected += TutorialMenuEntrySelected;
+ settingsMenuEntry.Selected += SettingsMenuEntrySelected;
+ aboutMenuEntry.Selected += AboutMenuEntrySelected;
+ exitMenuEntry.Selected += OnCancel;
+
+ // Add entries to the menu.
+ MenuEntries.Add(playMenuEntry);
+ MenuEntries.Add(tutorialMenuEntry);
+ MenuEntries.Add(settingsMenuEntry);
+ MenuEntries.Add(aboutMenuEntry);
+ MenuEntries.Add(exitMenuEntry);
+ }
+
+ private void SetLanguageText()
+ {
+ aboutMenuEntry.Text = Resources.About;
+ playMenuEntry.Text = Resources.Play;
+ settingsMenuEntry.Text = Resources.Settings;
+ tutorialMenuEntry.Text = Resources.Tutorial;
+ exitMenuEntry.Text = Resources.Exit;
+
+ Title = "Platformer2D"; // TODO uncomment this if you want it to use Resources.MainMenu; instead
+ }
+
+ ///
+ /// LoadContent will be called once per game and is the place to load
+ /// all of your content for the game.
+ ///
+ public override void LoadContent()
+ {
+ base.LoadContent();
+
+ if (content == null)
+ content = new ContentManager(ScreenManager.Game.Services, "Content");
+
+ particleManager ??= ScreenManager.Game.Services.GetService();
+
+ settingsManager ??= ScreenManager.Game.Services.GetService>();
+ settingsManager.Settings.PropertyChanged += (s, e) =>
+ {
+ SetLanguageText();
+ };
+
+ SetLanguageText();
+
+ // Load the level.
+ string levelPath = "Content/Levels/00.txt";
+ level = new Level(ScreenManager, levelPath, 00);
+ level.ParticleManager = particleManager;
+ level.Player.Mode = PlayerMode.Scripting;
+
+ gradientTexture = content.Load("Sprites/gradient");
+ }
+
+ ///
+ /// Unload graphics content used by the game.
+ ///
+ public override void UnloadContent()
+ {
+ if (level != null)
+ {
+ level.Dispose();
+ }
+
+ content.Unload();
+ }
+
+ ///
+ /// Allows the game to run logic such as updating the world,
+ /// checking for collisions, gathering input, and playing audio.
+ ///
+ /// This method checks the GameScreen.IsActive
+ /// property, so the game will stop updating when the pause menu is active,
+ /// or if you tab away to a different application.
+ ///
+ /// Provides a snapshot of timing values.
+ /// If another screen has focus
+ /// If currently covered by another screen
+ public override void Update(GameTime gameTime,
+ bool otherScreenHasFocus,
+ bool coveredByOtherScreen)
+ {
+ base.Update(gameTime, otherScreenHasFocus, coveredByOtherScreen);
+
+ if (readyToPlay)
+ {
+ if (!level.ReachedExit)
+ {
+ level.Player.Movement = 1.0f;
+
+ // Maybe get it to jump after moving??
+ level.Player.Move(gameTime);
+ }
+ else
+ {
+ if (level.ParticleManager.Finished)
+ {
+ LoadingScreen.Load(ScreenManager,
+ true,
+ playerIndex,
+ new GameplayScreen());
+ }
+ }
+ }
+
+ if (showTutorial)
+ {
+ UpdateTutorialSteps(gameTime);
+ }
+ }
+
+ ///
+ /// Responds to user input.
+ ///
+ public override void HandleInput(GameTime gameTime, InputState inputState)
+ {
+ base.HandleInput(gameTime, inputState);
+
+ // update our level, passing down the GameTime along with all of our input states
+ level.Update(gameTime,
+ inputState,
+ ScreenManager.Game.Window.CurrentOrientation,
+ readyToPlay);
+ }
+
+ private void UpdateTutorialSteps(GameTime gameTime)
+ {
+ if (gameTime.TotalGameTime - timeSinceLastMessage > TimeSpan.FromSeconds(3)) // Should the showtime be in settings?
+ {
+ tutorialStep++;
+ timeSinceLastMessage = gameTime.TotalGameTime;
+
+ if (Platformer2DGame.IsMobile)
+ {
+ if (tutorialStep > 3)
+ {
+ tutorialStep = -1;
+ showTutorial = false;
+ }
+ }
+ else
+ {
+ if (tutorialStep > 2)
+ {
+ tutorialStep = -1;
+ showTutorial = false;
+ }
+ }
+ }
+ }
+
+ ///
+ /// Draws the gameplay screen.
+ ///
+ public override void Draw(GameTime gameTime)
+ {
+ SpriteBatch spriteBatch = ScreenManager.SpriteBatch;
+
+ level.Draw(gameTime, spriteBatch);
+
+ if (showTutorial)
+ {
+ spriteBatch.Begin(SpriteSortMode.Deferred, null, null, null, null, null, ScreenManager.GlobalTransformation);
+
+ DrawTutorialSteps(spriteBatch);
+
+ spriteBatch.End();
+ }
+
+ base.Draw(gameTime);
+ }
+
+ private void DrawTutorialSteps(SpriteBatch spriteBatch)
+ {
+ SpriteFont font = ScreenManager.Font;
+
+ // The background includes a border somewhat larger than the text itself.
+ const int hPad = 32;
+ const int vPad = 16;
+
+ Rectangle backgroundRectangle = Rectangle.Empty;
+ Vector2 textSize;
+ string message = string.Empty;
+
+ switch (tutorialStep)
+ {
+ case 0:
+ message = Resources.CollectThese;
+ textSize = font.MeasureString(message);
+ backgroundRectangle = new Rectangle((int)level.Gems[0].Position.X - 50 - hPad,
+ (int)level.Gems[0].Position.Y - vPad - 60,
+ (int)textSize.X + hPad * 2,
+ (int)textSize.Y + vPad * 2);
+ break;
+
+ case 1:
+ message = Resources.GetToHere;
+ textSize = font.MeasureString(message);
+ backgroundRectangle = new Rectangle((int)level.Exit.X - 50 - hPad,
+ (int)level.Exit.Y - vPad - 60,
+ (int)textSize.X + hPad * 2,
+ (int)textSize.Y + vPad * 2);
+ break;
+
+ case 2:
+ message = Resources.DontDie;
+ textSize = font.MeasureString(message);
+ backgroundRectangle = new Rectangle((int)level.Player.Position.X - (int)(textSize.X /2) - hPad,
+ (int)level.Player.Position.Y - vPad - 100,
+ (int)textSize.X + hPad * 2,
+ (int)textSize.Y + vPad * 2);
+ break;
+
+ case 3:
+ message = Resources.TapToPause;
+ textSize = font.MeasureString(message);
+ backgroundRectangle = new Rectangle((int)level.BackpackPosition.X + hPad + 4,
+ (int)level.BackpackPosition.Y - 10,
+ (int)textSize.X + hPad * 2,
+ (int)textSize.Y + vPad * 2);
+ break;
+ }
+
+ Vector2 textPosition = new Vector2(backgroundRectangle.X + hPad, backgroundRectangle.Y + vPad);
+
+ // Draw the background rectangle.
+ spriteBatch.Draw(gradientTexture, backgroundRectangle, Color.White);
+
+ // Draw the tutorial text.
+ spriteBatch.DrawString(font, message, textPosition, Color.White);
+ }
+
+ ///
+ /// Event handler for when the Play menu entry is selected.
+ ///
+ void PlayMenuEntrySelected(object sender, PlayerIndexEventArgs e)
+ {
+ var toastMessageBox = new MessageBoxScreen(Resources.LetsGo, false, new TimeSpan(0, 0, 1), true);
+ toastMessageBox.Accepted += (sender, e) =>
+ {
+ playerIndex = e.PlayerIndex;
+ readyToPlay = true;
+ };
+ ScreenManager.AddScreen(toastMessageBox, e.PlayerIndex);
+ }
+
+ ///
+ /// Event handler for when the Tutorial menu entry is selected.
+ ///
+ private void TutorialMenuEntrySelected(object sender, PlayerIndexEventArgs e)
+ {
+ // You could create another screen and show the tutorial there.
+ // But modern games, have a more dynamic main menu
+ showTutorial = true;
+ }
+
+ ///
+ /// Event handler for when the Options menu entry is selected.
+ ///
+ void SettingsMenuEntrySelected(object sender, PlayerIndexEventArgs e)
+ {
+ ScreenManager.AddScreen(new SettingsScreen(), e.PlayerIndex);
+ }
+
+ ///
+ /// Event handler for when the Options menu entry is selected.
+ ///
+ void AboutMenuEntrySelected(object sender, PlayerIndexEventArgs e)
+ {
+ ScreenManager.AddScreen(new AboutScreen(), e.PlayerIndex);
+ }
+
+ ///
+ /// When the user cancels the main menu, ask if they want to exit the sample.
+ ///
+ protected override void OnCancel(PlayerIndex playerIndex)
+ {
+ string message = Resources.ExitQuestion;
+
+ MessageBoxScreen confirmExitMessageBox = new MessageBoxScreen(message);
+
+ confirmExitMessageBox.Accepted += ConfirmExitMessageBoxAccepted;
+
+ ScreenManager.AddScreen(confirmExitMessageBox, playerIndex);
+ }
+
+
+ ///
+ /// Event handler for when the user selects ok on the "are you sure
+ /// you want to exit" message box.
+ ///
+ void ConfirmExitMessageBoxAccepted(object sender, PlayerIndexEventArgs e)
+ {
+ ScreenManager.Game.Exit();
+ }
+}
\ No newline at end of file
diff --git a/Platformer2D/Core/Screens/MenuEntry.cs b/Platformer2D/Core/Screens/MenuEntry.cs
new file mode 100644
index 00000000..93c3a37c
--- /dev/null
+++ b/Platformer2D/Core/Screens/MenuEntry.cs
@@ -0,0 +1,160 @@
+using System;
+using Platformer2D.ScreenManagers;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace Platformer2D.Screens;
+
+///
+/// Helper class represents a single entry in a MenuScreen. By default this
+/// just draws the entry text string, but it can be customized to display menu
+/// entries in different ways. This also provides an event that will be raised
+/// when the menu entry is selected.
+///
+class MenuEntry
+{
+ ///
+ /// Tracks a fading selection effect on the entry.
+ ///
+ ///
+ /// The entries transition out of the selection effect when they are deselected.
+ ///
+ float selectionFade;
+
+ string text;
+ ///
+ /// Gets or sets the text of this menu entry.
+ ///
+ public string Text
+ {
+ get { return text; }
+ set { text = value; }
+ }
+
+ Vector2 position;
+ ///
+ /// The position at which the entry is drawn. This is set by the MenuScreen
+ /// each frame in Update.
+ ///
+ public Vector2 Position
+ {
+ get { return position; }
+ set { position = value; }
+ }
+
+ private bool enabled;
+ ///
+ /// Whether this menu option is enabled or not.
+ /// If not it will be displayed but skipped over during navigation
+ ///
+ public bool Enabled
+ {
+ get { return enabled; }
+ set { enabled = value; }
+ }
+
+ ///
+ /// Event raised when the menu entry is selected.
+ ///
+ public event EventHandler Selected;
+
+ ///
+ /// Method for raising the Selected event.
+ ///
+ /// The player index that selected the entry.
+ protected internal virtual void OnSelectEntry(PlayerIndex playerIndex)
+ {
+ Selected?.Invoke(this, new PlayerIndexEventArgs(playerIndex));
+ }
+
+ ///
+ /// Constructs a new menu entry with the specified text.
+ ///
+ /// The text to display for the menu entry.
+ /// Indicates whether the menu entry is enabled.
+ public MenuEntry(string text, bool enabled = true)
+ {
+ this.text = text;
+ this.enabled = enabled;
+ }
+
+ ///
+ /// Updates the menu entry's visual state.
+ ///
+ /// The menu screen containing this entry.
+ /// Indicates whether the entry is currently selected.
+ /// Provides a snapshot of timing values.
+ public virtual void Update(MenuScreen screen, bool isSelected, GameTime gameTime)
+ {
+ // When the menu selection changes, entries gradually fade between
+ // their selected and deselected appearance, rather than instantly
+ // popping to the new state.
+ float fadeSpeed = (float)gameTime.ElapsedGameTime.TotalSeconds * 4;
+
+ if (isSelected)
+ selectionFade = Math.Min(selectionFade + fadeSpeed, 1);
+ else
+ selectionFade = Math.Max(selectionFade - fadeSpeed, 0);
+ }
+
+
+ ///
+ /// Draws the menu entry. This can be overridden to customize the appearance.
+ ///
+ /// The menu screen containing this entry.
+ /// Indicates whether the entry is currently selected.
+ /// Provides a snapshot of timing values.
+ public virtual void Draw(MenuScreen screen, bool isSelected, GameTime gameTime)
+ {
+ Color color;
+ if (enabled)
+ {
+ // Draw the selected entry in yellow, otherwise white.
+ color = isSelected ? Color.Yellow : Color.White;
+ }
+ else
+ {
+ color = Color.Gray;
+ }
+
+ // Pulsate the size of the selected menu entry.
+ double time = gameTime.TotalGameTime.TotalSeconds;
+
+ float pulsate = (float)Math.Sin(time * 6) + 1;
+
+ float scale = 1 + pulsate * 0.05f * selectionFade;
+
+ // Modify the alpha to fade text out during transitions.
+ color *= screen.TransitionAlpha;
+
+ // Draw text, centered on the middle of each line.
+ ScreenManager screenManager = screen.ScreenManager;
+ SpriteBatch spriteBatch = screenManager.SpriteBatch;
+ SpriteFont font = screenManager.Font;
+
+ Vector2 origin = new Vector2(0, font.LineSpacing / 2);
+
+ spriteBatch.DrawString(font, text, position, color, 0,
+ origin, scale, SpriteEffects.None, 0);
+ }
+
+ ///
+ /// Queries how much vertical space this menu entry requires.
+ ///
+ /// The menu screen containing this entry.
+ /// The height of the menu entry in pixels.
+ public virtual int GetHeight(MenuScreen screen)
+ {
+ return screen.ScreenManager.Font.LineSpacing;
+ }
+
+ ///
+ /// Queries how much horizontal space this menu entry requires.
+ ///
+ /// The menu screen containing this entry.
+ /// The width of the menu entry in pixels.
+ public virtual int GetWidth(MenuScreen screen)
+ {
+ return (int)screen.ScreenManager.Font.MeasureString(Text).X;
+ }
+}
\ No newline at end of file
diff --git a/Platformer2D/Core/Screens/MenuScreen.cs b/Platformer2D/Core/Screens/MenuScreen.cs
new file mode 100644
index 00000000..ad19c392
--- /dev/null
+++ b/Platformer2D/Core/Screens/MenuScreen.cs
@@ -0,0 +1,299 @@
+using System;
+using System.Collections.Generic;
+using Platformer2D.Core;
+using Platformer2D.Core.Inputs;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+using Microsoft.Xna.Framework.Input;
+using Microsoft.Xna.Framework.Input.Touch;
+
+namespace Platformer2D.Screens;
+
+///
+/// Base class for screens that contain a menu of options. The user can
+/// move up and down to select an entry, or cancel to back out of the screen.
+///
+abstract class MenuScreen : GameScreen
+{
+ private List menuEntries = new List();
+ private int selectedEntry = 0;
+ private string menuTitle;
+ private Color menuTitleColor = new Color(0, 0, 0); // Default color is black. Use new Color(192, 192, 192) for off-white.
+
+ ///
+ /// Gets or sets the title of the menu screen.
+ ///
+ public string Title { get => menuTitle; set => menuTitle = value; }
+
+ ///
+ /// Gets the list of menu entries, so derived classes can add
+ /// or change the menu contents.
+ ///
+ protected IList MenuEntries
+ {
+ get { return menuEntries; }
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The title of the menu screen.
+ public MenuScreen(string menuTitle)
+ {
+ this.menuTitle = menuTitle;
+
+ TransitionOnTime = TimeSpan.FromSeconds(0.5);
+ TransitionOffTime = TimeSpan.FromSeconds(0.5);
+ }
+
+ ///
+ /// Loads content for the menu screen. This method is called once per game
+ /// and is the place to load all content specific to the menu screen.
+ ///
+ public override void LoadContent()
+ {
+ base.LoadContent();
+ }
+
+ ///
+ /// Responds to user input, changing the selected entry and accepting
+ /// or canceling the menu.
+ ///
+ /// Provides a snapshot of timing values.
+ /// Provides the current state of input devices.
+ public override void HandleInput(GameTime gameTime, InputState inputState)
+ {
+ base.HandleInput(gameTime, inputState);
+
+ // Handle touch input for mobile platforms.
+ if (Platformer2DGame.IsMobile)
+ {
+ var touchState = inputState.CurrentTouchState;
+ if (touchState.Count > 0)
+ {
+ foreach (var touch in touchState)
+ {
+ if (touch.State == TouchLocationState.Pressed)
+ {
+ TextSelectedCheck(inputState.CurrentCursorLocation);
+ }
+ }
+ }
+ }
+ // Handle mouse input for desktop platforms.
+ else if (Platformer2DGame.IsDesktop)
+ {
+ if (inputState.IsLeftMouseButtonClicked())
+ {
+ TextSelectedCheck(inputState.CurrentCursorLocation);
+ }
+ else if (inputState.IsMiddleMouseButtonClicked())
+ {
+ OnSelectEntry(selectedEntry, PlayerIndex.One);
+ }
+ }
+
+ // Move to the previous menu entry.
+ if (inputState.IsMenuUp(ControllingPlayer))
+ {
+ selectedEntry--;
+
+ if (selectedEntry < 0)
+ selectedEntry = menuEntries.Count - 1;
+
+ while (!menuEntries[selectedEntry].Enabled)
+ {
+ selectedEntry--;
+
+ if (selectedEntry < 0)
+ selectedEntry = menuEntries.Count - 1;
+ }
+ }
+
+ // Move to the next menu entry.
+ if (inputState.IsMenuDown(ControllingPlayer))
+ {
+ selectedEntry++;
+
+ if (selectedEntry >= menuEntries.Count)
+ selectedEntry = 0;
+
+ SetNextEnabledMenu();
+ }
+
+ // Accept or cancel the menu.
+ PlayerIndex playerIndex;
+
+ if (inputState.IsMenuSelect(ControllingPlayer, out playerIndex))
+ {
+ OnSelectEntry(selectedEntry, playerIndex);
+ }
+ else if (inputState.IsMenuCancel(ControllingPlayer, out playerIndex))
+ {
+ OnCancel(playerIndex);
+ }
+ }
+
+ ///
+ /// Checks if a touch or mouse click has selected a menu entry.
+ ///
+ /// The location of the touch or mouse click.
+ private void TextSelectedCheck(Vector2 touchLocation)
+ {
+ for (int i = 0; i < menuEntries.Count; i++)
+ {
+ var textSize = ScreenManager.Font.MeasureString(menuEntries[i].Text);
+ var entryBounds = new Rectangle((int)menuEntries[i].Position.X, (int)menuEntries[i].Position.Y, (int)textSize.X, (int)textSize.Y);
+
+ if (entryBounds.Contains(touchLocation))
+ {
+ selectedEntry = i;
+ OnSelectEntry(selectedEntry, ControllingPlayer ?? PlayerIndex.One);
+ break;
+ }
+ }
+ }
+
+ ///
+ /// Sets the next enabled menu entry as the selected entry.
+ ///
+ private void SetNextEnabledMenu()
+ {
+ while (!menuEntries[selectedEntry].Enabled)
+ {
+ selectedEntry++;
+
+ if (selectedEntry >= menuEntries.Count)
+ selectedEntry = 0;
+ }
+ }
+
+ ///
+ /// Handler for when the user has chosen a menu entry.
+ ///
+ /// The index of the selected menu entry.
+ /// The index of the player who triggered the selection.
+ protected virtual void OnSelectEntry(int entryIndex, PlayerIndex playerIndex)
+ {
+ menuEntries[entryIndex].OnSelectEntry(playerIndex);
+ }
+
+ ///
+ /// Handler for when the user has canceled the menu.
+ ///
+ /// The index of the player who triggered the cancellation.
+ protected virtual void OnCancel(PlayerIndex playerIndex)
+ {
+ ExitScreen();
+ }
+
+ ///
+ /// Helper overload makes it easy to use OnCancel as a MenuEntry event handler.
+ ///
+ /// The object that triggered the event.
+ /// Event arguments containing the player index.
+ protected void OnCancel(object sender, PlayerIndexEventArgs e)
+ {
+ OnCancel(e.PlayerIndex);
+ }
+
+ ///
+ /// Updates the positions of the menu entries. By default, all menu entries
+ /// are lined up in a vertical list, centered on the screen.
+ ///
+ protected virtual void UpdateMenuEntryLocations()
+ {
+ // Make the menu slide into place during transitions, using a
+ // power curve to make things look more interesting (this makes
+ // the movement slow down as it nears the end).
+ float transitionOffset = (float)Math.Pow(TransitionPosition, 2);
+
+ // Start at Y = 175; each X value is generated per entry.
+ Vector2 position = new Vector2(0f, 175f);
+
+ // Update each menu entry's location in turn.
+ for (int i = 0; i < menuEntries.Count; i++)
+ {
+ MenuEntry menuEntry = menuEntries[i];
+
+ // Each entry is to be centered horizontally.
+ position.X = ScreenManager.BaseScreenSize.X / 2 - menuEntry.GetWidth(this) / 2;
+
+ if (ScreenState == ScreenState.TransitionOn)
+ position.X -= transitionOffset * 256;
+ else
+ position.X += transitionOffset * 512;
+
+ // Set the entry's position.
+ menuEntry.Position = position;
+
+ // Move down for the next entry by the size of this entry.
+ position.Y += menuEntry.GetHeight(this);
+ }
+ }
+
+ ///
+ /// Updates the menu screen.
+ ///
+ /// Provides a snapshot of timing values.
+ /// Whether another screen currently has focus.
+ /// Whether this screen is covered by another screen.
+ public override void Update(GameTime gameTime, bool otherScreenHasFocus, bool coveredByOtherScreen)
+ {
+ base.Update(gameTime, otherScreenHasFocus, coveredByOtherScreen);
+
+ SetNextEnabledMenu();
+
+ // Update each nested MenuEntry object.
+ for (int i = 0; i < menuEntries.Count; i++)
+ {
+ bool isSelected = IsActive && (i == selectedEntry);
+
+ menuEntries[i].Update(this, isSelected, gameTime);
+ }
+ }
+
+ ///
+ /// Draws the menu screen.
+ ///
+ /// Provides a snapshot of timing values.
+ public override void Draw(GameTime gameTime)
+ {
+ // Make sure our entries are in the right place before we draw them.
+ UpdateMenuEntryLocations();
+
+ GraphicsDevice graphics = ScreenManager.GraphicsDevice;
+ SpriteBatch spriteBatch = ScreenManager.SpriteBatch;
+ SpriteFont font = ScreenManager.Font;
+
+ spriteBatch.Begin(SpriteSortMode.Deferred, null, null, null, null, null, ScreenManager.GlobalTransformation);
+
+ // Draw each menu entry in turn.
+ for (int i = 0; i < menuEntries.Count; i++)
+ {
+ MenuEntry menuEntry = menuEntries[i];
+
+ bool isSelected = IsActive && (i == selectedEntry);
+
+ menuEntry.Draw(this, isSelected, gameTime);
+ }
+
+ // Make the menu slide into place during transitions, using a
+ // power curve to make things look more interesting (this makes
+ // the movement slow down as it nears the end).
+ float transitionOffset = (float)Math.Pow(TransitionPosition, 2);
+
+ // Draw the menu title centered on the screen.
+ Vector2 titlePosition = new Vector2(ScreenManager.BaseScreenSize.X / 2, 80);
+ Vector2 titleOrigin = font.MeasureString(menuTitle) / 2;
+ Color titleColor = menuTitleColor * TransitionAlpha;
+ float titleScale = 1.25f;
+
+ titlePosition.Y -= transitionOffset * 100;
+
+ spriteBatch.DrawString(font, menuTitle, titlePosition, titleColor, 0,
+ titleOrigin, titleScale, SpriteEffects.None, 0);
+
+ spriteBatch.End();
+ }
+}
\ No newline at end of file
diff --git a/Platformer2D/Core/Screens/MessageBoxScreen.cs b/Platformer2D/Core/Screens/MessageBoxScreen.cs
new file mode 100644
index 00000000..276e13a6
--- /dev/null
+++ b/Platformer2D/Core/Screens/MessageBoxScreen.cs
@@ -0,0 +1,214 @@
+using System;
+using Platformer2D.Core;
+using Platformer2D.Core.Inputs;
+using Platformer2D.Core.Localization;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Content;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace Platformer2D.Screens;
+
+///
+/// A popup message box screen, used to display messages and prompt user input.
+///
+class MessageBoxScreen : GameScreen
+{
+ private string message;
+ private Texture2D gradientTexture;
+ private readonly bool toastMessage;
+ private readonly TimeSpan toastDuration;
+ private TimeSpan toastTimer;
+ private Vector2 yesButtonPosition;
+ private Vector2 noButtonPosition;
+ private Vector2 messageTextPosition;
+ private Vector2 yesTextSize;
+ private Vector2 noTextSize;
+ private Rectangle backgroundRectangle;
+
+ ///
+ /// Event raised when the user accepts the message box.
+ ///
+ public event EventHandler Accepted;
+
+ ///
+ /// Event raised when the user cancels the message box.
+ ///
+ public event EventHandler Cancelled;
+
+ // The background includes a border somewhat larger than the text itself.
+ private const int hPad = 32;
+ private const int vPad = 16;
+
+ ///
+ /// Initializes a new instance of the class, automatically including usage text.
+ ///
+ /// The message to display.
+ public MessageBoxScreen(string message)
+ : this(message, true, TimeSpan.Zero) { }
+
+ ///
+ /// Initializes a new instance of the class, allowing customization.
+ ///
+ /// The message to display.
+ /// Indicates whether to include usage text.
+ /// The duration for toast messages.
+ /// Indicates whether this is a toast message.
+ public MessageBoxScreen(string message, bool includeUsageText, TimeSpan toastDuration, bool toastMessage = false)
+ {
+ string usageText = $"{Environment.NewLine}{Environment.NewLine}{Resources.YesButtonHelp}{Environment.NewLine}{Resources.NoButtonHelp}";
+
+ if (includeUsageText && !toastMessage)
+ this.message = message + usageText;
+ else
+ this.message = message;
+
+ this.toastMessage = toastMessage;
+ this.toastDuration = toastDuration;
+ this.toastTimer = TimeSpan.Zero;
+
+ IsPopup = true;
+
+ TransitionOnTime = TimeSpan.FromSeconds(0.2);
+ TransitionOffTime = TimeSpan.FromSeconds(0.2);
+ }
+
+ ///
+ /// Loads graphics content for this screen.
+ ///
+ public override void LoadContent()
+ {
+ ContentManager content = ScreenManager.Game.Content;
+
+ gradientTexture = content.Load("Sprites/gradient");
+ }
+
+ ///
+ /// Responds to user input, accepting or cancelling the message box.
+ ///
+ /// Provides a snapshot of timing values.
+ /// The current input state.
+ public override void HandleInput(GameTime gameTime, InputState inputState)
+ {
+ base.HandleInput(gameTime, inputState);
+
+ // Ignore input if this is a ToastMessage
+ if (toastMessage)
+ {
+ return;
+ }
+
+ PlayerIndex playerIndex;
+
+ // We pass in our ControllingPlayer, which may either be null (to
+ // accept input from any player) or a specific index. If we pass a null
+ // controlling player, the InputState helper returns to us which player
+ // actually provided the input. We pass that through to our Accepted and
+ // Cancelled events, so they can tell which player triggered them.
+ if (inputState.IsMenuSelect(ControllingPlayer, out playerIndex)
+ || (Platformer2DGame.IsMobile
+ && inputState.IsUIClicked(new Rectangle((int)yesButtonPosition.X, (int)yesButtonPosition.Y, (int)yesTextSize.X, (int)yesTextSize.Y))))
+ {
+ // Raise the accepted event, then exit the message box.
+ Accepted?.Invoke(this, new PlayerIndexEventArgs(playerIndex));
+
+ ExitScreen();
+ }
+ else if (inputState.IsMenuCancel(ControllingPlayer, out playerIndex)
+ || (Platformer2DGame.IsMobile
+ && inputState.IsUIClicked(new Rectangle((int)noButtonPosition.X, (int)noButtonPosition.Y, (int)noTextSize.X, (int)noTextSize.Y))))
+ {
+ // Raise the cancelled event, then exit the message box.
+ Cancelled?.Invoke(this, new PlayerIndexEventArgs(playerIndex));
+
+ ExitScreen();
+ }
+ }
+
+ ///
+ /// Updates the screen, particularly for toast messages and positioning.
+ ///
+ /// Provides a snapshot of timing values.
+ /// Indicates whether another screen has focus.
+ /// Indicates whether the screen is covered by another screen.
+ public override void Update(GameTime gameTime, bool otherScreenHasFocus, bool coveredByOtherScreen)
+ {
+ base.Update(gameTime, otherScreenHasFocus, coveredByOtherScreen);
+
+ // Handle toast duration countdown.
+ if (toastMessage)
+ {
+ toastTimer += gameTime.ElapsedGameTime;
+ if (toastTimer >= toastDuration)
+ {
+ // Raise the accepted event, then exit the message box.
+ Accepted?.Invoke(this, new PlayerIndexEventArgs(PlayerIndex.One));
+
+ // Exit the screen when the toast time has elapsed.
+ ExitScreen();
+ }
+ }
+
+ // Center the message text in the BaseScreenSize.
+ // The GlobalTransformation will scale everything for us.
+ Vector2 textSize = ScreenManager.Font.MeasureString(message);
+ messageTextPosition = (ScreenManager.BaseScreenSize - textSize) / 2;
+
+ // Done here because language setting could change dynamically. Possibly overkill?
+ yesTextSize = ScreenManager.Font.MeasureString(Resources.YesButtonText);
+ noTextSize = ScreenManager.Font.MeasureString(Resources.NoButtonText);
+ if (Platformer2DGame.IsMobile
+ && !toastMessage)
+ {
+ textSize += yesTextSize;
+ textSize.Y += vPad * 2;
+ }
+
+ backgroundRectangle = new Rectangle((int)messageTextPosition.X - hPad,
+ (int)messageTextPosition.Y - vPad,
+ (int)textSize.X + hPad * 2,
+ (int)textSize.Y + vPad * 2);
+
+ if (Platformer2DGame.IsMobile
+ && !toastMessage)
+ {
+ yesButtonPosition = new Vector2(backgroundRectangle.X + backgroundRectangle.Width - (yesTextSize.X + hPad + noTextSize.X + hPad), backgroundRectangle.Y + backgroundRectangle.Height - yesTextSize.Y - vPad);
+ noButtonPosition = new Vector2(backgroundRectangle.X + backgroundRectangle.Width - (noTextSize.X + hPad), backgroundRectangle.Y + backgroundRectangle.Height - noTextSize.Y - vPad);
+ }
+ }
+
+ ///
+ /// Draws the message box.
+ ///
+ /// Provides a snapshot of timing values.
+ public override void Draw(GameTime gameTime)
+ {
+ SpriteBatch spriteBatch = ScreenManager.SpriteBatch;
+ SpriteFont font = ScreenManager.Font;
+
+ // Darken down any other screens that were drawn beneath the popup.
+ ScreenManager.FadeBackBufferToBlack(TransitionAlpha * 2 / 3);
+
+ // Fade the popup alpha during transitions.
+ Color color = Color.White * TransitionAlpha;
+
+ spriteBatch.Begin(SpriteSortMode.Deferred, null, null, null, null, null, ScreenManager.GlobalTransformation);
+
+ // Draw the background rectangle.
+ spriteBatch.Draw(gradientTexture, backgroundRectangle, color);
+
+ // Draw the message box text.
+ spriteBatch.DrawString(font, message, messageTextPosition, color);
+
+ if (Platformer2DGame.IsMobile
+ && !toastMessage)
+ {
+ color = Color.LimeGreen;
+ spriteBatch.DrawString(font, Resources.YesButtonText, yesButtonPosition, color);
+
+ color = Color.OrangeRed;
+ spriteBatch.DrawString(font, Resources.NoButtonText, noButtonPosition, color);
+ }
+
+ spriteBatch.End();
+ }
+}
\ No newline at end of file
diff --git a/Platformer2D/Core/Screens/PauseScreen.cs b/Platformer2D/Core/Screens/PauseScreen.cs
new file mode 100644
index 00000000..96352fd1
--- /dev/null
+++ b/Platformer2D/Core/Screens/PauseScreen.cs
@@ -0,0 +1,56 @@
+using Platformer2D.Core.Localization;
+
+namespace Platformer2D.Screens;
+
+///
+/// The pause menu comes up over the top of the game,
+/// giving the player options to resume or quit.
+///
+class PauseScreen : MenuScreen
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public PauseScreen()
+ : base(Resources.Paused)
+ {
+ // Create our menu entries.
+ MenuEntry resumeGameMenuEntry = new MenuEntry(Resources.Resume);
+ MenuEntry quitGameMenuEntry = new MenuEntry(Resources.Quit);
+
+ // Hook up menu event handlers.
+ resumeGameMenuEntry.Selected += OnCancel;
+ quitGameMenuEntry.Selected += QuitGameMenuEntrySelected;
+
+ // Add entries to the menu.
+ MenuEntries.Add(resumeGameMenuEntry);
+ MenuEntries.Add(quitGameMenuEntry);
+ }
+
+ ///
+ /// Event handler for when the Quit Game menu entry is selected.
+ ///
+ /// The source of the event.
+ /// The instance containing the event data.
+ private void QuitGameMenuEntrySelected(object sender, PlayerIndexEventArgs e)
+ {
+ string message = Resources.QuitQuestion;
+
+ MessageBoxScreen confirmQuitMessageBox = new MessageBoxScreen(message);
+
+ confirmQuitMessageBox.Accepted += ConfirmQuitMessageBoxAccepted;
+
+ ScreenManager.AddScreen(confirmQuitMessageBox, ControllingPlayer);
+ }
+
+ ///
+ /// Event handler for when the user selects ok on the "are you sure you want to quit" message box.
+ /// This uses the loading screen to transition from the game back to the main menu screen.
+ ///
+ /// The source of the event.
+ /// The instance containing the event data.
+ private void ConfirmQuitMessageBoxAccepted(object sender, PlayerIndexEventArgs e)
+ {
+ LoadingScreen.Load(ScreenManager, false, null, new BackgroundScreen(), new MainMenuScreen());
+ }
+}
\ No newline at end of file
diff --git a/Platformer2D/Core/Screens/PlayerIndexEventArgs.cs b/Platformer2D/Core/Screens/PlayerIndexEventArgs.cs
new file mode 100644
index 00000000..dfb7f08e
--- /dev/null
+++ b/Platformer2D/Core/Screens/PlayerIndexEventArgs.cs
@@ -0,0 +1,29 @@
+using System;
+using Microsoft.Xna.Framework;
+
+namespace Platformer2D.Screens;
+
+///
+/// Custom event argument which includes the index of the player who
+/// triggered the event. This is used by the MenuEntry.Selected event.
+///
+class PlayerIndexEventArgs : EventArgs
+{
+ PlayerIndex playerIndex;
+ ///
+ /// Gets the index of the player who triggered this event.
+ ///
+ public PlayerIndex PlayerIndex
+ {
+ get { return playerIndex; }
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The player index associated with the event.
+ public PlayerIndexEventArgs(PlayerIndex playerIndex)
+ {
+ this.playerIndex = playerIndex;
+ }
+}
\ No newline at end of file
diff --git a/Platformer2D/Core/Screens/ScreenState.cs b/Platformer2D/Core/Screens/ScreenState.cs
new file mode 100644
index 00000000..6fa301fc
--- /dev/null
+++ b/Platformer2D/Core/Screens/ScreenState.cs
@@ -0,0 +1,12 @@
+namespace Platformer2D.Screens;
+
+///
+/// Enum describes the screen transition state.
+///
+public enum ScreenState
+{
+ TransitionOn,
+ Active,
+ TransitionOff,
+ Hidden,
+}
\ No newline at end of file
diff --git a/Platformer2D/Core/Screens/SettingsScreen.cs b/Platformer2D/Core/Screens/SettingsScreen.cs
new file mode 100644
index 00000000..e77496fb
--- /dev/null
+++ b/Platformer2D/Core/Screens/SettingsScreen.cs
@@ -0,0 +1,193 @@
+using System.Collections.Generic;
+using System.Globalization;
+using Platformer2D.Core.Effects;
+using Platformer2D.Core.Localization;
+using Platformer2D.Core.Settings;
+using Platformer2D.ScreenManagers;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace Platformer2D.Screens;
+
+///
+/// The settings screen is brought up over the top of the main menu
+/// screen, and gives the user a chance to configure the game
+/// in various hopefully useful ways.
+///
+class SettingsScreen : MenuScreen
+{
+ private MenuEntry fullscreenMenuEntry;
+ private MenuEntry languageMenuEntry;
+ private MenuEntry particleEffectMenuEntry;
+ private MenuEntry backMenuEntry;
+ private static List languages;
+ private static int currentLanguage = 0;
+
+ private GraphicsDeviceManager gdm;
+
+ private static ParticleEffectType currentParticleEffect = ParticleEffectType.Fireworks;
+ ///
+ /// Gets the currently selected particle effect type.
+ ///
+ public static ParticleEffectType CurrentParticleEffect { get => currentParticleEffect; }
+
+ private SettingsManager settingsManager;
+ private ParticleManager particleManager;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public SettingsScreen()
+ : base(Resources.Settings)
+ {
+ List cultures = LocalizationManager.GetSupportedCultures();
+ languages = new List();
+ for (int i = 0; i < cultures.Count; i++)
+ {
+ languages.Add(cultures[i]);
+ }
+
+ // Create our menu entries.
+ fullscreenMenuEntry = new MenuEntry(string.Empty);
+ languageMenuEntry = new MenuEntry(string.Empty);
+ particleEffectMenuEntry = new MenuEntry(string.Empty);
+ backMenuEntry = new MenuEntry(string.Empty);
+
+ // Hook up menu event handlers.
+ fullscreenMenuEntry.Selected += FullScreenMenuEntrySelected;
+ languageMenuEntry.Selected += LanguageMenuEntrySelected;
+ particleEffectMenuEntry.Selected += ParticleEffectMenuEntrySelected;
+ backMenuEntry.Selected += OnCancel;
+
+ // Add entries to the menu.
+ MenuEntries.Add(fullscreenMenuEntry);
+ MenuEntries.Add(languageMenuEntry);
+ MenuEntries.Add(particleEffectMenuEntry);
+ MenuEntries.Add(backMenuEntry);
+ }
+
+ ///
+ /// Loads content for the settings screen, including lazy loading services and setting initial values.
+ ///
+ public override void LoadContent()
+ {
+ base.LoadContent();
+
+ // Lazy Load some things
+ gdm ??= ScreenManager.Game.Services.GetService();
+
+ settingsManager ??= ScreenManager.Game.Services.GetService>();
+
+ settingsManager.Settings.PropertyChanged += (s, e) =>
+ {
+ SetLanguageText();
+
+ settingsManager.Save();
+ };
+
+ currentLanguage = settingsManager.Settings.Language;
+ currentParticleEffect = settingsManager.Settings.ParticleEffect;
+ gdm.IsFullScreen = settingsManager.Settings.FullScreen;
+
+ SetLanguageText();
+
+ particleManager ??= ScreenManager.Game.Services.GetService();
+ }
+
+ ///
+ /// Updates the settings screen, including particle effects.
+ ///
+ /// Provides a snapshot of timing values.
+ /// Indicates whether another screen has focus.
+ /// Indicates whether the screen is covered by another screen.
+ public override void Update(GameTime gameTime, bool otherScreenHasFocus, bool coveredByOtherScreen)
+ {
+ base.Update(gameTime, otherScreenHasFocus, coveredByOtherScreen);
+
+ particleManager.Update(gameTime);
+ }
+
+ ///
+ /// Draws the settings screen, including particle effects.
+ ///
+ /// Provides a snapshot of timing values.
+ public override void Draw(GameTime gameTime)
+ {
+ SpriteBatch spriteBatch = ScreenManager.SpriteBatch;
+
+ spriteBatch.Begin(SpriteSortMode.Deferred, null, null, null, null, null, ScreenManager.GlobalTransformation);
+
+ particleManager.Draw(spriteBatch);
+
+ spriteBatch.End();
+
+ base.Draw(gameTime);
+ }
+
+ ///
+ /// Fills in the latest values for the options screen menu text.
+ ///
+ private void SetLanguageText()
+ {
+ fullscreenMenuEntry.Text = string.Format(Resources.DisplayMode, gdm.IsFullScreen ? Resources.FullScreen : Resources.Windowed);
+
+ var selectedLanguage = languages[currentLanguage].DisplayName;
+ if (selectedLanguage.Contains("Invariant"))
+ {
+ selectedLanguage = Resources.English;
+ }
+ languageMenuEntry.Text = Resources.Language + selectedLanguage;
+
+ particleEffectMenuEntry.Text = Resources.ParticleEffect + currentParticleEffect;
+
+ backMenuEntry.Text = Resources.Back;
+
+ Title = Resources.Settings;
+ }
+
+ ///
+ /// Event handler for when the Fullscreen menu entry is selected.
+ ///
+ /// The source of the event.
+ /// The instance containing the event data.
+ private void FullScreenMenuEntrySelected(object sender, PlayerIndexEventArgs e)
+ {
+ gdm.ToggleFullScreen();
+
+ settingsManager.Settings.FullScreen = gdm.IsFullScreen;
+ }
+
+ ///
+ /// Event handler for when the Language menu entry is selected.
+ ///
+ /// The source of the event.
+ /// The instance containing the event data.
+ private void LanguageMenuEntrySelected(object sender, PlayerIndexEventArgs e)
+ {
+ currentLanguage = (currentLanguage + 1) % languages.Count;
+
+ var selectedLanguage = languages[currentLanguage].Name;
+ LocalizationManager.SetCulture(selectedLanguage);
+
+ settingsManager.Settings.Language = currentLanguage;
+ }
+
+ ///
+ /// Event handler for when the Particle menu entry is selected.
+ ///
+ /// The source of the event.
+ /// The instance containing the event data.
+ private void ParticleEffectMenuEntrySelected(object sender, PlayerIndexEventArgs e)
+ {
+ currentParticleEffect++;
+
+ if (currentParticleEffect > ParticleEffectType.Sparkles)
+ {
+ currentParticleEffect = 0;
+ }
+
+ settingsManager.Settings.ParticleEffect = currentParticleEffect;
+
+ particleManager.Emit(100, currentParticleEffect); // Emit 100 particles
+ }
+}
\ No newline at end of file
diff --git a/Platformer2D/Core/Settings/BaseSettingsStorage.cs b/Platformer2D/Core/Settings/BaseSettingsStorage.cs
new file mode 100644
index 00000000..86ae0e54
--- /dev/null
+++ b/Platformer2D/Core/Settings/BaseSettingsStorage.cs
@@ -0,0 +1,94 @@
+using System;
+using System.IO;
+using System.Text.Json;
+
+namespace Platformer2D.Core.Settings;
+
+///
+/// Provides a base implementation for storing and retrieving application settings in JSON format.
+///
+public abstract class BaseSettingsStorage : ISettingsStorage
+{
+ ///
+ /// Initializes a new instance of the class with a default settings file name.
+ ///
+ protected BaseSettingsStorage()
+ {
+ SettingsFileName = "settings.json"; // Default settings file name
+ }
+
+ ///
+ /// Specifies the special folder path where settings will be stored.
+ ///
+ protected static Environment.SpecialFolder SpecialFolderPath { get; set; }
+
+ private string settingsFileName;
+
+ ///
+ /// Gets or sets the name of the settings file.
+ ///
+ public string SettingsFileName
+ {
+ get => settingsFileName;
+ set
+ {
+ if (settingsFileName != value)
+ {
+ settingsFileName = value;
+ }
+ }
+ }
+
+ ///
+ /// Gets the full path where the settings file will be stored.
+ ///
+ protected string SettingsFilePath => Path.Combine(
+ Environment.GetFolderPath(SpecialFolderPath),
+ "Platformer2D",
+ SettingsFileName);
+
+ ///
+ /// Saves the specified settings object to the designated file path in JSON format.
+ ///
+ /// The type of the settings object.
+ /// The settings data to save.
+ /// Thrown if writing to the file fails.
+ public virtual void SaveSettings(T settings) where T : new()
+ {
+ var options = new JsonSerializerOptions { WriteIndented = true };
+ string jsonString = JsonSerializer.Serialize(settings, options);
+
+ // Ensure that the directory exists before writing the file.
+ string directoryPath = Path.GetDirectoryName(SettingsFilePath);
+ if (!Directory.Exists(directoryPath))
+ {
+ Directory.CreateDirectory(directoryPath);
+ }
+
+ File.WriteAllText(SettingsFilePath, jsonString);
+ }
+
+ ///
+ /// Loads the settings object from the designated file path.
+ /// Returns a new instance of the settings object if no valid file exists.
+ ///
+ /// The type of the settings object.
+ /// The loaded settings object or a new instance if loading fails.
+ public virtual T LoadSettings() where T : new()
+ {
+ if (!SettingsExist())
+ return new T();
+
+ string jsonString = File.ReadAllText(SettingsFilePath);
+ return JsonSerializer.Deserialize(jsonString) ?? new T();
+ }
+
+ ///
+ /// Determines whether the settings file exists at the designated file path.
+ ///
+ /// true if the settings file exists; otherwise, false.
+ public bool SettingsExist()
+ {
+ return !string.IsNullOrEmpty(SettingsFilePath) && File.Exists(SettingsFilePath);
+ }
+}
\ No newline at end of file
diff --git a/Platformer2D/Core/Settings/ConsoleSettingsStorage.cs b/Platformer2D/Core/Settings/ConsoleSettingsStorage.cs
new file mode 100644
index 00000000..1b50cf6a
--- /dev/null
+++ b/Platformer2D/Core/Settings/ConsoleSettingsStorage.cs
@@ -0,0 +1,16 @@
+namespace Platformer2D.Core.Settings;
+
+///
+/// Provides a storage mechanism for game settings on console platforms.
+/// This class inherits from
+/// TODO Add your console specific implementation here. You may need one for each console
+///
+public class ConsoleSettingsStorage : BaseSettingsStorage
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ConsoleSettingsStorage()
+ {
+ }
+}
\ No newline at end of file
diff --git a/Platformer2D/Core/Settings/DesktopSettingsStorage.cs b/Platformer2D/Core/Settings/DesktopSettingsStorage.cs
new file mode 100644
index 00000000..633f35f7
--- /dev/null
+++ b/Platformer2D/Core/Settings/DesktopSettingsStorage.cs
@@ -0,0 +1,20 @@
+using System;
+
+namespace Platformer2D.Core.Settings;
+
+///
+/// Provides a storage mechanism for game settings on desktop platforms.
+/// This class inherits from and initializes
+/// the storage path to the application's data folder (e.g., AppData on Windows).
+///
+public class DesktopSettingsStorage : BaseSettingsStorage
+{
+ ///
+ /// Initializes a new instance of the class.
+ /// Sets the storage path to the application's data folder (e.g., AppData on Windows).
+ ///
+ public DesktopSettingsStorage()
+ {
+ SpecialFolderPath = Environment.SpecialFolder.ApplicationData;
+ }
+}
\ No newline at end of file
diff --git a/Platformer2D/Core/Settings/ISettingsStorage.cs b/Platformer2D/Core/Settings/ISettingsStorage.cs
new file mode 100644
index 00000000..ef7f58d9
--- /dev/null
+++ b/Platformer2D/Core/Settings/ISettingsStorage.cs
@@ -0,0 +1,38 @@
+using System.Threading.Tasks;
+
+namespace Platformer2D.Core.Settings;
+
+///
+/// Defines methods for saving and loading application settings.
+///
+public interface ISettingsStorage
+{
+ ///
+ /// Gets or sets the filename used to store settings.
+ ///
+ string SettingsFileName { get; set; }
+
+ ///
+ /// Saves the specified settings to storage.
+ ///
+ /// The type representing the settings data.
+ /// The settings data to save.
+ /// Thrown if saving fails due to file system issues.
+ /// Thrown if permission to write the file is denied.
+ void SaveSettings(T settings) where T : new();
+
+ ///
+ /// Loads settings from storage. If no settings exist, it should return null or a default instance.
+ ///
+ /// The type representing the settings data.
+ /// The loaded settings or null if no valid settings are found.
+ /// Thrown if the settings file is missing.
+ /// Thrown if loading fails due to file system issues.
+ T LoadSettings() where T : new();
+
+ ///
+ /// Checks whether the settings file exists in the storage.
+ ///
+ /// true if the settings file exists; otherwise, false.
+ bool SettingsExist();
+}
\ No newline at end of file
diff --git a/Platformer2D/Core/Settings/MobileSettingsStorage.cs b/Platformer2D/Core/Settings/MobileSettingsStorage.cs
new file mode 100644
index 00000000..d83eeb8e
--- /dev/null
+++ b/Platformer2D/Core/Settings/MobileSettingsStorage.cs
@@ -0,0 +1,20 @@
+using System;
+
+namespace Platformer2D.Core.Settings;
+
+///
+/// Provides a storage mechanism for game settings on mobile platforms.
+/// This class inherits from and initializes
+/// the storage path to the application's personal folder, as they are most likely to have write access there.
+///
+public class MobileSettingsStorage : BaseSettingsStorage
+{
+ ///
+ /// Initializes a new instance of the class.
+ /// Sets the storage path to the user's personal folder, as they are most likely to have write access there.
+ ///
+ public MobileSettingsStorage()
+ {
+ SpecialFolderPath = Environment.SpecialFolder.Personal;
+ }
+}
\ No newline at end of file
diff --git a/Platformer2D/Core/Settings/Platformer2DLeaderboard.cs b/Platformer2D/Core/Settings/Platformer2DLeaderboard.cs
new file mode 100644
index 00000000..1c9d38fd
--- /dev/null
+++ b/Platformer2D/Core/Settings/Platformer2DLeaderboard.cs
@@ -0,0 +1,48 @@
+using System;
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+
+namespace Platformer2D.Core.Settings;
+
+///
+/// Represents the leaderboard data for the game, including the fastest completion time
+/// and the number of gems collected. This class implements
+/// to notify subscribers when a property value changes, enabling data binding and UI updates.
+///
+internal class Platformer2DLeaderboard : INotifyPropertyChanged
+{
+ // TODO: Add PlayerName property in the future.
+ // public string PlayerName { get; set; }
+
+ ///
+ /// Gets or sets the fastest time taken to complete the game.
+ ///
+ ///
+ /// A representing the fastest completion time.
+ ///
+ public TimeSpan FastestTime { get; set; }
+
+ ///
+ /// Gets or sets the total number of gems collected in the game.
+ ///
+ ///
+ /// An integer representing the number of gems collected.
+ ///
+ public int GemsCollected { get; set; }
+
+ ///
+ /// Event triggered when a property value changes.
+ ///
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ ///
+ /// Raises the event to notify subscribers that a property value has changed.
+ ///
+ ///
+ /// The name of the property that changed. If not provided, the name of the calling member is used.
+ ///
+ protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+}
\ No newline at end of file
diff --git a/Platformer2D/Core/Settings/Platformer2DSettings.cs b/Platformer2D/Core/Settings/Platformer2DSettings.cs
new file mode 100644
index 00000000..e37c8c96
--- /dev/null
+++ b/Platformer2D/Core/Settings/Platformer2DSettings.cs
@@ -0,0 +1,93 @@
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+using Platformer2D.Core.Effects;
+
+namespace Platformer2D.Core.Settings;
+
+///
+/// Represents the game settings, including display, language, and particle effects.
+/// This class implements to notify subscribers
+/// when a property value changes, enabling data binding and UI updates.
+///
+public class Platformer2DSettings : INotifyPropertyChanged
+{
+ private bool fullScreen;
+ private int language = 2; // Default to English for now
+ private ParticleEffectType particleEffect;
+
+ ///
+ /// Gets or sets whether the game is in full-screen mode.
+ ///
+ ///
+ /// true if the game is in full-screen mode; otherwise, false.
+ ///
+ public bool FullScreen
+ {
+ get => fullScreen;
+ set
+ {
+ if (fullScreen != value)
+ {
+ fullScreen = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets the language setting for the game.
+ ///
+ ///
+ /// An integer representing the selected language. The value corresponds to a language
+ /// option in the game's localization system.
+ ///
+ public int Language
+ {
+ get => language;
+ set
+ {
+ if (language != value)
+ {
+ language = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets the type of particle effect used in the game.
+ ///
+ ///
+ /// A value representing the current particle effect.
+ ///
+ public ParticleEffectType ParticleEffect
+ {
+ get => particleEffect;
+ set
+ {
+ if (particleEffect != value)
+ {
+ particleEffect = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ // Add more settings as needed
+
+ ///
+ /// Event triggered when a property value changes.
+ ///
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ ///
+ /// Raises the event to notify subscribers that a property value has changed.
+ ///
+ ///
+ /// The name of the property that changed. If not provided, the name of the calling member is used.
+ ///
+ protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+}
\ No newline at end of file
diff --git a/Platformer2D/Core/Settings/SettingsManager.cs b/Platformer2D/Core/Settings/SettingsManager.cs
new file mode 100644
index 00000000..c3602d61
--- /dev/null
+++ b/Platformer2D/Core/Settings/SettingsManager.cs
@@ -0,0 +1,79 @@
+using System;
+using System.Diagnostics;
+
+namespace Platformer2D.Core.Settings;
+
+///
+/// Manages application settings using a specified storage implementation.
+///
+/// The type representing the settings data. Must have a parameterless constructor.
+internal class SettingsManager where T : new()
+{
+ private readonly ISettingsStorage storage;
+
+ ///
+ /// Provides access to the underlying settings storage mechanism.
+ ///
+ public ISettingsStorage Storage => storage;
+
+ private T settings;
+
+ ///
+ /// Provides access to the currently loaded settings. Will never be null.
+ ///
+ public T Settings => settings;
+
+ ///
+ /// Occurs when settings are successfully loaded.
+ ///
+ public event Action SettingsLoaded;
+
+ ///
+ /// Occurs when settings are successfully saved.
+ ///
+ public event Action SettingsSaved;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The storage implementation to handle saving and loading settings.
+ public SettingsManager(ISettingsStorage storage)
+ {
+ this.storage = storage ?? throw new ArgumentNullException(nameof(storage));
+ Load();
+ }
+
+ ///
+ /// Saves the current settings to the specified storage.
+ ///
+ public void Save()
+ {
+ try
+ {
+ storage.SaveSettings(settings);
+ SettingsSaved?.Invoke(settings);
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"Failed to save settings: {ex}");
+ }
+ }
+
+ ///
+ /// Loads settings from the specified storage. If loading fails, initializes with default values.
+ ///
+ public void Load()
+ {
+ try
+ {
+ settings = storage.LoadSettings() ?? new T();
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"Failed to load settings, initializing defaults: {ex}");
+ settings = new T();
+ }
+
+ SettingsLoaded?.Invoke(settings);
+ }
+}
\ No newline at end of file
diff --git a/Platformer2D/Desktop/AppIcon.xcassets/AppIcon.appiconset/Contents.json b/Platformer2D/Desktop/AppIcon.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 00000000..1831ecaf
--- /dev/null
+++ b/Platformer2D/Desktop/AppIcon.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,68 @@
+{
+ "images" : [
+ {
+ "filename": "icon_16x16.png",
+ "idiom": "mac",
+ "scale": "1x",
+ "size": "16x16"
+ },
+ {
+ "filename": "icon_32x32.png",
+ "idiom": "mac",
+ "scale": "2x",
+ "size": "16x16"
+ },
+ {
+ "filename": "icon_32x32.png",
+ "idiom": "mac",
+ "scale": "1x",
+ "size": "32x32"
+ },
+ {
+ "filename": "icon_64x64.png",
+ "idiom": "mac",
+ "scale": "2x",
+ "size": "32x32"
+ },
+ {
+ "filename": "icon_128x128.png",
+ "idiom": "mac",
+ "scale": "1x",
+ "size": "128x128"
+ },
+ {
+ "filename": "icon_256x256.png",
+ "idiom": "mac",
+ "scale": "2x",
+ "size": "128x128"
+ },
+ {
+ "filename": "icon_256x256.png",
+ "idiom": "mac",
+ "scale": "1x",
+ "size": "256x256"
+ },
+ {
+ "filename": "icon_512x512.png",
+ "idiom": "mac",
+ "scale": "2x",
+ "size": "256x256"
+ },
+ {
+ "filename": "icon_512x512.png",
+ "idiom": "mac",
+ "scale": "1x",
+ "size": "512x512"
+ },
+ {
+ "filename": "icon_1024x1024.png",
+ "idiom": "mac",
+ "scale": "2x",
+ "size": "512x512"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Platformer2D/Desktop/AppIcon.xcassets/AppIcon.appiconset/icon_1024x1024.png b/Platformer2D/Desktop/AppIcon.xcassets/AppIcon.appiconset/icon_1024x1024.png
new file mode 100644
index 00000000..99f7aee2
Binary files /dev/null and b/Platformer2D/Desktop/AppIcon.xcassets/AppIcon.appiconset/icon_1024x1024.png differ
diff --git a/Platformer2D/Desktop/AppIcon.xcassets/AppIcon.appiconset/icon_128x128.png b/Platformer2D/Desktop/AppIcon.xcassets/AppIcon.appiconset/icon_128x128.png
new file mode 100644
index 00000000..2f0de3a0
Binary files /dev/null and b/Platformer2D/Desktop/AppIcon.xcassets/AppIcon.appiconset/icon_128x128.png differ
diff --git a/Platformer2D/Desktop/AppIcon.xcassets/AppIcon.appiconset/icon_16x16.png b/Platformer2D/Desktop/AppIcon.xcassets/AppIcon.appiconset/icon_16x16.png
new file mode 100644
index 00000000..0d43d4fc
Binary files /dev/null and b/Platformer2D/Desktop/AppIcon.xcassets/AppIcon.appiconset/icon_16x16.png differ
diff --git a/Platformer2D/Desktop/AppIcon.xcassets/AppIcon.appiconset/icon_256x256.png b/Platformer2D/Desktop/AppIcon.xcassets/AppIcon.appiconset/icon_256x256.png
new file mode 100644
index 00000000..ac0c5b14
Binary files /dev/null and b/Platformer2D/Desktop/AppIcon.xcassets/AppIcon.appiconset/icon_256x256.png differ
diff --git a/Platformer2D/Desktop/AppIcon.xcassets/AppIcon.appiconset/icon_32x32.png b/Platformer2D/Desktop/AppIcon.xcassets/AppIcon.appiconset/icon_32x32.png
new file mode 100644
index 00000000..1bd9e59c
Binary files /dev/null and b/Platformer2D/Desktop/AppIcon.xcassets/AppIcon.appiconset/icon_32x32.png differ
diff --git a/Platformer2D/Desktop/AppIcon.xcassets/AppIcon.appiconset/icon_512x512.png b/Platformer2D/Desktop/AppIcon.xcassets/AppIcon.appiconset/icon_512x512.png
new file mode 100644
index 00000000..b92fc999
Binary files /dev/null and b/Platformer2D/Desktop/AppIcon.xcassets/AppIcon.appiconset/icon_512x512.png differ
diff --git a/Platformer2D/Desktop/AppIcon.xcassets/AppIcon.appiconset/icon_64x64.png b/Platformer2D/Desktop/AppIcon.xcassets/AppIcon.appiconset/icon_64x64.png
new file mode 100644
index 00000000..33f34b16
Binary files /dev/null and b/Platformer2D/Desktop/AppIcon.xcassets/AppIcon.appiconset/icon_64x64.png differ
diff --git a/Platformer2D/Platformer2D.DesktopGL/Info.plist b/Platformer2D/Desktop/Info.plist
similarity index 75%
rename from Platformer2D/Platformer2D.DesktopGL/Info.plist
rename to Platformer2D/Desktop/Info.plist
index 3db2bd59..82d82a6d 100644
--- a/Platformer2D/Platformer2D.DesktopGL/Info.plist
+++ b/Platformer2D/Desktop/Info.plist
@@ -3,9 +3,9 @@
CFBundleName
- Platformer2D.DesktopGL
+ Platformer2DCFBundleDisplayName
- Platformer2D.DesktopGL
+ Platformer2DCFBundleIdentifiercom.monogame.Platformer2DCFBundleVersion
@@ -13,9 +13,9 @@
CFBundlePackageTypeAPPLCFBundleExecutable
- Platformer2D.DesktopGL
+ Platformer2DCFBundleIconFile
- Platformer2D.DesktopGL.icns
+ Platformer2D.icnsNSHighResolutionCapable
diff --git a/Platformer2D/Desktop/Platformer2D.csproj b/Platformer2D/Desktop/Platformer2D.csproj
new file mode 100644
index 00000000..75b70843
--- /dev/null
+++ b/Platformer2D/Desktop/Platformer2D.csproj
@@ -0,0 +1,27 @@
+
+
+ Exe
+ net9.0
+ false
+ false
+ app.manifest
+ ../Core/Content/Icon.ico
+ Platformer2D
+ Platformer2D
+
+
+ true
+
+
+
+ Content\Platformer2D.mgcb
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Platformer2D/Desktop/Platformer2D.icns b/Platformer2D/Desktop/Platformer2D.icns
new file mode 100644
index 00000000..a9aadbc6
Binary files /dev/null and b/Platformer2D/Desktop/Platformer2D.icns differ
diff --git a/Platformer2D/Desktop/Program.cs b/Platformer2D/Desktop/Program.cs
new file mode 100644
index 00000000..5d634e58
--- /dev/null
+++ b/Platformer2D/Desktop/Program.cs
@@ -0,0 +1,32 @@
+using System;
+using Platformer2D.Core;
+
+internal class Program
+{
+ ///
+ /// The main entry point for the application.
+ /// This creates an instance of your game and calls it's Run() method
+ ///
+ /// Command-line arguments passed to the application.
+ [STAThread]
+ private static void Main(string[] args)
+ {
+ try
+ {
+ if (OperatingSystem.IsMacOS())
+ {
+ // On macOS, SDL2 requires specific configuration
+ Environment.SetEnvironmentVariable("SDL_VIDEO_MAC_FULLSCREEN_SPACES", "0");
+ }
+
+ using var game = new Platformer2DGame();
+ game.Run();
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Fatal error: {ex.Message}");
+ Console.WriteLine($"Stack trace: {ex.StackTrace}");
+ throw;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Platformer2D/Platformer2D.WindowsDX/app.manifest b/Platformer2D/Desktop/app.manifest
similarity index 64%
rename from Platformer2D/Platformer2D.WindowsDX/app.manifest
rename to Platformer2D/Desktop/app.manifest
index 4c64eb63..9c84c15b 100644
--- a/Platformer2D/Platformer2D.WindowsDX/app.manifest
+++ b/Platformer2D/Desktop/app.manifest
@@ -1,6 +1,6 @@
-
+
@@ -16,28 +16,27 @@
automatically selected the most compatible environment. -->
-
+
-
+
-
+
-
+
+
+
+
-
- true/pm
- permonitorv2,permonitor
-
diff --git a/Platformer2D/Platformer2D.Android/Activity1.cs b/Platformer2D/Platformer2D.Android/Activity1.cs
deleted file mode 100644
index 9136d3fb..00000000
--- a/Platformer2D/Platformer2D.Android/Activity1.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-using Android.App;
-using Android.Content.PM;
-using Android.OS;
-using Android.Views;
-using Microsoft.Xna.Framework;
-
-namespace Platformer2D.Android
-{
- [Activity(
- Label = "Platformer2D",
- MainLauncher = true,
- Icon = "@drawable/icon",
- Theme = "@style/Theme.Splash",
- AlwaysRetainTaskState = true,
- LaunchMode = LaunchMode.SingleInstance,
- ScreenOrientation = ScreenOrientation.SensorLandscape,
- ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.Keyboard | ConfigChanges.KeyboardHidden
- )]
- public class Activity1 : AndroidGameActivity
- {
- private PlatformerGame _game;
- private View _view;
-
- protected override void OnCreate(Bundle bundle)
- {
- base.OnCreate(bundle);
-
- _game = new PlatformerGame();
- _view = _game.Services.GetService(typeof(View)) as View;
-
- SetContentView(_view);
- _game.Run();
- }
- }
-}
diff --git a/Platformer2D/Platformer2D.Android/AndroidManifest.xml b/Platformer2D/Platformer2D.Android/AndroidManifest.xml
deleted file mode 100644
index aa07c248..00000000
--- a/Platformer2D/Platformer2D.Android/AndroidManifest.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Platformer2D/Platformer2D.Android/Platformer2D.Android.csproj b/Platformer2D/Platformer2D.Android/Platformer2D.Android.csproj
deleted file mode 100644
index 3a9464f5..00000000
--- a/Platformer2D/Platformer2D.Android/Platformer2D.Android.csproj
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
- net9.0-android
- 23
- Exe
- com.companyname.Platformer2D.Android
- 1
- 1.0
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Platformer2D/Platformer2D.Android/Resources/Drawable/Icon.png b/Platformer2D/Platformer2D.Android/Resources/Drawable/Icon.png
deleted file mode 100644
index 8de5549d..00000000
Binary files a/Platformer2D/Platformer2D.Android/Resources/Drawable/Icon.png and /dev/null differ
diff --git a/Platformer2D/Platformer2D.Android/Resources/Drawable/Splash.png b/Platformer2D/Platformer2D.Android/Resources/Drawable/Splash.png
deleted file mode 100644
index 75d9dd1a..00000000
Binary files a/Platformer2D/Platformer2D.Android/Resources/Drawable/Splash.png and /dev/null differ
diff --git a/Platformer2D/Platformer2D.Android/Resources/Values/Styles.xml b/Platformer2D/Platformer2D.Android/Resources/Values/Styles.xml
deleted file mode 100644
index 51021340..00000000
--- a/Platformer2D/Platformer2D.Android/Resources/Values/Styles.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
diff --git a/Platformer2D/Platformer2D.Core/Content/Levels/1.txt b/Platformer2D/Platformer2D.Core/Content/Levels/1.txt
deleted file mode 100644
index 0ca936ab..00000000
--- a/Platformer2D/Platformer2D.Core/Content/Levels/1.txt
+++ /dev/null
@@ -1,15 +0,0 @@
-....................
-....................
-..........X.........
-.......######.......
-..G..............G..
-####..G.G.G.G....###
-.......G.G.GCG......
-......--------......
-...--...........--..
-....................
-.G.G............G.G.
-####............####
-....................
-.1..................
-####################
diff --git a/Platformer2D/Platformer2D.Core/Content/Levels/2.txt b/Platformer2D/Platformer2D.Core/Content/Levels/2.txt
deleted file mode 100644
index 4908a653..00000000
--- a/Platformer2D/Platformer2D.Core/Content/Levels/2.txt
+++ /dev/null
@@ -1,15 +0,0 @@
-....................
-...G............X...
-...--..G.......--...
-....G.--........G...
-...--..........--...
-...G......G....G....
-...--....--....--...
-....G...........G...
-...--........G.--...
-...G........--.G....
-...--..........--...
-....G...........G...
-...--..........--...
-.1..................
-####################
diff --git a/Platformer2D/Platformer2D.Core/Content/Platformer2D.contentproj b/Platformer2D/Platformer2D.Core/Content/Platformer2D.contentproj
deleted file mode 100644
index 4af9a339..00000000
--- a/Platformer2D/Platformer2D.Core/Content/Platformer2D.contentproj
+++ /dev/null
@@ -1,320 +0,0 @@
-
-
-
- {935F72E1-0493-499D-ABDB-A65808B8D304}
- {96E2B04D-8817-42c6-938A-82C39BA4D311};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
- Windows
- x86
- Library
- Properties
- ContentBuilderContentContent
- v4.0
- v4.0
- Windows
- bin\$(MonoGamePlatform)\$(Configuration)
- Content
- x86
- ..\..\
- true
-
-
- Windows
-
-
- Windows8
-
-
- Android
-
-
- iOS
-
-
- OSX
-
-
- Linux
-
-
- PSM
-
-
-
-
-
-
-
-
-
- ..\..\packages\MonoGame.ContentProcessors.3.2.1\lib\MonoGameContentProcessors.dll
-
-
-
-
- Layer0_0
- TextureImporter
- TextureProcessor
-
-
- Layer0_1
- TextureImporter
- TextureProcessor
-
-
- Layer0_2
- TextureImporter
- TextureProcessor
-
-
- Layer1_0
- TextureImporter
- TextureProcessor
-
-
- Layer1_1
- TextureImporter
- TextureProcessor
-
-
- Layer1_2
- TextureImporter
- TextureProcessor
-
-
- Layer2_0
- TextureImporter
- TextureProcessor
-
-
- Layer2_1
- TextureImporter
- TextureProcessor
-
-
- Layer2_2
- TextureImporter
- TextureProcessor
-
-
- 0
- PreserveNewest
-
-
- 1
- PreserveNewest
-
-
- 2
- PreserveNewest
-
-
- you_died
- TextureImporter
- TextureProcessor
-
-
- you_lose
- TextureImporter
- TextureProcessor
-
-
- you_win
- TextureImporter
- TextureProcessor
-
-
- Music
- WmaImporter
- SongProcessor
-
-
- Gem
- TextureImporter
- TextureProcessor
-
-
- Idle
- TextureImporter
- TextureProcessor
-
-
- Run
- TextureImporter
- TextureProcessor
-
-
- Idle
- TextureImporter
- TextureProcessor
-
-
- Run
- TextureImporter
- TextureProcessor
-
-
- Idle
- TextureImporter
- TextureProcessor
-
-
- Run
- TextureImporter
- TextureProcessor
-
-
- Idle
- TextureImporter
- TextureProcessor
-
-
- Run
- TextureImporter
- TextureProcessor
-
-
- Celebrate
- TextureImporter
- TextureProcessor
-
-
- Die
- TextureImporter
- TextureProcessor
-
-
- Idle
- TextureImporter
- TextureProcessor
-
-
- Jump
- TextureImporter
- TextureProcessor
-
-
- Run
- TextureImporter
- TextureProcessor
-
-
- BlockA0
- TextureImporter
- TextureProcessor
-
-
- BlockA1
- TextureImporter
- TextureProcessor
-
-
- BlockA2
- TextureImporter
- TextureProcessor
-
-
- BlockA3
- TextureImporter
- TextureProcessor
-
-
- BlockA4
- TextureImporter
- TextureProcessor
-
-
- BlockA5
- TextureImporter
- TextureProcessor
-
-
- BlockA6
- TextureImporter
- TextureProcessor
-
-
- BlockB0
- TextureImporter
- TextureProcessor
-
-
- BlockB1
- TextureImporter
- TextureProcessor
-
-
- Exit
- TextureImporter
- TextureProcessor
-
-
- Platform
- TextureImporter
- TextureProcessor
-
-
- packages
-
-
-
-
- Hud
- FontDescriptionImporter
- FontDescriptionProcessor
-
-
-
-
- ExitReached
- WavImporter
- SoundEffectProcessor
-
-
- GemCollected
- WavImporter
- SoundEffectProcessor
-
-
- MonsterKilled
- WavImporter
- SoundEffectProcessor
-
-
- PlayerFall
- WavImporter
- SoundEffectProcessor
-
-
- PlayerJump
- WavImporter
- SoundEffectProcessor
-
-
- PlayerKilled
- WavImporter
- SoundEffectProcessor
-
-
- Powerup
- WavImporter
- SoundEffectProcessor
-
-
-
-
- VirtualControlArrow
- TextureImporter
- TextureProcessor
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Platformer2D/Platformer2D.Core/Content/Sounds/Music.wma b/Platformer2D/Platformer2D.Core/Content/Sounds/Music.wma
deleted file mode 100644
index ab25e88a..00000000
Binary files a/Platformer2D/Platformer2D.Core/Content/Sounds/Music.wma and /dev/null differ
diff --git a/Platformer2D/Platformer2D.Core/Content/Sprites/Player/Idle.png b/Platformer2D/Platformer2D.Core/Content/Sprites/Player/Idle.png
deleted file mode 100644
index de9cf357..00000000
Binary files a/Platformer2D/Platformer2D.Core/Content/Sprites/Player/Idle.png and /dev/null differ
diff --git a/Platformer2D/Platformer2D.Core/Game/Accelerometer.cs b/Platformer2D/Platformer2D.Core/Game/Accelerometer.cs
deleted file mode 100644
index 4cc52581..00000000
--- a/Platformer2D/Platformer2D.Core/Game/Accelerometer.cs
+++ /dev/null
@@ -1,99 +0,0 @@
-#region File Description
-//-----------------------------------------------------------------------------
-// Accelerometer.cs
-//
-// Microsoft XNA Community Game Platform
-// Copyright (C) Microsoft Corporation. All rights reserved.
-//-----------------------------------------------------------------------------
-#endregion
-
-#region Using Statements
-using Microsoft.Xna.Framework;
-using System;
-#endregion
-
-namespace Platformer2D
-{
- ///
- /// A static encapsulation of accelerometer input to provide games with a polling-based
- /// accelerometer system.
- ///
- public static class Accelerometer
- {
- // we want to prevent the Accelerometer from being initialized twice.
- private static bool isInitialized = false;
-
- // whether or not the accelerometer is active
- private static bool isActive = false;
-
- ///
- /// Initializes the Accelerometer for the current game. This method can only be called once per game.
- ///
- public static void Initialize()
- {
- // make sure we don't initialize the Accelerometer twice
- if (isInitialized)
- {
- throw new InvalidOperationException("Initialize can only be called once");
- }
-
- // remember that we are initialized
- isInitialized = true;
- }
-
- ///
- /// Gets the current state of the accelerometer.
- ///
- /// A new AccelerometerState with the current state of the accelerometer.
- public static AccelerometerState GetState()
- {
- // make sure we've initialized the Accelerometer before we try to get the state
- if (!isInitialized)
- {
- throw new InvalidOperationException("You must Initialize before you can call GetState");
- }
-
- // create a new value for our state
- Vector3 stateValue = new Vector3();
-
- return new AccelerometerState(stateValue, isActive);
- }
- }
-
- ///
- /// An encapsulation of the accelerometer's current state.
- ///
- public struct AccelerometerState
- {
- ///
- /// Gets the accelerometer's current value in G-force.
- ///
- public Vector3 Acceleration { get; private set; }
-
- ///
- /// Gets whether or not the accelerometer is active and running.
- ///
- public bool IsActive { get; private set; }
-
- ///
- /// Initializes a new AccelerometerState.
- ///
- /// The current acceleration (in G-force) of the accelerometer.
- /// Whether or not the accelerometer is active.
- public AccelerometerState(Vector3 acceleration, bool isActive)
- : this()
- {
- Acceleration = acceleration;
- IsActive = isActive;
- }
-
- ///
- /// Returns a string containing the values of the Acceleration and IsActive properties.
- ///
- /// A new string describing the state.
- public override string ToString()
- {
- return string.Format("Acceleration: {0}, IsActive: {1}", Acceleration, IsActive);
- }
- }
-}
\ No newline at end of file
diff --git a/Platformer2D/Platformer2D.Core/Game/AnimationPlayer.cs b/Platformer2D/Platformer2D.Core/Game/AnimationPlayer.cs
deleted file mode 100644
index 93840b06..00000000
--- a/Platformer2D/Platformer2D.Core/Game/AnimationPlayer.cs
+++ /dev/null
@@ -1,99 +0,0 @@
-#region File Description
-//-----------------------------------------------------------------------------
-// AnimationPlayer.cs
-//
-// Microsoft XNA Community Game Platform
-// Copyright (C) Microsoft Corporation. All rights reserved.
-//-----------------------------------------------------------------------------
-#endregion
-
-using System;
-using Microsoft.Xna.Framework;
-using Microsoft.Xna.Framework.Graphics;
-
-namespace Platformer2D
-{
- ///
- /// Controls playback of an Animation.
- ///
- struct AnimationPlayer
- {
- ///
- /// Gets the animation which is currently playing.
- ///
- public Animation Animation
- {
- get { return animation; }
- }
- Animation animation;
-
- ///
- /// Gets the index of the current frame in the animation.
- ///
- public int FrameIndex
- {
- get { return frameIndex; }
- }
- int frameIndex;
-
- ///
- /// The amount of time in seconds that the current frame has been shown for.
- ///
- private float time;
-
- ///
- /// Gets a texture origin at the bottom center of each frame.
- ///
- public Vector2 Origin
- {
- get { return new Vector2(Animation.FrameWidth / 2.0f, Animation.FrameHeight); }
- }
-
- ///
- /// Begins or continues playback of an animation.
- ///
- public void PlayAnimation(Animation animation)
- {
- // If this animation is already running, do not restart it.
- if (Animation == animation)
- return;
-
- // Start the new animation.
- this.animation = animation;
- this.frameIndex = 0;
- this.time = 0.0f;
- }
-
- ///
- /// Advances the time position and draws the current frame of the animation.
- ///
- public void Draw(GameTime gameTime, SpriteBatch spriteBatch, Vector2 position, SpriteEffects spriteEffects)
- {
- if (Animation == null)
- throw new NotSupportedException("No animation is currently playing.");
-
- // Process passing time.
- time += (float)gameTime.ElapsedGameTime.TotalSeconds;
- while (time > Animation.FrameTime)
- {
- time -= Animation.FrameTime;
-
- // Advance the frame index; looping or clamping as appropriate.
- if (Animation.IsLooping)
- {
- frameIndex = (frameIndex + 1) % Animation.FrameCount;
- }
- else
- {
- frameIndex = Math.Min(frameIndex + 1, Animation.FrameCount - 1);
- }
- }
-
- // Calculate the source rectangle of the current frame.
- Rectangle source = new Rectangle(FrameIndex * Animation.Texture.Height, 0, Animation.Texture.Height, Animation.Texture.Height);
-
- // Draw the current frame.
- spriteBatch.Draw(Animation.Texture, position, source, Color.White, 0.0f, Origin, 1.0f, spriteEffects, 0.0f);
- }
- }
-}
\ No newline at end of file
diff --git a/Platformer2D/Platformer2D.Core/Game/Circle.cs b/Platformer2D/Platformer2D.Core/Game/Circle.cs
deleted file mode 100644
index 908ebdb4..00000000
--- a/Platformer2D/Platformer2D.Core/Game/Circle.cs
+++ /dev/null
@@ -1,54 +0,0 @@
-#region File Description
-//-----------------------------------------------------------------------------
-// Circle.cs
-//
-// Microsoft XNA Community Game Platform
-// Copyright (C) Microsoft Corporation. All rights reserved.
-//-----------------------------------------------------------------------------
-#endregion
-
-using System;
-using Microsoft.Xna.Framework;
-
-namespace Platformer2D
-{
- ///
- /// Represents a 2D circle.
- ///
- struct Circle
- {
- ///
- /// Center position of the circle.
- ///
- public Vector2 Center;
-
- ///
- /// Radius of the circle.
- ///
- public float Radius;
-
- ///
- /// Constructs a new circle.
- ///
- public Circle(Vector2 position, float radius)
- {
- Center = position;
- Radius = radius;
- }
-
- ///
- /// Determines if a circle intersects a rectangle.
- ///
- /// True if the circle and rectangle overlap. False otherwise.
- public bool Intersects(Rectangle rectangle)
- {
- Vector2 v = new Vector2(MathHelper.Clamp(Center.X, rectangle.Left, rectangle.Right),
- MathHelper.Clamp(Center.Y, rectangle.Top, rectangle.Bottom));
-
- Vector2 direction = Center - v;
- float distanceSquared = direction.LengthSquared();
-
- return ((distanceSquared > 0) && (distanceSquared < Radius * Radius));
- }
- }
-}
\ No newline at end of file
diff --git a/Platformer2D/Platformer2D.Core/Game/Gem.cs b/Platformer2D/Platformer2D.Core/Game/Gem.cs
deleted file mode 100644
index 1aa51141..00000000
--- a/Platformer2D/Platformer2D.Core/Game/Gem.cs
+++ /dev/null
@@ -1,118 +0,0 @@
-#region File Description
-//-----------------------------------------------------------------------------
-// Gem.cs
-//
-// Microsoft XNA Community Game Platform
-// Copyright (C) Microsoft Corporation. All rights reserved.
-//-----------------------------------------------------------------------------
-#endregion
-
-using System;
-using Microsoft.Xna.Framework;
-using Microsoft.Xna.Framework.Graphics;
-using Microsoft.Xna.Framework.Audio;
-
-namespace Platformer2D
-{
- ///
- /// A valuable item the player can collect.
- ///
- class Gem
- {
- private Texture2D texture;
- private Vector2 origin;
- private SoundEffect collectedSound;
-
- public readonly int PointValue = 30;
- public readonly Color Color = Color.Yellow;
-
- // The gem is animated from a base position along the Y axis.
- private Vector2 basePosition;
- private float bounce;
-
- public Level Level
- {
- get { return level; }
- }
- Level level;
-
- ///
- /// Gets the current position of this gem in world space.
- ///
- public Vector2 Position
- {
- get
- {
- return basePosition + new Vector2(0.0f, bounce);
- }
- }
-
- ///
- /// Gets a circle which bounds this gem in world space.
- ///
- public Circle BoundingCircle
- {
- get
- {
- return new Circle(Position, Tile.Width / 3.0f);
- }
- }
-
- ///
- /// Constructs a new gem.
- ///
- public Gem(Level level, Vector2 position)
- {
- this.level = level;
- this.basePosition = position;
-
- LoadContent();
- }
-
- ///
- /// Loads the gem texture and collected sound.
- ///
- public void LoadContent()
- {
- texture = Level.Content.Load("Sprites/Gem");
- origin = new Vector2(texture.Width / 2.0f, texture.Height / 2.0f);
- collectedSound = Level.Content.Load("Sounds/GemCollected");
- }
-
- ///
- /// Bounces up and down in the air to entice players to collect them.
- ///
- public void Update(GameTime gameTime)
- {
- // Bounce control constants
- const float BounceHeight = 0.18f;
- const float BounceRate = 3.0f;
- const float BounceSync = -0.75f;
-
- // Bounce along a sine curve over time.
- // Include the X coordinate so that neighboring gems bounce in a nice wave pattern.
- double t = gameTime.TotalGameTime.TotalSeconds * BounceRate + Position.X * BounceSync;
- bounce = (float)Math.Sin(t) * BounceHeight * texture.Height;
- }
-
- ///
- /// Called when this gem has been collected by a player and removed from the level.
- ///
- ///
- /// The player who collected this gem. Although currently not used, this parameter would be
- /// useful for creating special power-up gems. For example, a gem could make the player invincible.
- ///
- public void OnCollected(Player collectedBy)
- {
- collectedSound.Play();
- }
-
- ///
- /// Draws a gem in the appropriate color.
- ///
- public void Draw(GameTime gameTime, SpriteBatch spriteBatch)
- {
- spriteBatch.Draw(texture, Position, null, Color, 0.0f, origin, 1.0f, SpriteEffects.None, 0.0f);
- }
- }
-}
\ No newline at end of file
diff --git a/Platformer2D/Platformer2D.Core/Game/Level.cs b/Platformer2D/Platformer2D.Core/Game/Level.cs
deleted file mode 100644
index 4f2e38ab..00000000
--- a/Platformer2D/Platformer2D.Core/Game/Level.cs
+++ /dev/null
@@ -1,551 +0,0 @@
-#region File Description
-//-----------------------------------------------------------------------------
-// Level.cs
-//
-// Microsoft XNA Community Game Platform
-// Copyright (C) Microsoft Corporation. All rights reserved.
-//-----------------------------------------------------------------------------
-#endregion
-
-using System;
-using System.Collections.Generic;
-using Microsoft.Xna.Framework;
-using Microsoft.Xna.Framework.Content;
-using Microsoft.Xna.Framework.Graphics;
-using Microsoft.Xna.Framework.Audio;
-using System.IO;
-using Microsoft.Xna.Framework.Input;
-
-namespace Platformer2D
-{
- ///
- /// A uniform grid of tiles with collections of gems and enemies.
- /// The level owns the player and controls the game's win and lose
- /// conditions as well as scoring.
- ///
- class Level : IDisposable
- {
- // Physical structure of the level.
- private Tile[,] tiles;
- private Texture2D[] layers;
- // The layer which entities are drawn on top of.
- private const int EntityLayer = 2;
-
- // Entities in the level.
- public Player Player
- {
- get { return player; }
- }
- Player player;
-
- private List gems = new List();
- private List enemies = new List();
-
- // Key locations in the level.
- private Vector2 start;
- private Point exit = InvalidPosition;
- private static readonly Point InvalidPosition = new Point(-1, -1);
-
- // Level game state.
- private Random random = new Random(354668); // Arbitrary, but constant seed
-
- public int Score
- {
- get { return score; }
- }
- int score;
-
- public bool ReachedExit
- {
- get { return reachedExit; }
- }
- bool reachedExit;
-
- public TimeSpan TimeRemaining
- {
- get { return timeRemaining; }
- }
- TimeSpan timeRemaining;
-
- private const int PointsPerSecond = 5;
-
- // Level content.
- public ContentManager Content
- {
- get { return content; }
- }
- ContentManager content;
-
- private SoundEffect exitReachedSound;
-
- #region Loading
-
- ///
- /// Constructs a new level.
- ///
- ///
- /// The service provider that will be used to construct a ContentManager.
- ///
- ///
- /// A stream containing the tile data.
- ///
- public Level(IServiceProvider serviceProvider, Stream fileStream, int levelIndex)
- {
- // Create a new content manager to load content used just by this level.
- content = new ContentManager(serviceProvider, "Content");
-
- timeRemaining = TimeSpan.FromMinutes(2.0);
-
- LoadTiles(fileStream);
-
- // Load background layer textures. For now, all levels must
- // use the same backgrounds and only use the left-most part of them.
- layers = new Texture2D[3];
- for (int i = 0; i < layers.Length; ++i)
- {
- // Choose a random segment if each background layer for level variety.
- int segmentIndex = levelIndex;
- layers[i] = Content.Load("Backgrounds/Layer" + i + "_" + segmentIndex);
- }
-
- // Load sounds.
- exitReachedSound = Content.Load("Sounds/ExitReached");
- }
-
- ///
- /// Iterates over every tile in the structure file and loads its
- /// appearance and behavior. This method also validates that the
- /// file is well-formed with a player start point, exit, etc.
- ///
- ///
- /// A stream containing the tile data.
- ///
- private void LoadTiles(Stream fileStream)
- {
- // Load the level and ensure all of the lines are the same length.
- int width;
- List lines = new List();
- using (StreamReader reader = new StreamReader(fileStream))
- {
- string line = reader.ReadLine();
- width = line.Length;
- while (line != null)
- {
- lines.Add(line);
- if (line.Length != width)
- throw new Exception(String.Format("The length of line {0} is different from all preceeding lines.", lines.Count));
- line = reader.ReadLine();
- }
- }
-
- // Allocate the tile grid.
- tiles = new Tile[width, lines.Count];
-
- // Loop over every tile position,
- for (int y = 0; y < Height; ++y)
- {
- for (int x = 0; x < Width; ++x)
- {
- // to load each tile.
- char tileType = lines[y][x];
- tiles[x, y] = LoadTile(tileType, x, y);
- }
- }
-
- // Verify that the level has a beginning and an end.
- if (Player == null)
- throw new NotSupportedException("A level must have a starting point.");
- if (exit == InvalidPosition)
- throw new NotSupportedException("A level must have an exit.");
-
- }
-
- ///
- /// Loads an individual tile's appearance and behavior.
- ///
- ///
- /// The character loaded from the structure file which
- /// indicates what should be loaded.
- ///
- ///
- /// The X location of this tile in tile space.
- ///
- ///
- /// The Y location of this tile in tile space.
- ///
- /// The loaded tile.
- private Tile LoadTile(char tileType, int x, int y)
- {
- switch (tileType)
- {
- // Blank space
- case '.':
- return new Tile(null, TileCollision.Passable);
-
- // Exit
- case 'X':
- return LoadExitTile(x, y);
-
- // Gem
- case 'G':
- return LoadGemTile(x, y);
-
- // Floating platform
- case '-':
- return LoadTile("Platform", TileCollision.Platform);
-
- // Various enemies
- case 'A':
- return LoadEnemyTile(x, y, "MonsterA");
- case 'B':
- return LoadEnemyTile(x, y, "MonsterB");
- case 'C':
- return LoadEnemyTile(x, y, "MonsterC");
- case 'D':
- return LoadEnemyTile(x, y, "MonsterD");
-
- // Platform block
- case '~':
- return LoadVarietyTile("BlockB", 2, TileCollision.Platform);
-
- // Passable block
- case ':':
- return LoadVarietyTile("BlockB", 2, TileCollision.Passable);
-
- // Player 1 start point
- case '1':
- return LoadStartTile(x, y);
-
- // Impassable block
- case '#':
- return LoadVarietyTile("BlockA", 7, TileCollision.Impassable);
-
- // Unknown tile type character
- default:
- throw new NotSupportedException(String.Format("Unsupported tile type character '{0}' at position {1}, {2}.", tileType, x, y));
- }
- }
-
- ///
- /// Creates a new tile. The other tile loading methods typically chain to this
- /// method after performing their special logic.
- ///
- ///
- /// Path to a tile texture relative to the Content/Tiles directory.
- ///
- ///
- /// The tile collision type for the new tile.
- ///
- /// The new tile.
- private Tile LoadTile(string name, TileCollision collision)
- {
- return new Tile(Content.Load("Tiles/" + name), collision);
- }
-
-
- ///
- /// Loads a tile with a random appearance.
- ///
- ///
- /// The content name prefix for this group of tile variations. Tile groups are
- /// name LikeThis0.png and LikeThis1.png and LikeThis2.png.
- ///
- ///
- /// The number of variations in this group.
- ///
- private Tile LoadVarietyTile(string baseName, int variationCount, TileCollision collision)
- {
- int index = random.Next(variationCount);
- return LoadTile(baseName + index, collision);
- }
-
-
- ///
- /// Instantiates a player, puts him in the level, and remembers where to put him when he is resurrected.
- ///
- private Tile LoadStartTile(int x, int y)
- {
- if (Player != null)
- throw new NotSupportedException("A level may only have one starting point.");
-
- start = RectangleExtensions.GetBottomCenter(GetBounds(x, y));
- player = new Player(this, start);
-
- return new Tile(null, TileCollision.Passable);
- }
-
- ///
- /// Remembers the location of the level's exit.
- ///
- private Tile LoadExitTile(int x, int y)
- {
- if (exit != InvalidPosition)
- throw new NotSupportedException("A level may only have one exit.");
-
- exit = GetBounds(x, y).Center;
-
- return LoadTile("Exit", TileCollision.Passable);
- }
-
- ///
- /// Instantiates an enemy and puts him in the level.
- ///
- private Tile LoadEnemyTile(int x, int y, string spriteSet)
- {
- Vector2 position = RectangleExtensions.GetBottomCenter(GetBounds(x, y));
- enemies.Add(new Enemy(this, position, spriteSet));
-
- return new Tile(null, TileCollision.Passable);
- }
-
- ///
- /// Instantiates a gem and puts it in the level.
- ///
- private Tile LoadGemTile(int x, int y)
- {
- Point position = GetBounds(x, y).Center;
- gems.Add(new Gem(this, new Vector2(position.X, position.Y)));
-
- return new Tile(null, TileCollision.Passable);
- }
-
- ///
- /// Unloads the level content.
- ///
- public void Dispose()
- {
- Content.Unload();
- }
-
- #endregion
-
- #region Bounds and collision
-
- ///
- /// Gets the collision mode of the tile at a particular location.
- /// This method handles tiles outside of the levels boundries by making it
- /// impossible to escape past the left or right edges, but allowing things
- /// to jump beyond the top of the level and fall off the bottom.
- ///
- public TileCollision GetCollision(int x, int y)
- {
- // Prevent escaping past the level ends.
- if (x < 0 || x >= Width)
- return TileCollision.Impassable;
- // Allow jumping past the level top and falling through the bottom.
- if (y < 0 || y >= Height)
- return TileCollision.Passable;
-
- return tiles[x, y].Collision;
- }
-
- ///
- /// Gets the bounding rectangle of a tile in world space.
- ///
- public Rectangle GetBounds(int x, int y)
- {
- return new Rectangle(x * Tile.Width, y * Tile.Height, Tile.Width, Tile.Height);
- }
-
- ///
- /// Width of level measured in tiles.
- ///
- public int Width
- {
- get { return tiles.GetLength(0); }
- }
-
- ///
- /// Height of the level measured in tiles.
- ///
- public int Height
- {
- get { return tiles.GetLength(1); }
- }
-
- #endregion
-
- #region Update
-
- ///
- /// Updates all objects in the world, performs collision between them,
- /// and handles the time limit with scoring.
- ///
- public void Update(
- GameTime gameTime,
- KeyboardState keyboardState,
- GamePadState gamePadState,
- AccelerometerState accelState,
- DisplayOrientation orientation)
- {
- // Pause while the player is dead or time is expired.
- if (!Player.IsAlive || TimeRemaining == TimeSpan.Zero)
- {
- // Still want to perform physics on the player.
- Player.ApplyPhysics(gameTime);
- }
- else if (ReachedExit)
- {
- // Animate the time being converted into points.
- int seconds = (int)Math.Round(gameTime.ElapsedGameTime.TotalSeconds * 100.0f);
- seconds = Math.Min(seconds, (int)Math.Ceiling(TimeRemaining.TotalSeconds));
- timeRemaining -= TimeSpan.FromSeconds(seconds);
- score += seconds * PointsPerSecond;
- }
- else
- {
- timeRemaining -= gameTime.ElapsedGameTime;
- Player.Update(gameTime, keyboardState, gamePadState, accelState, orientation);
- UpdateGems(gameTime);
-
- // Falling off the bottom of the level kills the player.
- if (Player.BoundingRectangle.Top >= Height * Tile.Height)
- OnPlayerKilled(null);
-
- UpdateEnemies(gameTime);
-
- // The player has reached the exit if they are standing on the ground and
- // his bounding rectangle contains the center of the exit tile. They can only
- // exit when they have collected all of the gems.
- if (Player.IsAlive &&
- Player.IsOnGround &&
- Player.BoundingRectangle.Contains(exit))
- {
- OnExitReached();
- }
- }
-
- // Clamp the time remaining at zero.
- if (timeRemaining < TimeSpan.Zero)
- timeRemaining = TimeSpan.Zero;
- }
-
- ///
- /// Animates each gem and checks to allows the player to collect them.
- ///
- private void UpdateGems(GameTime gameTime)
- {
- for (int i = 0; i < gems.Count; ++i)
- {
- Gem gem = gems[i];
-
- gem.Update(gameTime);
-
- if (gem.BoundingCircle.Intersects(Player.BoundingRectangle))
- {
- gems.RemoveAt(i--);
- OnGemCollected(gem, Player);
- }
- }
- }
-
- ///
- /// Animates each enemy and allow them to kill the player.
- ///
- private void UpdateEnemies(GameTime gameTime)
- {
- foreach (Enemy enemy in enemies)
- {
- enemy.Update(gameTime);
-
- // Touching an enemy instantly kills the player
- if (enemy.BoundingRectangle.Intersects(Player.BoundingRectangle))
- {
- OnPlayerKilled(enemy);
- }
- }
- }
-
- ///
- /// Called when a gem is collected.
- ///
- /// The gem that was collected.
- /// The player who collected this gem.
- private void OnGemCollected(Gem gem, Player collectedBy)
- {
- score += gem.PointValue;
-
- gem.OnCollected(collectedBy);
- }
-
- ///
- /// Called when the player is killed.
- ///
- ///
- /// The enemy who killed the player. This is null if the player was not killed by an
- /// enemy, such as when a player falls into a hole.
- ///
- private void OnPlayerKilled(Enemy killedBy)
- {
- Player.OnKilled(killedBy);
- }
-
- ///
- /// Called when the player reaches the level's exit.
- ///
- private void OnExitReached()
- {
- Player.OnReachedExit();
- exitReachedSound.Play();
- reachedExit = true;
- }
-
- ///
- /// Restores the player to the starting point to try the level again.
- ///
- public void StartNewLife()
- {
- Player.Reset(start);
- }
-
- #endregion
-
- #region Draw
-
- ///
- /// Draw everything in the level from background to foreground.
- ///
- public void Draw(GameTime gameTime, SpriteBatch spriteBatch)
- {
- for (int i = 0; i <= EntityLayer; ++i)
- spriteBatch.Draw(layers[i], Vector2.Zero, Color.White);
-
- DrawTiles(spriteBatch);
-
- foreach (Gem gem in gems)
- gem.Draw(gameTime, spriteBatch);
-
- Player.Draw(gameTime, spriteBatch);
-
- foreach (Enemy enemy in enemies)
- enemy.Draw(gameTime, spriteBatch);
-
- for (int i = EntityLayer + 1; i < layers.Length; ++i)
- spriteBatch.Draw(layers[i], Vector2.Zero, Color.White);
- }
-
- ///
- /// Draws each tile in the level.
- ///
- private void DrawTiles(SpriteBatch spriteBatch)
- {
- // For each tile position
- for (int y = 0; y < Height; ++y)
- {
- for (int x = 0; x < Width; ++x)
- {
- // If there is a visible tile in that position
- Texture2D texture = tiles[x, y].Texture;
- if (texture != null)
- {
- // Draw it in screen space.
- Vector2 position = new Vector2(x, y) * Tile.Size;
- spriteBatch.Draw(texture, position, Color.White);
- }
- }
- }
- }
-
- #endregion
- }
-}
\ No newline at end of file
diff --git a/Platformer2D/Platformer2D.Core/Game/Player.cs b/Platformer2D/Platformer2D.Core/Game/Player.cs
deleted file mode 100644
index f0ff5c2d..00000000
--- a/Platformer2D/Platformer2D.Core/Game/Player.cs
+++ /dev/null
@@ -1,457 +0,0 @@
-#region File Description
-//-----------------------------------------------------------------------------
-// Player.cs
-//
-// Microsoft XNA Community Game Platform
-// Copyright (C) Microsoft Corporation. All rights reserved.
-//-----------------------------------------------------------------------------
-#endregion
-
-using System;
-using Microsoft.Xna.Framework;
-using Microsoft.Xna.Framework.Audio;
-using Microsoft.Xna.Framework.Graphics;
-using Microsoft.Xna.Framework.Input;
-
-namespace Platformer2D
-{
- ///
- /// Our fearless adventurer!
- ///
- class Player
- {
- // Animations
- private Animation idleAnimation;
- private Animation runAnimation;
- private Animation jumpAnimation;
- private Animation celebrateAnimation;
- private Animation dieAnimation;
- private SpriteEffects flip = SpriteEffects.None;
- private AnimationPlayer sprite;
-
- // Sounds
- private SoundEffect killedSound;
- private SoundEffect jumpSound;
- private SoundEffect fallSound;
-
- public Level Level
- {
- get { return level; }
- }
- Level level;
-
- public bool IsAlive
- {
- get { return isAlive; }
- }
- bool isAlive;
-
- // Physics state
- public Vector2 Position
- {
- get { return position; }
- set { position = value; }
- }
- Vector2 position;
-
- private float previousBottom;
-
- public Vector2 Velocity
- {
- get { return velocity; }
- set { velocity = value; }
- }
- Vector2 velocity;
-
- // Constants for controlling horizontal movement
- private const float MoveAcceleration = 13000.0f;
- private const float MaxMoveSpeed = 1750.0f;
- private const float GroundDragFactor = 0.48f;
- private const float AirDragFactor = 0.58f;
-
- // Constants for controlling vertical movement
- private const float MaxJumpTime = 0.35f;
- private const float JumpLaunchVelocity = -3500.0f;
- private const float GravityAcceleration = 3400.0f;
- private const float MaxFallSpeed = 550.0f;
- private const float JumpControlPower = 0.14f;
-
- // Input configuration
- private const float MoveStickScale = 1.0f;
- private const float AccelerometerScale = 1.5f;
- private const Buttons JumpButton = Buttons.A;
-
- ///
- /// Gets whether or not the player's feet are on the ground.
- ///
- public bool IsOnGround
- {
- get { return isOnGround; }
- }
- bool isOnGround;
-
- ///
- /// Current user movement input.
- ///
- private float movement;
-
- // Jumping state
- private bool isJumping;
- private bool wasJumping;
- private float jumpTime;
-
- private Rectangle localBounds;
- ///
- /// Gets a rectangle which bounds this player in world space.
- ///
- public Rectangle BoundingRectangle
- {
- get
- {
- int left = (int)Math.Round(Position.X - sprite.Origin.X) + localBounds.X;
- int top = (int)Math.Round(Position.Y - sprite.Origin.Y) + localBounds.Y;
-
- return new Rectangle(left, top, localBounds.Width, localBounds.Height);
- }
- }
-
- ///
- /// Constructors a new player.
- ///
- public Player(Level level, Vector2 position)
- {
- this.level = level;
-
- LoadContent();
-
- Reset(position);
- }
-
- ///
- /// Loads the player sprite sheet and sounds.
- ///
- public void LoadContent()
- {
- // Load animated textures.
- idleAnimation = new Animation(Level.Content.Load("Sprites/Player/Idle"), 0.1f, true);
- runAnimation = new Animation(Level.Content.Load("Sprites/Player/Run"), 0.1f, true);
- jumpAnimation = new Animation(Level.Content.Load("Sprites/Player/Jump"), 0.1f, false);
- celebrateAnimation = new Animation(Level.Content.Load("Sprites/Player/Celebrate"), 0.1f, false);
- dieAnimation = new Animation(Level.Content.Load("Sprites/Player/Die"), 0.1f, false);
-
- // Calculate bounds within texture size.
- int width = (int)(idleAnimation.FrameWidth * 0.4);
- int left = (idleAnimation.FrameWidth - width) / 2;
- int height = (int)(idleAnimation.FrameHeight * 0.8);
- int top = idleAnimation.FrameHeight - height;
- localBounds = new Rectangle(left, top, width, height);
-
- // Load sounds.
- killedSound = Level.Content.Load("Sounds/PlayerKilled");
- jumpSound = Level.Content.Load("Sounds/PlayerJump");
- fallSound = Level.Content.Load("Sounds/PlayerFall");
- }
-
- ///
- /// Resets the player to life.
- ///
- /// The position to come to life at.
- public void Reset(Vector2 position)
- {
- Position = position;
- Velocity = Vector2.Zero;
- isAlive = true;
- sprite.PlayAnimation(idleAnimation);
- }
-
- ///
- /// Handles input, performs physics, and animates the player sprite.
- ///
- ///
- /// We pass in all of the input states so that our game is only polling the hardware
- /// once per frame. We also pass the game's orientation because when using the accelerometer,
- /// we need to reverse our motion when the orientation is in the LandscapeRight orientation.
- ///
- public void Update(
- GameTime gameTime,
- KeyboardState keyboardState,
- GamePadState gamePadState,
- AccelerometerState accelState,
- DisplayOrientation orientation)
- {
- GetInput(keyboardState, gamePadState, accelState, orientation);
-
- ApplyPhysics(gameTime);
-
- if (IsAlive && IsOnGround)
- {
- if (Math.Abs(Velocity.X) - 0.02f > 0)
- {
- sprite.PlayAnimation(runAnimation);
- }
- else
- {
- sprite.PlayAnimation(idleAnimation);
- }
- }
-
- // Clear input.
- movement = 0.0f;
- isJumping = false;
- }
-
- ///
- /// Gets player horizontal movement and jump commands from input.
- ///
- private void GetInput(
- KeyboardState keyboardState,
- GamePadState gamePadState,
- AccelerometerState accelState,
- DisplayOrientation orientation)
- {
- // Get analog horizontal movement.
- movement = gamePadState.ThumbSticks.Left.X * MoveStickScale;
-
- // Ignore small movements to prevent running in place.
- if (Math.Abs(movement) < 0.5f)
- movement = 0.0f;
-
- // Move the player with accelerometer
- if (Math.Abs(accelState.Acceleration.Y) > 0.10f)
- {
- // set our movement speed
- movement = MathHelper.Clamp(-accelState.Acceleration.Y * AccelerometerScale, -1f, 1f);
-
- // if we're in the LandscapeLeft orientation, we must reverse our movement
- if (orientation == DisplayOrientation.LandscapeRight)
- movement = -movement;
- }
-
- // If any digital horizontal movement input is found, override the analog movement.
- if (gamePadState.IsButtonDown(Buttons.DPadLeft) ||
- keyboardState.IsKeyDown(Keys.Left) ||
- keyboardState.IsKeyDown(Keys.A))
- {
- movement = -1.0f;
- }
- else if (gamePadState.IsButtonDown(Buttons.DPadRight) ||
- keyboardState.IsKeyDown(Keys.Right) ||
- keyboardState.IsKeyDown(Keys.D))
- {
- movement = 1.0f;
- }
-
- // Check if the player wants to jump.
- isJumping =
- gamePadState.IsButtonDown(JumpButton) ||
- keyboardState.IsKeyDown(Keys.Space) ||
- keyboardState.IsKeyDown(Keys.Up) ||
- keyboardState.IsKeyDown(Keys.W);
- }
-
- ///
- /// Updates the player's velocity and position based on input, gravity, etc.
- ///
- public void ApplyPhysics(GameTime gameTime)
- {
- float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds;
-
- Vector2 previousPosition = Position;
-
- // Base velocity is a combination of horizontal movement control and
- // acceleration downward due to gravity.
- velocity.X += movement * MoveAcceleration * elapsed;
- velocity.Y = MathHelper.Clamp(velocity.Y + GravityAcceleration * elapsed, -MaxFallSpeed, MaxFallSpeed);
-
- velocity.Y = DoJump(velocity.Y, gameTime);
-
- // Apply pseudo-drag horizontally.
- if (IsOnGround)
- velocity.X *= GroundDragFactor;
- else
- velocity.X *= AirDragFactor;
-
- // Prevent the player from running faster than his top speed.
- velocity.X = MathHelper.Clamp(velocity.X, -MaxMoveSpeed, MaxMoveSpeed);
-
- // Apply velocity.
- Position += velocity * elapsed;
- Position = new Vector2((float)Math.Round(Position.X), (float)Math.Round(Position.Y));
-
- // If the player is now colliding with the level, separate them.
- HandleCollisions();
-
- // If the collision stopped us from moving, reset the velocity to zero.
- if (Position.X == previousPosition.X)
- velocity.X = 0;
-
- if (Position.Y == previousPosition.Y)
- velocity.Y = 0;
- }
-
- ///
- /// Calculates the Y velocity accounting for jumping and
- /// animates accordingly.
- ///
- ///
- /// During the accent of a jump, the Y velocity is completely
- /// overridden by a power curve. During the decent, gravity takes
- /// over. The jump velocity is controlled by the jumpTime field
- /// which measures time into the accent of the current jump.
- ///
- ///
- /// The player's current velocity along the Y axis.
- ///
- ///
- /// A new Y velocity if beginning or continuing a jump.
- /// Otherwise, the existing Y velocity.
- ///
- private float DoJump(float velocityY, GameTime gameTime)
- {
- // If the player wants to jump
- if (isJumping)
- {
- // Begin or continue a jump
- if ((!wasJumping && IsOnGround) || jumpTime > 0.0f)
- {
- if (jumpTime == 0.0f)
- jumpSound.Play();
-
- jumpTime += (float)gameTime.ElapsedGameTime.TotalSeconds;
- sprite.PlayAnimation(jumpAnimation);
- }
-
- // If we are in the ascent of the jump
- if (0.0f < jumpTime && jumpTime <= MaxJumpTime)
- {
- // Fully override the vertical velocity with a power curve that gives players more control over the top of the jump
- velocityY = JumpLaunchVelocity * (1.0f - (float)Math.Pow(jumpTime / MaxJumpTime, JumpControlPower));
- }
- else
- {
- // Reached the apex of the jump
- jumpTime = 0.0f;
- }
- }
- else
- {
- // Continues not jumping or cancels a jump in progress
- jumpTime = 0.0f;
- }
- wasJumping = isJumping;
-
- return velocityY;
- }
-
- ///
- /// Detects and resolves all collisions between the player and his neighboring
- /// tiles. When a collision is detected, the player is pushed away along one
- /// axis to prevent overlapping. There is some special logic for the Y axis to
- /// handle platforms which behave differently depending on direction of movement.
- ///
- private void HandleCollisions()
- {
- // Get the player's bounding rectangle and find neighboring tiles.
- Rectangle bounds = BoundingRectangle;
- int leftTile = (int)Math.Floor((float)bounds.Left / Tile.Width);
- int rightTile = (int)Math.Ceiling(((float)bounds.Right / Tile.Width)) - 1;
- int topTile = (int)Math.Floor((float)bounds.Top / Tile.Height);
- int bottomTile = (int)Math.Ceiling(((float)bounds.Bottom / Tile.Height)) - 1;
-
- // Reset flag to search for ground collision.
- isOnGround = false;
-
- // For each potentially colliding tile,
- for (int y = topTile; y <= bottomTile; ++y)
- {
- for (int x = leftTile; x <= rightTile; ++x)
- {
- // If this tile is collidable,
- TileCollision collision = Level.GetCollision(x, y);
- if (collision != TileCollision.Passable)
- {
- // Determine collision depth (with direction) and magnitude.
- Rectangle tileBounds = Level.GetBounds(x, y);
- Vector2 depth = RectangleExtensions.GetIntersectionDepth(bounds, tileBounds);
- if (depth != Vector2.Zero)
- {
- float absDepthX = Math.Abs(depth.X);
- float absDepthY = Math.Abs(depth.Y);
-
- // Resolve the collision along the shallow axis.
- if (absDepthY < absDepthX || collision == TileCollision.Platform)
- {
- // If we crossed the top of a tile, we are on the ground.
- if (previousBottom <= tileBounds.Top)
- isOnGround = true;
-
- // Ignore platforms, unless we are on the ground.
- if (collision == TileCollision.Impassable || IsOnGround)
- {
- // Resolve the collision along the Y axis.
- Position = new Vector2(Position.X, Position.Y + depth.Y);
-
- // Perform further collisions with the new bounds.
- bounds = BoundingRectangle;
- }
- }
- else if (collision == TileCollision.Impassable) // Ignore platforms.
- {
- // Resolve the collision along the X axis.
- Position = new Vector2(Position.X + depth.X, Position.Y);
-
- // Perform further collisions with the new bounds.
- bounds = BoundingRectangle;
- }
- }
- }
- }
- }
-
- // Save the new bounds bottom.
- previousBottom = bounds.Bottom;
- }
-
- ///
- /// Called when the player has been killed.
- ///
- ///
- /// The enemy who killed the player. This parameter is null if the player was
- /// not killed by an enemy (fell into a hole).
- ///
- public void OnKilled(Enemy killedBy)
- {
- isAlive = false;
-
- if (killedBy != null)
- killedSound.Play();
- else
- fallSound.Play();
-
- sprite.PlayAnimation(dieAnimation);
- }
-
- ///
- /// Called when this player reaches the level's exit.
- ///
- public void OnReachedExit()
- {
- sprite.PlayAnimation(celebrateAnimation);
- }
-
- ///
- /// Draws the animated player.
- ///
- public void Draw(GameTime gameTime, SpriteBatch spriteBatch)
- {
- // Flip the sprite to face the way we are moving.
- if (Velocity.X > 0)
- flip = SpriteEffects.FlipHorizontally;
- else if (Velocity.X < 0)
- flip = SpriteEffects.None;
-
- // Draw that sprite.
- sprite.Draw(gameTime, spriteBatch, Position, flip);
- }
- }
-}
\ No newline at end of file
diff --git a/Platformer2D/Platformer2D.Core/Game/RectangleExtensions.cs b/Platformer2D/Platformer2D.Core/Game/RectangleExtensions.cs
deleted file mode 100644
index 1a7ceef4..00000000
--- a/Platformer2D/Platformer2D.Core/Game/RectangleExtensions.cs
+++ /dev/null
@@ -1,66 +0,0 @@
-#region File Description
-//-----------------------------------------------------------------------------
-// RectangleExtensions.cs
-//
-// Microsoft XNA Community Game Platform
-// Copyright (C) Microsoft Corporation. All rights reserved.
-//-----------------------------------------------------------------------------
-#endregion
-
-using System;
-using Microsoft.Xna.Framework;
-
-namespace Platformer2D
-{
- ///
- /// A set of helpful methods for working with rectangles.
- ///
- public static class RectangleExtensions
- {
- ///
- /// Calculates the signed depth of intersection between two rectangles.
- ///
- ///
- /// The amount of overlap between two intersecting rectangles. These
- /// depth values can be negative depending on which wides the rectangles
- /// intersect. This allows callers to determine the correct direction
- /// to push objects in order to resolve collisions.
- /// If the rectangles are not intersecting, Vector2.Zero is returned.
- ///
- public static Vector2 GetIntersectionDepth(this Rectangle rectA, Rectangle rectB)
- {
- // Calculate half sizes.
- float halfWidthA = rectA.Width / 2.0f;
- float halfHeightA = rectA.Height / 2.0f;
- float halfWidthB = rectB.Width / 2.0f;
- float halfHeightB = rectB.Height / 2.0f;
-
- // Calculate centers.
- Vector2 centerA = new Vector2(rectA.Left + halfWidthA, rectA.Top + halfHeightA);
- Vector2 centerB = new Vector2(rectB.Left + halfWidthB, rectB.Top + halfHeightB);
-
- // Calculate current and minimum-non-intersecting distances between centers.
- float distanceX = centerA.X - centerB.X;
- float distanceY = centerA.Y - centerB.Y;
- float minDistanceX = halfWidthA + halfWidthB;
- float minDistanceY = halfHeightA + halfHeightB;
-
- // If we are not intersecting at all, return (0, 0).
- if (Math.Abs(distanceX) >= minDistanceX || Math.Abs(distanceY) >= minDistanceY)
- return Vector2.Zero;
-
- // Calculate and return intersection depths.
- float depthX = distanceX > 0 ? minDistanceX - distanceX : -minDistanceX - distanceX;
- float depthY = distanceY > 0 ? minDistanceY - distanceY : -minDistanceY - distanceY;
- return new Vector2(depthX, depthY);
- }
-
- ///
- /// Gets the position of the center of the bottom edge of the rectangle.
- ///
- public static Vector2 GetBottomCenter(this Rectangle rect)
- {
- return new Vector2(rect.X + rect.Width / 2.0f, rect.Bottom);
- }
- }
-}
\ No newline at end of file
diff --git a/Platformer2D/Platformer2D.Core/Game/Tile.cs b/Platformer2D/Platformer2D.Core/Game/Tile.cs
deleted file mode 100644
index 2ba667fa..00000000
--- a/Platformer2D/Platformer2D.Core/Game/Tile.cs
+++ /dev/null
@@ -1,62 +0,0 @@
-#region File Description
-//-----------------------------------------------------------------------------
-// Tile.cs
-//
-// Microsoft XNA Community Game Platform
-// Copyright (C) Microsoft Corporation. All rights reserved.
-//-----------------------------------------------------------------------------
-#endregion
-
-using System;
-using Microsoft.Xna.Framework;
-using Microsoft.Xna.Framework.Graphics;
-
-namespace Platformer2D
-{
- ///
- /// Controls the collision detection and response behavior of a tile.
- ///
- enum TileCollision
- {
- ///
- /// A passable tile is one which does not hinder player motion at all.
- ///
- Passable = 0,
-
- ///
- /// An impassable tile is one which does not allow the player to move through
- /// it at all. It is completely solid.
- ///
- Impassable = 1,
-
- ///
- /// A platform tile is one which behaves like a passable tile except when the
- /// player is above it. A player can jump up through a platform as well as move
- /// past it to the left and right, but can not fall down through the top of it.
- ///
- Platform = 2,
- }
-
- ///
- /// Stores the appearance and collision behavior of a tile.
- ///
- struct Tile
- {
- public Texture2D Texture;
- public TileCollision Collision;
-
- public const int Width = 40;
- public const int Height = 32;
-
- public static readonly Vector2 Size = new Vector2(Width, Height);
-
- ///
- /// Constructs a new tile.
- ///
- public Tile(Texture2D texture, TileCollision collision)
- {
- Texture = texture;
- Collision = collision;
- }
- }
-}
\ No newline at end of file
diff --git a/Platformer2D/Platformer2D.Core/Game/TouchCollectionExtensions.cs b/Platformer2D/Platformer2D.Core/Game/TouchCollectionExtensions.cs
deleted file mode 100644
index cbd6f3e7..00000000
--- a/Platformer2D/Platformer2D.Core/Game/TouchCollectionExtensions.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-#region File Description
-//-----------------------------------------------------------------------------
-// TouchCollectionExtensions.cs
-//
-// Microsoft XNA Community Game Platform
-// Copyright (C) Microsoft Corporation. All rights reserved.
-//-----------------------------------------------------------------------------
-#endregion
-
-using Microsoft.Xna.Framework.Input.Touch;
-
-namespace Platformer2D
-{
- ///
- /// Provides extension methods for the TouchCollection type.
- ///
- public static class TouchCollectionExtensions
- {
- ///
- /// Determines if there are any touches on the screen.
- ///
- /// The current TouchCollection.
- /// True if there are any touches in the Pressed or Moved state, false otherwise
- public static bool AnyTouch(this TouchCollection touchState)
- {
- foreach (TouchLocation location in touchState)
- {
- if (location.State == TouchLocationState.Pressed || location.State == TouchLocationState.Moved)
- {
- return true;
- }
- }
- return false;
- }
- }
-}
\ No newline at end of file
diff --git a/Platformer2D/Platformer2D.Core/Game/VirtualGamePad.cs b/Platformer2D/Platformer2D.Core/Game/VirtualGamePad.cs
deleted file mode 100644
index f669bc84..00000000
--- a/Platformer2D/Platformer2D.Core/Game/VirtualGamePad.cs
+++ /dev/null
@@ -1,106 +0,0 @@
-using System;
-using Microsoft.Xna.Framework;
-using Microsoft.Xna.Framework.Graphics;
-using Microsoft.Xna.Framework.Input;
-using Microsoft.Xna.Framework.Input.Touch;
-
-namespace Platformer2D
-{
- class VirtualGamePad
- {
- private readonly Vector2 baseScreenSize;
- private Matrix globalTransformation;
- private readonly Texture2D texture;
-
- private float secondsSinceLastInput;
- private float opacity;
-
- public VirtualGamePad(Vector2 baseScreenSize, Matrix globalTransformation, Texture2D texture)
- {
- this.baseScreenSize = baseScreenSize;
- this.globalTransformation = Matrix.Invert(globalTransformation);
- this.texture = texture;
- secondsSinceLastInput = float.MaxValue;
- }
-
- public void NotifyPlayerIsMoving()
- {
- secondsSinceLastInput = 0;
- }
-
- public void Update(GameTime gameTime)
- {
- var secondsElapsed = (float)gameTime.ElapsedGameTime.TotalSeconds;
- secondsSinceLastInput += secondsElapsed;
-
- //If the player is moving, fade the controls out
- // otherwise, if they haven't moved in 4 seconds, fade the controls back in
- if (secondsSinceLastInput < 4)
- opacity = Math.Max(0, opacity - secondsElapsed * 4);
- else
- opacity = Math.Min(1, opacity + secondsElapsed * 2);
- }
-
- public void Draw(SpriteBatch spriteBatch)
- {
- var spriteCenter = new Vector2(64, 64);
- var color = Color.Multiply(Color.White, opacity);
-
- spriteBatch.Draw(texture, new Vector2(64, baseScreenSize.Y - 64), null, color, -MathHelper.PiOver2, spriteCenter, 1, SpriteEffects.None, 0);
- spriteBatch.Draw(texture, new Vector2(192, baseScreenSize.Y - 64), null, color, MathHelper.PiOver2, spriteCenter, 1, SpriteEffects.None, 0);
- spriteBatch.Draw(texture, new Vector2(baseScreenSize.X - 128, baseScreenSize.Y - 128), null, color, 0, Vector2.Zero, 1, SpriteEffects.None, 0);
- }
-
- ///
- /// Generates a GamePadState based on the touch input provided (as applied to the on screen controls) and the gamepad state
- ///
- public GamePadState GetState(TouchCollection touchState, GamePadState gpState)
- {
- //Work out what buttons are pressed based on the touchState
- Buttons buttonsPressed = 0;
-
- foreach (var touch in touchState)
- {
- if (touch.State == TouchLocationState.Moved || touch.State == TouchLocationState.Pressed)
- {
- //Scale the touch position to be in _baseScreenSize coordinates
- Vector2 pos = touch.Position;
- Vector2.Transform(ref pos, ref globalTransformation, out pos);
-
- if (pos.X < 128)
- buttonsPressed |= Buttons.DPadLeft;
- else if (pos.X < 256)
- buttonsPressed |= Buttons.DPadRight;
- else if (pos.X >= baseScreenSize.X - 128)
- buttonsPressed |= Buttons.A;
- }
- }
-
- //Combine the buttons of the real gamepad
- var gpButtons = gpState.Buttons;
- buttonsPressed |= (gpButtons.A == ButtonState.Pressed ? Buttons.A : 0);
- buttonsPressed |= (gpButtons.B == ButtonState.Pressed ? Buttons.B : 0);
- buttonsPressed |= (gpButtons.X == ButtonState.Pressed ? Buttons.X : 0);
- buttonsPressed |= (gpButtons.Y == ButtonState.Pressed ? Buttons.Y : 0);
-
- buttonsPressed |= (gpButtons.Start == ButtonState.Pressed ? Buttons.Start : 0);
- buttonsPressed |= (gpButtons.Back == ButtonState.Pressed ? Buttons.Back : 0);
-
- buttonsPressed |= gpState.IsButtonDown(Buttons.DPadDown) ? Buttons.DPadDown : 0;
- buttonsPressed |= gpState.IsButtonDown(Buttons.DPadLeft) ? Buttons.DPadLeft : 0;
- buttonsPressed |= gpState.IsButtonDown(Buttons.DPadRight) ? Buttons.DPadRight : 0;
- buttonsPressed |= gpState.IsButtonDown(Buttons.DPadUp) ? Buttons.DPadUp : 0;
-
- buttonsPressed |= (gpButtons.BigButton == ButtonState.Pressed ? Buttons.BigButton : 0);
- buttonsPressed |= (gpButtons.LeftShoulder == ButtonState.Pressed ? Buttons.LeftShoulder : 0);
- buttonsPressed |= (gpButtons.RightShoulder == ButtonState.Pressed ? Buttons.RightShoulder : 0);
-
- buttonsPressed |= (gpButtons.LeftStick == ButtonState.Pressed ? Buttons.LeftStick : 0);
- buttonsPressed |= (gpButtons.RightStick == ButtonState.Pressed ? Buttons.RightStick : 0);
-
- var buttons = new GamePadButtons(buttonsPressed);
-
- return new GamePadState(gpState.ThumbSticks, gpState.Triggers, buttons, gpState.DPad);
- }
- }
-}
\ No newline at end of file
diff --git a/Platformer2D/Platformer2D.Core/Platformer2D.Core.csproj b/Platformer2D/Platformer2D.Core/Platformer2D.Core.csproj
deleted file mode 100644
index b435112b..00000000
--- a/Platformer2D/Platformer2D.Core/Platformer2D.Core.csproj
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
- net9.0
- AnyCPU;x64
-
-
-
- All
-
-
-
\ No newline at end of file
diff --git a/Platformer2D/Platformer2D.Core/PlatformerGame.cs b/Platformer2D/Platformer2D.Core/PlatformerGame.cs
deleted file mode 100644
index b2409065..00000000
--- a/Platformer2D/Platformer2D.Core/PlatformerGame.cs
+++ /dev/null
@@ -1,304 +0,0 @@
-#region File Description
-//-----------------------------------------------------------------------------
-// PlatformerGame.cs
-//
-// Microsoft XNA Community Game Platform
-// Copyright (C) Microsoft Corporation. All rights reserved.
-//-----------------------------------------------------------------------------
-#endregion
-
-using System;
-using System.IO;
-using Microsoft.Xna.Framework;
-using Microsoft.Xna.Framework.Content;
-using Microsoft.Xna.Framework.Graphics;
-using Microsoft.Xna.Framework.Input;
-using Microsoft.Xna.Framework.Input.Touch;
-using Microsoft.Xna.Framework.Media;
-
-namespace Platformer2D
-{
- ///
- /// This is the main type for your game
- ///
- public class PlatformerGame : Microsoft.Xna.Framework.Game
- {
- // Resources for drawing.
- private GraphicsDeviceManager graphics;
- private SpriteBatch spriteBatch;
- Vector2 baseScreenSize = new Vector2(800, 480);
- private Matrix globalTransformation;
- int backbufferWidth, backbufferHeight;
-
- // Global content.
- private SpriteFont hudFont;
-
- private Texture2D winOverlay;
- private Texture2D loseOverlay;
- private Texture2D diedOverlay;
-
- // Meta-level game state.
- private int levelIndex = -1;
- private Level level;
- private bool wasContinuePressed;
-
- // When the time remaining is less than the warning time, it blinks on the hud
- private static readonly TimeSpan WarningTime = TimeSpan.FromSeconds(30);
-
- // We store our input states so that we only poll once per frame,
- // then we use the same input state wherever needed
- private GamePadState gamePadState;
- private KeyboardState keyboardState;
- private TouchCollection touchState;
- private AccelerometerState accelerometerState;
-
- private VirtualGamePad virtualGamePad;
-
- // The number of levels in the Levels directory of our content. We assume that
- // levels in our content are 0-based and that all numbers under this constant
- // have a level file present. This allows us to not need to check for the file
- // or handle exceptions, both of which can add unnecessary time to level loading.
- private const int numberOfLevels = 3;
-
- public PlatformerGame()
- {
- graphics = new GraphicsDeviceManager(this);
-
-#if WINDOWS_PHONE
- TargetElapsedTime = TimeSpan.FromTicks(333333);
-#endif
- graphics.IsFullScreen = false;
-
- //graphics.PreferredBackBufferWidth = 800;
- //graphics.PreferredBackBufferHeight = 480;
- graphics.SupportedOrientations = DisplayOrientation.LandscapeLeft | DisplayOrientation.LandscapeRight;
-
- Accelerometer.Initialize();
- }
-
- ///
- /// LoadContent will be called once per game and is the place to load
- /// all of your content.
- ///
- protected override void LoadContent()
- {
- this.Content.RootDirectory = "Content";
-
- // Create a new SpriteBatch, which can be used to draw textures.
- spriteBatch = new SpriteBatch(GraphicsDevice);
-
- // Load fonts
- hudFont = Content.Load("Fonts/Hud");
-
- // Load overlay textures
- winOverlay = Content.Load("Overlays/you_win");
- loseOverlay = Content.Load("Overlays/you_lose");
- diedOverlay = Content.Load("Overlays/you_died");
-
- ScalePresentationArea();
-
- virtualGamePad = new VirtualGamePad(baseScreenSize, globalTransformation, Content.Load("Sprites/VirtualControlArrow"));
-
- if (!OperatingSystem.IsIOS())
- {
- //Known issue that you get exceptions if you use Media PLayer while connected to your PC
- //See http://social.msdn.microsoft.com/Forums/en/windowsphone7series/thread/c8a243d2-d360-46b1-96bd-62b1ef268c66
- //Which means its impossible to test this from VS.
- //So we have to catch the exception and throw it away
- try
- {
- MediaPlayer.IsRepeating = true;
- MediaPlayer.Play(Content.Load("Sounds/Music"));
- }
- catch { }
- }
-
- LoadNextLevel();
- }
-
- public void ScalePresentationArea()
- {
- //Work out how much we need to scale our graphics to fill the screen
- backbufferWidth = GraphicsDevice.PresentationParameters.BackBufferWidth;
- backbufferHeight = GraphicsDevice.PresentationParameters.BackBufferHeight;
- float horScaling = backbufferWidth / baseScreenSize.X;
- float verScaling = backbufferHeight / baseScreenSize.Y;
- Vector3 screenScalingFactor = new Vector3(horScaling, verScaling, 1);
- globalTransformation = Matrix.CreateScale(screenScalingFactor);
- System.Diagnostics.Debug.WriteLine("Screen Size - Width[" + GraphicsDevice.PresentationParameters.BackBufferWidth + "] Height [" + GraphicsDevice.PresentationParameters.BackBufferHeight + "]");
- }
-
-
- ///
- /// Allows the game to run logic such as updating the world,
- /// checking for collisions, gathering input, and playing audio.
- ///
- /// Provides a snapshot of timing values.
- protected override void Update(GameTime gameTime)
- {
- //Confirm the screen has not been resized by the user
- if (backbufferHeight != GraphicsDevice.PresentationParameters.BackBufferHeight ||
- backbufferWidth != GraphicsDevice.PresentationParameters.BackBufferWidth)
- {
- ScalePresentationArea();
- }
- // Handle polling for our input and handling high-level input
- HandleInput(gameTime);
-
- // update our level, passing down the GameTime along with all of our input states
- level.Update(gameTime, keyboardState, gamePadState,
- accelerometerState, Window.CurrentOrientation);
-
- if (level.Player.Velocity != Vector2.Zero)
- virtualGamePad.NotifyPlayerIsMoving();
-
- base.Update(gameTime);
- }
-
- private void HandleInput(GameTime gameTime)
- {
- // get all of our input states
- keyboardState = Keyboard.GetState();
- touchState = TouchPanel.GetState();
- gamePadState = virtualGamePad.GetState(touchState, GamePad.GetState(PlayerIndex.One));
- accelerometerState = Accelerometer.GetState();
-
- if (!OperatingSystem.IsIOS())
- {
- // Exit the game when back is pressed.
- if (gamePadState.Buttons.Back == ButtonState.Pressed)
- Exit();
- }
-
- bool continuePressed =
- keyboardState.IsKeyDown(Keys.Space) ||
- gamePadState.IsButtonDown(Buttons.A) ||
- touchState.AnyTouch();
-
- // Perform the appropriate action to advance the game and
- // to get the player back to playing.
- if (!wasContinuePressed && continuePressed)
- {
- if (!level.Player.IsAlive)
- {
- level.StartNewLife();
- }
- else if (level.TimeRemaining == TimeSpan.Zero)
- {
- if (level.ReachedExit)
- LoadNextLevel();
- else
- ReloadCurrentLevel();
- }
- }
-
- wasContinuePressed = continuePressed;
-
- virtualGamePad.Update(gameTime);
- }
-
- private void LoadNextLevel()
- {
- // move to the next level
- levelIndex = (levelIndex + 1) % numberOfLevels;
-
- // Unloads the content for the current level before loading the next one.
- if (level != null)
- level.Dispose();
-
- // Load the level.
- string levelPath = string.Format("Content/Levels/{0}.txt", levelIndex);
- using (Stream fileStream = TitleContainer.OpenStream(levelPath))
- level = new Level(Services, fileStream, levelIndex);
- }
-
- private void ReloadCurrentLevel()
- {
- --levelIndex;
- LoadNextLevel();
- }
-
- ///
- /// Draws the game from background to foreground.
- ///
- /// Provides a snapshot of timing values.
- protected override void Draw(GameTime gameTime)
- {
- graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
-
- spriteBatch.Begin(SpriteSortMode.Deferred, null, null, null, null,null, globalTransformation);
-
- level.Draw(gameTime, spriteBatch);
-
- DrawHud();
-
- spriteBatch.End();
-
- base.Draw(gameTime);
- }
-
- private void DrawHud()
- {
- Rectangle titleSafeArea = GraphicsDevice.Viewport.TitleSafeArea;
- Vector2 hudLocation = new Vector2(titleSafeArea.X, titleSafeArea.Y);
- //Vector2 center = new Vector2(titleSafeArea.X + titleSafeArea.Width / 2.0f,
- // titleSafeArea.Y + titleSafeArea.Height / 2.0f);
-
- Vector2 center = new Vector2(baseScreenSize.X / 2, baseScreenSize.Y / 2);
-
- // Draw time remaining. Uses modulo division to cause blinking when the
- // player is running out of time.
- string timeString = "TIME: " + level.TimeRemaining.Minutes.ToString("00") + ":" + level.TimeRemaining.Seconds.ToString("00");
- Color timeColor;
- if (level.TimeRemaining > WarningTime ||
- level.ReachedExit ||
- (int)level.TimeRemaining.TotalSeconds % 2 == 0)
- {
- timeColor = Color.Yellow;
- }
- else
- {
- timeColor = Color.Red;
- }
- DrawShadowedString(hudFont, timeString, hudLocation, timeColor);
-
- // Draw score
- float timeHeight = hudFont.MeasureString(timeString).Y;
- DrawShadowedString(hudFont, "SCORE: " + level.Score.ToString(), hudLocation + new Vector2(0.0f, timeHeight * 1.2f), Color.Yellow);
-
- // Determine the status overlay message to show.
- Texture2D status = null;
- if (level.TimeRemaining == TimeSpan.Zero)
- {
- if (level.ReachedExit)
- {
- status = winOverlay;
- }
- else
- {
- status = loseOverlay;
- }
- }
- else if (!level.Player.IsAlive)
- {
- status = diedOverlay;
- }
-
- if (status != null)
- {
- // Draw status message.
- Vector2 statusSize = new Vector2(status.Width, status.Height);
- spriteBatch.Draw(status, center - statusSize / 2, Color.White);
- }
-
- if (touchState.IsConnected)
- virtualGamePad.Draw(spriteBatch);
- }
-
- private void DrawShadowedString(SpriteFont font, string value, Vector2 position, Color color)
- {
- spriteBatch.DrawString(font, value, position + new Vector2(1.0f, 1.0f), Color.Black);
- spriteBatch.DrawString(font, value, position, color);
- }
- }
-}
diff --git a/Platformer2D/Platformer2D.DesktopGL/Icon.bmp b/Platformer2D/Platformer2D.DesktopGL/Icon.bmp
deleted file mode 100644
index 2b481653..00000000
Binary files a/Platformer2D/Platformer2D.DesktopGL/Icon.bmp and /dev/null differ
diff --git a/Platformer2D/Platformer2D.DesktopGL/Icon.ico b/Platformer2D/Platformer2D.DesktopGL/Icon.ico
deleted file mode 100644
index 7d9dec18..00000000
Binary files a/Platformer2D/Platformer2D.DesktopGL/Icon.ico and /dev/null differ
diff --git a/Platformer2D/Platformer2D.DesktopGL/Platformer2D.DesktopGL.csproj b/Platformer2D/Platformer2D.DesktopGL/Platformer2D.DesktopGL.csproj
deleted file mode 100644
index 065c5a27..00000000
--- a/Platformer2D/Platformer2D.DesktopGL/Platformer2D.DesktopGL.csproj
+++ /dev/null
@@ -1,36 +0,0 @@
-
-
- WinExe
- net9.0
- false
- false
-
-
- app.manifest
- Icon.ico
-
-
-
-
-
-
-
-
-
-
-
- Content\Platformer2D.mgcb
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Platformer2D/Platformer2D.DesktopGL/Platformer2D.DesktopGL.icns b/Platformer2D/Platformer2D.DesktopGL/Platformer2D.DesktopGL.icns
deleted file mode 100644
index 1198990e..00000000
Binary files a/Platformer2D/Platformer2D.DesktopGL/Platformer2D.DesktopGL.icns and /dev/null differ
diff --git a/Platformer2D/Platformer2D.DesktopGL/Program.cs b/Platformer2D/Platformer2D.DesktopGL/Program.cs
deleted file mode 100644
index 182a8e00..00000000
--- a/Platformer2D/Platformer2D.DesktopGL/Program.cs
+++ /dev/null
@@ -1,5 +0,0 @@
-
-using Platformer2D;
-
-using var game = new PlatformerGame();
-game.Run();
diff --git a/Platformer2D/Platformer2D.WindowsDX/Icon.ico b/Platformer2D/Platformer2D.WindowsDX/Icon.ico
deleted file mode 100644
index 7d9dec18..00000000
Binary files a/Platformer2D/Platformer2D.WindowsDX/Icon.ico and /dev/null differ
diff --git a/Platformer2D/Platformer2D.WindowsDX/Platformer2D.WindowsDX.csproj b/Platformer2D/Platformer2D.WindowsDX/Platformer2D.WindowsDX.csproj
deleted file mode 100644
index 195dfee8..00000000
--- a/Platformer2D/Platformer2D.WindowsDX/Platformer2D.WindowsDX.csproj
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
- WinExe
- net9.0-windows
- Major
- false
- false
- true
-
-
- app.manifest
- Icon.ico
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Platformer2D/Platformer2D.WindowsDX/Program.cs b/Platformer2D/Platformer2D.WindowsDX/Program.cs
deleted file mode 100644
index 182a8e00..00000000
--- a/Platformer2D/Platformer2D.WindowsDX/Program.cs
+++ /dev/null
@@ -1,5 +0,0 @@
-
-using Platformer2D;
-
-using var game = new PlatformerGame();
-game.Run();
diff --git a/Platformer2D/Platformer2D.iOS/Default.png b/Platformer2D/Platformer2D.iOS/Default.png
deleted file mode 100644
index 1f9b909f..00000000
Binary files a/Platformer2D/Platformer2D.iOS/Default.png and /dev/null differ
diff --git a/Platformer2D/Platformer2D.iOS/GameThumbnail.png b/Platformer2D/Platformer2D.iOS/GameThumbnail.png
deleted file mode 100644
index 99814c32..00000000
Binary files a/Platformer2D/Platformer2D.iOS/GameThumbnail.png and /dev/null differ
diff --git a/Platformer2D/Platformer2D.iOS/Platformer2D.iOS.csproj b/Platformer2D/Platformer2D.iOS/Platformer2D.iOS.csproj
deleted file mode 100644
index 1dcfb3c2..00000000
--- a/Platformer2D/Platformer2D.iOS/Platformer2D.iOS.csproj
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
- net9.0-ios
- Exe
- 11.2
- iPhone Developer
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Platformer2D/Platformer2D.iOS/Program.cs b/Platformer2D/Platformer2D.iOS/Program.cs
deleted file mode 100644
index e1227c86..00000000
--- a/Platformer2D/Platformer2D.iOS/Program.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-using System;
-using Foundation;
-using UIKit;
-
-namespace Platformer2D.iOS
-{
- [Register("AppDelegate")]
- internal class Program : UIApplicationDelegate
- {
- private static PlatformerGame game;
-
- internal static void RunGame()
- {
- game = new PlatformerGame();
- game.Run();
- }
-
- ///
- /// The main entry point for the application.
- ///
- static void Main(string[] args)
- {
- UIApplication.Main(args, null, typeof(Program));
- }
-
- public override void FinishedLaunching(UIApplication app)
- {
- RunGame();
- }
- }
-}
\ No newline at end of file
diff --git a/Platformer2D/Platformer2D.iOS/monogameicon.png b/Platformer2D/Platformer2D.iOS/monogameicon.png
deleted file mode 100644
index 25bcd9b9..00000000
Binary files a/Platformer2D/Platformer2D.iOS/monogameicon.png and /dev/null differ
diff --git a/Platformer2D/Platformer2D.sln b/Platformer2D/Platformer2D.sln
index 4ce9bb84..f310fb81 100644
--- a/Platformer2D/Platformer2D.sln
+++ b/Platformer2D/Platformer2D.sln
@@ -1,61 +1,63 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
-VisualStudioVersion = 17.12.35527.113
-MinimumVisualStudioVersion = 10.0.40219.1
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Platformer2D.DesktopGL", "Platformer2D.DesktopGL\Platformer2D.DesktopGL.csproj", "{74BABA3E-D424-4620-9661-C1F767759A52}"
+VisualStudioVersion = 17.9.34723.18
+MinimumVisualStudioVersion = 15.0.26124.0
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Platformer2D.Android", "Android\Platformer2D.csproj", "{6161B64E-0BF7-42D9-B2BA-6BB16460BFE3}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Platformer2D.Core", "Platformer2D.Core\Platformer2D.Core.csproj", "{64C2A1E6-09A1-4F46-8765-8B780C70122D}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Platformer2D.iOS", "iOS\Platformer2D.csproj", "{109593E3-17AE-4482-9556-4B2068F9479B}"
EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Platforms", "Platforms", "{75DBB690-40CC-46CF-B201-F190F5CF804C}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Platformer2D.Core", "Core\Core.csproj", "{47BD21C1-2170-456C-AF98-6D92C5F73BA9}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Platformer2D.Android", "Platformer2D.Android\Platformer2D.Android.csproj", "{4E1A4A78-F079-47C5-909F-FEF38E120091}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Platformer2D.DesktopGL", "Desktop\Platformer2D.csproj", "{85B968E5-2E46-45A3-AD10-1F3F1FB6F9B6}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Platformer2D.iOS", "Platformer2D.iOS\Platformer2D.iOS.csproj", "{7234F83E-51D9-4BE2-9795-568B9A1764F4}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Platformer2D.Windows", "Windows\Platformer2D.csproj", "{A7C9F312-8D3E-4B5A-9C1E-6F8B2D4E3A91}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Platformer2D.WindowsDX", "Platformer2D.WindowsDX\Platformer2D.WindowsDX.csproj", "{D936BF4C-AE14-4ADF-9C24-2A6B585713BE}"
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Platforms", "Platforms", "{B4A0E918-51B4-40FF-9845-60F2616B3F27}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {74BABA3E-D424-4620-9661-C1F767759A52}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {74BABA3E-D424-4620-9661-C1F767759A52}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {74BABA3E-D424-4620-9661-C1F767759A52}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {74BABA3E-D424-4620-9661-C1F767759A52}.Release|Any CPU.Build.0 = Release|Any CPU
- {64C2A1E6-09A1-4F46-8765-8B780C70122D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {64C2A1E6-09A1-4F46-8765-8B780C70122D}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {64C2A1E6-09A1-4F46-8765-8B780C70122D}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {64C2A1E6-09A1-4F46-8765-8B780C70122D}.Release|Any CPU.Build.0 = Release|Any CPU
- {4E1A4A78-F079-47C5-909F-FEF38E120091}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {4E1A4A78-F079-47C5-909F-FEF38E120091}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {4E1A4A78-F079-47C5-909F-FEF38E120091}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
- {4E1A4A78-F079-47C5-909F-FEF38E120091}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {4E1A4A78-F079-47C5-909F-FEF38E120091}.Release|Any CPU.Build.0 = Release|Any CPU
- {4E1A4A78-F079-47C5-909F-FEF38E120091}.Release|Any CPU.Deploy.0 = Release|Any CPU
- {7234F83E-51D9-4BE2-9795-568B9A1764F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {7234F83E-51D9-4BE2-9795-568B9A1764F4}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {7234F83E-51D9-4BE2-9795-568B9A1764F4}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
- {7234F83E-51D9-4BE2-9795-568B9A1764F4}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {7234F83E-51D9-4BE2-9795-568B9A1764F4}.Release|Any CPU.Build.0 = Release|Any CPU
- {7234F83E-51D9-4BE2-9795-568B9A1764F4}.Release|Any CPU.Deploy.0 = Release|Any CPU
- {D936BF4C-AE14-4ADF-9C24-2A6B585713BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {D936BF4C-AE14-4ADF-9C24-2A6B585713BE}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {D936BF4C-AE14-4ADF-9C24-2A6B585713BE}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {D936BF4C-AE14-4ADF-9C24-2A6B585713BE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6161B64E-0BF7-42D9-B2BA-6BB16460BFE3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6161B64E-0BF7-42D9-B2BA-6BB16460BFE3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6161B64E-0BF7-42D9-B2BA-6BB16460BFE3}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
+ {6161B64E-0BF7-42D9-B2BA-6BB16460BFE3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6161B64E-0BF7-42D9-B2BA-6BB16460BFE3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6161B64E-0BF7-42D9-B2BA-6BB16460BFE3}.Release|Any CPU.Deploy.0 = Release|Any CPU
+ {109593E3-17AE-4482-9556-4B2068F9479B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {109593E3-17AE-4482-9556-4B2068F9479B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {109593E3-17AE-4482-9556-4B2068F9479B}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
+ {109593E3-17AE-4482-9556-4B2068F9479B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {109593E3-17AE-4482-9556-4B2068F9479B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {109593E3-17AE-4482-9556-4B2068F9479B}.Release|Any CPU.Deploy.0 = Release|Any CPU
+ {47BD21C1-2170-456C-AF98-6D92C5F73BA9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {47BD21C1-2170-456C-AF98-6D92C5F73BA9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {47BD21C1-2170-456C-AF98-6D92C5F73BA9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {47BD21C1-2170-456C-AF98-6D92C5F73BA9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {85B968E5-2E46-45A3-AD10-1F3F1FB6F9B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {85B968E5-2E46-45A3-AD10-1F3F1FB6F9B6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {85B968E5-2E46-45A3-AD10-1F3F1FB6F9B6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {85B968E5-2E46-45A3-AD10-1F3F1FB6F9B6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A7C9F312-8D3E-4B5A-9C1E-6F8B2D4E3A91}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A7C9F312-8D3E-4B5A-9C1E-6F8B2D4E3A91}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A7C9F312-8D3E-4B5A-9C1E-6F8B2D4E3A91}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A7C9F312-8D3E-4B5A-9C1E-6F8B2D4E3A91}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
- {74BABA3E-D424-4620-9661-C1F767759A52} = {75DBB690-40CC-46CF-B201-F190F5CF804C}
- {4E1A4A78-F079-47C5-909F-FEF38E120091} = {75DBB690-40CC-46CF-B201-F190F5CF804C}
- {7234F83E-51D9-4BE2-9795-568B9A1764F4} = {75DBB690-40CC-46CF-B201-F190F5CF804C}
- {D936BF4C-AE14-4ADF-9C24-2A6B585713BE} = {75DBB690-40CC-46CF-B201-F190F5CF804C}
+ {6161B64E-0BF7-42D9-B2BA-6BB16460BFE3} = {B4A0E918-51B4-40FF-9845-60F2616B3F27}
+ {109593E3-17AE-4482-9556-4B2068F9479B} = {B4A0E918-51B4-40FF-9845-60F2616B3F27}
+ {85B968E5-2E46-45A3-AD10-1F3F1FB6F9B6} = {B4A0E918-51B4-40FF-9845-60F2616B3F27}
+ {A7C9F312-8D3E-4B5A-9C1E-6F8B2D4E3A91} = {B4A0E918-51B4-40FF-9845-60F2616B3F27}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
- SolutionGuid = {F72D49A5-A441-4121-B119-98D295CA3697}
+ SolutionGuid = {51ECE597-0299-4753-B7F5-4E8EDECD7BB4}
EndGlobalSection
EndGlobal
diff --git a/Platformer2D/Windows/Platformer2D.csproj b/Platformer2D/Windows/Platformer2D.csproj
new file mode 100644
index 00000000..cd4212b4
--- /dev/null
+++ b/Platformer2D/Windows/Platformer2D.csproj
@@ -0,0 +1,26 @@
+
+
+ WinExe
+ net9.0-windows
+ Major
+ false
+ false
+ true
+ Platformer2D
+ Platformer2D
+ app.manifest
+ ../Core/Content/Icon.ico
+
+
+
+ Content\Platformer2D.mgcb
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Platformer2D/Windows/Program.cs b/Platformer2D/Windows/Program.cs
new file mode 100644
index 00000000..89780b58
--- /dev/null
+++ b/Platformer2D/Windows/Program.cs
@@ -0,0 +1,15 @@
+using Platformer2D.Core;
+
+internal class Program
+{
+ ///
+ /// The main entry point for the application.
+ /// This creates an instance of your game and calls it's Run() method
+ ///
+ /// Command-line arguments passed to the application.
+ private static void Main(string[] args)
+ {
+ using var game = new Platformer2DGame();
+ game.Run();
+ }
+}
\ No newline at end of file
diff --git a/Platformer2D/Platformer2D.DesktopGL/app.manifest b/Platformer2D/Windows/app.manifest
similarity index 63%
rename from Platformer2D/Platformer2D.DesktopGL/app.manifest
rename to Platformer2D/Windows/app.manifest
index 721db537..716fdcdb 100644
--- a/Platformer2D/Platformer2D.DesktopGL/app.manifest
+++ b/Platformer2D/Windows/app.manifest
@@ -1,6 +1,6 @@
-
+
@@ -16,28 +16,27 @@
automatically selected the most compatible environment. -->
-
+
-
+
-
+
-
+
+
+
+
-
- true/pm
- permonitorv2,permonitor
-
-
+
\ No newline at end of file
diff --git a/Platformer2D/iOS/AppIcon.xcassets/AppIcon.appiconset/Contents.json b/Platformer2D/iOS/AppIcon.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 00000000..c915b6e2
--- /dev/null
+++ b/Platformer2D/iOS/AppIcon.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,116 @@
+{
+ "images" : [
+ {
+ "filename" : "icon_40x40.png",
+ "idiom" : "iphone",
+ "scale" : "2x",
+ "size" : "20x20"
+ },
+ {
+ "filename" : "icon_60x60.png",
+ "idiom" : "iphone",
+ "scale" : "3x",
+ "size" : "20x20"
+ },
+ {
+ "filename" : "icon_58x58.png",
+ "idiom" : "iphone",
+ "scale" : "2x",
+ "size" : "29x29"
+ },
+ {
+ "filename" : "icon_87x87.png",
+ "idiom" : "iphone",
+ "scale" : "3x",
+ "size" : "29x29"
+ },
+ {
+ "filename" : "icon_80x80.png",
+ "idiom" : "iphone",
+ "scale" : "2x",
+ "size" : "40x40"
+ },
+ {
+ "filename" : "icon_120x120.png",
+ "idiom" : "iphone",
+ "scale" : "3x",
+ "size" : "40x40"
+ },
+ {
+ "filename" : "icon_120x120.png",
+ "idiom" : "iphone",
+ "scale" : "2x",
+ "size" : "60x60"
+ },
+ {
+ "filename" : "icon_180x180.png",
+ "idiom" : "iphone",
+ "scale" : "3x",
+ "size" : "60x60"
+ },
+ {
+ "filename" : "icon_20x20.png",
+ "idiom" : "ipad",
+ "scale" : "1x",
+ "size" : "20x20"
+ },
+ {
+ "filename" : "icon_40x40.png",
+ "idiom" : "ipad",
+ "scale" : "2x",
+ "size" : "20x20"
+ },
+ {
+ "filename" : "icon_29x29.png",
+ "idiom" : "ipad",
+ "scale" : "1x",
+ "size" : "29x29"
+ },
+ {
+ "filename" : "icon_58x58.png",
+ "idiom" : "ipad",
+ "scale" : "2x",
+ "size" : "29x29"
+ },
+ {
+ "filename" : "icon_40x40.png",
+ "idiom" : "ipad",
+ "scale" : "1x",
+ "size" : "40x40"
+ },
+ {
+ "filename" : "icon_80x80.png",
+ "idiom" : "ipad",
+ "scale" : "2x",
+ "size" : "40x40"
+ },
+ {
+ "filename" : "icon_76x76.png",
+ "idiom" : "ipad",
+ "scale" : "1x",
+ "size" : "76x76"
+ },
+ {
+ "filename" : "icon_152x152.png",
+ "idiom" : "ipad",
+ "scale" : "2x",
+ "size" : "76x76"
+ },
+ {
+ "filename" : "icon_167x167.png",
+ "idiom" : "ipad",
+ "scale" : "2x",
+ "size" : "83.5x83.5"
+ },
+ {
+ "filename" : "icon_1024x1024.png",
+ "idiom" : "ios-marketing",
+ "scale" : "1x",
+ "size" : "1024x1024"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Platformer2D/iOS/AppIcon.xcassets/AppIcon.appiconset/icon_1024x1024.png b/Platformer2D/iOS/AppIcon.xcassets/AppIcon.appiconset/icon_1024x1024.png
new file mode 100644
index 00000000..99f7aee2
Binary files /dev/null and b/Platformer2D/iOS/AppIcon.xcassets/AppIcon.appiconset/icon_1024x1024.png differ
diff --git a/Platformer2D/iOS/AppIcon.xcassets/AppIcon.appiconset/icon_120x120.png b/Platformer2D/iOS/AppIcon.xcassets/AppIcon.appiconset/icon_120x120.png
new file mode 100644
index 00000000..9961e517
Binary files /dev/null and b/Platformer2D/iOS/AppIcon.xcassets/AppIcon.appiconset/icon_120x120.png differ
diff --git a/Platformer2D/iOS/AppIcon.xcassets/AppIcon.appiconset/icon_152x152.png b/Platformer2D/iOS/AppIcon.xcassets/AppIcon.appiconset/icon_152x152.png
new file mode 100644
index 00000000..f050788d
Binary files /dev/null and b/Platformer2D/iOS/AppIcon.xcassets/AppIcon.appiconset/icon_152x152.png differ
diff --git a/Platformer2D/iOS/AppIcon.xcassets/AppIcon.appiconset/icon_167x167.png b/Platformer2D/iOS/AppIcon.xcassets/AppIcon.appiconset/icon_167x167.png
new file mode 100644
index 00000000..1652536d
Binary files /dev/null and b/Platformer2D/iOS/AppIcon.xcassets/AppIcon.appiconset/icon_167x167.png differ
diff --git a/Platformer2D/iOS/AppIcon.xcassets/AppIcon.appiconset/icon_180x180.png b/Platformer2D/iOS/AppIcon.xcassets/AppIcon.appiconset/icon_180x180.png
new file mode 100644
index 00000000..7cda634e
Binary files /dev/null and b/Platformer2D/iOS/AppIcon.xcassets/AppIcon.appiconset/icon_180x180.png differ
diff --git a/Platformer2D/iOS/AppIcon.xcassets/AppIcon.appiconset/icon_20x20.png b/Platformer2D/iOS/AppIcon.xcassets/AppIcon.appiconset/icon_20x20.png
new file mode 100644
index 00000000..2d1a0a6a
Binary files /dev/null and b/Platformer2D/iOS/AppIcon.xcassets/AppIcon.appiconset/icon_20x20.png differ
diff --git a/Platformer2D/iOS/AppIcon.xcassets/AppIcon.appiconset/icon_29x29.png b/Platformer2D/iOS/AppIcon.xcassets/AppIcon.appiconset/icon_29x29.png
new file mode 100644
index 00000000..062a086a
Binary files /dev/null and b/Platformer2D/iOS/AppIcon.xcassets/AppIcon.appiconset/icon_29x29.png differ
diff --git a/Platformer2D/iOS/AppIcon.xcassets/AppIcon.appiconset/icon_40x40.png b/Platformer2D/iOS/AppIcon.xcassets/AppIcon.appiconset/icon_40x40.png
new file mode 100644
index 00000000..239ee506
Binary files /dev/null and b/Platformer2D/iOS/AppIcon.xcassets/AppIcon.appiconset/icon_40x40.png differ
diff --git a/Platformer2D/iOS/AppIcon.xcassets/AppIcon.appiconset/icon_58x58.png b/Platformer2D/iOS/AppIcon.xcassets/AppIcon.appiconset/icon_58x58.png
new file mode 100644
index 00000000..2ffcffc8
Binary files /dev/null and b/Platformer2D/iOS/AppIcon.xcassets/AppIcon.appiconset/icon_58x58.png differ
diff --git a/Platformer2D/iOS/AppIcon.xcassets/AppIcon.appiconset/icon_60x60.png b/Platformer2D/iOS/AppIcon.xcassets/AppIcon.appiconset/icon_60x60.png
new file mode 100644
index 00000000..96a55d78
Binary files /dev/null and b/Platformer2D/iOS/AppIcon.xcassets/AppIcon.appiconset/icon_60x60.png differ
diff --git a/Platformer2D/iOS/AppIcon.xcassets/AppIcon.appiconset/icon_76x76.png b/Platformer2D/iOS/AppIcon.xcassets/AppIcon.appiconset/icon_76x76.png
new file mode 100644
index 00000000..942ea5c2
Binary files /dev/null and b/Platformer2D/iOS/AppIcon.xcassets/AppIcon.appiconset/icon_76x76.png differ
diff --git a/Platformer2D/iOS/AppIcon.xcassets/AppIcon.appiconset/icon_80x80.png b/Platformer2D/iOS/AppIcon.xcassets/AppIcon.appiconset/icon_80x80.png
new file mode 100644
index 00000000..3bb9c779
Binary files /dev/null and b/Platformer2D/iOS/AppIcon.xcassets/AppIcon.appiconset/icon_80x80.png differ
diff --git a/Platformer2D/iOS/AppIcon.xcassets/AppIcon.appiconset/icon_87x87.png b/Platformer2D/iOS/AppIcon.xcassets/AppIcon.appiconset/icon_87x87.png
new file mode 100644
index 00000000..53e614ae
Binary files /dev/null and b/Platformer2D/iOS/AppIcon.xcassets/AppIcon.appiconset/icon_87x87.png differ
diff --git a/Platformer2D/Platformer2D.iOS/Entitlements.plist b/Platformer2D/iOS/Entitlements.plist
similarity index 100%
rename from Platformer2D/Platformer2D.iOS/Entitlements.plist
rename to Platformer2D/iOS/Entitlements.plist
diff --git a/Platformer2D/Platformer2D.iOS/Info.plist b/Platformer2D/iOS/Info.plist
similarity index 75%
rename from Platformer2D/Platformer2D.iOS/Info.plist
rename to Platformer2D/iOS/Info.plist
index 9a335cf5..767b1f98 100644
--- a/Platformer2D/Platformer2D.iOS/Info.plist
+++ b/Platformer2D/iOS/Info.plist
@@ -2,16 +2,8 @@
- CFBundleDisplayName
- Platformer2D
- CFBundleIconFiles
-
- monogameicon.png
-
- CFBundleIdentifier
- project.MonoGame.Platformer2D.iOSMinimumOSVersion
- 11.2
+ 12.2UISupportedInterfaceOrientationsUIInterfaceOrientationLandscapeLeft
@@ -26,9 +18,15 @@
UILaunchStoryboardNameLaunchScreenUIDeviceFamily
-
- 1
- 2
-
+
+ 1
+ 2
+
+ CFBundleIdentifier
+ com.companyname.Platformer2D
+ CFBundleDisplayName
+ Platformer2D
+ XSAppIconAssets
+ AppIcon.xcassets/AppIcon.appiconset
-
\ No newline at end of file
+
diff --git a/Platformer2D/Platformer2D.iOS/LaunchScreen.storyboard b/Platformer2D/iOS/LaunchScreen.storyboard
similarity index 100%
rename from Platformer2D/Platformer2D.iOS/LaunchScreen.storyboard
rename to Platformer2D/iOS/LaunchScreen.storyboard
diff --git a/Platformer2D/iOS/Platformer2D.csproj b/Platformer2D/iOS/Platformer2D.csproj
new file mode 100644
index 00000000..e832ea18
--- /dev/null
+++ b/Platformer2D/iOS/Platformer2D.csproj
@@ -0,0 +1,22 @@
+
+
+ Exe
+ net9.0-ios
+ 12.2
+ iPhone Developer
+ Platformer2D
+ Platformer2D
+
+
+
+ Content\Platformer2D.mgcb
+
+
+
+
+
+
+
+
+
+
diff --git a/Platformer2D/iOS/Program.cs b/Platformer2D/iOS/Program.cs
new file mode 100644
index 00000000..d51304e4
--- /dev/null
+++ b/Platformer2D/iOS/Program.cs
@@ -0,0 +1,43 @@
+using Platformer2D.Core;
+using Foundation;
+using UIKit;
+
+namespace Platformer2D.iOS
+{
+ [Register("AppDelegate")]
+ internal class Program : UIApplicationDelegate
+ {
+ private static Platformer2DGame _game;
+
+ ///
+ /// Initializes and starts the game by creating an instance of the
+ /// Game class and invoking its Run method.
+ ///
+ internal static void RunGame()
+ {
+ _game = new Platformer2DGame();
+ _game.Run();
+ }
+
+ ///
+ /// Called when the application has finished launching.
+ /// This method starts the game by calling RunGame.
+ ///
+ /// The UIApplication instance representing the application.
+ public override void FinishedLaunching(UIApplication app)
+ {
+ RunGame();
+ }
+
+ ///
+ /// The main entry point for the application.
+ /// This sets up the application and specifies the UIApplicationDelegate
+ /// class to handle application lifecycle events.
+ ///
+ /// Command-line arguments passed to the application.
+ static void Main(string[] args)
+ {
+ UIApplication.Main(args, null, typeof(Program));
+ }
+ }
+}
\ No newline at end of file