├── .gitignore
├── Editor
├── App.config
├── App.xaml
├── App.xaml.cs
├── AssetManager.cs
├── BindingSource.cs
├── ChunkViewModel.cs
├── Command.cs
├── CommandOfT.cs
├── Configuration.cs
├── Context.cs
├── Editor.csproj
├── Editor.ruleset
├── Extensions
│ ├── AddRange.cs
│ ├── AsToolWindow.cs
│ └── ForEach.cs
├── GlobalSuppressions.cs
├── MainViewModel.cs
├── MainWindow.xaml
├── MainWindow.xaml.cs
├── MapBrowserViewModel.cs
├── MapBrowserWindow.xaml
├── MapBrowserWindow.xaml.cs
├── MapViewModel.cs
├── Properties
│ ├── AssemblyInfo.cs
│ ├── Resources.Designer.cs
│ ├── Resources.resx
│ ├── Settings.Designer.cs
│ └── Settings.settings
├── Tileset.cs
├── TilesetViewModel.cs
├── TilesetWindow.xaml
├── TilesetWindow.xaml.cs
├── ViewModel.cs
├── packages.config
└── tileset.png
├── Engine
├── Assets
│ └── tileset.png
├── Basic-Fragment.300.glsles
├── Basic-Fragment.330.glsl
├── Basic-Fragment.450.glsl
├── Basic-Fragment.450.spv
├── Basic-Fragment.hlsl
├── Basic-Fragment.hlsl.bytes
├── Basic-Fragment.metal
├── Basic-Fragment.metallib
├── Basic-Vertex.300.glsles
├── Basic-Vertex.330.glsl
├── Basic-Vertex.450.glsl
├── Basic-Vertex.450.spv
├── Basic-Vertex.hlsl
├── Basic-Vertex.hlsl.bytes
├── Basic-Vertex.metal
├── Basic-Vertex.metallib
├── Chunk.cs
├── ChunkManager.cs
├── ChunkProcessor.cs
├── Configuration.cs
├── ContentManager.cs
├── Engine.csproj
├── Engine.ruleset
├── Engine.xml
├── Game.cs
├── GameWindow.cs
├── GlobalSuppressions.cs
├── IGameWindow.cs
├── InputTracker.cs
├── Maps
│ └── Overworld
│ │ ├── -1,-1.chunk
│ │ ├── -1,-2.chunk
│ │ ├── -1,-3.chunk
│ │ ├── -1,-4.chunk
│ │ ├── -1,0.chunk
│ │ ├── -1,1.chunk
│ │ ├── -2,-1.chunk
│ │ ├── -2,0.chunk
│ │ ├── -2,1.chunk
│ │ ├── -3,-1.chunk
│ │ ├── -3,0.chunk
│ │ ├── -3,1.chunk
│ │ ├── -4,-1.chunk
│ │ ├── -4,0.chunk
│ │ ├── -4,1.chunk
│ │ ├── 0,-1.chunk
│ │ ├── 0,-2.chunk
│ │ ├── 0,-3.chunk
│ │ ├── 0,-4.chunk
│ │ ├── 0,0.chunk
│ │ ├── 0,1.chunk
│ │ ├── 1,-1.chunk
│ │ ├── 1,-2.chunk
│ │ ├── 1,-3.chunk
│ │ ├── 1,-4.chunk
│ │ ├── 1,0.chunk
│ │ └── 1,1.chunk
├── Pool.cs
├── Program.cs
├── SampleGame.cs
├── Tileset.cs
├── Utility.cs
├── compile-hlsl.cmd
└── compile-spirv.cmd
├── LICENSE
├── README.md
└── Tilester.sln
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.suo
8 | *.user
9 | *.userosscache
10 | *.sln.docstates
11 |
12 | # User-specific files (MonoDevelop/Xamarin Studio)
13 | *.userprefs
14 |
15 | # Build results
16 | [Dd]ebug/
17 | [Dd]ebugPublic/
18 | [Rr]elease/
19 | [Rr]eleases/
20 | x64/
21 | x86/
22 | bld/
23 | [Bb]in/
24 | [Oo]bj/
25 | [Ll]og/
26 |
27 | # Visual Studio 2015/2017 cache/options directory
28 | .vs/
29 | # Uncomment if you have tasks that create the project's static files in wwwroot
30 | #wwwroot/
31 |
32 | # Visual Studio 2017 auto generated files
33 | Generated\ Files/
34 |
35 | # MSTest test Results
36 | [Tt]est[Rr]esult*/
37 | [Bb]uild[Ll]og.*
38 |
39 | # NUNIT
40 | *.VisualState.xml
41 | TestResult.xml
42 |
43 | # Build Results of an ATL Project
44 | [Dd]ebugPS/
45 | [Rr]eleasePS/
46 | dlldata.c
47 |
48 | # Benchmark Results
49 | BenchmarkDotNet.Artifacts/
50 |
51 | # .NET Core
52 | project.lock.json
53 | project.fragment.lock.json
54 | artifacts/
55 | **/Properties/launchSettings.json
56 |
57 | # StyleCop
58 | StyleCopReport.xml
59 |
60 | # Files built by Visual Studio
61 | *_i.c
62 | *_p.c
63 | *_i.h
64 | *.ilk
65 | *.meta
66 | *.obj
67 | *.iobj
68 | *.pch
69 | *.pdb
70 | *.ipdb
71 | *.pgc
72 | *.pgd
73 | *.rsp
74 | *.sbr
75 | *.tlb
76 | *.tli
77 | *.tlh
78 | *.tmp
79 | *.tmp_proj
80 | *.log
81 | *.vspscc
82 | *.vssscc
83 | .builds
84 | *.pidb
85 | *.svclog
86 | *.scc
87 |
88 | # Chutzpah Test files
89 | _Chutzpah*
90 |
91 | # Visual C++ cache files
92 | ipch/
93 | *.aps
94 | *.ncb
95 | *.opendb
96 | *.opensdf
97 | *.sdf
98 | *.cachefile
99 | *.VC.db
100 | *.VC.VC.opendb
101 |
102 | # Visual Studio profiler
103 | *.psess
104 | *.vsp
105 | *.vspx
106 | *.sap
107 |
108 | # Visual Studio Trace Files
109 | *.e2e
110 |
111 | # TFS 2012 Local Workspace
112 | $tf/
113 |
114 | # Guidance Automation Toolkit
115 | *.gpState
116 |
117 | # ReSharper is a .NET coding add-in
118 | _ReSharper*/
119 | *.[Rr]e[Ss]harper
120 | *.DotSettings.user
121 |
122 | # JustCode is a .NET coding add-in
123 | .JustCode
124 |
125 | # TeamCity is a build add-in
126 | _TeamCity*
127 |
128 | # DotCover is a Code Coverage Tool
129 | *.dotCover
130 |
131 | # AxoCover is a Code Coverage Tool
132 | .axoCover/*
133 | !.axoCover/settings.json
134 |
135 | # Visual Studio code coverage results
136 | *.coverage
137 | *.coveragexml
138 |
139 | # NCrunch
140 | _NCrunch_*
141 | .*crunch*.local.xml
142 | nCrunchTemp_*
143 |
144 | # MightyMoose
145 | *.mm.*
146 | AutoTest.Net/
147 |
148 | # Web workbench (sass)
149 | .sass-cache/
150 |
151 | # Installshield output folder
152 | [Ee]xpress/
153 |
154 | # DocProject is a documentation generator add-in
155 | DocProject/buildhelp/
156 | DocProject/Help/*.HxT
157 | DocProject/Help/*.HxC
158 | DocProject/Help/*.hhc
159 | DocProject/Help/*.hhk
160 | DocProject/Help/*.hhp
161 | DocProject/Help/Html2
162 | DocProject/Help/html
163 |
164 | # Click-Once directory
165 | publish/
166 |
167 | # Publish Web Output
168 | *.[Pp]ublish.xml
169 | *.azurePubxml
170 | # Note: Comment the next line if you want to checkin your web deploy settings,
171 | # but database connection strings (with potential passwords) will be unencrypted
172 | *.pubxml
173 | *.publishproj
174 |
175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
176 | # checkin your Azure Web App publish settings, but sensitive information contained
177 | # in these scripts will be unencrypted
178 | PublishScripts/
179 |
180 | # NuGet Packages
181 | *.nupkg
182 | # The packages folder can be ignored because of Package Restore
183 | **/[Pp]ackages/*
184 | # except build/, which is used as an MSBuild target.
185 | !**/[Pp]ackages/build/
186 | # Uncomment if necessary however generally it will be regenerated when needed
187 | #!**/[Pp]ackages/repositories.config
188 | # NuGet v3's project.json files produces more ignorable files
189 | *.nuget.props
190 | *.nuget.targets
191 |
192 | # Microsoft Azure Build Output
193 | csx/
194 | *.build.csdef
195 |
196 | # Microsoft Azure Emulator
197 | ecf/
198 | rcf/
199 |
200 | # Windows Store app package directories and files
201 | AppPackages/
202 | BundleArtifacts/
203 | Package.StoreAssociation.xml
204 | _pkginfo.txt
205 | *.appx
206 |
207 | # Visual Studio cache files
208 | # files ending in .cache can be ignored
209 | *.[Cc]ache
210 | # but keep track of directories ending in .cache
211 | !*.[Cc]ache/
212 |
213 | # Others
214 | ClientBin/
215 | ~$*
216 | *~
217 | *.dbmdl
218 | *.dbproj.schemaview
219 | *.jfm
220 | *.pfx
221 | *.publishsettings
222 | orleans.codegen.cs
223 |
224 | # Including strong name files can present a security risk
225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
226 | #*.snk
227 |
228 | # Since there are multiple workflows, uncomment next line to ignore bower_components
229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
230 | #bower_components/
231 |
232 | # RIA/Silverlight projects
233 | Generated_Code/
234 |
235 | # Backup & report files from converting an old project file
236 | # to a newer Visual Studio version. Backup files are not needed,
237 | # because we have git ;-)
238 | _UpgradeReport_Files/
239 | Backup*/
240 | UpgradeLog*.XML
241 | UpgradeLog*.htm
242 | ServiceFabricBackup/
243 | *.rptproj.bak
244 |
245 | # SQL Server files
246 | *.mdf
247 | *.ldf
248 | *.ndf
249 |
250 | # Business Intelligence projects
251 | *.rdl.data
252 | *.bim.layout
253 | *.bim_*.settings
254 | *.rptproj.rsuser
255 |
256 | # Microsoft Fakes
257 | FakesAssemblies/
258 |
259 | # GhostDoc plugin setting file
260 | *.GhostDoc.xml
261 |
262 | # Node.js Tools for Visual Studio
263 | .ntvs_analysis.dat
264 | node_modules/
265 |
266 | # Visual Studio 6 build log
267 | *.plg
268 |
269 | # Visual Studio 6 workspace options file
270 | *.opt
271 |
272 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
273 | *.vbw
274 |
275 | # Visual Studio LightSwitch build output
276 | **/*.HTMLClient/GeneratedArtifacts
277 | **/*.DesktopClient/GeneratedArtifacts
278 | **/*.DesktopClient/ModelManifest.xml
279 | **/*.Server/GeneratedArtifacts
280 | **/*.Server/ModelManifest.xml
281 | _Pvt_Extensions
282 |
283 | # Paket dependency manager
284 | .paket/paket.exe
285 | paket-files/
286 |
287 | # FAKE - F# Make
288 | .fake/
289 |
290 | # JetBrains Rider
291 | .idea/
292 | *.sln.iml
293 |
294 | # CodeRush
295 | .cr/
296 |
297 | # Python Tools for Visual Studio (PTVS)
298 | __pycache__/
299 | *.pyc
300 |
301 | # Cake - Uncomment if you are using it
302 | # tools/**
303 | # !tools/packages.config
304 |
305 | # Tabs Studio
306 | *.tss
307 |
308 | # Telerik's JustMock configuration file
309 | *.jmconfig
310 |
311 | # BizTalk build output
312 | *.btp.cs
313 | *.btm.cs
314 | *.odx.cs
315 | *.xsd.cs
316 |
317 | # OpenCover UI analysis results
318 | OpenCover/
319 |
320 | # Azure Stream Analytics local run output
321 | ASALocalRun/
322 |
323 | # MSBuild Binary and Structured Log
324 | *.binlog
325 |
326 | # NVidia Nsight GPU debugger configuration file
327 | *.nvuser
328 |
329 | # MFractors (Xamarin productivity tool) working folder
330 | .mfractor/
331 |
--------------------------------------------------------------------------------
/Editor/App.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/Editor/App.xaml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/Editor/App.xaml.cs:
--------------------------------------------------------------------------------
1 | namespace Editor
2 | {
3 | using System.IO;
4 | using System.Windows;
5 |
6 | ///
7 | /// Interaction logic for App.xaml.
8 | ///
9 | public partial class App
10 | {
11 | ///
12 | protected override void OnStartup(StartupEventArgs e)
13 | {
14 | base.OnStartup(e);
15 |
16 | // Ensure that the default map is empty at the start
17 | Directory.CreateDirectory($@"{Configuration.MapsFolder}\{Configuration.DefaultMapName}");
18 | new DirectoryInfo($@"{Configuration.MapsFolder}\{Configuration.DefaultMapName}").EnumerateFiles().ForEach(file => file.Delete());
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Editor/AssetManager.cs:
--------------------------------------------------------------------------------
1 | namespace Editor
2 | {
3 | using System;
4 | using System.Diagnostics;
5 | using System.IO;
6 |
7 | ///
8 | /// Provides access to game assets.
9 | ///
10 | public class AssetManager
11 | {
12 | ///
13 | /// Stores the global tileset.
14 | ///
15 | private static Tileset cachedTileset;
16 |
17 | ///
18 | /// Keeps track of the current map folder.
19 | ///
20 | private string currentMap;
21 |
22 | ///
23 | /// Initializes a new instance of the class.
24 | ///
25 | internal AssetManager()
26 | {
27 | }
28 |
29 | ///
30 | /// Gets the tileset.
31 | ///
32 | /// The .
33 | public Tileset GetTileset()
34 | {
35 | return cachedTileset ?? (cachedTileset = new Tileset("tileset.png", Configuration.TileSize));
36 | }
37 |
38 | ///
39 | /// Sets the current map.
40 | ///
41 | /// The name.
42 | public void SetCurrentMap(string name) => this.currentMap = name != null ? Path.Combine(Configuration.MapsFolder, name) : null;
43 |
44 | ///
45 | /// Gets the specified chunk's data and sets the array with it.
46 | ///
47 | /// The chunk X coordinate in chunk units.
48 | /// The chunk Y coordinate in chunk units.
49 | /// /// The tiles.
50 | public void LoadTiles(int chunkX, int chunkY, int[,] tiles)
51 | {
52 | var folder = this.currentMap;
53 | if (string.IsNullOrWhiteSpace(folder) || !Directory.Exists(folder))
54 | {
55 | FillEmpty();
56 | return;
57 | }
58 |
59 | var chunkFilename = Path.Combine(folder, FilenameForChunk(chunkX, chunkY));
60 | if (!File.Exists(chunkFilename))
61 | {
62 | FillEmpty();
63 | return;
64 | }
65 |
66 | try
67 | {
68 | using (var stream = File.OpenRead(chunkFilename))
69 | {
70 | using (var reader = new BinaryReader(stream))
71 | {
72 | for (var y = 0; y < Configuration.ChunkHeight; y++)
73 | {
74 | for (var x = 0; x < Configuration.ChunkWidth; x++)
75 | {
76 | var value = reader.ReadUInt16();
77 | tiles[x, y] = value;
78 | }
79 | }
80 | }
81 | }
82 | }
83 | catch (EndOfStreamException)
84 | {
85 | Trace.TraceError($"The chunk file '{chunkFilename}' had incomplete data.");
86 | FillEmpty();
87 | }
88 |
89 | void FillEmpty()
90 | {
91 | for (var y = 0; y < Configuration.ChunkHeight; y++)
92 | {
93 | for (var x = 0; x < Configuration.ChunkWidth; x++)
94 | {
95 | tiles[x, y] = 0;
96 | }
97 | }
98 | }
99 | }
100 |
101 | ///
102 | /// Stores the specified chunk's data.
103 | ///
104 | /// The chunk X coordinate in chunk units.
105 | /// The chunk Y coordinate in chunk units.
106 | /// The tiles.
107 | public void SaveTiles(int chunkX, int chunkY, int[,] tiles)
108 | {
109 | var folder = this.currentMap;
110 | if (string.IsNullOrWhiteSpace(folder) || !Directory.Exists(folder))
111 | {
112 | Debug.Fail("The map directory is not properly set.");
113 | return;
114 | }
115 |
116 | var chunkFilename = Path.Combine(folder, FilenameForChunk(chunkX, chunkY));
117 |
118 | try
119 | {
120 | using (var stream = File.OpenWrite(chunkFilename))
121 | {
122 | using (var writer = new BinaryWriter(stream))
123 | {
124 | for (var y = 0; y < Configuration.ChunkHeight; y++)
125 | {
126 | for (var x = 0; x < Configuration.ChunkWidth; x++)
127 | {
128 | var value = (ushort)tiles[x, y];
129 | writer.Write(value);
130 | }
131 | }
132 | }
133 | }
134 | }
135 | catch (Exception e)
136 | {
137 | Trace.TraceError(e.Message);
138 | }
139 | }
140 |
141 | ///
142 | /// Determines a filename for the given chunk coordinates.
143 | ///
144 | /// The chunk X coordinate.
145 | /// The chunk Y coordinate.
146 | /// The filename.
147 | private static string FilenameForChunk(int x, int y) => $"{x},{y}.chunk";
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/Editor/BindingSource.cs:
--------------------------------------------------------------------------------
1 | namespace Editor
2 | {
3 | using System;
4 | using System.ComponentModel;
5 | using System.Runtime.CompilerServices;
6 |
7 | ///
8 | /// Provides data binding support.
9 | ///
10 | ///
11 | public abstract class BindingSource : INotifyPropertyChanged
12 | {
13 | ///
14 | public event PropertyChangedEventHandler PropertyChanged;
15 |
16 | ///
17 | /// Raises the event.
18 | ///
19 | /// The property name. If not specified, defaults to the caller member name.
20 | public virtual void RaisePropertyChanged([CallerMemberName] string propertyName = null)
21 | {
22 | if (propertyName == null)
23 | {
24 | throw new ArgumentNullException(nameof(propertyName));
25 | }
26 |
27 | this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
28 | }
29 |
30 | ///
31 | /// Raises the event.
32 | ///
33 | /// The first property name.
34 | /// The second property name.
35 | /// The other property names.
36 | public void RaisePropertyChanged(string propertyName1, string propertyName2, params string[] propertyNames)
37 | {
38 | if (propertyName1 == null)
39 | {
40 | throw new ArgumentNullException(nameof(propertyName1));
41 | }
42 |
43 | if (propertyName2 == null)
44 | {
45 | throw new ArgumentNullException(nameof(propertyName2));
46 | }
47 |
48 | if (propertyNames == null)
49 | {
50 | throw new ArgumentNullException(nameof(propertyNames));
51 | }
52 |
53 | this.RaisePropertyChanged(propertyName1);
54 | this.RaisePropertyChanged(propertyName2);
55 |
56 | foreach (var propertyName in propertyNames)
57 | {
58 | this.RaisePropertyChanged(propertyName);
59 | }
60 | }
61 |
62 | ///
63 | /// Sets the property value. Raises the event, if needed.
64 | ///
65 | /// The type of the property value.
66 | /// The property backing field.
67 | /// The new property value.
68 | /// The property name. If not specified, defaults to the caller member name.
69 | /// True if the property value was modified, otherwise false.
70 | protected bool SetValue(ref T field, T value, [CallerMemberName] string propertyName = null)
71 | {
72 | if (propertyName == null)
73 | {
74 | throw new ArgumentNullException(nameof(propertyName));
75 | }
76 |
77 | if (!object.Equals(field, value))
78 | {
79 | field = value;
80 | this.RaisePropertyChanged(propertyName);
81 | return true;
82 | }
83 |
84 | return false;
85 | }
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/Editor/ChunkViewModel.cs:
--------------------------------------------------------------------------------
1 | namespace Editor
2 | {
3 | using System.Windows.Media;
4 | using System.Windows.Media.Imaging;
5 |
6 | ///
7 | /// View model for a chunk.
8 | ///
9 | ///
10 | public class ChunkViewModel : ViewModel
11 | {
12 | ///
13 | /// The color used for clearing a tile when it has no value.
14 | ///
15 | private static readonly Color ClearColor = Colors.Black;
16 |
17 | ///
18 | /// The backend field for the property.
19 | ///
20 | private int x;
21 |
22 | ///
23 | /// The backend field for the property.
24 | ///
25 | private int y;
26 |
27 | ///
28 | /// Initializes a new instance of the class.
29 | ///
30 | public ChunkViewModel()
31 | {
32 | this.Bitmap = new WriteableBitmap(
33 | (int)this.PixelWidth,
34 | (int)this.PixelHeight,
35 | 96,
36 | 96,
37 | PixelFormats.Bgr32,
38 | null);
39 | }
40 |
41 | ///
42 | /// Gets the bitmap used for rendering the chunk.
43 | ///
44 | public WriteableBitmap Bitmap { get; }
45 |
46 | ///
47 | /// Gets the pixel width of the chunk.
48 | ///
49 | public double PixelWidth => Configuration.ChunkWidth * Configuration.TileSize;
50 |
51 | ///
52 | /// Gets the pixel height of the chunk.
53 | ///
54 | public double PixelHeight => Configuration.ChunkHeight * Configuration.TileSize;
55 |
56 | ///
57 | /// Gets the X coordinate, in chunk units.
58 | ///
59 | public int X
60 | {
61 | get => this.x;
62 | internal set => this.SetValue(ref this.x, value);
63 | }
64 |
65 | ///
66 | /// Gets the Y coordinate, in chunk units.
67 | ///
68 | public int Y
69 | {
70 | get => this.y;
71 | internal set => this.SetValue(ref this.y, value);
72 | }
73 |
74 | ///
75 | /// Gets the tile data.
76 | ///
77 | public int[,] Tiles { get; } = new int[Configuration.ChunkWidth, Configuration.ChunkHeight];
78 |
79 | ///
80 | /// Updates the chunk bitmap using the specified tileset.
81 | ///
82 | /// The tileset.
83 | internal void Render(Tileset tileset)
84 | {
85 | if (tileset == null)
86 | {
87 | this.Bitmap.Clear(ClearColor);
88 | return;
89 | }
90 |
91 | for (var y = 0; y < Configuration.ChunkHeight; y++)
92 | {
93 | for (var x = 0; x < Configuration.ChunkWidth; x++)
94 | {
95 | var value = this.Tiles[x, y];
96 | if (value > 0)
97 | {
98 | tileset.BlitTo(
99 | value - 1,
100 | this.Bitmap,
101 | x * Configuration.TileSize,
102 | y * Configuration.TileSize);
103 | }
104 | else
105 | {
106 | this.Bitmap.FillRectangle(
107 | x * Configuration.TileSize,
108 | y * Configuration.TileSize,
109 | (x + 1) * Configuration.TileSize,
110 | (y + 1) * Configuration.TileSize,
111 | ClearColor);
112 | }
113 | }
114 | }
115 | }
116 |
117 | ///
118 | /// Loads the chunk data.
119 | ///
120 | internal void Load()
121 | {
122 | this.Context.AssetManager.LoadTiles(this.X, this.Y, this.Tiles);
123 | }
124 |
125 | ///
126 | /// Saves the chunk data.
127 | ///
128 | internal void Save()
129 | {
130 | this.Context.AssetManager.SaveTiles(this.X, this.Y, this.Tiles);
131 | }
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/Editor/Command.cs:
--------------------------------------------------------------------------------
1 | namespace Editor
2 | {
3 | using System;
4 | using System.Windows.Input;
5 |
6 | ///
7 | /// The implementation.
8 | ///
9 | ///
10 | public class Command : ICommand
11 | {
12 | ///
13 | /// Stores the execute delegate.
14 | ///
15 | private readonly Action execute;
16 |
17 | ///
18 | /// Stores the can execute delegate.
19 | ///
20 | private readonly Func canExecute;
21 |
22 | ///
23 | /// Initializes a new instance of the class.
24 | ///
25 | /// The execute delegate.
26 | /// The can execute delegate.
27 | public Command(Action execute, Func canExecute)
28 | {
29 | this.execute = execute;
30 | this.canExecute = canExecute;
31 | }
32 |
33 | ///
34 | /// Initializes a new instance of the class.
35 | ///
36 | /// The execute delegate.
37 | public Command(Action execute)
38 | : this(execute, null)
39 | {
40 | }
41 |
42 | ///
43 | public event EventHandler CanExecuteChanged
44 | {
45 | add => CommandManager.RequerySuggested += value;
46 | remove => CommandManager.RequerySuggested -= value;
47 | }
48 |
49 | ///
50 | public bool CanExecute(object parameter)
51 | {
52 | return this.canExecute == null || this.canExecute.Invoke();
53 | }
54 |
55 | ///
56 | public void Execute(object parameter)
57 | {
58 | this.execute.Invoke();
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/Editor/CommandOfT.cs:
--------------------------------------------------------------------------------
1 | namespace Editor
2 | {
3 | using System;
4 | using System.Windows.Input;
5 |
6 | ///
7 | /// The implementation.
8 | ///
9 | /// The command parameter's type.
10 | ///
11 | public class Command : ICommand
12 | {
13 | ///
14 | /// Stores the execute delegate.
15 | ///
16 | private readonly Action execute;
17 |
18 | ///
19 | /// Stores the can execute delegate.
20 | ///
21 | private readonly Func canExecute;
22 |
23 | ///
24 | /// Initializes a new instance of the class.
25 | ///
26 | /// The execute delegate.
27 | /// The can execute delegate.
28 | public Command(Action execute, Func canExecute)
29 | {
30 | this.execute = execute;
31 | this.canExecute = canExecute;
32 | }
33 |
34 | ///
35 | /// Initializes a new instance of the class.
36 | ///
37 | /// The execute delegate.
38 | public Command(Action execute)
39 | : this(execute, null)
40 | {
41 | }
42 |
43 | ///
44 | public event EventHandler CanExecuteChanged
45 | {
46 | add => CommandManager.RequerySuggested += value;
47 | remove => CommandManager.RequerySuggested -= value;
48 | }
49 |
50 | ///
51 | public bool CanExecute(object parameter)
52 | {
53 | return this.canExecute?.Invoke((T)parameter) ?? false;
54 | }
55 |
56 | ///
57 | public void Execute(object parameter)
58 | {
59 | this.execute?.Invoke((T)parameter);
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/Editor/Configuration.cs:
--------------------------------------------------------------------------------
1 | namespace Editor
2 | {
3 | ///
4 | /// Constants.
5 | ///
6 | public static class Configuration
7 | {
8 | ///
9 | /// The tile width and height, in pixels.
10 | ///
11 | public const int TileSize = 32;
12 |
13 | ///
14 | /// The chunk width, in tile units.
15 | ///
16 | public const int ChunkWidth = 9;
17 |
18 | ///
19 | /// The chunk height, in tile units.
20 | ///
21 | public const int ChunkHeight = 9;
22 |
23 | ///
24 | /// The folder that contains maps.
25 | ///
26 | public const string MapsFolder = "Maps";
27 |
28 | ///
29 | /// The default map name.
30 | ///
31 | public const string DefaultMapName = "Default";
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Editor/Context.cs:
--------------------------------------------------------------------------------
1 | namespace Editor
2 | {
3 | ///
4 | /// Shared application state.
5 | ///
6 | ///
7 | public sealed class Context : BindingSource
8 | {
9 | ///
10 | /// The backend field for the property.
11 | ///
12 | private string currentMap;
13 |
14 | ///
15 | /// The backend field for the property.
16 | ///
17 | private int foreTileIndex = 1;
18 |
19 | ///
20 | /// The backend field for the property.
21 | ///
22 | private int backTileIndex;
23 |
24 | ///
25 | /// Initializes a new instance of the class.
26 | ///
27 | internal Context()
28 | {
29 | this.AssetManager = new AssetManager();
30 | }
31 |
32 | ///
33 | /// Gets the asset manager.
34 | ///
35 | public AssetManager AssetManager { get; }
36 |
37 | ///
38 | /// Gets or sets the current map.
39 | ///
40 | public string CurrentMap
41 | {
42 | get
43 | {
44 | return this.currentMap;
45 | }
46 |
47 | set
48 | {
49 | // Note: Set the asset manager first because other event listeners may assume that the asset manager is 'ready'
50 | this.AssetManager?.SetCurrentMap(value);
51 | this.SetValue(ref this.currentMap, value);
52 | }
53 | }
54 |
55 | ///
56 | /// Gets or sets tile index for the fore tile. Zero clears, otherwise the 1-based index within the tileset.
57 | ///
58 | public int ForeTileIndex
59 | {
60 | get => this.foreTileIndex;
61 | set => this.SetValue(ref this.foreTileIndex, value);
62 | }
63 |
64 | ///
65 | /// Gets or sets tile index for the back tile. Zero clears, otherwise the 1-based index within the tileset.
66 | ///
67 | public int BackTileIndex
68 | {
69 | get => this.backTileIndex;
70 | set => this.SetValue(ref this.backTileIndex, value);
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/Editor/Editor.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Debug
6 | AnyCPU
7 | {CB588834-5696-4B4B-A53D-1F737E95A8D6}
8 | WinExe
9 | Editor
10 | Editor
11 | v4.7.2
12 | 512
13 | {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
14 | 4
15 | true
16 | true
17 |
18 |
19 | AnyCPU
20 | true
21 | full
22 | false
23 | bin\Debug\
24 | DEBUG;TRACE
25 | prompt
26 | 4
27 | true
28 | Editor.ruleset
29 | bin\Debug\Editor.xml
30 |
31 |
32 | AnyCPU
33 | pdbonly
34 | true
35 | bin\Release\
36 | TRACE
37 | prompt
38 | 4
39 | Editor.ruleset
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | 4.0
52 |
53 |
54 |
55 |
56 |
57 | ..\packages\WriteableBitmapEx.1.6.8\lib\net40\WriteableBitmapEx.Wpf.dll
58 |
59 |
60 |
61 |
62 | MSBuild:Compile
63 | Designer
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 | MapBrowserWindow.xaml
79 |
80 |
81 |
82 |
83 |
84 | TilesetWindow.xaml
85 |
86 |
87 |
88 | MSBuild:Compile
89 | Designer
90 |
91 |
92 | App.xaml
93 | Code
94 |
95 |
96 |
97 | MainWindow.xaml
98 | Code
99 |
100 |
101 | Designer
102 | MSBuild:Compile
103 |
104 |
105 | Designer
106 | MSBuild:Compile
107 |
108 |
109 |
110 |
111 | Code
112 |
113 |
114 | True
115 | True
116 | Resources.resx
117 |
118 |
119 | True
120 | Settings.settings
121 | True
122 |
123 |
124 | ResXFileCodeGenerator
125 | Resources.Designer.cs
126 |
127 |
128 |
129 |
130 | SettingsSingleFileGenerator
131 | Settings.Designer.cs
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 | Always
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
--------------------------------------------------------------------------------
/Editor/Editor.ruleset:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/Editor/Extensions/AddRange.cs:
--------------------------------------------------------------------------------
1 | namespace Editor
2 | {
3 | using System.Collections.Generic;
4 |
5 | ///
6 | /// Provides common extension methods.
7 | ///
8 | public static partial class Extensions
9 | {
10 | ///
11 | /// Adds the items into the target collection.
12 | ///
13 | /// The element type.
14 | /// The destination collection.
15 | /// The items to add.
16 | public static void AddRange(this ICollection destination, IEnumerable source)
17 | {
18 | if (destination is List list)
19 | {
20 | list.AddRange(source);
21 | }
22 | else
23 | {
24 | foreach (var item in source)
25 | {
26 | destination.Add(item);
27 | }
28 | }
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Editor/Extensions/AsToolWindow.cs:
--------------------------------------------------------------------------------
1 | namespace Editor
2 | {
3 | using System;
4 | using System.Runtime.InteropServices;
5 | using System.Windows;
6 | using System.Windows.Interop;
7 |
8 | ///
9 | /// Provides common extension methods.
10 | ///
11 | public static partial class Extensions
12 | {
13 | ///
14 | /// Sets the window as a tool window.
15 | ///
16 | /// The window.
17 | public static void AsToolWindow(this Window window)
18 | {
19 | if (window == null)
20 | {
21 | return;
22 | }
23 |
24 | window.ResizeMode = ResizeMode.NoResize;
25 | window.WindowStyle = WindowStyle.ToolWindow;
26 |
27 | var hwnd = new WindowInteropHelper(window).Handle;
28 | Win32.SetWindowLong(hwnd, Win32.GWL_STYLE, Win32.GetWindowLong(hwnd, Win32.GWL_STYLE) & ~Win32.WS_SYSMENU);
29 | }
30 |
31 | ///
32 | /// The Win32 members.
33 | ///
34 | private static class Win32
35 | {
36 | #pragma warning disable SA1310 // Field names must not contain underscore // Justification = Win32 conventions
37 | internal const int GWL_STYLE = -16;
38 | internal const int WS_SYSMENU = 0x80000;
39 | #pragma warning restore SA1310 // Field names must not contain underscore
40 |
41 | [DllImport("user32.dll", SetLastError = true)]
42 | internal static extern int GetWindowLong(IntPtr hWnd, int nIndex);
43 |
44 | [DllImport("user32.dll")]
45 | internal static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/Editor/Extensions/ForEach.cs:
--------------------------------------------------------------------------------
1 | namespace Editor
2 | {
3 | using System;
4 | using System.Collections.Generic;
5 |
6 | ///
7 | /// Provides common extension methods.
8 | ///
9 | public static partial class Extensions
10 | {
11 | ///
12 | /// Performs an action for each element in the enumerable sequence.
13 | ///
14 | /// The sequence element type.
15 | /// The enumerable sequence.
16 | /// The delegate to a method that performs the action for an element.
17 | public static void ForEach(this IEnumerable sequence, Action action)
18 | {
19 | if (action == null)
20 | {
21 | throw new ArgumentNullException(nameof(action));
22 | }
23 |
24 | foreach (var element in sequence)
25 | {
26 | action(element);
27 | }
28 | }
29 |
30 | ///
31 | /// Performs an action for each element in the enumerable sequence.
32 | ///
33 | /// The type of the key.
34 | /// The type of the value.
35 | /// The enumerable sequence.
36 | /// The delegate to a method that performs the action for an element.
37 | public static void ForEach(this IEnumerable> sequence, Action action)
38 | {
39 | if (action == null)
40 | {
41 | throw new ArgumentNullException(nameof(action));
42 | }
43 |
44 | foreach (var element in sequence)
45 | {
46 | action(element.Key, element.Value);
47 | }
48 | }
49 |
50 | ///
51 | /// Performs an action for each element in the enumerable sequence.
52 | ///
53 | /// The type of the first item.
54 | /// The enumerable sequence.
55 | /// The delegate to a method that performs the action for an element.
56 | public static void ForEach(this IEnumerable> sequence, Action action)
57 | {
58 | if (action == null)
59 | {
60 | throw new ArgumentNullException(nameof(action));
61 | }
62 |
63 | foreach (var element in sequence)
64 | {
65 | action(element.Item1);
66 | }
67 | }
68 |
69 | ///
70 | /// Performs an action for each element in the enumerable sequence.
71 | ///
72 | /// The type of the first item.
73 | /// The type of the second item.
74 | /// The enumerable sequence.
75 | /// The delegate to a method that performs the action for an element.
76 | public static void ForEach(this IEnumerable> sequence, Action action)
77 | {
78 | if (action == null)
79 | {
80 | throw new ArgumentNullException(nameof(action));
81 | }
82 |
83 | foreach (var (item1, item2) in sequence)
84 | {
85 | action(item1, item2);
86 | }
87 | }
88 |
89 | ///
90 | /// Performs an action for each element in the enumerable sequence.
91 | ///
92 | /// The type of the first item.
93 | /// The type of the second item.
94 | /// The type of the third item.
95 | /// The enumerable sequence.
96 | /// The delegate to a method that performs the action for an element.
97 | public static void ForEach(this IEnumerable> sequence, Action action)
98 | {
99 | if (action == null)
100 | {
101 | throw new ArgumentNullException(nameof(action));
102 | }
103 |
104 | foreach (var (item1, item2, item3) in sequence)
105 | {
106 | action(item1, item2, item3);
107 | }
108 | }
109 |
110 | ///
111 | /// Performs an action for each element in the enumerable sequence.
112 | ///
113 | /// The type of the first item.
114 | /// The type of the second item.
115 | /// The type of the third item.
116 | /// The type of the fourth item.
117 | /// The enumerable sequence.
118 | /// The delegate to a method that performs the action for an element.
119 | public static void ForEach(this IEnumerable> sequence, Action action)
120 | {
121 | if (action == null)
122 | {
123 | throw new ArgumentNullException(nameof(action));
124 | }
125 |
126 | foreach (var (item1, item2, item3, item4) in sequence)
127 | {
128 | action(item1, item2, item3, item4);
129 | }
130 | }
131 |
132 | ///
133 | /// Performs an action for each element in the enumerable sequence.
134 | ///
135 | /// The type of the first item.
136 | /// The type of the second item.
137 | /// The type of the third item.
138 | /// The type of the fourth item.
139 | /// The type of the fifth item.
140 | /// The enumerable sequence.
141 | /// The delegate to a method that performs the action for an element.
142 | public static void ForEach(this IEnumerable> sequence, Action action)
143 | {
144 | if (action == null)
145 | {
146 | throw new ArgumentNullException(nameof(action));
147 | }
148 |
149 | foreach (var (item1, item2, item3, item4, item5) in sequence)
150 | {
151 | action(item1, item2, item3, item4, item5);
152 | }
153 | }
154 |
155 | ///
156 | /// Performs an action for each element in the enumerable sequence.
157 | ///
158 | /// The type of the first item.
159 | /// The type of the second item.
160 | /// The type of the third item.
161 | /// The type of the fourth item.
162 | /// The type of the fifth item.
163 | /// The type of the sixth item.
164 | /// The enumerable sequence.
165 | /// The delegate to a method that performs the action for an element.
166 | public static void ForEach(this IEnumerable> sequence, Action action)
167 | {
168 | if (action == null)
169 | {
170 | throw new ArgumentNullException(nameof(action));
171 | }
172 |
173 | foreach (var (item1, item2, item3, item4, item5, item6) in sequence)
174 | {
175 | action(item1, item2, item3, item4, item5, item6);
176 | }
177 | }
178 |
179 | ///
180 | /// Performs an action for each element in the enumerable sequence.
181 | ///
182 | /// The type of the first item.
183 | /// The type of the second item.
184 | /// The type of the third item.
185 | /// The type of the fourth item.
186 | /// The type of the fifth item.
187 | /// The type of the sixth item.
188 | /// The type of the seventh item.
189 | /// The enumerable sequence.
190 | /// The delegate to a method that performs the action for an element.
191 | public static void ForEach(this IEnumerable> sequence, Action action)
192 | {
193 | if (action == null)
194 | {
195 | throw new ArgumentNullException(nameof(action));
196 | }
197 |
198 | foreach (var (item1, item2, item3, item4, item5, item6, item7) in sequence)
199 | {
200 | action(item1, item2, item3, item4, item5, item6, item7);
201 | }
202 | }
203 |
204 | ///
205 | /// Performs an action for each element in the enumerable sequence.
206 | ///
207 | /// The type of the first item.
208 | /// The type of the second item.
209 | /// The type of the third item.
210 | /// The type of the fourth item.
211 | /// The type of the fifth item.
212 | /// The type of the sixth item.
213 | /// The type of the seventh item.
214 | /// The type of the eight item.
215 | /// The enumerable sequence.
216 | /// The delegate to a method that performs the action for an element.
217 | public static void ForEach(this IEnumerable> sequence, Action action)
218 | {
219 | if (action == null)
220 | {
221 | throw new ArgumentNullException(nameof(action));
222 | }
223 |
224 | foreach (var element in sequence)
225 | {
226 | action(element.Item1, element.Item2, element.Item3, element.Item4, element.Item5, element.Item6, element.Item7, element.Rest);
227 | }
228 | }
229 | }
230 | }
231 |
--------------------------------------------------------------------------------
/Editor/GlobalSuppressions.cs:
--------------------------------------------------------------------------------
1 | [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:File name must match first type name", Justification = "Reviewed. Suppression is OK here.", Scope = "type", Target = "~T:Editor.Command`1")]
--------------------------------------------------------------------------------
/Editor/MainViewModel.cs:
--------------------------------------------------------------------------------
1 | namespace Editor
2 | {
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using System.Windows.Input;
6 |
7 | ///
8 | /// View model for the main window.
9 | ///
10 | ///
11 | public class MainViewModel : ViewModel
12 | {
13 | ///
14 | /// The backend field for the property.
15 | ///
16 | private int cameraX;
17 |
18 | ///
19 | /// The backend field for the property.
20 | ///
21 | private int cameraY;
22 |
23 | ///
24 | /// Initializes a new instance of the class.
25 | ///
26 | /// true to immediately load the map.
27 | public MainViewModel(bool loadMap = true)
28 | {
29 | // Initialize the chunk pool
30 | for (var y = 0; y < 3; y++)
31 | {
32 | for (var x = 0; x < 3; x++)
33 | {
34 | this.Chunks[x, y] = new ChunkViewModel();
35 | }
36 | }
37 |
38 | // Synchronize data changes from the Context
39 | this.Context.PropertyChanged += (sender, args) =>
40 | {
41 | if (args.PropertyName == nameof(this.Context.CurrentMap))
42 | {
43 | this.RaisePropertyChanged(nameof(this.CurrentMap));
44 | this.LoadMap();
45 | }
46 | };
47 |
48 | // Create commands
49 | this.MoveLeftCommand = new Command(() => this.MoveCamera(--this.CameraX, this.CameraY));
50 | this.MoveRightCommand = new Command(() => this.MoveCamera(++this.CameraX, this.CameraY));
51 | this.MoveUpCommand = new Command(() => this.MoveCamera(this.CameraX, --this.CameraY));
52 | this.MoveDownCommand = new Command(() => this.MoveCamera(this.CameraX, ++this.CameraY));
53 | }
54 |
55 | ///
56 | /// Initializes a new instance of the class.
57 | ///
58 | public MainViewModel()
59 | : this(false)
60 | {
61 | }
62 |
63 | ///
64 | /// Gets the move left command.
65 | ///
66 | public ICommand MoveLeftCommand { get; }
67 |
68 | ///
69 | /// Gets the move right command.
70 | ///
71 | public ICommand MoveRightCommand { get; }
72 |
73 | ///
74 | /// Gets the move up command.
75 | ///
76 | public ICommand MoveUpCommand { get; }
77 |
78 | ///
79 | /// Gets the move down command.
80 | ///
81 | public ICommand MoveDownCommand { get; }
82 |
83 | ///
84 | /// Gets the visible chunks.
85 | ///
86 | ///
87 | /// Enumerates the chunk array from left to right, from top to bottom.
88 | ///
89 | public IEnumerable VisibleChunks => Enumerable.Range(0, 3).SelectMany(y => Enumerable.Range(0, 3).Select(x => this.Chunks[x, y]));
90 |
91 | ///
92 | /// Gets the current map name.
93 | ///
94 | public string CurrentMap => this.Context.CurrentMap;
95 |
96 | ///
97 | /// Gets the camera X coordinate, in chunk units.
98 | ///
99 | public int CameraX
100 | {
101 | get => this.cameraX;
102 | private set => this.SetValue(ref this.cameraX, value);
103 | }
104 |
105 | ///
106 | /// Gets the camera Y coordinate, in chunk units.
107 | ///
108 | public int CameraY
109 | {
110 | get => this.cameraY;
111 | private set => this.SetValue(ref this.cameraY, value);
112 | }
113 |
114 | ///
115 | /// Gets the visible chunks.
116 | ///
117 | private ChunkViewModel[,] Chunks { get; } = new ChunkViewModel[3, 3];
118 |
119 | ///
120 | /// Called when the 'paint' action has occurred.
121 | ///
122 | /// The chunk.
123 | /// The location.
124 | internal void OnPaint(ChunkViewModel chunk, (int x, int y) location)
125 | {
126 | if (chunk == null
127 | || location.x < 0
128 | || location.x >= Configuration.ChunkWidth
129 | || location.y < 0
130 | || location.y >= Configuration.ChunkHeight)
131 | {
132 | return;
133 | }
134 |
135 | var tile = this.Context.ForeTileIndex;
136 | if (chunk.Tiles[location.x, location.y] != tile)
137 | {
138 | chunk.Tiles[location.x, location.y] = tile;
139 |
140 | chunk.Save();
141 | chunk.Render(this.Context.AssetManager.GetTileset());
142 | }
143 | }
144 |
145 | ///
146 | /// Called when the 'erase' action has occurred.
147 | ///
148 | /// The chunk.
149 | /// The location.
150 | internal void OnErase(ChunkViewModel chunk, (int x, int y) location)
151 | {
152 | if (chunk == null
153 | || location.x < 0
154 | || location.x >= Configuration.ChunkWidth
155 | || location.y < 0
156 | || location.y >= Configuration.ChunkHeight)
157 | {
158 | return;
159 | }
160 |
161 | var tile = this.Context.BackTileIndex;
162 | if (chunk.Tiles[location.x, location.y] != tile)
163 | {
164 | chunk.Tiles[location.x, location.y] = tile;
165 |
166 | chunk.Save();
167 | chunk.Render(this.Context.AssetManager.GetTileset());
168 | }
169 | }
170 |
171 | ///
172 | /// Moves the camera so that the chunk at the specified coordinates is centered.
173 | ///
174 | /// The X coordinate, in chunk units.
175 | /// The Y coordinate, in chunk units.
176 | private void MoveCamera(int x, int y)
177 | {
178 | for (var yy = 0; yy < 3; yy++)
179 | {
180 | for (var xx = 0; xx < 3; xx++)
181 | {
182 | var chunk = this.Chunks[xx, yy];
183 | var chunkX = x + xx - 1;
184 | var chunkY = y + yy - 1;
185 | chunk.X = chunkX;
186 | chunk.Y = chunkY;
187 |
188 | chunk.Load();
189 | chunk.Render(this.Context.AssetManager.GetTileset());
190 | }
191 | }
192 | }
193 |
194 | ///
195 | /// Resets the camera and moves it to position (0, 0).
196 | ///
197 | private void ResetCamera()
198 | {
199 | this.MoveCamera(this.CameraX = 0, this.CameraY = 0);
200 | }
201 |
202 | ///
203 | /// Loads the map.
204 | ///
205 | private void LoadMap()
206 | {
207 | this.ResetCamera();
208 | }
209 | }
210 | }
211 |
--------------------------------------------------------------------------------
/Editor/MainWindow.xaml:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
43 |
44 |
45 |
46 |
47 |
48 |
56 |
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/Editor/MainWindow.xaml.cs:
--------------------------------------------------------------------------------
1 | namespace Editor
2 | {
3 | using System.ComponentModel;
4 | using System.Threading.Tasks;
5 | using System.Windows;
6 | using System.Windows.Input;
7 |
8 | ///
9 | /// Interaction logic for MainWindow.xaml.
10 | ///
11 | public partial class MainWindow
12 | {
13 | ///
14 | /// The picker dependency property.
15 | ///
16 | public static readonly DependencyProperty PickerProperty = DependencyProperty.Register(
17 | nameof(Picker),
18 | typeof(Rect),
19 | typeof(MainWindow),
20 | new PropertyMetadata(default(Rect)));
21 |
22 | ///
23 | /// Initializes a new instance of the class.
24 | ///
25 | public MainWindow()
26 | {
27 | this.InitializeComponent();
28 |
29 | this.Loaded += (sender, args) => this.ShowToolWindows();
30 |
31 | this.DataContext = new MainViewModel(loadMap: !DesignerProperties.GetIsInDesignMode(this));
32 | }
33 |
34 | ///
35 | /// Gets or sets the picker.
36 | ///
37 | public Rect Picker
38 | {
39 | get => (Rect)this.GetValue(PickerProperty);
40 | set => this.SetValue(PickerProperty, value);
41 | }
42 |
43 | ///
44 | /// Displays the map browser and tileset tool windows.
45 | ///
46 | private async void ShowToolWindows()
47 | {
48 | this.UpdateLayout();
49 | await Task.Delay(100);
50 |
51 | var mapBrowserWindow = new MapBrowserWindow();
52 | mapBrowserWindow.Show();
53 | mapBrowserWindow.AsToolWindow();
54 |
55 | var tilesetWindow = new TilesetWindow();
56 | tilesetWindow.Show();
57 | tilesetWindow.AsToolWindow();
58 | }
59 |
60 | ///
61 | /// Called when the event is raised.
62 | ///
63 | /// The sender.
64 | /// The instance containing the event data.
65 | private void OnChunkMouseDown(object sender, MouseButtonEventArgs e)
66 | {
67 | if (sender is FrameworkElement element)
68 | {
69 | this.HandlePaint(element, e.GetPosition(element), e.LeftButton == MouseButtonState.Pressed, e.RightButton == MouseButtonState.Pressed);
70 | }
71 | }
72 |
73 | ///
74 | /// Called when the event is raised.
75 | ///
76 | /// The sender.
77 | /// The instance containing the event data.
78 | private void OnChunkMouseMove(object sender, MouseEventArgs e)
79 | {
80 | if (sender is FrameworkElement element)
81 | {
82 | this.HandlePaint(element, e.GetPosition(element), e.LeftButton == MouseButtonState.Pressed, e.RightButton == MouseButtonState.Pressed);
83 | }
84 | }
85 |
86 | ///
87 | /// Updates the viewmodel based on mouse painting.
88 | ///
89 | /// The element that sent the event.
90 | /// The relative mouse position.
91 | /// true is the left mouse button is down.
92 | /// true is the right mouse button is down.
93 | private void HandlePaint(FrameworkElement element, Point relativePosition, bool leftButton, bool rightButton)
94 | {
95 | if (!(element?.DataContext is ChunkViewModel viewmodel))
96 | {
97 | return;
98 | }
99 |
100 | var location = (x: (int)relativePosition.X / Configuration.TileSize, y: (int)relativePosition.Y / Configuration.TileSize);
101 |
102 | if (leftButton)
103 | {
104 | (this.DataContext as MainViewModel)?.OnPaint(viewmodel, location);
105 | }
106 | else if (rightButton)
107 | {
108 | (this.DataContext as MainViewModel)?.OnErase(viewmodel, location);
109 | }
110 | }
111 |
112 | ///
113 | /// Called when the event is raised.
114 | ///
115 | /// The sender.
116 | /// The instance containing the event data.
117 | private void OnContainerMouseMove(object sender, MouseEventArgs e)
118 | {
119 | var pos = e.GetPosition((UIElement)sender);
120 | var (x, y) = ((int)pos.X / Configuration.TileSize, (int)pos.Y / Configuration.TileSize);
121 |
122 | this.Picker = new Rect(
123 | new Point(x * Configuration.TileSize, y * Configuration.TileSize),
124 | new Size(Configuration.TileSize, Configuration.TileSize));
125 | }
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/Editor/MapBrowserViewModel.cs:
--------------------------------------------------------------------------------
1 | namespace Editor
2 | {
3 | using System;
4 | using System.Collections.ObjectModel;
5 | using System.Diagnostics;
6 | using System.IO;
7 | using System.Linq;
8 | using System.Windows;
9 | using System.Windows.Input;
10 | using System.Windows.Media;
11 | using System.Windows.Media.Imaging;
12 |
13 | ///
14 | /// View model for the map browser window.
15 | ///
16 | ///
17 | public class MapBrowserViewModel : ViewModel
18 | {
19 | ///
20 | /// Stores the characters that are forbidden in a map name.
21 | ///
22 | private static readonly char[] InvalidNameCharacters = Path.GetInvalidPathChars().Union(Path.GetInvalidPathChars()).ToArray();
23 |
24 | ///
25 | /// Stores the file system watcher for map folders.
26 | ///
27 | private readonly FileSystemWatcher fileSystemWatcher;
28 |
29 | ///
30 | /// The backend field for the property.
31 | ///
32 | private WriteableBitmap preview;
33 |
34 | ///
35 | /// The backend field for the property.
36 | ///
37 | private MapViewModel currentMap;
38 |
39 | ///
40 | /// The backend field for the property.
41 | ///
42 | private string newMapName;
43 |
44 | ///
45 | /// Initializes a new instance of the class.
46 | ///
47 | public MapBrowserViewModel()
48 | {
49 | this.Preview = new WriteableBitmap(
50 | 1,
51 | 1,
52 | 96,
53 | 96,
54 | PixelFormats.Bgr32,
55 | null);
56 |
57 | this.fileSystemWatcher = new FileSystemWatcher { Path = Configuration.MapsFolder };
58 |
59 | this.CreateNewMapCommand = new Command(this.CreateNewMap, this.IsValidNewMapName);
60 |
61 | this.Refresh();
62 | }
63 |
64 | ///
65 | /// Gets the create new map command.
66 | ///
67 | public ICommand CreateNewMapCommand { get; }
68 |
69 | ///
70 | /// Gets the maps.
71 | ///
72 | public ObservableCollection Maps { get; } = new ObservableCollection();
73 |
74 | ///
75 | /// Gets the preview image.
76 | ///
77 | public WriteableBitmap Preview
78 | {
79 | get => this.preview;
80 | private set => this.SetValue(ref this.preview, value);
81 | }
82 |
83 | ///
84 | /// Gets or sets the current map.
85 | ///
86 | public MapViewModel CurrentMap
87 | {
88 | get
89 | {
90 | return this.currentMap;
91 | }
92 |
93 | set
94 | {
95 | this.SetValue(ref this.currentMap, value);
96 | this.Context.CurrentMap = value?.Name;
97 | this.RefreshPreview(value?.Name);
98 | }
99 | }
100 |
101 | ///
102 | /// Gets or sets the new map name.
103 | ///
104 | public string NewMapName
105 | {
106 | get => this.newMapName;
107 | set => this.SetValue(ref this.newMapName, value);
108 | }
109 |
110 | ///
111 | /// Removes the specified from the map list.
112 | ///
113 | /// The item.
114 | internal void DeleteMap(MapViewModel item)
115 | {
116 | if (item != null && this.Maps.Contains(item) && ConfirmDelete())
117 | {
118 | item.DeleteChunks();
119 | Directory.Delete(item.FullName);
120 | }
121 |
122 | bool ConfirmDelete() =>
123 | MessageBox.Show(
124 | $"Permanently delete map \"{item.Name}\"?",
125 | "Delete",
126 | MessageBoxButton.YesNo,
127 | MessageBoxImage.Exclamation,
128 | MessageBoxResult.No) == MessageBoxResult.Yes;
129 | }
130 |
131 | ///
132 | /// Called when the view model is attached and should subscribe to events.
133 | ///
134 | internal void OnAttached()
135 | {
136 | this.fileSystemWatcher.Created += this.OnFolderCreated;
137 | this.fileSystemWatcher.Renamed += this.OnFolderRenamed;
138 | this.fileSystemWatcher.Deleted += this.OnFolderDeleted;
139 | this.fileSystemWatcher.EnableRaisingEvents = true;
140 | }
141 |
142 | ///
143 | /// Called when the view model is detached and should unsubscribe from events.
144 | ///
145 | internal void OnDetached()
146 | {
147 | this.fileSystemWatcher.EnableRaisingEvents = false;
148 | this.fileSystemWatcher.Created -= this.OnFolderCreated;
149 | this.fileSystemWatcher.Renamed -= this.OnFolderRenamed;
150 | this.fileSystemWatcher.Deleted -= this.OnFolderDeleted;
151 | }
152 |
153 | ///
154 | /// Called when the file system watcher detects a created entry.
155 | ///
156 | /// The sender.
157 | /// The instance containing the event data.
158 | private void OnFolderCreated(object sender, FileSystemEventArgs e) => Application.Current.Dispatcher.Invoke(this.Refresh);
159 |
160 | ///
161 | /// Called when the file system watcher detects a renamed entry.
162 | ///
163 | /// The sender.
164 | /// The instance containing the event data.
165 | private void OnFolderRenamed(object sender, FileSystemEventArgs e) => Application.Current.Dispatcher.Invoke(this.Refresh);
166 |
167 | ///
168 | /// Called when the file system watcher detects a deleted entry.
169 | ///
170 | /// The sender.
171 | /// The instance containing the event data.
172 | private void OnFolderDeleted(object sender, FileSystemEventArgs e) => Application.Current.Dispatcher.Invoke(this.Refresh);
173 |
174 | ///
175 | /// Adds the specified map.
176 | ///
177 | /// The name.
178 | private void CreateNewMap(string name)
179 | {
180 | Debug.Assert(this.IsValidNewMapName(name), "The map name is invalid.");
181 |
182 | Directory.CreateDirectory(Path.Combine(Configuration.MapsFolder, name));
183 |
184 | this.NewMapName = string.Empty;
185 | }
186 |
187 | ///
188 | /// Determines whether the specified name is suitable for a map.
189 | ///
190 | /// The map name candidate.
191 | ///
192 | /// true if name is valid; otherwise, false.
193 | ///
194 | private bool IsValidNewMapName(string name)
195 | {
196 | return !string.IsNullOrWhiteSpace(name)
197 | && !this.Maps.Any(map => string.Equals(name, map.Name, StringComparison.InvariantCultureIgnoreCase))
198 | && !InvalidNameCharacters.Any(name.Contains);
199 | }
200 |
201 | ///
202 | /// Refreshes the map list.
203 | ///
204 | private void Refresh()
205 | {
206 | var previousSelection = this.CurrentMap;
207 |
208 | this.Maps.Clear();
209 |
210 | this.Maps.AddRange(
211 | new DirectoryInfo(Configuration.MapsFolder)
212 | .EnumerateDirectories()
213 | .OrderByDescending(folder => string.Equals(folder.Name, Configuration.DefaultMapName, StringComparison.InvariantCultureIgnoreCase))
214 | .ThenBy(folder => folder.Name)
215 | .Select(folder => new MapViewModel(this, folder)));
216 |
217 | this.CurrentMap = this.Maps.FirstOrDefault(map => string.Equals(map.Name, previousSelection?.Name, StringComparison.InvariantCultureIgnoreCase))
218 | ?? this.Maps.FirstOrDefault(map => string.Equals(map.Name, Configuration.DefaultMapName, StringComparison.InvariantCultureIgnoreCase))
219 | ?? this.Maps.FirstOrDefault();
220 | }
221 |
222 | ///
223 | /// Refreshes the preview image for the specified map.
224 | ///
225 | /// Name of the map.
226 | private void RefreshPreview(string mapName)
227 | {
228 | if (string.IsNullOrWhiteSpace(mapName))
229 | {
230 | Clear();
231 | return;
232 | }
233 |
234 | var chunkFiles = Directory.GetFiles(Path.Combine(Configuration.MapsFolder, mapName), "*.chunk").Select(Path.GetFileName);
235 | var coordinateTuples = chunkFiles.Select(Parse).Where(coordinates => coordinates != null).Cast<(int x, int y)>().ToArray();
236 |
237 | if (coordinateTuples.Any())
238 | {
239 | var minX = coordinateTuples.Min(t => t.x);
240 | var maxX = coordinateTuples.Max(t => t.x);
241 | var minY = coordinateTuples.Min(t => t.y);
242 | var maxY = coordinateTuples.Max(t => t.y);
243 |
244 | var width = maxX - minX + 1;
245 | var height = maxY - minY + 1;
246 |
247 | Debug.WriteLine($"x:({minX} → {maxX}), y:({minY} → {maxY}), size:({width}, {height})");
248 |
249 | var bitmap = this.Preview.Resize(width, height, WriteableBitmapExtensions.Interpolation.NearestNeighbor);
250 | this.Preview = bitmap;
251 | bitmap.Clear(Colors.Black);
252 |
253 | if (bitmap.TryLock(Duration.Forever))
254 | {
255 | unsafe
256 | {
257 | var pBackBuffer = bitmap.BackBuffer;
258 | var pBuff = (byte*)pBackBuffer.ToPointer();
259 | var stride = bitmap.BackBufferStride;
260 |
261 | foreach (var coordinateTuple in coordinateTuples)
262 | {
263 | Plot(coordinateTuple.x, coordinateTuple.y, Colors.White);
264 | }
265 |
266 | Plot(0, 0, Colors.Red);
267 |
268 | void Plot(int x, int y, Color color)
269 | {
270 | var index = (y - minY) * stride + (x - minX) * 4;
271 | pBuff[index] = color.B;
272 | pBuff[index + 1] = color.G;
273 | pBuff[index + 2] = color.R;
274 | pBuff[index + 3] = 255;
275 | }
276 | }
277 |
278 | bitmap.AddDirtyRect(new Int32Rect(0, 0, width, height));
279 | bitmap.Unlock();
280 | }
281 | }
282 | else
283 | {
284 | Clear();
285 | }
286 |
287 | (int x, int y)? Parse(string name)
288 | {
289 | var parts = name.Split(',', '.');
290 | if (parts.Length != 3 || !int.TryParse(parts[0], out var x) || !int.TryParse(parts[1], out var y))
291 | {
292 | return null;
293 | }
294 |
295 | return (x, y);
296 | }
297 |
298 | void Clear()
299 | {
300 | this.Preview = this.Preview.Resize(1, 1, WriteableBitmapExtensions.Interpolation.NearestNeighbor);
301 | this.Preview.Clear(Colors.Black);
302 | }
303 | }
304 | }
305 | }
306 |
--------------------------------------------------------------------------------
/Editor/MapBrowserWindow.xaml:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/Editor/MapBrowserWindow.xaml.cs:
--------------------------------------------------------------------------------
1 | namespace Editor
2 | {
3 | using System.Windows;
4 |
5 | ///
6 | /// Interaction logic for MapBrowserWindow.xaml.
7 | ///
8 | public partial class MapBrowserWindow
9 | {
10 | ///
11 | /// Initializes a new instance of the class.
12 | ///
13 | public MapBrowserWindow()
14 | {
15 | this.DataContextChanged += this.OnDataContextChanged;
16 |
17 | this.InitializeComponent();
18 |
19 | this.SizeChanged += this.OnSizeChanged;
20 |
21 | this.DataContext = new MapBrowserViewModel();
22 | }
23 |
24 | ///
25 | /// Called when the event is raised.
26 | ///
27 | /// The sender.
28 | /// The instance containing the event data.
29 | private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
30 | {
31 | // Since the view model subscribes to some events, make sure to provide lifetime methods for proper unsubscribing (to prevent memory leaks)
32 | (e.OldValue as MapBrowserViewModel)?.OnDetached();
33 | (e.NewValue as MapBrowserViewModel)?.OnAttached();
34 | }
35 |
36 | ///
37 | /// Called when the event is raised.
38 | ///
39 | /// The sender.
40 | /// The instance containing the event data.
41 | private void OnSizeChanged(object sender, SizeChangedEventArgs e)
42 | {
43 | var mainWindow = Application.Current?.MainWindow;
44 | if (mainWindow != null)
45 | {
46 | this.Top = mainWindow.Top;
47 | this.Left = mainWindow.Left - this.ActualWidth - 10;
48 | }
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/Editor/MapViewModel.cs:
--------------------------------------------------------------------------------
1 | namespace Editor
2 | {
3 | using System;
4 | using System.Diagnostics;
5 | using System.IO;
6 | using System.Linq;
7 | using System.Windows.Input;
8 |
9 | ///
10 | /// View model for a single map item.
11 | ///
12 | public class MapViewModel
13 | {
14 | ///
15 | /// Stores the who owns this item.
16 | ///
17 | private readonly MapBrowserViewModel parent;
18 |
19 | ///
20 | /// Stores the associated .
21 | ///
22 | private readonly DirectoryInfo directory;
23 |
24 | ///
25 | /// Initializes a new instance of the class.
26 | ///
27 | /// The browser.
28 | /// The directory.
29 | ///
30 | /// The cannot be null.
31 | /// or
32 | /// The cannot be null.
33 | ///
34 | public MapViewModel(MapBrowserViewModel browser, DirectoryInfo directory)
35 | {
36 | this.parent = browser ?? throw new ArgumentNullException(nameof(browser), $"The {nameof(browser)} cannot be null.");
37 | this.directory = directory ?? throw new ArgumentNullException(nameof(directory), $"The {nameof(directory)} cannot be null.");
38 |
39 | this.DeleteCommand = new Command(
40 | item => this.parent.DeleteMap(item),
41 | item => !string.Equals(item?.Name, Configuration.DefaultMapName, StringComparison.InvariantCultureIgnoreCase));
42 |
43 | this.CleanCommand = new Command(this.DeleteEmptyChunks);
44 | }
45 |
46 | ///
47 | /// Gets the delete command.
48 | ///
49 | public ICommand DeleteCommand { get; }
50 |
51 | ///
52 | /// Gets the clean command.
53 | ///
54 | public ICommand CleanCommand { get; }
55 |
56 | ///
57 | /// Gets the name.
58 | ///
59 | public string Name => this.directory.Name;
60 |
61 | ///
62 | /// Gets the absolute path.
63 | ///
64 | public string FullName => this.directory.FullName;
65 |
66 | ///
67 | /// Deletes all chunks from this map.
68 | ///
69 | internal void DeleteChunks()
70 | {
71 | this.directory.EnumerateFiles().ForEach(file => file.Delete());
72 | }
73 |
74 | ///
75 | /// Examines all chunks that belong to this map and deletes those that contain no visible tile data.
76 | ///
77 | private void DeleteEmptyChunks()
78 | {
79 | foreach (var chunkFile in this.directory.EnumerateFiles("*.chunk").ToArray())
80 | {
81 | chunkFile.Refresh();
82 | if (!chunkFile.Exists)
83 | {
84 | continue;
85 | }
86 |
87 | var filename = chunkFile.FullName;
88 | var fileContainsNonEmptyValue = false;
89 |
90 | try
91 | {
92 | using (var stream = chunkFile.OpenRead())
93 | {
94 | int b;
95 | while ((b = stream.ReadByte()) >= 0)
96 | {
97 | if (b > 0)
98 | {
99 | fileContainsNonEmptyValue = true;
100 | break;
101 | }
102 | }
103 | }
104 | }
105 | catch
106 | {
107 | continue;
108 | }
109 |
110 | if (!fileContainsNonEmptyValue)
111 | {
112 | Trace.TraceInformation($"Deleting chunk file '{chunkFile.Name}'...");
113 | File.Delete(filename);
114 | }
115 | }
116 | }
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/Editor/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.InteropServices;
3 | using System.Windows;
4 |
5 | [assembly: AssemblyTitle("Editor")]
6 | [assembly: AssemblyDescription("")]
7 | [assembly: AssemblyConfiguration("")]
8 | [assembly: AssemblyCompany("")]
9 | [assembly: AssemblyProduct("Editor")]
10 | [assembly: AssemblyCopyright("Copyright © 2019")]
11 | [assembly: AssemblyTrademark("")]
12 | [assembly: AssemblyCulture("")]
13 |
14 | [assembly: ComVisible(false)]
15 | [assembly: ThemeInfo(ResourceDictionaryLocation.None, ResourceDictionaryLocation.SourceAssembly)]
16 |
17 | [assembly: AssemblyVersion("1.0.0.0")]
18 | [assembly: AssemblyFileVersion("1.0.0.0")]
19 |
--------------------------------------------------------------------------------
/Editor/Properties/Resources.Designer.cs:
--------------------------------------------------------------------------------
1 | //------------------------------------------------------------------------------
2 | //
3 | // This code was generated by a tool.
4 | // Runtime Version:4.0.30319.42000
5 | //
6 | // Changes to this file may cause incorrect behavior and will be lost if
7 | // the code is regenerated.
8 | //
9 | //------------------------------------------------------------------------------
10 |
11 | namespace Editor.Properties
12 | {
13 |
14 |
15 | ///
16 | /// A strongly-typed resource class, for looking up localized strings, etc.
17 | ///
18 | // This class was auto-generated by the StronglyTypedResourceBuilder
19 | // class via a tool like ResGen or Visual Studio.
20 | // To add or remove a member, edit your .ResX file then rerun ResGen
21 | // with the /str option, or rebuild your VS project.
22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
25 | internal class Resources
26 | {
27 |
28 | private static global::System.Resources.ResourceManager resourceMan;
29 |
30 | private static global::System.Globalization.CultureInfo resourceCulture;
31 |
32 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
33 | internal Resources()
34 | {
35 | }
36 |
37 | ///
38 | /// Returns the cached ResourceManager instance used by this class.
39 | ///
40 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
41 | internal static global::System.Resources.ResourceManager ResourceManager
42 | {
43 | get
44 | {
45 | if ((resourceMan == null))
46 | {
47 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Editor.Properties.Resources", typeof(Resources).Assembly);
48 | resourceMan = temp;
49 | }
50 | return resourceMan;
51 | }
52 | }
53 |
54 | ///
55 | /// Overrides the current thread's CurrentUICulture property for all
56 | /// resource lookups using this strongly typed resource class.
57 | ///
58 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
59 | internal static global::System.Globalization.CultureInfo Culture
60 | {
61 | get
62 | {
63 | return resourceCulture;
64 | }
65 | set
66 | {
67 | resourceCulture = value;
68 | }
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/Editor/Properties/Resources.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 | text/microsoft-resx
107 |
108 |
109 | 2.0
110 |
111 |
112 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
113 |
114 |
115 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
116 |
117 |
--------------------------------------------------------------------------------
/Editor/Properties/Settings.Designer.cs:
--------------------------------------------------------------------------------
1 | //------------------------------------------------------------------------------
2 | //
3 | // This code was generated by a tool.
4 | // Runtime Version:4.0.30319.42000
5 | //
6 | // Changes to this file may cause incorrect behavior and will be lost if
7 | // the code is regenerated.
8 | //
9 | //------------------------------------------------------------------------------
10 |
11 | namespace Editor.Properties
12 | {
13 |
14 |
15 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
16 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")]
17 | internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase
18 | {
19 |
20 | private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
21 |
22 | public static Settings Default
23 | {
24 | get
25 | {
26 | return defaultInstance;
27 | }
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Editor/Properties/Settings.settings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/Editor/Tileset.cs:
--------------------------------------------------------------------------------
1 | namespace Editor
2 | {
3 | using System;
4 | using System.Diagnostics;
5 | using System.IO;
6 | using System.Windows;
7 | using System.Windows.Media.Imaging;
8 |
9 | ///
10 | /// Represents the texture atlas that contains all tiles usable in a map.
11 | ///
12 | public class Tileset
13 | {
14 | ///
15 | /// Stores the atlas bitmap.
16 | ///
17 | private readonly WriteableBitmap bitmap;
18 |
19 | ///
20 | /// Stores the tile size.
21 | ///
22 | private readonly Size tileSize;
23 |
24 | ///
25 | /// Initializes a new instance of the class.
26 | ///
27 | /// The filename.
28 | /// The tile size, in pixels.
29 | ///
30 | /// The cannot be null or empty.
31 | /// or
32 | /// The cannot be less or equal to zero.
33 | ///
34 | /// The bitmap dimensions should be exactly a multiple of the tile size.
35 | public Tileset(string filename, int tileSize)
36 | {
37 | Debug.Assert(tileSize == Configuration.TileSize, "The tile size should equal to that of Configuration.");
38 |
39 | if (string.IsNullOrEmpty(filename))
40 | {
41 | throw new ArgumentException($"The {nameof(filename)} cannot be null or empty.", nameof(filename));
42 | }
43 |
44 | if (tileSize <= 0)
45 | {
46 | throw new ArgumentException($"The {nameof(tileSize)} cannot be less or equal to zero.", nameof(tileSize));
47 | }
48 |
49 | this.bitmap = new WriteableBitmap(new BitmapImage(new Uri(filename, UriKind.Relative)));
50 |
51 | if (tileSize > this.bitmap.PixelWidth
52 | || tileSize > this.bitmap.PixelHeight
53 | || this.bitmap.PixelWidth % tileSize != 0
54 | || this.bitmap.PixelHeight % tileSize != 0)
55 | {
56 | throw new InvalidOperationException("The bitmap dimensions should be exactly a multiple of the tile size.");
57 | }
58 |
59 | this.Filename = filename;
60 | this.tileSize = new Size(tileSize, tileSize);
61 | this.Width = this.bitmap.PixelWidth / tileSize;
62 | this.Height = this.bitmap.PixelHeight / tileSize;
63 | }
64 |
65 | ///
66 | /// Gets the filename.
67 | ///
68 | public string Filename { get; }
69 |
70 | ///
71 | /// Gets the tileset's asset name.
72 | ///
73 | public string Name => Path.GetFileNameWithoutExtension(this.Filename);
74 |
75 | ///
76 | /// Gets the width, in tile units.
77 | ///
78 | public int Width { get; }
79 |
80 | ///
81 | /// Gets the height, in tile units.
82 | ///
83 | public int Height { get; }
84 |
85 | ///
86 | /// Gets the total tile count.
87 | ///
88 | public int Count => this.Width * this.Height;
89 |
90 | ///
91 | /// Gets the tileset bitmap width in pixels.
92 | ///
93 | public int PixelWidth => this.bitmap.PixelWidth;
94 |
95 | ///
96 | /// Gets the tileset bitmap height in pixels.
97 | ///
98 | public int PixelHeight => this.bitmap.PixelHeight;
99 |
100 | ///
101 | /// Gets the tile index at the given row and column. The index is zero-based.
102 | ///
103 | /// The row. Zero-based.
104 | /// The column. Zero-based.
105 | /// The zero-based tile index.
106 | ///
107 | /// The row was out of range.
108 | /// or
109 | /// The column was out of range.
110 | ///
111 | public int IndexAt(int row, int column)
112 | {
113 | if (row < 0 || row >= this.Height)
114 | {
115 | throw new ArgumentOutOfRangeException(nameof(row), $"The {nameof(row)} was out of range. Expected [0, {this.Height - 1}].");
116 | }
117 |
118 | if (column < 0 || column >= this.Width)
119 | {
120 | throw new ArgumentOutOfRangeException(nameof(column), $"The {nameof(column)} was out of range. Expected [0, {this.Width - 1}].");
121 | }
122 |
123 | return row * this.Width + column;
124 | }
125 |
126 | ///
127 | /// Gets the row and column for the corresponding index. All units are zero-based.
128 | ///
129 | /// The index. Zero-based.
130 | /// The row and column. Zero-based.
131 | /// The index was out of range.
132 | public (int row, int column) FromIndex(int index)
133 | {
134 | if (index < 0 || index >= this.Count)
135 | {
136 | throw new ArgumentOutOfRangeException(nameof(index), $"The {nameof(index)} was out of range. Expected [0, {this.Count - 1}].");
137 | }
138 |
139 | var row = index / this.Width;
140 | var column = index % this.Width;
141 |
142 | return (row, column);
143 | }
144 |
145 | ///
146 | /// Copies the pixels of the tile at the given index into the target bitmap at the given location. The location is given in pixel units.
147 | ///
148 | /// The tile index. Zero-based.
149 | /// The target .
150 | /// The target X coordinate, in pixels.
151 | /// The target Y coordinate, in pixels.
152 | public void BlitTo(int tileIndex, WriteableBitmap target, int targetX, int targetY)
153 | {
154 | var (row, column) = this.FromIndex(tileIndex);
155 | var sourceRect = new Rect(new Point(column * this.tileSize.Width, row * this.tileSize.Height), this.tileSize);
156 |
157 | target?.Blit(new Rect(new Point(targetX, targetY), this.tileSize), this.bitmap, sourceRect);
158 | }
159 |
160 | ///
161 | /// Copies the entire tileset into the target bitmap.
162 | ///
163 | /// The target .
164 | public void BlitTo(WriteableBitmap target) => target?.BlitRender(this.bitmap);
165 | }
166 | }
167 |
--------------------------------------------------------------------------------
/Editor/TilesetViewModel.cs:
--------------------------------------------------------------------------------
1 | namespace Editor
2 | {
3 | using System.Windows.Input;
4 | using System.Windows.Media;
5 | using System.Windows.Media.Imaging;
6 |
7 | ///
8 | /// View model for the tileset window.
9 | ///
10 | ///
11 | public class TilesetViewModel : ViewModel
12 | {
13 | ///
14 | /// Represents the color of 'no value'.
15 | ///
16 | private static readonly Color ClearColor = Colors.Black;
17 |
18 | ///
19 | /// Stores the associated tileset.
20 | ///
21 | private readonly Tileset tileset;
22 |
23 | ///
24 | /// Initializes a new instance of the class.
25 | ///
26 | public TilesetViewModel()
27 | {
28 | this.tileset = this.Context.AssetManager.GetTileset();
29 |
30 | this.Bitmap = new WriteableBitmap(
31 | this.tileset.PixelWidth,
32 | this.tileset.PixelHeight,
33 | 96,
34 | 96,
35 | PixelFormats.Bgr32,
36 | null);
37 |
38 | this.tileset.BlitTo(this.Bitmap);
39 |
40 | this.ForeTile = new WriteableBitmap(
41 | Configuration.TileSize,
42 | Configuration.TileSize,
43 | 96,
44 | 96,
45 | PixelFormats.Bgr32,
46 | null);
47 |
48 | this.BackTile = new WriteableBitmap(
49 | Configuration.TileSize,
50 | Configuration.TileSize,
51 | 96,
52 | 96,
53 | PixelFormats.Bgr32,
54 | null);
55 |
56 | this.ClearForeCommand = new Command(() => this.ForeTileIndex = 0);
57 | this.ClearBackCommand = new Command(() => this.BackTileIndex = 0);
58 |
59 | this.ForeTileIndex = this.Context.ForeTileIndex;
60 | this.BackTileIndex = this.Context.BackTileIndex;
61 | }
62 |
63 | ///
64 | /// Gets the clear fore command.
65 | ///
66 | public ICommand ClearForeCommand { get; }
67 |
68 | ///
69 | /// Gets the clear back command.
70 | ///
71 | public ICommand ClearBackCommand { get; }
72 |
73 | ///
74 | /// Gets the bitmap.
75 | ///
76 | public WriteableBitmap Bitmap { get; }
77 |
78 | ///
79 | /// Gets .
80 | ///
81 | public double TileWidth => Configuration.TileSize;
82 |
83 | ///
84 | /// Gets .
85 | ///
86 | public double TileHeight => Configuration.TileSize;
87 |
88 | ///
89 | /// Gets the fore tile bitmap.
90 | ///
91 | public WriteableBitmap ForeTile { get; }
92 |
93 | ///
94 | /// Gets the back tile bitmap.
95 | ///
96 | public WriteableBitmap BackTile { get; }
97 |
98 | ///
99 | /// Gets or sets tile index for the fore tile. Zero clears, otherwise the 1-based index within the tileset.
100 | ///
101 | public int ForeTileIndex
102 | {
103 | get
104 | {
105 | return this.Context.ForeTileIndex;
106 | }
107 |
108 | set
109 | {
110 | this.Context.ForeTileIndex = value;
111 | this.RaisePropertyChanged();
112 | this.UpdateForeTile();
113 | }
114 | }
115 |
116 | ///
117 | /// Gets or sets tile index for the back tile. Zero clears, otherwise the 1-based index within the tileset.
118 | ///
119 | public int BackTileIndex
120 | {
121 | get
122 | {
123 | return this.Context.BackTileIndex;
124 | }
125 |
126 | set
127 | {
128 | this.Context.BackTileIndex = value;
129 | this.RaisePropertyChanged();
130 | this.UpdateBackTile();
131 | }
132 | }
133 |
134 | ///
135 | /// Called when user picked a tile in the given coordinates. Coordinates are in 0-based tile units.
136 | ///
137 | /// The X coordinate in tile units.
138 | /// The Y coordinate in tile units.
139 | internal void OnPickFore(int x, int y) => this.ForeTileIndex = this.tileset.IndexAt(y, x) + 1;
140 |
141 | ///
142 | /// Called when user picked a tile in the given coordinates. Coordinates are in 0-based tile units.
143 | ///
144 | /// The X coordinate in tile units.
145 | /// The Y coordinate in tile units.
146 | internal void OnPickBack(int x, int y) => this.BackTileIndex = this.tileset.IndexAt(y, x) + 1;
147 |
148 | ///
149 | /// Updates the selected fore tile image.
150 | ///
151 | private void UpdateForeTile()
152 | {
153 | var index = this.ForeTileIndex;
154 | if (index <= 0)
155 | {
156 | this.ForeTile.Clear(ClearColor);
157 | }
158 | else
159 | {
160 | this.tileset.BlitTo(index - 1, this.ForeTile, 0, 0);
161 | }
162 | }
163 |
164 | ///
165 | /// Updates the selected back tile image.
166 | ///
167 | private void UpdateBackTile()
168 | {
169 | var index = this.BackTileIndex;
170 | if (index <= 0)
171 | {
172 | this.BackTile.Clear(ClearColor);
173 | }
174 | else
175 | {
176 | this.tileset.BlitTo(index - 1, this.BackTile, 0, 0);
177 | }
178 | }
179 | }
180 | }
181 |
--------------------------------------------------------------------------------
/Editor/TilesetWindow.xaml:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | Fore
41 |
42 |
43 |
44 |
45 |
46 |
47 | Back
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
61 |
62 |
74 |
75 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/Editor/TilesetWindow.xaml.cs:
--------------------------------------------------------------------------------
1 | namespace Editor
2 | {
3 | using System.Windows;
4 | using System.Windows.Input;
5 | using System.Windows.Media.Animation;
6 |
7 | ///
8 | /// Interaction logic for TilesetWindow.xaml.
9 | ///
10 | public partial class TilesetWindow
11 | {
12 | ///
13 | /// The picker dependency property.
14 | ///
15 | public static readonly DependencyProperty PickerProperty = DependencyProperty.Register(
16 | nameof(Picker),
17 | typeof(Rect),
18 | typeof(TilesetWindow),
19 | new PropertyMetadata(default(Rect)));
20 |
21 | ///
22 | /// Initializes a new instance of the class.
23 | ///
24 | public TilesetWindow()
25 | {
26 | this.InitializeComponent();
27 |
28 | this.SizeChanged += this.OnSizeChanged;
29 |
30 | this.DataContext = new TilesetViewModel();
31 | }
32 |
33 | ///
34 | /// Gets or sets the picker.
35 | ///
36 | public Rect Picker
37 | {
38 | get => (Rect)this.GetValue(PickerProperty);
39 | set => this.SetValue(PickerProperty, value);
40 | }
41 |
42 | ///
43 | /// Called when the event is raised.
44 | ///
45 | /// The sender.
46 | /// The instance containing the event data.
47 | private void OnSizeChanged(object sender, SizeChangedEventArgs e)
48 | {
49 | var mainWindow = Application.Current?.MainWindow;
50 | if (mainWindow != null)
51 | {
52 | this.Top = mainWindow.Top;
53 | this.Left = mainWindow.Left + mainWindow.ActualWidth + 10;
54 | }
55 | }
56 |
57 | ///
58 | /// Called when the event is raised.
59 | ///
60 | /// The sender.
61 | /// The instance containing the event data.
62 | private void OnMouseDown(object sender, MouseButtonEventArgs e)
63 | {
64 | if (!(sender is FrameworkElement element))
65 | {
66 | return;
67 | }
68 |
69 | var pos = e.GetPosition(element);
70 | var (x, y) = ((int)pos.X / Configuration.TileSize, (int)pos.Y / Configuration.TileSize);
71 |
72 | if (e.LeftButton == MouseButtonState.Pressed)
73 | {
74 | (this.DataContext as TilesetViewModel)?.OnPickFore(x, y);
75 | }
76 | else if (e.RightButton == MouseButtonState.Pressed)
77 | {
78 | (this.DataContext as TilesetViewModel)?.OnPickBack(x, y);
79 | }
80 |
81 | this.UpdateVisuals(pos);
82 | }
83 |
84 | ///
85 | /// Called when the event is raised.
86 | ///
87 | /// The sender.
88 | /// The instance containing the event data.
89 | private void OnMouseMove(object sender, MouseEventArgs e)
90 | {
91 | if (sender is FrameworkElement element)
92 | {
93 | this.UpdateVisuals(e.GetPosition(element));
94 | }
95 | }
96 |
97 | ///
98 | /// Updates the picker visuals.
99 | ///
100 | /// The relative mouse position.
101 | private void UpdateVisuals(Point relativePosition)
102 | {
103 | var (x, y) = ((int)relativePosition.X / Configuration.TileSize, (int)relativePosition.Y / Configuration.TileSize);
104 |
105 | this.Picker = new Rect(
106 | new Point(x * Configuration.TileSize, y * Configuration.TileSize),
107 | new Size(Configuration.TileSize, Configuration.TileSize));
108 |
109 | if (this.FindResource("PlayAnimation") is Storyboard storyboard)
110 | {
111 | Storyboard.SetTarget(storyboard, this.PickerRectangle);
112 | storyboard.Begin();
113 | }
114 | }
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/Editor/ViewModel.cs:
--------------------------------------------------------------------------------
1 | namespace Editor
2 | {
3 | using System;
4 |
5 | ///
6 | /// Base class for view models.
7 | ///
8 | ///
9 | public class ViewModel : BindingSource
10 | {
11 | ///
12 | /// The singleton context factory.
13 | ///
14 | private static readonly Lazy SharedContext = new Lazy(() => new Context());
15 |
16 | ///
17 | /// Gets the context.
18 | ///
19 | protected Context Context => SharedContext.Value;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Editor/packages.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/Editor/tileset.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zerppa/StreamingTilemap/f07b071087079341125c83bf212137c955cd0de2/Editor/tileset.png
--------------------------------------------------------------------------------
/Engine/Assets/tileset.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zerppa/StreamingTilemap/f07b071087079341125c83bf212137c955cd0de2/Engine/Assets/tileset.png
--------------------------------------------------------------------------------
/Engine/Basic-Fragment.300.glsles:
--------------------------------------------------------------------------------
1 | #version 300 es
2 | precision highp float;
3 |
4 | uniform sampler2D SpriteTexture;
5 |
6 | in vec4 color;
7 | in vec2 texCoord;
8 |
9 | out vec4 outputColor;
10 |
11 | void main()
12 | {
13 | outputColor = color * texture(SpriteTexture, texCoord);
14 | }
15 |
--------------------------------------------------------------------------------
/Engine/Basic-Fragment.330.glsl:
--------------------------------------------------------------------------------
1 | #version 330 core
2 |
3 | uniform sampler2D SpriteSampler;
4 |
5 | in vec4 color;
6 | in vec2 texCoord;
7 |
8 | out vec4 outputColor;
9 |
10 | void main()
11 | {
12 | outputColor = color * texture(SpriteSampler, texCoord);
13 | }
14 |
--------------------------------------------------------------------------------
/Engine/Basic-Fragment.450.glsl:
--------------------------------------------------------------------------------
1 | #version 450
2 |
3 | #extension GL_ARB_separate_shader_objects : enable
4 | #extension GL_ARB_shading_language_420pack : enable
5 |
6 | layout(set = 1, binding = 0) uniform texture2D SpriteTexture;
7 | layout(set = 0, binding = 1) uniform sampler SpriteSampler;
8 |
9 | layout (location = 0) in vec4 color;
10 | layout (location = 1) in vec2 texCoord;
11 | layout (location = 0) out vec4 outputColor;
12 |
13 | void FS()
14 | {
15 | outputColor = color * texture(sampler2D(SpriteTexture, SpriteSampler), texCoord);
16 | }
17 |
--------------------------------------------------------------------------------
/Engine/Basic-Fragment.450.spv:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zerppa/StreamingTilemap/f07b071087079341125c83bf212137c955cd0de2/Engine/Basic-Fragment.450.spv
--------------------------------------------------------------------------------
/Engine/Basic-Fragment.hlsl:
--------------------------------------------------------------------------------
1 | struct PS_INPUT
2 | {
3 | float4 pos : SV_POSITION;
4 | float4 col : COLOR0;
5 | float2 uv : TEXCOORD0;
6 | };
7 |
8 | Texture2D SpriteTexture : register(t0);
9 | sampler SpriteSampler : register(s0);
10 |
11 | float4 FS(PS_INPUT input) : SV_Target
12 | {
13 | float4 out_col = input.col * SpriteTexture.Sample(SpriteSampler, input.uv);
14 | return out_col;
15 | }
--------------------------------------------------------------------------------
/Engine/Basic-Fragment.hlsl.bytes:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zerppa/StreamingTilemap/f07b071087079341125c83bf212137c955cd0de2/Engine/Basic-Fragment.hlsl.bytes
--------------------------------------------------------------------------------
/Engine/Basic-Fragment.metal:
--------------------------------------------------------------------------------
1 | #include
2 | using namespace metal;
3 |
4 | struct PS_INPUT
5 | {
6 | float4 pos [[ position ]];
7 | float4 col;
8 | float2 uv;
9 | };
10 |
11 | fragment float4 FS(
12 | PS_INPUT input [[ stage_in ]],
13 | texture2d SpriteTexture [[ texture(0) ]],
14 | sampler SpriteSampler [[ sampler(0) ]])
15 | {
16 | float4 out_col = input.col * SpriteTexture.sample(SpriteSampler, input.uv);
17 | return out_col;
18 | }
--------------------------------------------------------------------------------
/Engine/Basic-Fragment.metallib:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zerppa/StreamingTilemap/f07b071087079341125c83bf212137c955cd0de2/Engine/Basic-Fragment.metallib
--------------------------------------------------------------------------------
/Engine/Basic-Vertex.300.glsles:
--------------------------------------------------------------------------------
1 | #version 300 es
2 |
3 | uniform Projection
4 | {
5 | mat4 projection_matrix;
6 | };
7 |
8 | in vec2 in_position;
9 | in vec2 in_texCoord;
10 | in vec4 in_color;
11 |
12 | out vec4 color;
13 | out vec2 texCoord;
14 |
15 | void main()
16 | {
17 | gl_Position = projection_matrix * vec4(in_position, 0, 1);
18 | color = in_color;
19 | texCoord = in_texCoord;
20 | }
21 |
--------------------------------------------------------------------------------
/Engine/Basic-Vertex.330.glsl:
--------------------------------------------------------------------------------
1 | #version 330 core
2 |
3 | uniform Projection
4 | {
5 | mat4 projection_matrix;
6 | };
7 |
8 | in vec2 in_position;
9 | in vec2 in_texCoord;
10 | in vec4 in_color;
11 |
12 | out vec4 color;
13 | out vec2 texCoord;
14 |
15 | void main()
16 | {
17 | gl_Position = projection_matrix * vec4(in_position, 0, 1);
18 | color = in_color;
19 | texCoord = in_texCoord;
20 | }
21 |
--------------------------------------------------------------------------------
/Engine/Basic-Vertex.450.glsl:
--------------------------------------------------------------------------------
1 | #version 450
2 |
3 | #extension GL_ARB_separate_shader_objects : enable
4 | #extension GL_ARB_shading_language_420pack : enable
5 |
6 | layout (location = 0) in vec2 vsin_position;
7 | layout (location = 1) in vec2 vsin_texCoord;
8 | layout (location = 2) in vec4 vsin_color;
9 |
10 | layout (binding = 0) uniform Projection
11 | {
12 | mat4 projection;
13 | };
14 |
15 | layout (location = 0) out vec4 vsout_color;
16 | layout (location = 1) out vec2 vsout_texCoord;
17 |
18 | out gl_PerVertex
19 | {
20 | vec4 gl_Position;
21 | };
22 |
23 | void VS()
24 | {
25 | gl_Position = projection * vec4(vsin_position.x, vsin_position.y, 0, 1);
26 | vsout_color = vsin_color;
27 | vsout_texCoord = vsin_texCoord;
28 | gl_Position.y = -gl_Position.y;
29 | }
30 |
--------------------------------------------------------------------------------
/Engine/Basic-Vertex.450.spv:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zerppa/StreamingTilemap/f07b071087079341125c83bf212137c955cd0de2/Engine/Basic-Vertex.450.spv
--------------------------------------------------------------------------------
/Engine/Basic-Vertex.hlsl:
--------------------------------------------------------------------------------
1 | cbuffer ProjectionMatrixBuffer : register(b0)
2 | {
3 | float4x4 ProjectionMatrix;
4 | };
5 |
6 | struct VS_INPUT
7 | {
8 | float2 pos : POSITION;
9 | float2 uv : TEXCOORD0;
10 | float4 col : COLOR0;
11 | };
12 |
13 | struct PS_INPUT
14 | {
15 | float4 pos : SV_POSITION;
16 | float4 col : COLOR0;
17 | float2 uv : TEXCOORD0;
18 | };
19 |
20 | PS_INPUT VS(VS_INPUT input)
21 | {
22 | PS_INPUT output;
23 | output.pos = mul(ProjectionMatrix, float4(input.pos.xy, 0.f, 1.f));
24 | output.col = input.col;
25 | output.uv = input.uv;
26 | return output;
27 | }
--------------------------------------------------------------------------------
/Engine/Basic-Vertex.hlsl.bytes:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zerppa/StreamingTilemap/f07b071087079341125c83bf212137c955cd0de2/Engine/Basic-Vertex.hlsl.bytes
--------------------------------------------------------------------------------
/Engine/Basic-Vertex.metal:
--------------------------------------------------------------------------------
1 | #include
2 | using namespace metal;
3 |
4 | struct VS_INPUT
5 | {
6 | float2 pos [[ attribute(0) ]];
7 | float2 uv [[ attribute(1) ]];
8 | float4 col [[ attribute(2) ]];
9 | };
10 |
11 | struct PS_INPUT
12 | {
13 | float4 pos [[ position ]];
14 | float4 col;
15 | float2 uv;
16 | };
17 |
18 | vertex PS_INPUT VS(
19 | VS_INPUT input [[ stage_in ]],
20 | constant float4x4 &ProjectionMatrix [[ buffer(1) ]])
21 | {
22 | PS_INPUT output;
23 | output.pos = ProjectionMatrix * float4(input.pos.xy, 0.f, 1.f);
24 | output.col = input.col;
25 | output.uv = input.uv;
26 | return output;
27 | }
--------------------------------------------------------------------------------
/Engine/Basic-Vertex.metallib:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zerppa/StreamingTilemap/f07b071087079341125c83bf212137c955cd0de2/Engine/Basic-Vertex.metallib
--------------------------------------------------------------------------------
/Engine/Chunk.cs:
--------------------------------------------------------------------------------
1 | namespace Engine
2 | {
3 | using System;
4 |
5 | ///
6 | /// Represents a chunk (tilemaps are made out of chunks).
7 | ///
8 | public class Chunk
9 | {
10 | #pragma warning disable SA1401 // Fields should be private
11 | ///
12 | /// The flag indicating whether this chunk is currently active (and not in the pool).
13 | ///
14 | internal volatile bool IsInUse;
15 |
16 | ///
17 | /// The lock for multi-threaded access.
18 | ///
19 | private readonly object syncRoot = new ();
20 |
21 | ///
22 | /// Points to the currently used tile buffer.
23 | ///
24 | private ushort[] currentBuffer;
25 |
26 | ///
27 | /// Points to the currently used tile buffer.
28 | ///
29 | private ushort[] backBuffer;
30 |
31 | ///
32 | /// Stores a flag indicating whether the tile buffer has been rewritten.
33 | ///
34 | private volatile bool hasPendingChanges;
35 | #pragma warning restore SA1401 // Fields should be private
36 |
37 | ///
38 | /// Initializes a new instance of the class.
39 | ///
40 | public Chunk()
41 | {
42 | this.currentBuffer = new ushort[Configuration.ChunkWidth * Configuration.ChunkHeight];
43 | this.backBuffer = new ushort[Configuration.ChunkWidth * Configuration.ChunkHeight];
44 | }
45 |
46 | ///
47 | /// Gets or sets the X coordinate, in chunk units.
48 | ///
49 | public int X { get; set; }
50 |
51 | ///
52 | /// Gets or sets the Y coordinate, in chunk units.
53 | ///
54 | public int Y { get; set; }
55 |
56 | ///
57 | /// Gets the tile array.
58 | ///
59 | ///
60 | /// Although chunks are 2-dimensional, the tiles are stored in a 1-dimensional array for performance reasons.
61 | /// Calculate the tile index manually using and .
62 | /// >
63 | public ushort[] Tiles => this.currentBuffer;
64 |
65 | ///
66 | /// Resets all properties.
67 | ///
68 | public void Clear()
69 | {
70 | lock (this.syncRoot)
71 | {
72 | this.X = 0;
73 | this.Y = 0;
74 | Array.Clear(this.currentBuffer, 0, this.Tiles.Length);
75 | Array.Clear(this.backBuffer, 0, this.Tiles.Length);
76 | this.hasPendingChanges = false;
77 | }
78 | }
79 |
80 | ///
81 | /// Flips the tile buffers if there are pending changes.
82 | ///
83 | internal void ApplyPendingChanges()
84 | {
85 | if (!this.IsInUse)
86 | {
87 | return;
88 | }
89 |
90 | lock (this.syncRoot)
91 | {
92 | if (this.hasPendingChanges)
93 | {
94 | (this.backBuffer, this.currentBuffer) = (this.currentBuffer, this.backBuffer);
95 | this.hasPendingChanges = false;
96 | }
97 | }
98 | }
99 |
100 | ///
101 | /// Updates the chunk based on the supplied information.
102 | /// The tile data in is copied to the chunk's internal array.
103 | /// The array size should be exactly ChunkWidth * ChunkHeight.
104 | ///
105 | /// The chunk X coordinate, in chunk units.
106 | /// The chunk Y coordinate, in chunk units.
107 | /// The data.
108 | /// The cannot be null.
109 | internal void SetData(int x, int y, ushort[] data)
110 | {
111 | if (!this.IsInUse || x != this.X || y != this.Y)
112 | {
113 | return;
114 | }
115 |
116 | lock (this.syncRoot)
117 | {
118 | Array.Copy(
119 | data ?? throw new ArgumentNullException(nameof(data), $"The {nameof(data)} cannot be null."),
120 | this.backBuffer,
121 | data.Length);
122 |
123 | this.hasPendingChanges = true;
124 | }
125 | }
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/Engine/ChunkManager.cs:
--------------------------------------------------------------------------------
1 | namespace Engine
2 | {
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Diagnostics;
6 | using System.Linq;
7 |
8 | using static Utility;
9 |
10 | ///
11 | /// Manages the set of active chunks around the camera.
12 | ///
13 | ///
14 | public sealed class ChunkManager : IDisposable
15 | {
16 | ///
17 | /// Represents the empty default tile data for an unloaded chunk.
18 | ///
19 | private static readonly ushort[] EmptyChunkData = new ushort[Configuration.ChunkWidth * Configuration.ChunkHeight];
20 |
21 | ///
22 | /// Stores the internal chunk pool.
23 | ///
24 | private readonly Pool chunkPool;
25 |
26 | ///
27 | /// Stores the list of currently managed active chunks.
28 | ///
29 | private readonly Dictionary<(int x, int y), Chunk> chunkCache;
30 |
31 | ///
32 | /// The temporary list of chunks that have fallen off the managed area and are subject of being recycled.
33 | ///
34 | private readonly List> doomedChunks = new ();
35 |
36 | ///
37 | /// Contains the pre-calculated ordered list of offsets that can be applied to chunk coordinates
38 | /// in order to create a spiral outwards of it until it exceeds the view range from the origin.
39 | ///
40 | private readonly IReadOnlyList<(int dx, int dy, int sd)> preCalculatedNearbyChunkOffsets;
41 |
42 | ///
43 | /// Stores the internal chunk processor.
44 | ///
45 | private readonly ChunkProcessor chunkProcessor;
46 |
47 | ///
48 | /// Initializes a new instance of the class.
49 | ///
50 | /// The content manager.
51 | public ChunkManager(ContentManager contentManager)
52 | {
53 | this.chunkProcessor = new ChunkProcessor(contentManager);
54 |
55 | // Create the pre-calculated surrounding chunk offsets based on range
56 | var chunkOffsets = new List<(int ox, int oy, int squaredDistance)>((int)Math.Ceiling(Math.PI * (Configuration.ChunkViewRange * Configuration.ChunkViewRange)));
57 | const int squaredMaxDistance = Configuration.ChunkViewRange * Configuration.ChunkViewRange;
58 | for (var y = -Configuration.ChunkViewRange; y <= Configuration.ChunkViewRange; y++)
59 | {
60 | for (var x = -Configuration.ChunkViewRange; x <= Configuration.ChunkViewRange; x++)
61 | {
62 | var dx = Math.Abs(x);
63 | var dy = Math.Abs(y);
64 | var squaredDistance = dx * dx + dy * dy;
65 | if (squaredDistance <= squaredMaxDistance)
66 | {
67 | chunkOffsets.Add((x, y, squaredDistance));
68 | }
69 | }
70 | }
71 |
72 | this.preCalculatedNearbyChunkOffsets = chunkOffsets.OrderBy(tuple => tuple.squaredDistance).ToList();
73 |
74 | // Create the chunk pool and cache
75 | var managedChunkCount = (int)Math.Ceiling(Math.PI * (Configuration.ChunkViewFalloffRange * Configuration.ChunkViewFalloffRange));
76 | this.chunkPool = new Pool(managedChunkCount);
77 | this.chunkCache = new Dictionary<(int x, int y), Chunk>(managedChunkCount);
78 | }
79 |
80 | ///
81 | /// Sets the focused chunk where the camera is centered at and updates the managed chunk list accordingly.
82 | /// Chunks that are too far off from the focused chunk will be recycled.
83 | /// New chunks are then asynchronously loaded surrounding the focused chunk.
84 | ///
85 | /// The focus chunk X coordinate, in chunk units.
86 | /// The focus chunk Y coordinate, in chunk units.
87 | public void SetCameraAt(int chunkX, int chunkY)
88 | {
89 | // Inform the chunk loader about the current camera position so that it can discard obsolete jobs
90 | this.chunkProcessor.SetOrigin((chunkX, chunkY));
91 |
92 | // Recycle the chunks that are too far away from the camera
93 | const int squaredFalloffDistance = Configuration.ChunkViewFalloffRange * Configuration.ChunkViewFalloffRange;
94 | foreach (var entry in this.chunkCache)
95 | {
96 | var chunk = entry.Value;
97 | if (SquaredDistance((chunkX, chunkY), (chunk.X, chunk.Y)) > squaredFalloffDistance)
98 | {
99 | this.doomedChunks.Add(entry);
100 | }
101 | }
102 |
103 | foreach (var (key, chunk) in this.doomedChunks)
104 | {
105 | var removed = this.chunkCache.Remove(key);
106 | Debug.Assert(removed, "The chunk was not found in the cache. Removal failed.");
107 |
108 | this.chunkPool.Return(chunk);
109 | chunk.Clear();
110 | chunk.IsInUse = false;
111 | }
112 |
113 | this.doomedChunks.Clear();
114 |
115 | // Load chunks surrounding the camera
116 | foreach (var requiredChunk in this.GetChunksAround(chunkX, chunkY))
117 | {
118 | var position = (requiredChunk.x, requiredChunk.y);
119 | if (!this.chunkCache.TryGetValue(position, out _))
120 | {
121 | if (this.chunkPool.CanWithdraw)
122 | {
123 | var chunk = this.chunkPool.Withdraw();
124 | chunk.X = position.x;
125 | chunk.Y = position.y;
126 | chunk.IsInUse = true;
127 | this.chunkCache.Add(position, chunk);
128 | this.chunkProcessor.Enqueue(position, chunk);
129 | }
130 | else
131 | {
132 | throw new InvalidOperationException("The chunk pool is exhausted.");
133 | }
134 | }
135 | }
136 | }
137 |
138 | ///
139 | /// Applies all pending chunk changes.
140 | ///
141 | /// This method should be called from the main thread.
142 | public void ApplyAllPendingChanges()
143 | {
144 | foreach (var chunk in this.chunkCache.Values)
145 | {
146 | chunk.ApplyPendingChanges();
147 | }
148 | }
149 |
150 | ///
151 | /// Requests the tile data for the specific chunk. If no data is available at those coordinates, an empty array (of suitable size) is returned.
152 | ///
153 | /// The chunk X coordinate, in chunk units.
154 | /// The chunk Y coordinate, in chunk units.
155 | /// The array containing tile data for the chunk.
156 | ///
157 | /// The empty default array is cached, and will be the same instance every time.
158 | /// The array is mutable, but you should never do that.
159 | ///
160 | public ushort[] GetChunkData(int chunkX, int chunkY) => this.chunkCache.TryGetValue((chunkX, chunkY), out var chunk) ? chunk.Tiles : EmptyChunkData;
161 |
162 | ///
163 | /// Determines whether tile data is available for the specified chunk.
164 | ///
165 | /// The chunk X coordinate, in chunk units.
166 | /// The chunk Y coordinate, in chunk units.
167 | ///
168 | /// true if the chunk is currently being managed; otherwise, false.
169 | ///
170 | public bool IsChunkDataAvailable(int chunkX, int chunkY) => this.chunkCache.ContainsKey((chunkX, chunkY));
171 |
172 | ///
173 | public void Dispose()
174 | {
175 | this.chunkProcessor?.Dispose();
176 | }
177 |
178 | ///
179 | /// Gets the chunks surrounding and including the center chunk.
180 | /// The returned collection is sorted from closest to farthest.
181 | ///
182 | /// The center chunk X coordinate, in chunk units.
183 | /// The center chunk Y coordinate, in chunk units.
184 | /// Collection of chunk coordinates.
185 | private IEnumerable<(int x, int y)> GetChunksAround(int centerChunkX, int centerChunkY)
186 | {
187 | foreach (var offset in this.preCalculatedNearbyChunkOffsets)
188 | {
189 | yield return (centerChunkX + offset.dx, centerChunkY + offset.dy);
190 | }
191 | }
192 | }
193 | }
194 |
--------------------------------------------------------------------------------
/Engine/ChunkProcessor.cs:
--------------------------------------------------------------------------------
1 | namespace Engine
2 | {
3 | using System;
4 | using System.Collections.Concurrent;
5 | using System.Threading.Tasks;
6 |
7 | using static Utility;
8 |
9 | ///
10 | /// Provides asynchronous chunk loading services.
11 | ///
12 | ///
13 | public sealed class ChunkProcessor : IDisposable
14 | {
15 | ///
16 | /// Stores the content manager.
17 | ///
18 | private readonly ContentManager contentManager;
19 |
20 | ///
21 | /// The producer-consumer implementation.
22 | ///
23 | private readonly BlockingCollection<(int, int, Chunk)> jobProcessor = new (100);
24 |
25 | ///
26 | /// Keeps track of the currently queued chunks.
27 | ///
28 | private readonly ConcurrentDictionary<(int x, int y), Chunk> queuedJobs = new ();
29 |
30 | ///
31 | /// The origin around which chunks are managed. Loading jobs that are too far away from the origin will be skipped.
32 | ///
33 | private (int x, int y) origin;
34 |
35 | ///
36 | /// Initializes a new instance of the class.
37 | ///
38 | /// The content manager.
39 | /// The cannot be null.
40 | public ChunkProcessor(ContentManager contentManager)
41 | {
42 | this.contentManager = contentManager ?? throw new ArgumentNullException(nameof(contentManager), $"The {nameof(contentManager)} cannot be null.");
43 |
44 | Task.Run(this.Loop);
45 | }
46 |
47 | ///
48 | /// Places an order for the certain chunk's data to be loaded and populated.
49 | ///
50 | /// The chunk's position, in chunk units.
51 | /// The target object, whom the loaded data is pushed to.
52 | public void Enqueue((int x, int y) position, Chunk chunk)
53 | {
54 | if (this.queuedJobs.ContainsKey(position))
55 | {
56 | // Already queued -> skip
57 | return;
58 | }
59 |
60 | if (this.queuedJobs.TryAdd(position, chunk))
61 | {
62 | this.jobProcessor.Add((position.x, position.y, chunk));
63 | }
64 | }
65 |
66 | ///
67 | /// Sets the origin.
68 | ///
69 | /// The position.
70 | ///
71 | /// Loading jobs that are too far away from the origin will be skipped.
72 | ///
73 | public void SetOrigin((int chunkX, int chunkY) position) => this.origin = position;
74 |
75 | ///
76 | public void Dispose()
77 | {
78 | this.jobProcessor.CompleteAdding();
79 | this.jobProcessor.Dispose();
80 | }
81 |
82 | ///
83 | /// Processes all the incoming loading requests.
84 | ///
85 | /// The that persists until the application closes.
86 | private async Task Loop()
87 | {
88 | const int squaredFalloffDistance = Configuration.ChunkViewFalloffRange * Configuration.ChunkViewFalloffRange;
89 |
90 | var staging = new ushort[Configuration.ChunkWidth * Configuration.ChunkHeight];
91 |
92 | while (!this.jobProcessor.IsCompleted)
93 | {
94 | (int x, int y, Chunk chunk) data;
95 | try
96 | {
97 | data = this.jobProcessor.Take();
98 | }
99 | catch (InvalidOperationException)
100 | {
101 | break;
102 | }
103 |
104 | var position = (data.x, data.y);
105 |
106 | // If the job is obsolete (the chunk to be loaded is too far from the camera), just skip it
107 | if (SquaredDistance(position, this.origin) > squaredFalloffDistance)
108 | {
109 | this.queuedJobs.TryRemove(position, out _);
110 | continue;
111 | }
112 |
113 | await this.contentManager.LoadChunkData(position, staging).ConfigureAwait(false);
114 |
115 | data.chunk.SetData(data.x, data.y, staging);
116 | this.queuedJobs.TryRemove(position, out _);
117 |
118 | Array.Clear(staging, 0, staging.Length);
119 | }
120 |
121 | this.jobProcessor.Dispose();
122 | }
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/Engine/Configuration.cs:
--------------------------------------------------------------------------------
1 | namespace Engine
2 | {
3 | ///
4 | /// Global settings for the chunk engine.
5 | ///
6 | public static class Configuration
7 | {
8 | ///
9 | /// The tile width and height, in pixels.
10 | ///
11 | public const int TileSize = 32;
12 |
13 | ///
14 | /// The chunk width, in tile units.
15 | ///
16 | public const int ChunkWidth = 9;
17 |
18 | ///
19 | /// The chunk height, in tile units.
20 | ///
21 | public const int ChunkHeight = 9;
22 |
23 | ///
24 | /// The distance how far ahead the chunks get preloaded in relation to the current chunk (where the character or camera is located).
25 | /// Lower values mean more aggressive loading. Higher values pre-load further in advance, but more at a time.
26 | ///
27 | public const int ChunkViewRange = 2;
28 |
29 | ///
30 | /// The distance how far off a chunk can get until it gets recycled.
31 | ///
32 | public const int ChunkViewFalloffRange = ChunkViewRange + 2;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Engine/ContentManager.cs:
--------------------------------------------------------------------------------
1 | namespace Engine
2 | {
3 | using System;
4 | using System.Diagnostics;
5 | using System.IO;
6 | using System.Runtime.InteropServices;
7 | using System.Threading.Tasks;
8 |
9 | using Veldrid;
10 | using Veldrid.ImageSharp;
11 |
12 | ///
13 | /// Provides access to the game assets.
14 | ///
15 | public class ContentManager
16 | {
17 | ///
18 | /// Stores the associated .
19 | ///
20 | private readonly Game game;
21 |
22 | ///
23 | /// Initializes a new instance of the class.
24 | ///
25 | /// The game.
26 | /// The cannot be null.
27 | public ContentManager(Game game)
28 | {
29 | this.game = game ?? throw new ArgumentNullException(nameof(game), $"The {nameof(game)} cannot be null.");
30 | }
31 |
32 | ///
33 | /// Loads the specified map chunk.
34 | ///
35 | /// The position, in chunk units.
36 | /// The target array to which the data will be written.
37 | /// The .
38 | /// The cannot be null.
39 | public async Task LoadChunkData((int x, int y) position, ushort[] fillTo)
40 | {
41 | if (fillTo == null)
42 | {
43 | throw new ArgumentNullException(nameof(fillTo), $"The {nameof(fillTo)} cannot be null.");
44 | }
45 |
46 | // Simulate slight network latency
47 | await Task.Delay(100).ConfigureAwait(false);
48 |
49 | var chunkFile = FilenameForChunk(position.x, position.y);
50 | if (!File.Exists(chunkFile))
51 | {
52 | Array.Clear(fillTo, 0, fillTo.Length);
53 | return;
54 | }
55 |
56 | try
57 | {
58 | await using (var stream = File.OpenRead(chunkFile))
59 | {
60 | using (var reader = new BinaryReader(stream))
61 | {
62 | for (var i = 0; i < Configuration.ChunkWidth * Configuration.ChunkHeight; i++)
63 | {
64 | var value = reader.ReadUInt16();
65 | fillTo[i] = value;
66 | }
67 | }
68 | }
69 | }
70 | catch (Exception e)
71 | {
72 | Trace.TraceError($"Cannot load chunk data ({position.x}, {position.y}) from disk. {e}");
73 | Array.Clear(fillTo, 0, fillTo.Length);
74 | }
75 | }
76 |
77 | ///
78 | /// Loads the shader.
79 | ///
80 | /// The resource factory.
81 | /// The shader name.
82 | /// Either 'Vertex' (for vertex shader), or 'Fragment' (for fragment/pixel shader).
83 | /// The entry point.
84 | /// The .
85 | /// Supports HLSL, GLSL, Metal and Vulcan shaders.
86 | public Shader LoadShader(ResourceFactory factory, string set, ShaderStages stage, string entryPoint)
87 | {
88 | var name = $"{nameof(Engine)}.{set}-{stage}.{GetShaderExtension(factory.BackendType)}";
89 | return factory.CreateShader(new ShaderDescription(stage, this.game.ReadEmbeddedAssetBytes(name), entryPoint));
90 | }
91 |
92 | ///
93 | /// Loads the texture.
94 | ///
95 | /// The resource factory.
96 | /// The filename.
97 | /// The .
98 | public (Texture, TextureView) LoadTexture(ResourceFactory factory, string filename)
99 | {
100 | using (var stream = File.OpenRead(filename))
101 | {
102 | return this.LoadTexture(factory, stream);
103 | }
104 | }
105 |
106 | ///
107 | /// Loads the texture.
108 | ///
109 | /// The resource factory.
110 | /// The stream.
111 | /// The .
112 | public (Texture, TextureView) LoadTexture(ResourceFactory factory, Stream stream)
113 | {
114 | var image = new ImageSharpTexture(stream, false);
115 | var texture = image.CreateDeviceTexture(this.game.GraphicsDevice, factory);
116 | var textureView = factory.CreateTextureView(texture);
117 |
118 | return (texture, textureView);
119 | }
120 |
121 | ///
122 | /// Gets the shader file extension based on the graphics backend.
123 | ///
124 | /// The backend type.
125 | /// The file extension.
126 | /// Unknown graphics backend.
127 | private static string GetShaderExtension(GraphicsBackend backendType)
128 | {
129 | var isMacOS = RuntimeInformation.OSDescription.Contains("Darwin", StringComparison.InvariantCultureIgnoreCase);
130 |
131 | return backendType switch
132 | {
133 | GraphicsBackend.Direct3D11 => "hlsl.bytes",
134 | GraphicsBackend.Vulkan => "450.spv",
135 | GraphicsBackend.OpenGL => "330.glsl",
136 | GraphicsBackend.Metal => isMacOS ? "metallib" : "ios.metallib",
137 | GraphicsBackend.OpenGLES => "300.glsles",
138 | _ => throw new ArgumentOutOfRangeException(nameof(backendType), backendType, null)
139 | };
140 | }
141 |
142 | ///
143 | /// Determines a filename for the given chunk coordinates.
144 | ///
145 | /// The chunk X coordinate.
146 | /// The chunk Y coordinate.
147 | /// The filename.
148 | private static string FilenameForChunk(int x, int y) => $"Maps/Overworld/{x},{y}.chunk";
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/Engine/Engine.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net6.0
6 | .\Engine.ruleset
7 | latest
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | Always
60 |
61 |
62 | Always
63 |
64 |
65 | Always
66 |
67 |
68 | Always
69 |
70 |
71 | Always
72 |
73 |
74 | Always
75 |
76 |
77 | Always
78 |
79 |
80 | Always
81 |
82 |
83 | Always
84 |
85 |
86 | Always
87 |
88 |
89 | Always
90 |
91 |
92 | Always
93 |
94 |
95 | Always
96 |
97 |
98 | Always
99 |
100 |
101 | Always
102 |
103 |
104 | Always
105 |
106 |
107 | Always
108 |
109 |
110 | Always
111 |
112 |
113 | Always
114 |
115 |
116 | Always
117 |
118 |
119 | Always
120 |
121 |
122 | Always
123 |
124 |
125 | Always
126 |
127 |
128 | Always
129 |
130 |
131 | Always
132 |
133 |
134 | Always
135 |
136 |
137 | Always
138 |
139 |
140 | Always
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 | all
166 | runtime; build; native; contentfiles; analyzers
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
--------------------------------------------------------------------------------
/Engine/Engine.ruleset:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/Engine/Game.cs:
--------------------------------------------------------------------------------
1 | namespace Engine
2 | {
3 | using System;
4 | using System.IO;
5 |
6 | using Veldrid;
7 |
8 | ///
9 | /// The game base class.
10 | ///
11 | public abstract class Game
12 | {
13 | ///
14 | /// Initializes a new instance of the class.
15 | ///
16 | /// The window.
17 | /// The cannot be null.
18 | protected Game(IGameWindow window)
19 | {
20 | this.Window = window ?? throw new ArgumentNullException(nameof(window), $"The {nameof(window)} cannot be null.");
21 |
22 | this.Window.Resized += () => this.OnWindowResized(this.Window.Width, this.Window.Height);
23 | this.Window.GraphicsDeviceCreated += this.OnGraphicsDeviceCreated;
24 | this.Window.GraphicsDeviceDestroyed += this.OnDeviceDestroyed;
25 | this.Window.Rendering += this.Update;
26 | this.Window.Rendering += this.Draw;
27 | this.Window.KeyPressed += this.OnKeyDown;
28 | }
29 |
30 | ///
31 | /// Gets the window.
32 | ///
33 | public IGameWindow Window { get; }
34 |
35 | ///
36 | /// Gets the graphics device.
37 | ///
38 | public GraphicsDevice GraphicsDevice { get; private set; }
39 |
40 | ///
41 | /// Gets the resource factory.
42 | ///
43 | public ResourceFactory ResourceFactory { get; private set; }
44 |
45 | ///
46 | /// Gets the swapchain.
47 | ///
48 | public Swapchain Swapchain { get; private set; }
49 |
50 | ///
51 | /// Opens the embedded asset stream.
52 | ///
53 | /// The resource name.
54 | /// The .
55 | public Stream OpenEmbeddedAssetStream(string name) => this.GetType().Assembly.GetManifestResourceStream(name);
56 |
57 | ///
58 | /// Reads the embedded asset into a byte array.
59 | ///
60 | /// The resource name.
61 | /// The byte array.
62 | public byte[] ReadEmbeddedAssetBytes(string name)
63 | {
64 | using (var stream = this.OpenEmbeddedAssetStream(name))
65 | {
66 | var bytes = new byte[stream.Length];
67 | using (var memory = new MemoryStream(bytes))
68 | {
69 | stream.CopyTo(memory);
70 | return bytes;
71 | }
72 | }
73 | }
74 |
75 | ///
76 | /// Loads the game resources.
77 | ///
78 | /// The resource factory.
79 | protected abstract void CreateResources(ResourceFactory factory);
80 |
81 | ///
82 | /// Frees all allocated resources.
83 | ///
84 | protected abstract void FreeResources();
85 |
86 | ///
87 | /// Updates the game state before rendering.
88 | ///
89 | /// The elapsed seconds since the last frame.
90 | protected virtual void Update(float deltaSeconds)
91 | {
92 | // Override in the derived class.
93 | }
94 |
95 | ///
96 | /// Renders the game.
97 | ///
98 | /// The elapsed seconds since the last frame.
99 | protected abstract void Draw(float deltaSeconds);
100 |
101 | ///
102 | /// Called when the window has been resized.
103 | ///
104 | /// The new pixel width.
105 | /// The new pixel height.
106 | protected virtual void OnWindowResized(uint width, uint height)
107 | {
108 | // Override in a derived class.
109 | }
110 |
111 | ///
112 | /// Called when a key has been pressed down.
113 | ///
114 | /// The key state info.
115 | protected virtual void OnKeyDown(KeyEvent state)
116 | {
117 | // Override in a derived class.
118 | }
119 |
120 | ///
121 | /// Closes the application.
122 | ///
123 | protected void Exit()
124 | {
125 | this.Window?.Close();
126 | }
127 |
128 | ///
129 | /// Called when the graphics device has been created.
130 | ///
131 | /// The graphics device.
132 | /// The resource factory.
133 | /// The swapchain.
134 | private void OnGraphicsDeviceCreated(GraphicsDevice device, ResourceFactory factory, Swapchain swapchain)
135 | {
136 | this.GraphicsDevice = device;
137 | this.ResourceFactory = factory;
138 | this.Swapchain = swapchain;
139 | this.CreateResources(factory);
140 | }
141 |
142 | ///
143 | /// Called when the device has been destroyed.
144 | ///
145 | private void OnDeviceDestroyed()
146 | {
147 | this.FreeResources();
148 | this.GraphicsDevice = null;
149 | this.ResourceFactory = null;
150 | this.Swapchain = null;
151 | }
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/Engine/GameWindow.cs:
--------------------------------------------------------------------------------
1 | namespace Engine
2 | {
3 | using System;
4 | using System.Diagnostics;
5 |
6 | using Veldrid;
7 | using Veldrid.Sdl2;
8 | using Veldrid.StartupUtilities;
9 | using Veldrid.Utilities;
10 |
11 | ///
12 | /// The game window.
13 | ///
14 | ///
15 | public class GameWindow : IGameWindow
16 | {
17 | ///
18 | /// The SDL window.
19 | ///
20 | private readonly Sdl2Window window;
21 |
22 | ///
23 | /// The graphics device.
24 | ///
25 | private GraphicsDevice graphicsDevice;
26 |
27 | ///
28 | /// The resource factory.
29 | ///
30 | private DisposeCollectorResourceFactory resourceFactory;
31 |
32 | ///
33 | /// Temporarily stores a flag indicating whether the window has just been resized.
34 | ///
35 | private bool windowResized = true;
36 |
37 | ///
38 | /// Initializes a new instance of the class.
39 | ///
40 | /// The title.
41 | public GameWindow(string title)
42 | {
43 | var windowCreateInfo = new WindowCreateInfo
44 | {
45 | X = Sdl2Native.SDL_WINDOWPOS_CENTERED,
46 | Y = Sdl2Native.SDL_WINDOWPOS_CENTERED,
47 | WindowWidth = Configuration.ChunkWidth * Configuration.TileSize,
48 | WindowHeight = Configuration.ChunkHeight * Configuration.TileSize,
49 | WindowTitle = title,
50 | };
51 |
52 | this.window = VeldridStartup.CreateWindow(ref windowCreateInfo);
53 | this.window.Resizable = false;
54 | this.window.Resized += () => { this.windowResized = true; };
55 | this.window.KeyDown += this.OnKeyDown;
56 | }
57 |
58 | ///
59 | public event Action Rendering;
60 |
61 | ///
62 | public event Action GraphicsDeviceCreated;
63 |
64 | ///
65 | public event Action GraphicsDeviceDestroyed;
66 |
67 | ///
68 | public event Action Resized;
69 |
70 | ///
71 | public event Action KeyPressed;
72 |
73 | ///
74 | public uint Width => (uint)this.window.Width;
75 |
76 | ///
77 | public uint Height => (uint)this.window.Height;
78 |
79 | ///
80 | public string Title
81 | {
82 | get => this.window.Title;
83 | set => this.window.Title = value;
84 | }
85 |
86 | ///
87 | public void Close() => this.window.Close();
88 |
89 | ///
90 | public void Run(GraphicsBackend graphicsAPI = GraphicsBackend.Direct3D11)
91 | {
92 | var options = new GraphicsDeviceOptions(
93 | debug: false,
94 | swapchainDepthFormat: null,
95 | syncToVerticalBlank: true,
96 | resourceBindingModel: ResourceBindingModel.Improved,
97 | preferDepthRangeZeroToOne: false,
98 | preferStandardClipSpaceYDirection: false);
99 | #if DEBUG
100 | options.Debug = true;
101 | #endif
102 | this.graphicsDevice = VeldridStartup.CreateGraphicsDevice(this.window, options, graphicsAPI);
103 | this.resourceFactory = new DisposeCollectorResourceFactory(this.graphicsDevice.ResourceFactory);
104 | this.GraphicsDeviceCreated?.Invoke(this.graphicsDevice, this.resourceFactory, this.graphicsDevice.MainSwapchain);
105 |
106 | var timer = Stopwatch.StartNew();
107 | var previousElapsed = timer.Elapsed.TotalSeconds;
108 |
109 | while (this.window.Exists)
110 | {
111 | var newElapsed = timer.Elapsed.TotalSeconds;
112 | var deltaSeconds = (float)(newElapsed - previousElapsed);
113 |
114 | var inputSnapshot = this.window.PumpEvents();
115 | InputTracker.UpdateFrameInput(inputSnapshot);
116 |
117 | if (this.window.Exists)
118 | {
119 | previousElapsed = newElapsed;
120 | if (this.windowResized)
121 | {
122 | this.windowResized = false;
123 |
124 | this.graphicsDevice.ResizeMainWindow((uint)this.window.Width, (uint)this.window.Height);
125 | this.Resized?.Invoke();
126 | }
127 |
128 | this.Rendering?.Invoke(deltaSeconds);
129 | }
130 | }
131 |
132 | this.graphicsDevice.WaitForIdle();
133 | this.resourceFactory.DisposeCollector.DisposeAll();
134 | this.graphicsDevice.Dispose();
135 | this.GraphicsDeviceDestroyed?.Invoke();
136 | }
137 |
138 | ///
139 | /// Called when a key has been pressed.
140 | ///
141 | /// The key event.
142 | protected void OnKeyDown(KeyEvent keyEvent)
143 | {
144 | this.KeyPressed?.Invoke(keyEvent);
145 | }
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/Engine/GlobalSuppressions.cs:
--------------------------------------------------------------------------------
1 | [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1819:Properties should not return arrays", Justification = "Reviewed. Suppression is OK here.", Scope = "member", Target = "~P:Engine.Chunk.Tiles")]
--------------------------------------------------------------------------------
/Engine/IGameWindow.cs:
--------------------------------------------------------------------------------
1 | namespace Engine
2 | {
3 | using System;
4 |
5 | using Veldrid;
6 |
7 | ///
8 | /// The game window.
9 | ///
10 | public interface IGameWindow
11 | {
12 | ///
13 | /// Occurs when the game is rendering.
14 | ///
15 | event Action Rendering;
16 |
17 | ///
18 | /// Occurs when the graphics device has been created.
19 | ///
20 | event Action GraphicsDeviceCreated;
21 |
22 | ///
23 | /// Occurs when the graphics device has been destroyed.
24 | ///
25 | event Action GraphicsDeviceDestroyed;
26 |
27 | ///
28 | /// Occurs when the game window has been resized (and the buffers might need resized as well).
29 | ///
30 | event Action Resized;
31 |
32 | ///
33 | /// Occurs when a key has been pressed.
34 | ///
35 | event Action KeyPressed;
36 |
37 | ///
38 | /// Gets the window width, in pixels.
39 | ///
40 | uint Width { get; }
41 |
42 | ///
43 | /// Gets the window height, in pixels.
44 | ///
45 | uint Height { get; }
46 |
47 | ///
48 | /// Gets or sets the window title.
49 | ///
50 | string Title { get; set; }
51 |
52 | ///
53 | /// Closes the window.
54 | ///
55 | void Close();
56 |
57 | ///
58 | /// Runs the game.
59 | ///
60 | /// The graphics API.
61 | void Run(GraphicsBackend graphicsAPI);
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Engine/InputTracker.cs:
--------------------------------------------------------------------------------
1 | namespace Engine
2 | {
3 | using System.Collections.Generic;
4 | using System.Numerics;
5 |
6 | using Veldrid;
7 |
8 | ///
9 | /// Provides access to the latest snapshot of keyboard and mouse state.
10 | ///
11 | public static class InputTracker
12 | {
13 | #pragma warning disable CA2211 // Non-constant fields should not be visible
14 | #pragma warning disable SA1401 // Fields should be private
15 | ///
16 | /// The current mouse position.
17 | ///
18 | public static Vector2 MousePosition;
19 | #pragma warning restore SA1401 // Fields should be private
20 | #pragma warning restore CA2211 // Non-constant fields should not be visible
21 |
22 | ///
23 | /// Keeps track of the pressed keys.
24 | ///
25 | private static readonly HashSet CurrentlyPressedKeys = new ();
26 |
27 | ///
28 | /// Lists the key state changes in the most recent update.
29 | ///
30 | private static readonly HashSet NewKeysThisFrame = new ();
31 |
32 | ///
33 | /// Keeps track of the pressed mouse buttons.
34 | ///
35 | private static readonly HashSet CurrentlyPressedMouseButtons = new ();
36 |
37 | ///
38 | /// Lists the mouse button state changes in the most recent update.
39 | ///
40 | private static readonly HashSet NewMouseButtonsThisFrame = new ();
41 |
42 | ///
43 | /// Gets the most recent snapshot.
44 | ///
45 | public static InputSnapshot FrameSnapshot { get; private set; }
46 |
47 | ///
48 | /// Gets the key state.
49 | ///
50 | /// The key.
51 | /// true if the key is currently pressed.
52 | public static bool GetKey(Key key) => CurrentlyPressedKeys.Contains(key);
53 |
54 | ///
55 | /// Gets the key state.
56 | ///
57 | /// The key.
58 | /// true if the key was just pressed down in the most recent update.
59 | public static bool GetKeyDown(Key key) => NewKeysThisFrame.Contains(key);
60 |
61 | ///
62 | /// Gets the mouse button state.
63 | ///
64 | /// The button.
65 | /// true if the button is currently pressed.
66 | public static bool GetMouseButton(MouseButton button) => CurrentlyPressedMouseButtons.Contains(button);
67 |
68 | ///
69 | /// Gets the mouse button state.
70 | ///
71 | /// The button.
72 | /// true if the button was just pressed down in the most recent update.
73 | public static bool GetMouseButtonDown(MouseButton button) => NewMouseButtonsThisFrame.Contains(button);
74 |
75 | ///
76 | /// Processes the snapshot.
77 | ///
78 | /// The snapshot.
79 | public static void UpdateFrameInput(InputSnapshot snapshot)
80 | {
81 | FrameSnapshot = snapshot;
82 | NewKeysThisFrame.Clear();
83 | NewMouseButtonsThisFrame.Clear();
84 |
85 | MousePosition = snapshot.MousePosition;
86 |
87 | for (var i = 0; i < snapshot.KeyEvents.Count; i++)
88 | {
89 | var keyEvent = snapshot.KeyEvents[i];
90 | if (keyEvent.Down)
91 | {
92 | KeyDown(keyEvent.Key);
93 | }
94 | else
95 | {
96 | KeyUp(keyEvent.Key);
97 | }
98 | }
99 |
100 | for (var i = 0; i < snapshot.MouseEvents.Count; i++)
101 | {
102 | var mouseEvent = snapshot.MouseEvents[i];
103 | if (mouseEvent.Down)
104 | {
105 | MouseDown(mouseEvent.MouseButton);
106 | }
107 | else
108 | {
109 | MouseUp(mouseEvent.MouseButton);
110 | }
111 | }
112 | }
113 |
114 | ///
115 | /// Handles a mouse button release event.
116 | ///
117 | /// The mouse button.
118 | private static void MouseUp(MouseButton mouseButton)
119 | {
120 | CurrentlyPressedMouseButtons.Remove(mouseButton);
121 | NewMouseButtonsThisFrame.Remove(mouseButton);
122 | }
123 |
124 | ///
125 | /// Handles a mouse button press event.
126 | ///
127 | /// The mouse button.
128 | private static void MouseDown(MouseButton mouseButton)
129 | {
130 | if (CurrentlyPressedMouseButtons.Add(mouseButton))
131 | {
132 | NewMouseButtonsThisFrame.Add(mouseButton);
133 | }
134 | }
135 |
136 | ///
137 | /// Handles a key release event.
138 | ///
139 | /// The key.
140 | private static void KeyUp(Key key)
141 | {
142 | CurrentlyPressedKeys.Remove(key);
143 | NewKeysThisFrame.Remove(key);
144 | }
145 |
146 | ///
147 | /// Handles a key press event.
148 | ///
149 | /// The key.
150 | private static void KeyDown(Key key)
151 | {
152 | if (CurrentlyPressedKeys.Add(key))
153 | {
154 | NewKeysThisFrame.Add(key);
155 | }
156 | }
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/Engine/Maps/Overworld/-1,-1.chunk:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Engine/Maps/Overworld/-1,-2.chunk:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Engine/Maps/Overworld/-1,-3.chunk:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Engine/Maps/Overworld/-1,-4.chunk:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/Engine/Maps/Overworld/-1,0.chunk:
--------------------------------------------------------------------------------
1 | % &