├── .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 | 49 | 55 | 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 | 63 | 71 | 72 | 73 | 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 |   %&&( -..05668 =>?@     -------------------------------------------------------------------------------- /Engine/Maps/Overworld/-1,1.chunk: -------------------------------------------------------------------------------- 1 |         -------------------------------------------------------------------------------- /Engine/Maps/Overworld/-2,-1.chunk: -------------------------------------------------------------------------------- 1 |      -------------------------------------------------------------------------------- /Engine/Maps/Overworld/-2,0.chunk: -------------------------------------------------------------------------------- 1 |   2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |  10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |  -------------------------------------------------------------------------------- /Engine/Maps/Overworld/-2,1.chunk: -------------------------------------------------------------------------------- 1 |           -------------------------------------------------------------------------------- /Engine/Maps/Overworld/-3,-1.chunk: -------------------------------------------------------------------------------- 1 |     -------------------------------------------------------------------------------- /Engine/Maps/Overworld/-3,0.chunk: -------------------------------------------------------------------------------- 1 |   12249:;< 1224 -------------------------------------------------------------------------------- /Engine/Maps/Overworld/-3,1.chunk: -------------------------------------------------------------------------------- 1 | 9:;< 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /Engine/Maps/Overworld/-4,-1.chunk: -------------------------------------------------------------------------------- 1 |   %&&&&(-....0 -------------------------------------------------------------------------------- /Engine/Maps/Overworld/-4,0.chunk: -------------------------------------------------------------------------------- 1 | 2 | 3 | 566668 566668 =>;;;@        -------------------------------------------------------------------------------- /Engine/Maps/Overworld/-4,1.chunk: -------------------------------------------------------------------------------- 1 |   2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Engine/Maps/Overworld/0,-1.chunk: -------------------------------------------------------------------------------- 1 |   -------------------------------------------------------------------------------- /Engine/Maps/Overworld/0,-2.chunk: -------------------------------------------------------------------------------- 1 |              -------------------------------------------------------------------------------- /Engine/Maps/Overworld/0,-3.chunk: -------------------------------------------------------------------------------- 1 | 1249:<  -------------------------------------------------------------------------------- /Engine/Maps/Overworld/0,-4.chunk: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /Engine/Maps/Overworld/0,0.chunk: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 19 7 |  -------------------------------------------------------------------------------- /Engine/Maps/Overworld/0,1.chunk: -------------------------------------------------------------------------------- 1 | 1223349:;;;< 2 | 3 | 4 |   -------------------------------------------------------------------------------- /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 | 4 | -------------------------------------------------------------------------------- /Engine/Maps/Overworld/1,0.chunk: -------------------------------------------------------------------------------- 1 |     224 :;<  2 | 3 | 4 |   -------------------------------------------------------------------------------- /Engine/Maps/Overworld/1,1.chunk: -------------------------------------------------------------------------------- 1 |      2 |    -------------------------------------------------------------------------------- /Engine/Pool.cs: -------------------------------------------------------------------------------- 1 | namespace Engine 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | 7 | /// 8 | /// Implements a pool of recyclable resources. 9 | /// 10 | /// The object type. 11 | public class Pool 12 | where T : new() 13 | { 14 | /// 15 | /// Stores the internal object instance reserve. 16 | /// 17 | private readonly Queue pool; 18 | 19 | /// 20 | /// Keeps track of the withdrawn objects. 21 | /// 22 | private readonly HashSet borrowedItems; 23 | 24 | /// 25 | /// Initializes a new instance of the class. 26 | /// 27 | /// The capacity. 28 | /// The must be greater than zero. 29 | public Pool(int capacity) 30 | { 31 | if (capacity <= 0) 32 | { 33 | throw new ArgumentException($"{nameof(capacity)} must be greater than zero", nameof(capacity)); 34 | } 35 | 36 | this.pool = new Queue(capacity); 37 | this.borrowedItems = new HashSet(capacity); 38 | foreach (var _ in Enumerable.Range(1, capacity)) 39 | { 40 | this.pool.Enqueue(new T()); 41 | } 42 | } 43 | 44 | /// 45 | /// Gets a value indicating whether the pool has any items left for withdrawal. 46 | /// 47 | public bool CanWithdraw => this.pool.Count > 0; 48 | 49 | /// 50 | /// Pulls a free item from the pool. 51 | /// 52 | /// The item. 53 | /// The pool is exhausted. 54 | public T Withdraw() 55 | { 56 | if (!this.CanWithdraw) 57 | { 58 | throw new InvalidOperationException("The pool is exhausted."); 59 | } 60 | 61 | var availableItem = this.pool.Dequeue(); 62 | this.borrowedItems.Add(availableItem); 63 | return availableItem; 64 | } 65 | 66 | /// 67 | /// Returns the specified item back to the pool. 68 | /// 69 | /// The item. 70 | /// The returned item did not belong to the pool. 71 | public void Return(T item) 72 | { 73 | if (this.borrowedItems.Remove(item)) 74 | { 75 | this.pool.Enqueue(item); 76 | } 77 | else 78 | { 79 | throw new InvalidOperationException("The returned item did not belong to the pool."); 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Engine/Program.cs: -------------------------------------------------------------------------------- 1 | namespace Engine 2 | { 3 | using Veldrid; 4 | 5 | /// 6 | /// The demo app. 7 | /// 8 | public static class Program 9 | { 10 | /// 11 | /// Program entry point. 12 | /// 13 | /// The arguments. 14 | public static void Main(string[] args) 15 | { 16 | var window = new GameWindow("Streaming Tilemap"); 17 | var game = new SampleGame(window); 18 | window.Run(GraphicsBackend.Direct3D11); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Engine/Tileset.cs: -------------------------------------------------------------------------------- 1 | namespace Engine 2 | { 3 | using System; 4 | using System.Numerics; 5 | 6 | /// 7 | /// Provides tileset specific calculations. 8 | /// 9 | public class Tileset 10 | { 11 | /// 12 | /// Initializes a new instance of the class. 13 | /// 14 | /// The width of the tileset atlas texture, in pixels. 15 | /// The height of the tileset atlas texture, in pixels. 16 | /// Tileset must have positive area. 17 | public Tileset(uint pixelWidth, uint pixelHeight) 18 | { 19 | if (pixelWidth == 0 || pixelHeight == 0) 20 | { 21 | throw new InvalidOperationException("Tileset must have positive area."); 22 | } 23 | 24 | this.Width = (int)pixelWidth / Configuration.TileSize; 25 | this.Height = this.Width = (int)pixelHeight / Configuration.TileSize; 26 | } 27 | 28 | /// 29 | /// Gets the width, in tile units. 30 | /// 31 | public int Width { get; } 32 | 33 | /// 34 | /// Gets the height, in tile units. 35 | /// 36 | public int Height { get; } 37 | 38 | /// 39 | /// Gets the total tile count. 40 | /// 41 | public int Count => this.Width * this.Height; 42 | 43 | /// 44 | /// Gets the tile index at the given row and column. The index is zero-based. 45 | /// 46 | /// The row. Zero-based. 47 | /// The column. Zero-based. 48 | /// The zero-based tile index. 49 | /// 50 | /// The row was out of range. 51 | /// or 52 | /// The column was out of range. 53 | /// 54 | public int IndexAt(int row, int column) 55 | { 56 | if (row < 0 || row >= this.Height) 57 | { 58 | throw new ArgumentOutOfRangeException(nameof(row), $"The {nameof(row)} was out of range. Expected [0, {this.Height - 1}]."); 59 | } 60 | 61 | if (column < 0 || column >= this.Width) 62 | { 63 | throw new ArgumentOutOfRangeException(nameof(column), $"The {nameof(column)} was out of range. Expected [0, {this.Width - 1}]."); 64 | } 65 | 66 | return row * this.Width + column; 67 | } 68 | 69 | /// 70 | /// Gets the row and column for the corresponding index. All units are zero-based. 71 | /// 72 | /// The index. Zero-based. 73 | /// The row and column. Zero-based. 74 | /// The index was out of range. 75 | public (int row, int column) FromIndex(int index) 76 | { 77 | if (index < 0 || index >= this.Count) 78 | { 79 | throw new ArgumentOutOfRangeException(nameof(index), $"The {nameof(index)} was out of range. Expected [0, {this.Count - 1}]."); 80 | } 81 | 82 | var row = index / this.Width; 83 | var column = index % this.Width; 84 | 85 | return (row, column); 86 | } 87 | 88 | /// 89 | /// Gets the texture UV coordinates for the specified tile index. 90 | /// 91 | /// The index. Zero-based. 92 | /// The representing the top-left and bottom-right corners of the texture region. 93 | /// Each is between 0 and 1 where 0 is left/top and 1 right/bottom. 94 | public (Vector2 topLeft, Vector2 bottomRight) GetTileUV(int index) 95 | { 96 | if (index < 0 || index > this.Count - 1) 97 | { 98 | // Consider error 99 | return (new Vector2(0f), new Vector2(0f)); 100 | } 101 | 102 | var (row, column) = this.FromIndex(index); 103 | 104 | return (new Vector2(column / (float)this.Width, row / (float)this.Height), new Vector2((column + 1) / (float)this.Width, (row + 1) / (float)this.Height)); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Engine/Utility.cs: -------------------------------------------------------------------------------- 1 | namespace Engine 2 | { 3 | using System; 4 | 5 | /// 6 | /// Provides various utility methods. 7 | /// 8 | public static class Utility 9 | { 10 | /// 11 | /// Divides the two integers, rounding the result towards negative infinity. 12 | /// The operation is equivalent to (int)Math.Floor((double)a / b), but operates on integers, is faster, and does not need rounding or conversion. 13 | /// 14 | /// The dividend. 15 | /// The divisor. 16 | /// The Floor value after the division. 17 | public static int DivFloor(int a, int b) => (a < 0) ^ (b < 0) && a % b != 0 ? a / b - 1 : a / b; 18 | 19 | /// 20 | /// Calculates the remainder after division of the two integers. The division result is first rounded towards negative infinity. 21 | /// This operation is the modulo of the corresponding operation. 22 | /// 23 | /// The dividend. 24 | /// The divisor. 25 | /// The remainder of the floored division operation. 26 | public static int ModFloor(int a, int b) => (a < 0) ^ (b < 0) && a % b != 0 ? a - DivFloor(a, b) * b : a % b; 27 | 28 | /// 29 | /// Calculates the squared distance between two points. 30 | /// 31 | /// The first point. 32 | /// The second point. 33 | /// The squared distance. 34 | public static int SquaredDistance((int x, int y) p1, (int x, int y) p2) 35 | { 36 | var dx = Math.Abs(p2.x - p1.x); 37 | var dy = Math.Abs(p2.y - p1.y); 38 | 39 | return dx * dx + dy * dy; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Engine/compile-hlsl.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | fxc /E VS /T vs_5_0 Basic-Vertex.hlsl /Fo Basic-Vertex.hlsl.bytes 4 | fxc /E FS /T ps_5_0 Basic-Fragment.hlsl /Fo Basic-Fragment.hlsl.bytes -------------------------------------------------------------------------------- /Engine/compile-spirv.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | glslangvalidator -V -S vert -e VS --source-entrypoint main Basic-Vertex.450.glsl -o Basic-Vertex.450.spv 4 | glslangvalidator -V -S frag -e FS --source-entrypoint main Basic-Fragment.450.glsl -o Basic-Fragment.450.spv -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Jukka Lavonen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Tilester.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.28010.2036 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Editor", "Editor\Editor.csproj", "{CB588834-5696-4B4B-A53D-1F737E95A8D6}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Engine", "Engine\Engine.csproj", "{DC40924E-3C4B-4194-BCD3-604D783FD410}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {CB588834-5696-4B4B-A53D-1F737E95A8D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {CB588834-5696-4B4B-A53D-1F737E95A8D6}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {CB588834-5696-4B4B-A53D-1F737E95A8D6}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {CB588834-5696-4B4B-A53D-1F737E95A8D6}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {DC40924E-3C4B-4194-BCD3-604D783FD410}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {DC40924E-3C4B-4194-BCD3-604D783FD410}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {DC40924E-3C4B-4194-BCD3-604D783FD410}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {DC40924E-3C4B-4194-BCD3-604D783FD410}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {0D34800C-7612-469B-A1C9-0AECAFEC1944} 30 | EndGlobalSection 31 | EndGlobal 32 | --------------------------------------------------------------------------------