├── .github └── workflows │ ├── build.yml │ └── publish.yml ├── .gitignore ├── .vscode └── tasks.json ├── COPYING ├── Directory.Build.props ├── README.md ├── ReleaseNotes.md ├── Simplecs.sln ├── Simplecs ├── Containers │ ├── ChunkedStorage.cs │ ├── CircularBuffer.cs │ ├── ComponentTable.cs │ └── EntityAllocator.cs ├── Entity.cs ├── EntityBuilder.cs ├── Properties │ └── AssemblyInfo.cs ├── Simplecs.csproj ├── ViewBuilder.cs ├── Views │ ├── EntityEnumerator.cs │ ├── View.cs │ ├── ViewEnumerator.cs │ ├── ViewPredicate.cs │ └── ViewRow.cs └── World.cs ├── SimplecsTests ├── ComponentTableTest.cs ├── Components.cs ├── EntityAllocatorTest.cs ├── SimplecsTests.csproj ├── ViewTest.cs └── WorldTest.cs └── omnisharp.json /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: [] 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | build: 11 | name: Build 12 | runs-on: ubuntu-16.04 13 | 14 | strategy: 15 | matrix: 16 | dotnet: [ '3.0.100' ] 17 | config: [ Debug, Release ] 18 | 19 | steps: 20 | - uses: actions/checkout@master 21 | - name: Setup dotnet 22 | uses: actions/setup-dotnet@v1 23 | with: 24 | dotnet-version: ${{ matrix.dotnet }} 25 | - run: dotnet restore 26 | name: Restore 27 | - run: dotnet build -c ${{ matrix.config }} 28 | name: Build 29 | - run: dotnet test -c ${{ matrix.config }} 30 | name: Test 31 | - run: dotnet pack -c ${{ matrix.config }} --include-symbols --version-suffix ci-${{ matrix.config }}-${{ github.sha }} 32 | name: Package 33 | - uses: actions/upload-artifact@master 34 | with: 35 | name: ci-packages 36 | path: pkg/Simplecs 37 | name: Artifacts 38 | 39 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*' 7 | 8 | jobs: 9 | build: 10 | name: Build 11 | runs-on: ubuntu-16.04 12 | 13 | steps: 14 | - uses: actions/checkout@master 15 | - name: Setup dotnet 16 | uses: actions/setup-dotnet@v1 17 | with: 18 | dotnet-version: 3.0.100 19 | - run: dotnet restore 20 | name: Restore 21 | - run: dotnet build -c Release 22 | name: Build 23 | - run: dotnet test -c Release 24 | name: Test 25 | - run: dotnet pack -c Release --include-symbols --include-source 26 | name: Package 27 | - uses: actions/upload-artifact@master 28 | with: 29 | name: packages 30 | path: pkg/Simplecs 31 | name: Artifacts 32 | - name: Release to GitHub 33 | uses: softprops/action-gh-release@v1 34 | with: 35 | draft: true 36 | body_path: ReleaseNotes.md 37 | files: pkg/Simplecs 38 | env: 39 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 40 | - run: dotnet nuget push pkg/Simplecs/Simplecs.*.nupkg -s https://api.nuget.org/v3/index.json -k ${{ secrets.NUGET_KEY }} 41 | name: Publish to NuGet 42 | 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | obj 3 | pkg 4 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "build", 8 | "command": "dotnet", 9 | "type": "shell", 10 | "args": [ 11 | "build", 12 | // Ask dotnet build to generate full paths for file names. 13 | "/property:GenerateFullPaths=true", 14 | // Do not generate summary otherwise it leads to duplicate errors in Problems panel 15 | "/consoleloggerparameters:NoSummary" 16 | ], 17 | "group": "build", 18 | "presentation": { 19 | "reveal": "silent" 20 | }, 21 | "problemMatcher": "$msCompile" 22 | }, 23 | { 24 | "label": "test", 25 | "command": "dotnet", 26 | "type": "shell", 27 | "args": [ 28 | "test", 29 | // Ask dotnet build to generate full paths for file names. 30 | "/property:GenerateFullPaths=true", 31 | // Do not generate summary otherwise it leads to duplicate errors in Problems panel 32 | "/consoleloggerparameters:NoSummary" 33 | ], 34 | "dependsOn": [ 35 | "build" 36 | ], 37 | "group": "test", 38 | "presentation": { 39 | "reveal": "always" 40 | }, 41 | "problemMatcher": "$msCompile" 42 | } 43 | ] 44 | } -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(SolutionDir)\bin\$(MSBuildProjectName) 5 | $(SolutionDir)\obj\$(MSBuildProjectName) 6 | $(SolutionDir)\pkg\$(MSBuildProjectName) 7 | 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Simplecs - Simple ECS for C# 2 | ============================ 3 | 4 | 5 | 6 | Simple Entity-Component-System library for C# with no dependencies on any game engine. 7 | 8 | Pronounced "simplex." 9 | 10 | This library is not the fastest nor most powerful. It's useful for simple cases. More 11 | sophisticated games or users may need to look elsewhere for a suitable library. 12 | The intended use cases are small demos, toys, and examples. 13 | 14 | This library uses a naive approach based on sparse sets to implement O(1) component 15 | lookup. Iteration of a component table is cache-friendly, but iteration of a `View` 16 | over multiple components or using required/excluded components will incur non-cache- 17 | friendly memory access patterns. 18 | 19 | The ECS concept of a "System" is actually not found in this library. A `View` can be used 20 | to implement a System, but the scheduling and logic execution is up to the library user. 21 | This library primarily implements a way of allocating `Entity` keys and storing Component 22 | instances associated via these keys, along with a `View` concept to iterate Entities with 23 | a matching set of Components. 24 | 25 | A Component in Simplecs is any `struct` type that is `Attach`ed to an `Entity`. There are 26 | no other restrictions or requirements on Components. 27 | 28 | This library does not attempt to provide any form of multi-threading, scheduling, 29 | advanced debugging, serialization, or reflection. These are all up to the library user. 30 | Feature additions that keep the library simple in nature may be considered in the future. 31 | 32 | Usage 33 | ----- 34 | 35 | The `World` class is the main container that stores all entities and components. 36 | 37 | ```c# 38 | var world = new World(); 39 | ``` 40 | 41 | Component types _must_ be `struct` types in Simplecs. 42 | 43 | ```c# 44 | struct PositionComponent { 45 | public int x, y z; 46 | } 47 | ``` 48 | 49 | Entities are identified by the `Entity` type, which is just an opaque handle. An entity 50 | can be constructed on a `World` using a builder interface, which can also be used to 51 | attach components. 52 | 53 | ```c# 54 | var entity = world.CreateEntity() 55 | .Attach(new PositionComponent{x = 1, y = -2, z = 0}) 56 | .Entity; 57 | ``` 58 | 59 | Components can also be attached and detached after creation. 60 | 61 | ```c# 62 | world.Attach(entity, new NameComponent("Bob")); 63 | world.Detach(entity); 64 | ``` 65 | 66 | Entities can be destroyed, which detaches all components. Unused entities should be 67 | destroyed to reclaim memory. 68 | 69 | ```c# 70 | world.Destroy(entity); 71 | ``` 72 | 73 | Querying entities uses a `View<>` generic, which can be constructed via a builder on 74 | `World` like entities. 75 | 76 | ```c# 77 | var view = world.CreateView().Select(); 78 | ``` 79 | 80 | Views can be iterated to retrieve a list of entities and references to the selected 81 | components. Note that these references are fully mutable, which can be handy for some 82 | kinds of frequently-modified heavy components. 83 | 84 | ```c# 85 | var view = world.CreateView().Select(); 86 | foreach (var row in view) { 87 | Console.WriteLine(row.Component2.Name); 88 | } 89 | ``` 90 | 91 | A `View` can also be constructed with required components or excluded components. 92 | These components are used to find matching entities, but are not included in the 93 | iterated list of components like `Select<>` does. Excluded components are those which 94 | must not exist on an entity for it to match. Note that `Select<>` must be the final 95 | method in the builder chain. 96 | 97 | ```c# 98 | // select all entities with a position, and which also 99 | // have an actor component but do not have an AI component 100 | // 101 | var view = world.CreateView() 102 | .Require() 103 | .Exclude() 104 | .Select(); 105 | 106 | // move all non-AI actors slightly to the right 107 | // 108 | foreach (var row in view) { 109 | row.Component.x += 1; 110 | } 111 | ``` 112 | 113 | Additional support in the public interface exists to query which entities exist 114 | and to find all components on a given entity, which can be useful for tooling or 115 | serialization. Note that some of these interfaces necessitate boxing components 116 | which can be a significant performance penalty. 117 | 118 | License 119 | ------- 120 | 121 | Written in 2019 by Sean Middleditch (). 122 | 123 | To the extent possible under law, the author(s) have dedicated all copyright 124 | and related and neighboring rights to this software to the public domain worldwide. 125 | This software is distributed without any warranty. 126 | 127 | You should have received a copy of the CC0 Public Domain Dedication along 128 | with this software. If not, see . 129 | -------------------------------------------------------------------------------- /ReleaseNotes.md: -------------------------------------------------------------------------------- 1 | Simplecs Release Notes 2 | ====================== 3 | 4 | Alpha 0.1.1 5 | ----------- 6 | 7 | - Small performance improvements for `View`s with large selected tables 8 | but small sets of required components. 9 | 10 | Alpha 0.1.0 11 | ----------- 12 | 13 | Initial release. 14 | -------------------------------------------------------------------------------- /Simplecs.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26124.0 5 | MinimumVisualStudioVersion = 15.0.26124.0 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Simplecs", "Simplecs\Simplecs.csproj", "{F0081220-8152-4F4F-AF61-2013026406D0}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimplecsTests", "SimplecsTests\SimplecsTests.csproj", "{DA32C3B9-CE4A-4412-9CF6-731A7C5AAE3A}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Debug|x64 = Debug|x64 14 | Debug|x86 = Debug|x86 15 | Release|Any CPU = Release|Any CPU 16 | Release|x64 = Release|x64 17 | Release|x86 = Release|x86 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 23 | {F0081220-8152-4F4F-AF61-2013026406D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {F0081220-8152-4F4F-AF61-2013026406D0}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {F0081220-8152-4F4F-AF61-2013026406D0}.Debug|x64.ActiveCfg = Debug|Any CPU 26 | {F0081220-8152-4F4F-AF61-2013026406D0}.Debug|x64.Build.0 = Debug|Any CPU 27 | {F0081220-8152-4F4F-AF61-2013026406D0}.Debug|x86.ActiveCfg = Debug|Any CPU 28 | {F0081220-8152-4F4F-AF61-2013026406D0}.Debug|x86.Build.0 = Debug|Any CPU 29 | {F0081220-8152-4F4F-AF61-2013026406D0}.Release|Any CPU.ActiveCfg = Release|Any CPU 30 | {F0081220-8152-4F4F-AF61-2013026406D0}.Release|Any CPU.Build.0 = Release|Any CPU 31 | {F0081220-8152-4F4F-AF61-2013026406D0}.Release|x64.ActiveCfg = Release|Any CPU 32 | {F0081220-8152-4F4F-AF61-2013026406D0}.Release|x64.Build.0 = Release|Any CPU 33 | {F0081220-8152-4F4F-AF61-2013026406D0}.Release|x86.ActiveCfg = Release|Any CPU 34 | {F0081220-8152-4F4F-AF61-2013026406D0}.Release|x86.Build.0 = Release|Any CPU 35 | {DA32C3B9-CE4A-4412-9CF6-731A7C5AAE3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 36 | {DA32C3B9-CE4A-4412-9CF6-731A7C5AAE3A}.Debug|Any CPU.Build.0 = Debug|Any CPU 37 | {DA32C3B9-CE4A-4412-9CF6-731A7C5AAE3A}.Debug|x64.ActiveCfg = Debug|Any CPU 38 | {DA32C3B9-CE4A-4412-9CF6-731A7C5AAE3A}.Debug|x64.Build.0 = Debug|Any CPU 39 | {DA32C3B9-CE4A-4412-9CF6-731A7C5AAE3A}.Debug|x86.ActiveCfg = Debug|Any CPU 40 | {DA32C3B9-CE4A-4412-9CF6-731A7C5AAE3A}.Debug|x86.Build.0 = Debug|Any CPU 41 | {DA32C3B9-CE4A-4412-9CF6-731A7C5AAE3A}.Release|Any CPU.ActiveCfg = Release|Any CPU 42 | {DA32C3B9-CE4A-4412-9CF6-731A7C5AAE3A}.Release|Any CPU.Build.0 = Release|Any CPU 43 | {DA32C3B9-CE4A-4412-9CF6-731A7C5AAE3A}.Release|x64.ActiveCfg = Release|Any CPU 44 | {DA32C3B9-CE4A-4412-9CF6-731A7C5AAE3A}.Release|x64.Build.0 = Release|Any CPU 45 | {DA32C3B9-CE4A-4412-9CF6-731A7C5AAE3A}.Release|x86.ActiveCfg = Release|Any CPU 46 | {DA32C3B9-CE4A-4412-9CF6-731A7C5AAE3A}.Release|x86.Build.0 = Release|Any CPU 47 | EndGlobalSection 48 | EndGlobal 49 | -------------------------------------------------------------------------------- /Simplecs/Containers/ChunkedStorage.cs: -------------------------------------------------------------------------------- 1 | // Simplecs - Simple ECS for C# 2 | // 3 | // Written in 2019 by Sean Middleditch (http://github.com/seanmiddleditch) 4 | // 5 | // To the extent possible under law, the author(s) have dedicated all copyright 6 | // and related and neighboring rights to this software to the public domain 7 | // worldwide. This software is distributed without any warranty. 8 | // 9 | // You should have received a copy of the CC0 Public Domain Dedication along 10 | // with this software. If not, see 11 | // . 12 | 13 | using System.Collections.Generic; 14 | using System.Runtime.InteropServices; 15 | 16 | namespace Simplecs.Containers { 17 | /// 18 | /// Stores a collection of values in semi-contiguous but stable memory. 19 | /// 20 | /// Backing stores are allocated in fixed-sized chunks. This storage can 21 | /// be indexed efficiently in O(1). Further, insertion is always append and 22 | /// also O(1), and removal always uses swap-and-pop and is also O(1). 23 | /// 24 | /// Note that as consequence, ordering is _not_ maintained in this container. 25 | /// 26 | /// Type stored. 27 | public class ChunkedStorage where T : struct { 28 | private readonly List _chunks = new List(); 29 | private readonly int _chunkElementCount = 1024; 30 | private int _count = 0; 31 | 32 | /// 33 | /// Default size of chunks in bytes. 34 | /// 35 | public const int defaultChunkSize = 16 * 1024; 36 | 37 | /// 38 | /// Number of elements stored. 39 | /// 40 | public int Count => _count; 41 | 42 | /// 43 | /// Creates a new chunked storage container. 44 | /// 45 | /// Size of chunks in bytes. 46 | public ChunkedStorage(int chunkSize = defaultChunkSize) { 47 | int elementSize = Marshal.SizeOf(); 48 | _chunkElementCount = chunkSize / elementSize; 49 | } 50 | 51 | /// 52 | /// Adds a value to container. 53 | /// 54 | /// Value to add. 55 | public void Add(in T value) { 56 | var (chunk, index) = AllocateSlot(); 57 | chunk[index] = value; 58 | } 59 | 60 | /// 61 | /// Removes an element at a given index. 62 | /// 63 | /// This will swap in the last element in the container and 64 | /// hence mutates the order of elements. 65 | /// 66 | /// Index at which to remove an item. 67 | public void RemoveAt(int index) { 68 | if (index < 0 || index >= _count) { 69 | return; 70 | } 71 | 72 | if (index < _count - 1) { 73 | int chunkIndex = index / _chunkElementCount; 74 | int slotIndex = index % _chunkElementCount; 75 | 76 | _chunks[chunkIndex][slotIndex] = _chunks[_chunks.Count - 1][(_count % _chunkElementCount) - 1]; 77 | } 78 | 79 | --_count; 80 | } 81 | 82 | /// 83 | /// Removes all values from the container. 84 | /// 85 | public void Clear() { 86 | _count = 0; 87 | } 88 | 89 | /// 90 | /// Accesses the value at the given index. 91 | /// 92 | public ref T this[int index] => ref _chunks[index / _chunkElementCount][index % _chunkElementCount]; 93 | 94 | private (T[] chunk, int index) AllocateSlot() { 95 | int chunkIndex = _count / _chunkElementCount; 96 | int slotIndex = _count % _chunkElementCount; 97 | 98 | if (_chunks.Count == chunkIndex) { 99 | AllocateChunk(); 100 | } 101 | 102 | ++_count; 103 | return (_chunks[_chunks.Count - 1], slotIndex); 104 | } 105 | 106 | private T[] AllocateChunk() { 107 | T[] chunk = new T[_chunkElementCount]; 108 | _chunks.Add(chunk); 109 | return chunk; 110 | } 111 | } 112 | } -------------------------------------------------------------------------------- /Simplecs/Containers/CircularBuffer.cs: -------------------------------------------------------------------------------- 1 | // Simplecs - Simple ECS for C# 2 | // 3 | // Written in 2019 by Sean Middleditch (http://github.com/seanmiddleditch) 4 | // 5 | // To the extent possible under law, the author(s) have dedicated all copyright 6 | // and related and neighboring rights to this software to the public domain 7 | // worldwide. This software is distributed without any warranty. 8 | // 9 | // You should have received a copy of the CC0 Public Domain Dedication along 10 | // with this software. If not, see 11 | // . 12 | 13 | using System; 14 | using System.Collections.Generic; 15 | using System.Runtime.CompilerServices; 16 | 17 | namespace Simplecs.Containers { 18 | /// 19 | /// Simple circule buffer implementation. 20 | /// 21 | public class CircularBuffer { 22 | private readonly List _data = new List(); 23 | private int _head = 0; 24 | private int _count = 0; 25 | 26 | /// 27 | /// Number of elements in the container. 28 | /// 29 | public int Count => _count; 30 | 31 | /// 32 | /// Add a new element to the container at the end. 33 | /// 34 | public void Add(T value) { 35 | if (_count < _data.Count) { 36 | _data[OffsetOf(_head + _count)] = value; 37 | } else { 38 | _data.Add(value); 39 | } 40 | 41 | ++_count; 42 | } 43 | 44 | /// 45 | /// Pop an element from the front of the buffer and return it. 46 | /// 47 | public T PopFront() { 48 | if (_count == 0) { 49 | throw new InvalidOperationException(message: "CircularBuffer is empty"); 50 | } 51 | 52 | T value = _data[_head]; 53 | ++_head; 54 | --_count; 55 | return value; 56 | } 57 | 58 | /// 59 | /// Pop an element from the back of the buffer and return it. 60 | /// 61 | public T PopBack() { 62 | if (_count == 0) { 63 | throw new InvalidOperationException(message: "CircularBuffer is empty"); 64 | } 65 | 66 | T value = _data[OffsetOf(_head + _count - 1)]; 67 | --_count; 68 | return value; 69 | } 70 | 71 | [MethodImplAttribute(MethodImplOptions.AggressiveInlining)] 72 | private int OffsetOf(int index) => index % _data.Count; 73 | } 74 | } -------------------------------------------------------------------------------- /Simplecs/Containers/ComponentTable.cs: -------------------------------------------------------------------------------- 1 | // Simplecs - Simple ECS for C# 2 | // 3 | // Written in 2019 by Sean Middleditch (http://github.com/seanmiddleditch) 4 | // 5 | // To the extent possible under law, the author(s) have dedicated all copyright 6 | // and related and neighboring rights to this software to the public domain 7 | // worldwide. This software is distributed without any warranty. 8 | // 9 | // You should have received a copy of the CC0 Public Domain Dedication along 10 | // with this software. If not, see 11 | // . 12 | 13 | using System; 14 | using System.Collections.Generic; 15 | using System.Linq; 16 | 17 | namespace Simplecs.Containers { 18 | internal interface IComponentTable { 19 | Type Type { get; } 20 | int Count { get; } 21 | 22 | bool Contains(Entity entity); 23 | bool Remove(Entity entity); 24 | void Add(Entity entity, object component); 25 | bool TryGet(Entity entity, out object data); 26 | int IndexOf(Entity entity); 27 | Entity EntityAt(int index); 28 | void Clear(); 29 | } 30 | 31 | /// 32 | /// Stores a table of components mapped by unique entity keys. 33 | /// 34 | /// Struct type containing component data. 35 | internal class ComponentTable : IComponentTable where T : struct { 36 | private readonly ChunkedStorage _data = new ChunkedStorage(); 37 | private readonly List _entities = new List(); 38 | private readonly List _mapping = new List(); 39 | 40 | public delegate void Callback(Entity entity, ref T component); 41 | 42 | public Type Type => typeof(T); 43 | public int Count => _entities.Count; 44 | 45 | public bool Contains(Entity entity) => IndexOf(entity) != -1; 46 | 47 | public int IndexOf(Entity entity) { 48 | int mappingIndex = EntityUtil.DecomposeIndex(entity); 49 | 50 | if (mappingIndex < 0 || mappingIndex >= _mapping.Count) { 51 | return -1; 52 | } 53 | 54 | int dataIndex = _mapping[mappingIndex]; 55 | if (dataIndex >= _entities.Count) { 56 | return -1; 57 | } 58 | 59 | return _entities[dataIndex] == entity ? dataIndex : -1; 60 | } 61 | 62 | public bool Remove(Entity entity) { 63 | int dataIndex = IndexOf(entity); 64 | if (dataIndex == -1) { 65 | return false; 66 | } 67 | 68 | // Mark the mapping as invalid so the entity can no longer be 69 | // queried directly. 70 | // 71 | _mapping[EntityUtil.DecomposeIndex(entity)] = int.MaxValue; 72 | 73 | // Update mapping for the last component which is going to be moved into 74 | // the removed location. 75 | // 76 | int newMappingIndex = EntityUtil.DecomposeIndex(_entities[_entities.Count - 1]); 77 | _mapping[newMappingIndex] = dataIndex; 78 | 79 | // Move the component and entity information into the removed index. 80 | // 81 | _entities[dataIndex] = _entities[_entities.Count - 1]; 82 | _data[dataIndex] = _data[_data.Count - 1]; 83 | 84 | // Pop the final component. 85 | // 86 | _entities.RemoveAt(_entities.Count - 1); 87 | _data.RemoveAt(_data.Count - 1); 88 | 89 | return true; 90 | } 91 | 92 | public void Add(Entity entity, in T data) { 93 | int dataIndex = IndexOf(entity); 94 | if (dataIndex != -1) { 95 | _data[dataIndex] = data; 96 | return; 97 | } 98 | 99 | int mappingIndex = EntityUtil.DecomposeIndex(entity); 100 | if (mappingIndex >= _mapping.Count) { 101 | _mapping.AddRange(Enumerable.Repeat(int.MaxValue, mappingIndex - _mapping.Count + 1)); 102 | } 103 | 104 | _mapping[mappingIndex] = _entities.Count; 105 | 106 | _entities.Add(entity); 107 | _data.Add(data); 108 | } 109 | 110 | void IComponentTable.Add(Entity entity, object component) { 111 | if (component.GetType() != typeof(T)) { 112 | throw new InvalidOperationException(message: "Incorrect component type"); 113 | } 114 | 115 | Add(entity, (T)component); 116 | } 117 | 118 | bool IComponentTable.TryGet(Entity entity, out object data) { 119 | if (!TryGet(entity, out T component)) { 120 | data = false; 121 | return false; 122 | } 123 | 124 | data = component; 125 | return true; 126 | } 127 | 128 | public bool TryGet(Entity entity, out T data) { 129 | int dataIndex = IndexOf(entity); 130 | bool isValid = dataIndex != -1; 131 | 132 | data = isValid ? _data[dataIndex] : default(T); 133 | return isValid; 134 | } 135 | 136 | public Entity EntityAt(int index) => _entities[index]; 137 | 138 | public ref T ReferenceAt(Entity entity, int index) { 139 | if (_entities[index] != entity) { 140 | throw new InvalidOperationException(message:"Dereference on invalidated binding."); 141 | } 142 | 143 | return ref _data[index]; 144 | } 145 | 146 | public void Clear() { 147 | _data.Clear(); 148 | _entities.Clear(); 149 | } 150 | } 151 | } -------------------------------------------------------------------------------- /Simplecs/Containers/EntityAllocator.cs: -------------------------------------------------------------------------------- 1 | // Simplecs - Simple ECS for C# 2 | // 3 | // Written in 2019 by Sean Middleditch (http://github.com/seanmiddleditch) 4 | // 5 | // To the extent possible under law, the author(s) have dedicated all copyright 6 | // and related and neighboring rights to this software to the public domain 7 | // worldwide. This software is distributed without any warranty. 8 | // 9 | // You should have received a copy of the CC0 Public Domain Dedication along 10 | // with this software. If not, see 11 | // . 12 | 13 | using System.Collections.Generic; 14 | using System.Linq; 15 | using Simplecs.Containers; 16 | 17 | namespace Simplecs.Containers { 18 | /// 19 | /// Allocates and maintains a unique set of entity keys. 20 | /// 21 | internal class EntityAllocator { 22 | private readonly List _generations = new List(); 23 | private readonly CircularBuffer _freeIndices = new CircularBuffer(); 24 | private int _nextUnusedIndex = 0; 25 | 26 | public const int FreeMinimum = 64; 27 | public const uint Invalid = 0; 28 | 29 | public Entity Allocate() { 30 | int index = AcquireIndex(); 31 | 32 | // Determine the generation of the key at the given index; 33 | // for new indices, create a new generation. 34 | // 35 | if (index >= _generations.Count) { 36 | _generations.AddRange(Enumerable.Repeat((byte)1, index - _generations.Count + 1)); 37 | } 38 | 39 | return EntityUtil.MakeKey(index, _generations[index]); 40 | } 41 | 42 | public bool Deallocate(Entity entity) { 43 | if (!IsValid(entity)) { 44 | return false; 45 | } 46 | 47 | (int index, byte generation) = EntityUtil.DecomposeKey(entity); 48 | 49 | // Bump generation so the Entity key remains stale for a long time even 50 | // when the index portion is recycled. 51 | // 52 | if (++_generations[index] == 0) { 53 | // Ensure generation can never be 0, so we can find 54 | // invalid keys easily. 55 | // 56 | _generations[index] = 1; 57 | } 58 | 59 | // Recycle index. 60 | // 61 | _freeIndices.Add(index); 62 | 63 | return true; 64 | } 65 | 66 | public bool IsValid(Entity entity) { 67 | (int index, byte generation) = EntityUtil.DecomposeKey(entity); 68 | return index >= 0 && index < _generations.Count && _generations[index] == generation; 69 | } 70 | 71 | // Only consume from the freelist if we have some items in it, 72 | // to avoid recycling the same id too often. 73 | // 74 | private int AcquireIndex() => _freeIndices.Count < FreeMinimum ? _nextUnusedIndex++ : _freeIndices.PopFront(); 75 | } 76 | } -------------------------------------------------------------------------------- /Simplecs/Entity.cs: -------------------------------------------------------------------------------- 1 | // Simplecs - Simple ECS for C# 2 | // 3 | // Written in 2019 by Sean Middleditch (http://github.com/seanmiddleditch) 4 | // 5 | // To the extent possible under law, the author(s) have dedicated all copyright 6 | // and related and neighboring rights to this software to the public domain 7 | // worldwide. This software is distributed without any warranty. 8 | // 9 | // You should have received a copy of the CC0 Public Domain Dedication along 10 | // with this software. If not, see 11 | // . 12 | 13 | using System; 14 | using System.Diagnostics.CodeAnalysis; 15 | using System.Runtime.CompilerServices; 16 | 17 | namespace Simplecs { 18 | /// 19 | /// Entity key holder. 20 | /// 21 | public readonly struct Entity : IEquatable { 22 | internal readonly uint Key; 23 | 24 | internal Entity(uint key) => Key = key; 25 | 26 | /// 27 | /// Invalid Entity constant. 28 | /// 29 | public static Entity Invalid = new Entity(); 30 | 31 | /// 32 | /// Tests equality. 33 | /// 34 | [MethodImplAttribute(MethodImplOptions.AggressiveInlining)] 35 | public static bool operator ==(Entity lhs, Entity rhs) => lhs.Key == rhs.Key; 36 | 37 | /// 38 | /// Tests inequality. 39 | /// 40 | [MethodImplAttribute(MethodImplOptions.AggressiveInlining)] 41 | public static bool operator !=(Entity lhs, Entity rhs) => lhs.Key != rhs.Key; 42 | 43 | /// 44 | /// Comparison for sorted containers. 45 | /// 46 | [MethodImplAttribute(MethodImplOptions.AggressiveInlining)] 47 | public static bool operator <(Entity lhs, Entity rhs) => lhs.Key < rhs.Key; 48 | 49 | /// 50 | /// Comparison for sorted containers. 51 | /// 52 | [MethodImplAttribute(MethodImplOptions.AggressiveInlining)] 53 | public static bool operator >(Entity lhs, Entity rhs) => lhs.Key > rhs.Key; 54 | 55 | /// 56 | /// Tests equality. 57 | /// 58 | [MethodImplAttribute(MethodImplOptions.AggressiveInlining)] 59 | public bool Equals(Entity rhs) => Key == rhs.Key; 60 | 61 | /// 62 | /// Tests equality. 63 | /// 64 | public override bool Equals(object? rhs) => rhs is Entity rhsEntity && Key == rhsEntity.Key; 65 | 66 | /// 67 | /// Hash code of the entity. 68 | /// 69 | public override int GetHashCode() { 70 | return Key.GetHashCode(); 71 | } 72 | } 73 | 74 | internal static class EntityUtil { 75 | [MethodImplAttribute(MethodImplOptions.AggressiveInlining)] 76 | public static Entity MakeKey(int index, byte generation = 1) { 77 | return new Entity(((uint)generation << 24) | ((uint)index & 0x00FFFFFF)); 78 | } 79 | 80 | [MethodImplAttribute(MethodImplOptions.AggressiveInlining)] 81 | public static (int index, byte generation) DecomposeKey(Entity entity) { 82 | int index = DecomposeIndex(entity); 83 | byte generation = (byte)(entity.Key >> 24); 84 | return (index, generation); 85 | } 86 | 87 | [MethodImplAttribute(MethodImplOptions.AggressiveInlining)] 88 | public static int DecomposeIndex(Entity entity) { 89 | return (int)(entity.Key & 0x00FFFFFF); 90 | } 91 | } 92 | } -------------------------------------------------------------------------------- /Simplecs/EntityBuilder.cs: -------------------------------------------------------------------------------- 1 | // Simplecs - Simple ECS for C# 2 | // 3 | // Written in 2019 by Sean Middleditch (http://github.com/seanmiddleditch) 4 | // 5 | // To the extent possible under law, the author(s) have dedicated all copyright 6 | // and related and neighboring rights to this software to the public domain 7 | // worldwide. This software is distributed without any warranty. 8 | // 9 | // You should have received a copy of the CC0 Public Domain Dedication along 10 | // with this software. If not, see 11 | // . 12 | 13 | namespace Simplecs { 14 | /// 15 | /// Assists in creating new entities. 16 | /// 17 | public readonly struct EntityBuilder { 18 | private readonly World _world; 19 | private readonly Entity _entity; 20 | 21 | /// 22 | /// Retrieves the created entity. 23 | /// 24 | public Entity Entity => _entity; 25 | 26 | internal EntityBuilder(World world, Entity entity) { 27 | _world = world; 28 | _entity = entity; 29 | } 30 | 31 | /// 32 | /// Attaches a component to the newly created entity. 33 | /// 34 | public EntityBuilder Attach(in T component) where T : struct { 35 | _world.Attach(_entity, component); 36 | return this; 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /Simplecs/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly:InternalsVisibleTo("SimplecsTests")] -------------------------------------------------------------------------------- /Simplecs/Simplecs.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | netcoreapp3.0 6 | 8.0 7 | true 8 | 9 | 10 | 11 | 12 | enable 13 | enable 14 | true 15 | 16 | 17 | 18 | 19 | Simplecs 20 | 0.1.2 21 | Sean Middleditch 22 | CC0-1.0 23 | https://github.com/seanmiddleditch/Simplecs 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Simplecs/ViewBuilder.cs: -------------------------------------------------------------------------------- 1 | // Simplecs - Simple ECS for C# 2 | // 3 | // Written in 2019 by Sean Middleditch (http://github.com/seanmiddleditch) 4 | // 5 | // To the extent possible under law, the author(s) have dedicated all copyright 6 | // and related and neighboring rights to this software to the public domain 7 | // worldwide. This software is distributed without any warranty. 8 | // 9 | // You should have received a copy of the CC0 Public Domain Dedication along 10 | // with this software. If not, see 11 | // . 12 | 13 | using System.Collections.Generic; 14 | using System.Linq; 15 | using Simplecs.Containers; 16 | using Simplecs.Views; 17 | 18 | namespace Simplecs { 19 | /// 20 | /// Builder for a View object. 21 | /// 22 | /// Options for required and excluded components can be set. 23 | /// 24 | /// The Select method creates the View. 25 | /// 26 | public class ViewBuilder { 27 | private readonly World _world; 28 | private List? _required; 29 | private List? _excluded; 30 | 31 | internal ViewBuilder(World world) => _world = world; 32 | 33 | /// 34 | /// Marks the specified component as required for the view, 35 | /// though it will not be in the selected component set. 36 | /// 37 | public ViewBuilder Require() where T : struct { 38 | _required ??= new List(); 39 | _required.Add(_world.GetTable()); 40 | return this; 41 | } 42 | 43 | /// 44 | /// Marks the specified component as excluded for the view, 45 | /// meaning that it cannot be present on matched entities. 46 | /// 47 | public ViewBuilder Exclude() where T : struct { 48 | _excluded ??= new List(); 49 | _excluded.Add(_world.GetTable()); 50 | return this; 51 | } 52 | 53 | /// 54 | /// Creates a View that selects the specified component. 55 | /// 56 | public View Select() where T : struct => new View(Table(), Predicate()); 57 | 58 | /// 59 | /// Creates a View that selects the specified components. 60 | /// 61 | public View Select() where T1 : struct where T2 : struct => new View(Table(), Table(), Predicate()); 62 | 63 | /// 64 | /// Creates a View that selects the specified components. 65 | /// 66 | public View Select() where T1 : struct where T2 : struct where T3 : struct => new View(Table(), Table(), Table(), Predicate()); 67 | 68 | private ViewPredicate Predicate() => new ViewPredicate(tables: _excluded != null ? (_required != null ? _excluded.Concat(_required).ToArray() : _excluded.ToArray()) : _required?.ToArray(), excludedCount: _excluded?.Count ?? 0); 69 | private ComponentTable Table() where T : struct => _world.GetTable(); 70 | } 71 | } -------------------------------------------------------------------------------- /Simplecs/Views/EntityEnumerator.cs: -------------------------------------------------------------------------------- 1 | // Simplecs - Simple ECS for C# 2 | // 3 | // Written in 2019 by Sean Middleditch (http://github.com/seanmiddleditch) 4 | // 5 | // To the extent possible under law, the author(s) have dedicated all copyright 6 | // and related and neighboring rights to this software to the public domain 7 | // worldwide. This software is distributed without any warranty. 8 | // 9 | // You should have received a copy of the CC0 Public Domain Dedication along 10 | // with this software. If not, see 11 | // . 12 | 13 | using Simplecs.Containers; 14 | 15 | namespace Simplecs.Views { 16 | internal struct EntityEnumerator { 17 | private readonly IComponentTable _table; 18 | 19 | public Entity Entity { get; private set; } 20 | public int Index { get; private set; } 21 | 22 | public EntityEnumerator(IComponentTable table) => (_table, Entity, Index) = (table, Entity.Invalid, -1); 23 | 24 | internal bool MoveNext(IView view, out RowT row) where RowT : struct { 25 | // If the current entity has changed (e.g. been deleted from under us) 26 | // then don't initially increment the index. This allows Views to be used 27 | // to loop over entities and destroy them. 28 | // 29 | if (Index == -1 || (Index < _table.Count && Entity == _table.EntityAt(Index))) { 30 | ++Index; 31 | } 32 | 33 | while (Index < _table.Count) { 34 | Entity = _table.EntityAt(Index); 35 | if (view.TryBindRow(Entity, out row)) { 36 | return true; 37 | } 38 | 39 | ++Index; 40 | } 41 | 42 | row = default(RowT); 43 | return false; 44 | } 45 | 46 | internal void Reset() => (Entity, Index) = (Entity.Invalid, -1); 47 | } 48 | } -------------------------------------------------------------------------------- /Simplecs/Views/View.cs: -------------------------------------------------------------------------------- 1 | // Simplecs - Simple ECS for C# 2 | // 3 | // Written in 2019 by Sean Middleditch (http://github.com/seanmiddleditch) 4 | // 5 | // To the extent possible under law, the author(s) have dedicated all copyright 6 | // and related and neighboring rights to this software to the public domain 7 | // worldwide. This software is distributed without any warranty. 8 | // 9 | // You should have received a copy of the CC0 Public Domain Dedication along 10 | // with this software. If not, see 11 | // . 12 | 13 | using System; 14 | using System.Collections; 15 | using System.Collections.Generic; 16 | using Simplecs.Containers; 17 | 18 | namespace Simplecs.Views { 19 | internal static class ViewUtility { 20 | internal static IComponentTable SmallestTable(IComponentTable table, IComponentTable? extraTable) => extraTable != null && extraTable.Count < table.Count ? extraTable : table; 21 | } 22 | 23 | /// 24 | /// A collection of entities that match a particular signature. 25 | /// 26 | public interface IView : IEnumerable where RowT : struct { 27 | /// 28 | /// Retrieves the row for a given entity. 29 | /// 30 | /// True if the entity is contained by view. 31 | bool TryBindRow(Entity entity, out RowT row); 32 | 33 | /// 34 | /// Enumerator for matched entities and components. 35 | /// 36 | new ViewEnumerator GetEnumerator(); 37 | 38 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 39 | IEnumerator IEnumerable.GetEnumerator() => throw new NotImplementedException(); 40 | } 41 | 42 | /// 43 | /// View over entities matching a specific signature. 44 | /// 45 | public sealed class View : IView> where T : struct { 46 | private readonly ViewPredicate _predicate; 47 | 48 | internal readonly ComponentTable Table; 49 | 50 | internal View(ComponentTable table, ViewPredicate predicate) => (_predicate, Table) = (predicate, table); 51 | 52 | bool IView>.TryBindRow(Entity entity, out ViewRow row) { 53 | row = default(ViewRow); 54 | int index = Table.IndexOf(entity); 55 | if (index == -1) return false; 56 | row = new ViewRow(this, entity, index); 57 | return _predicate.IsAllowed(entity); 58 | } 59 | 60 | ViewEnumerator> IView>.GetEnumerator() => new ViewEnumerator>(this, ViewUtility.SmallestTable(Table, _predicate.SmallestTable())); 61 | } 62 | 63 | /// 64 | /// View over entities matching a specific signature. 65 | /// 66 | public sealed class View : IView> 67 | where T1 : struct 68 | where T2 : struct { 69 | private readonly ViewPredicate _predicate; 70 | 71 | internal readonly ComponentTable Table1; 72 | internal readonly ComponentTable Table2; 73 | 74 | internal View(ComponentTable table1, ComponentTable table2, ViewPredicate predicate) => (_predicate, Table1, Table2) = (predicate, table1, table2); 75 | 76 | bool IView>.TryBindRow(Entity entity, out ViewRow row) { 77 | row = default(ViewRow); 78 | int index1 = Table1.IndexOf(entity); 79 | if (index1 == -1) return false; 80 | int index2 = Table2.IndexOf(entity); 81 | if (index2 == -1) return false; 82 | row = new ViewRow(this, entity, index1, index2); 83 | return _predicate.IsAllowed(entity); 84 | } 85 | 86 | ViewEnumerator> IView>.GetEnumerator() => new ViewEnumerator>(this, ViewUtility.SmallestTable(SmallestTable(), _predicate.SmallestTable())); 87 | private IComponentTable SmallestTable() { 88 | if (Table2.Count < Table1.Count) return Table2; 89 | return Table1; 90 | } 91 | } 92 | 93 | /// 94 | /// View over entities matching a specific signature. 95 | /// 96 | public sealed class View : IView> 97 | where T1 : struct 98 | where T2 : struct 99 | where T3 : struct { 100 | private readonly ViewPredicate _predicate; 101 | 102 | internal readonly ComponentTable Table1; 103 | internal readonly ComponentTable Table2; 104 | internal readonly ComponentTable Table3; 105 | 106 | internal View(ComponentTable table1, ComponentTable table2, ComponentTable table3, ViewPredicate predicate) => (_predicate, Table1, Table2, Table3) = (predicate, table1, table2, table3); 107 | 108 | bool IView>.TryBindRow(Entity entity, out ViewRow row) { 109 | row = default(ViewRow); 110 | int index1 = Table1.IndexOf(entity); 111 | if (index1 == -1) return false; 112 | int index2 = Table2.IndexOf(entity); 113 | if (index2 == -1) return false; 114 | int index3 = Table3.IndexOf(entity); 115 | if (index3 == -1) return false; 116 | row = new ViewRow(this, entity, index1, index2, index3); 117 | return _predicate.IsAllowed(entity); 118 | } 119 | 120 | ViewEnumerator> IView>.GetEnumerator() => new ViewEnumerator>(this, ViewUtility.SmallestTable(SmallestTable(), _predicate.SmallestTable())); 121 | private IComponentTable SmallestTable() { 122 | if (Table2.Count < Table1.Count && Table2.Count < Table3.Count) return Table2; 123 | if (Table3.Count < Table1.Count && Table3.Count < Table2.Count) return Table3; 124 | return Table1; 125 | } 126 | } 127 | } -------------------------------------------------------------------------------- /Simplecs/Views/ViewEnumerator.cs: -------------------------------------------------------------------------------- 1 | // Simplecs - Simple ECS for C# 2 | // 3 | // Written in 2019 by Sean Middleditch (http://github.com/seanmiddleditch) 4 | // 5 | // To the extent possible under law, the author(s) have dedicated all copyright 6 | // and related and neighboring rights to this software to the public domain 7 | // worldwide. This software is distributed without any warranty. 8 | // 9 | // You should have received a copy of the CC0 Public Domain Dedication along 10 | // with this software. If not, see 11 | // . 12 | 13 | using System; 14 | using System.Collections; 15 | using System.Collections.Generic; 16 | using Simplecs.Containers; 17 | 18 | namespace Simplecs.Views { 19 | /// 20 | /// Enumerator for rows in a view. 21 | /// 22 | public struct ViewEnumerator : IEnumerator where RowT : struct { 23 | private readonly IView _view; 24 | private EntityEnumerator _entities; 25 | private RowT _row; 26 | 27 | internal ViewEnumerator(IView view, IComponentTable table) => (_view, _entities, _row) = (view, new EntityEnumerator(table), default(RowT)); 28 | 29 | /// Current row. 30 | public RowT Current => _row; 31 | object? IEnumerator.Current => throw new NotImplementedException(); 32 | 33 | /// Attempt to increment enumerator. 34 | public bool MoveNext() => _entities.MoveNext(_view, out _row); 35 | /// Reset enumerator to initial state. 36 | public void Reset() => _entities.Reset(); 37 | /// Dispose of the enumerator. 38 | public void Dispose() { } 39 | } 40 | } -------------------------------------------------------------------------------- /Simplecs/Views/ViewPredicate.cs: -------------------------------------------------------------------------------- 1 | // Simplecs - Simple ECS for C# 2 | // 3 | // Written in 2019 by Sean Middleditch (http://github.com/seanmiddleditch) 4 | // 5 | // To the extent possible under law, the author(s) have dedicated all copyright 6 | // and related and neighboring rights to this software to the public domain 7 | // worldwide. This software is distributed without any warranty. 8 | // 9 | // You should have received a copy of the CC0 Public Domain Dedication along 10 | // with this software. If not, see 11 | // . 12 | 13 | using Simplecs.Containers; 14 | 15 | namespace Simplecs.Views { 16 | internal struct ViewPredicate { 17 | private IComponentTable[]? _tables; 18 | private int _excludedCount; 19 | 20 | internal ViewPredicate(IComponentTable[]? tables, int excludedCount) => (_tables, _excludedCount) = (tables, excludedCount); 21 | 22 | internal bool IsAllowed(Entity entity) { 23 | // If we have no tables at all, there's nothing to check. 24 | // 25 | if (_tables == null) { 26 | return true; 27 | } 28 | 29 | // If the entity exists in any excluded table, the entity is not allowed. 30 | // 31 | for (int index = 0; index != _excludedCount; ++index) { 32 | if (_tables[index].Contains(entity)) { 33 | return false; 34 | } 35 | } 36 | 37 | // If the entity is missing from any required table, the entity is not allowed. 38 | // 39 | for (int index = _excludedCount; index != _tables.Length; ++index) { 40 | if (!_tables[index].Contains(entity)) { 41 | return false; 42 | } 43 | } 44 | 45 | // The entity was not rejected by any test so it must be allowed. 46 | // 47 | return true; 48 | } 49 | 50 | internal IComponentTable? SmallestTable() { 51 | IComponentTable? bestTable = null; 52 | int bestCount = int.MaxValue; 53 | 54 | if (_tables == null) { 55 | return null; 56 | } 57 | 58 | // Find the smallest _required component_ table 59 | // 60 | for (int index = _excludedCount; index != _tables.Length; ++index) { 61 | if (_tables[index].Count < bestCount) { 62 | bestTable = _tables[index]; 63 | } 64 | } 65 | 66 | return bestTable; 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /Simplecs/Views/ViewRow.cs: -------------------------------------------------------------------------------- 1 | // Simplecs - Simple ECS for C# 2 | // 3 | // Written in 2019 by Sean Middleditch (http://github.com/seanmiddleditch) 4 | // 5 | // To the extent possible under law, the author(s) have dedicated all copyright 6 | // and related and neighboring rights to this software to the public domain 7 | // worldwide. This software is distributed without any warranty. 8 | // 9 | // You should have received a copy of the CC0 Public Domain Dedication along 10 | // with this software. If not, see 11 | // . 12 | 13 | namespace Simplecs.Views { 14 | /// 15 | /// Single row or set of components for a given view. 16 | /// 17 | public readonly struct ViewRow where T : struct { 18 | private readonly View _view; 19 | private readonly int _index; 20 | 21 | /// Entity of current row. 22 | public Entity Entity { get; } 23 | /// Component of current row. 24 | public ref T Component => ref _view.Table.ReferenceAt(Entity, _index); 25 | 26 | internal ViewRow(View view, Entity entity, int index) => (_view, Entity, _index) = (view, entity, index); 27 | } 28 | 29 | /// 30 | /// Single row or set of components for a given view. 31 | /// 32 | public readonly struct ViewRow 33 | where T1 : struct 34 | where T2 : struct { 35 | private readonly View _view; 36 | private readonly int _index1, _index2; 37 | 38 | /// Entity of current row. 39 | public Entity Entity { get; } 40 | /// First component of current row. 41 | public ref T1 Component1 => ref _view.Table1.ReferenceAt(Entity, _index1); 42 | /// Second component of current row. 43 | public ref T2 Component2 => ref _view.Table2.ReferenceAt(Entity, _index2); 44 | 45 | internal ViewRow(View view, Entity entity, int index1, int index2) => (_view, Entity, _index1, _index2) = (view, entity, index1, index2); 46 | } 47 | 48 | /// 49 | /// Single row or set of components for a given view. 50 | /// 51 | public readonly struct ViewRow 52 | where T1 : struct 53 | where T2 : struct 54 | where T3 : struct { 55 | private readonly View _view; 56 | private readonly int _index1, _index2, _index3; 57 | 58 | /// Entity of current row. 59 | public Entity Entity { get; } 60 | /// First component of current row. 61 | public ref T1 Component1 => ref _view.Table1.ReferenceAt(Entity, _index1); 62 | /// Second component of current row. 63 | public ref T2 Component2 => ref _view.Table2.ReferenceAt(Entity, _index2); 64 | /// Third component of current row. 65 | public ref T3 Component3 => ref _view.Table3.ReferenceAt(Entity, _index3); 66 | 67 | internal ViewRow(View view, Entity entity, int index1, int index2, int index3) => (_view, Entity, _index1, _index2, _index3) = (view, entity, index1, index2, index3); 68 | 69 | } 70 | } -------------------------------------------------------------------------------- /Simplecs/World.cs: -------------------------------------------------------------------------------- 1 | // Simplecs - Simple ECS for C# 2 | // 3 | // Written in 2019 by Sean Middleditch (http://github.com/seanmiddleditch) 4 | // 5 | // To the extent possible under law, the author(s) have dedicated all copyright 6 | // and related and neighboring rights to this software to the public domain 7 | // worldwide. This software is distributed without any warranty. 8 | // 9 | // You should have received a copy of the CC0 Public Domain Dedication along 10 | // with this software. If not, see 11 | // . 12 | 13 | using System; 14 | using System.Collections.Generic; 15 | using Simplecs.Containers; 16 | using Simplecs.Views; 17 | 18 | namespace Simplecs { 19 | /// 20 | /// Contains a collection of entities and associated components. 21 | /// 22 | public sealed class World { 23 | private readonly EntityAllocator _entities = new EntityAllocator(); 24 | private readonly Dictionary _tables = new Dictionary(); 25 | 26 | /// 27 | /// Creates a new entity. 28 | /// 29 | /// A builder object that can be used to attach components or extract the id. 30 | public EntityBuilder CreateEntity() => new EntityBuilder(world: this, entity: _entities.Allocate()); 31 | 32 | /// 33 | /// Creates a new view builder. 34 | /// 35 | /// A builder object used to construct a view. 36 | public ViewBuilder CreateView() => new ViewBuilder(this); 37 | 38 | /// 39 | /// Destroys a given entity and all associated components. 40 | /// 41 | /// Entity to destroy 42 | /// True if the given entity was found and destroyed. 43 | public bool Destroy(Entity entity) { 44 | if (!_entities.IsValid(entity)) { 45 | return false; 46 | } 47 | 48 | foreach (var table in _tables.Values) { 49 | table.Remove(entity); 50 | } 51 | 52 | return _entities.Deallocate(entity); 53 | } 54 | 55 | /// 56 | /// Checks if a given entity exists. 57 | /// 58 | /// The entity may have no components, however. 59 | /// 60 | /// Entity to test. 61 | /// True if the entity exists. 62 | public bool Exists(Entity entity) => _entities.IsValid(entity); 63 | 64 | /// 65 | /// Attaches a component to an existing entity. 66 | /// 67 | /// Entity the component will be attached to. 68 | /// Component to attach. 69 | public void Attach(Entity entity, in T component) where T : struct { 70 | if (!_entities.IsValid(entity)) { 71 | throw new InvalidOperationException(message: "Invalid entity key"); 72 | } 73 | 74 | var table = GetTable(); 75 | table.Add(entity, component); 76 | } 77 | 78 | /// 79 | /// Attaches a component to an existing entity. 80 | /// 81 | /// Entity the component will be attached to. 82 | /// Component to attach. 83 | public void Attach(Entity entity, object component) { 84 | if (!_entities.IsValid(entity)) { 85 | throw new InvalidOperationException(message: "Invalid entity key"); 86 | } 87 | 88 | var table = GetTable(component.GetType()); 89 | table.Add(entity, component); 90 | } 91 | 92 | /// 93 | /// Removes a component from an existing entity. 94 | /// 95 | /// Entity whose component should be destroyed. 96 | /// True if the given component was found and destroyed. 97 | public bool Detach(Entity entity) where T : struct { 98 | _tables.TryGetValue(typeof(T), out IComponentTable? table); 99 | var components = table as ComponentTable; 100 | return components != null && components.Remove(entity); 101 | } 102 | 103 | /// 104 | /// Removes a component from an existing entity. 105 | /// 106 | /// Entity whose component should be destroyed. 107 | /// /// Type of component that should be destroyed. 108 | /// True if the given component was found and destroyed. 109 | public bool Detach(Entity entity, Type componentType) { 110 | return _tables.TryGetValue(componentType, out IComponentTable? table) && table != null && table.Remove(entity); 111 | } 112 | 113 | /// 114 | /// Checks if the specified entity has the specified component type attached. 115 | /// 116 | public bool HasComponent(Entity entity) where T : struct { 117 | return HasComponent(entity, typeof(T)); 118 | } 119 | 120 | /// 121 | /// Checks if the specified entity has the specified component type attached. 122 | /// 123 | public bool HasComponent(Entity entity, Type component) { 124 | return _tables.TryGetValue(component, out IComponentTable? table) && table != null && table.Contains(entity); 125 | } 126 | 127 | /// 128 | /// Retrives the component on the specified entity.null 129 | /// 130 | /// Illegal to call if the component does not exist. 131 | /// 132 | public ref T GetComponent(Entity entity) where T : struct { 133 | if (!_tables.TryGetValue(typeof(T), out IComponentTable? generic) || !(generic is ComponentTable typed)) { 134 | throw new InvalidOperationException(message: $"Unknown component type: {typeof(T).FullName}"); 135 | } 136 | 137 | int index = typed.IndexOf(entity); 138 | if (index == -1) { 139 | throw new InvalidOperationException(message: "Entity does not have requested component"); 140 | } 141 | 142 | return ref typed.ReferenceAt(entity, index); 143 | } 144 | 145 | /// 146 | /// Retrives the component on the specified entity.null 147 | /// 148 | /// Illegal to call if the component does not exist. 149 | /// 150 | public object GetComponent(Entity entity, Type component) { 151 | if (!_tables.TryGetValue(component, out IComponentTable? table) || table == null || !table.TryGet(entity, out object data)) { 152 | throw new InvalidOperationException(message: "Entity does not have requested component"); 153 | } 154 | 155 | return data; 156 | } 157 | 158 | /// 159 | /// Enumerates all the components attached to a given entity. 160 | /// 161 | /// Note that these are returned as objects, so boxing for each component will 162 | /// be involved! Further, modifications to these instances will not apply 163 | /// automatically to the stored components; if modified, components should be 164 | /// re-Attached. 165 | /// 166 | /// Entity whose components should be enumerated. 167 | /// Iterator of boxed copies of the component on the entity. 168 | public IEnumerable ComponentsOn(Entity entity) { 169 | if (!_entities.IsValid(entity)) { 170 | throw new InvalidOperationException(message: "Invalid entity key"); 171 | } 172 | 173 | foreach ((var componentType, IComponentTable table) in _tables) { 174 | if (table.Contains(entity)) { 175 | yield return componentType; 176 | } 177 | } 178 | } 179 | 180 | internal ComponentTable GetTable() where T : struct { 181 | if (_tables.TryGetValue(typeof(T), out IComponentTable? generic) && generic is ComponentTable typed) { 182 | return typed; 183 | } 184 | 185 | var table = new ComponentTable(); 186 | _tables.Add(table.Type, table); 187 | return table; 188 | } 189 | 190 | internal IComponentTable GetTable(Type type) { 191 | if (_tables.TryGetValue(type, out IComponentTable? generic) && generic != null) { 192 | return generic; 193 | } 194 | 195 | var table = CreateTable(type); 196 | _tables.Add(table.Type, table); 197 | return table; 198 | } 199 | 200 | private IComponentTable CreateTable(Type type) { 201 | if (!type.IsValueType) { 202 | throw new InvalidOperationException(message: "Component types must be value types"); 203 | } 204 | 205 | var tableGenericType = typeof(ComponentTable<>); 206 | var tableType = tableGenericType.MakeGenericType(type); 207 | 208 | var tableObject = Activator.CreateInstance(tableType); 209 | var table = tableType as IComponentTable; 210 | if (table == null) { 211 | throw new InvalidCastException(message: "Failed to cast created table to IComponentTable"); 212 | } 213 | 214 | return (IComponentTable)table; 215 | } 216 | } 217 | } -------------------------------------------------------------------------------- /SimplecsTests/ComponentTableTest.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using Simplecs; 3 | using Simplecs.Containers; 4 | using System.Linq; 5 | 6 | namespace SimplecsTests { 7 | public class ComponentTableTest { 8 | static readonly Entity key1 = EntityUtil.MakeKey(7); 9 | static readonly Entity key2 = EntityUtil.MakeKey(90, generation: 4); 10 | static readonly Entity key3 = EntityUtil.MakeKey(2, generation: 2); 11 | 12 | [Test] 13 | public void Set() { 14 | var table = new ComponentTable(); 15 | 16 | table.Add(key1, new NameComponent { name = "Bob" }); 17 | table.Add(key2, new NameComponent { name = "Susan" }); 18 | table.Add(key3, new NameComponent { name = "Frank" }); 19 | 20 | Assert.AreEqual(expected: 3, actual: table.Count); 21 | } 22 | 23 | [Test] 24 | public void SetReplace() { 25 | var table = new ComponentTable(); 26 | 27 | table.Add(key1, new NameComponent { name = "Bob" }); 28 | table.Add(key1, new NameComponent { name = "Susan" }); 29 | 30 | Assert.AreEqual(expected: 1, table.Count); 31 | Assert.AreEqual(expected: key1, actual: table.EntityAt(0)); 32 | Assert.AreEqual(expected: "Susan", actual: table.TryGet(key1, out var name) ? name.name : null); 33 | } 34 | 35 | [Test] 36 | public void Cycle() { 37 | var table = new ComponentTable(); 38 | 39 | table.Add(key1, new NameComponent { name = "Bob" }); 40 | table.Add(key3, new NameComponent { name = "Frank" }); 41 | Assert.AreEqual(expected: 2, actual: table.Count); 42 | Assert.AreEqual(expected: key1, actual: table.EntityAt(0)); 43 | Assert.AreEqual(expected: "Bob", actual: table.TryGet(key1, out var name) ? name.name : null); 44 | 45 | Assert.IsTrue(table.Remove(key1)); 46 | Assert.IsFalse(table.Remove(key1)); 47 | Assert.AreEqual(expected: 1, actual: table.Count); 48 | 49 | Assert.AreEqual(expected: key3, actual: table.EntityAt(0)); 50 | Assert.AreEqual(expected: "Frank", actual: table.TryGet(key3, out name) ? name.name : null); 51 | 52 | table.Add(key2, new NameComponent { name = "Susan" }); 53 | Assert.AreEqual(expected: 2, actual: table.Count); 54 | 55 | Assert.IsTrue(table.Remove(key3)); 56 | Assert.AreEqual(expected: 1, actual: table.Count); 57 | 58 | Assert.AreEqual(expected: key2, actual: table.EntityAt(0)); 59 | Assert.AreEqual(expected: "Susan", actual: table.TryGet(key2, out name) ? name.name : null); 60 | } 61 | 62 | [Test] 63 | public void Index() { 64 | var table = new ComponentTable(); 65 | 66 | table.Add(key1, new IntComponent { x = 1 }); 67 | table.Add(key2, new IntComponent { x = 2 }); 68 | table.Add(key3, new IntComponent { x = 3 }); 69 | 70 | Assert.AreEqual(expected: 2, actual: table.TryGet(key2, out var intComp) ? intComp.x : 0); 71 | Assert.AreEqual(expected: 3, actual: table.TryGet(key3, out intComp) ? intComp.x : 0); 72 | 73 | table.Remove(key2); 74 | 75 | table.Add(key2, new IntComponent { x = 4 }); 76 | 77 | Assert.AreEqual(expected: 4, actual: table.TryGet(key2, out intComp) ? intComp.x : 0); 78 | Assert.AreEqual(expected: 3, actual: table.TryGet(key3, out intComp) ? intComp.x : 0); 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /SimplecsTests/Components.cs: -------------------------------------------------------------------------------- 1 | using Simplecs; 2 | 3 | namespace SimplecsTests { 4 | internal struct NameComponent { 5 | public string name; 6 | } 7 | 8 | internal struct IntComponent { 9 | public int x; 10 | } 11 | } -------------------------------------------------------------------------------- /SimplecsTests/EntityAllocatorTest.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using NUnit.Framework; 3 | using Simplecs; 4 | using Simplecs.Containers; 5 | 6 | namespace SimplecsTests { 7 | public class EntityAllocatorTest { 8 | [Test] 9 | public void AllocateAndFreeList() { 10 | var allocator = new EntityAllocator(); 11 | 12 | Assert.AreEqual(expected: EntityUtil.MakeKey(0), actual: allocator.Allocate()); 13 | Assert.AreEqual(expected: EntityUtil.MakeKey(1), actual: allocator.Allocate()); 14 | Assert.AreEqual(expected: EntityUtil.MakeKey(2), actual: allocator.Allocate()); 15 | 16 | // Generate enough trash to ensure ids are recycled after we delete them below. 17 | // 18 | var trash = new List(capacity: EntityAllocator.FreeMinimum); 19 | for (int counter = 0; counter != EntityAllocator.FreeMinimum; ++counter) { 20 | trash.Add(allocator.Allocate()); 21 | } 22 | 23 | Assert.IsTrue(allocator.Deallocate(EntityUtil.MakeKey(1))); 24 | Assert.IsTrue(allocator.Deallocate(EntityUtil.MakeKey(2))); 25 | Assert.IsFalse(allocator.Deallocate(EntityUtil.MakeKey(EntityAllocator.FreeMinimum + 3))); 26 | Assert.IsTrue(allocator.Deallocate(EntityUtil.MakeKey(0))); 27 | 28 | // Deallocate all the trash so the free list is full 29 | // 30 | foreach (Entity trashEntity in trash) { 31 | allocator.Deallocate(trashEntity); 32 | } 33 | 34 | // Generation will be bumped for the recycled indices. 35 | // 36 | Assert.AreEqual(expected: EntityUtil.MakeKey(1, generation: 2), actual: allocator.Allocate()); 37 | Assert.AreEqual(expected: EntityUtil.MakeKey(2, generation: 2), actual: allocator.Allocate()); 38 | Assert.AreEqual(expected: EntityUtil.MakeKey(0, generation: 2), actual: allocator.Allocate()); 39 | 40 | // Exhaust keys again so we are allocating only new entities 41 | for (int counter = 0; counter != EntityAllocator.FreeMinimum; ++counter) { 42 | allocator.Allocate(); 43 | } 44 | 45 | // But not for new indices. 46 | // 47 | int totalAllocated = EntityAllocator.FreeMinimum + EntityAllocator.FreeMinimum + 2; 48 | Assert.AreEqual(expected: EntityUtil.MakeKey(totalAllocated), actual: allocator.Allocate()); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /SimplecsTests/SimplecsTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.0 5 | false 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /SimplecsTests/ViewTest.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using Simplecs; 3 | using Simplecs.Containers; 4 | using System; 5 | using System.Linq; 6 | 7 | namespace SimplecsTests { 8 | public class ViewTest { 9 | [Test] 10 | public void EachMutable() { 11 | var world = new World(); 12 | world.CreateEntity().Attach(new IntComponent { x = 1 }); 13 | world.CreateEntity().Attach(new IntComponent { x = 2 }); 14 | world.CreateEntity().Attach(new IntComponent { x = 3 }); 15 | 16 | var view = world.CreateView().Select(); 17 | foreach (var row in view) { 18 | row.Component.x *= row.Component.x; 19 | } 20 | 21 | Assert.AreEqual(expected: 1, actual: view.ElementAt(0).Component.x); 22 | Assert.AreEqual(expected: 4, actual: view.ElementAt(1).Component.x); 23 | Assert.AreEqual(expected: 9, actual: view.ElementAt(2).Component.x); 24 | } 25 | 26 | [Test] 27 | public void Require() { 28 | var world = new World(); 29 | world.CreateEntity().Attach(new IntComponent { x = 1 }); 30 | world.CreateEntity().Attach(new NameComponent { name = "Bob" }).Attach(new IntComponent { x = 2 }); 31 | world.CreateEntity().Attach(new IntComponent { x = 3 }); 32 | world.CreateEntity().Attach(new NameComponent { name = "Susan" }).Attach(new IntComponent { x = 4 }); 33 | 34 | var view = world.CreateView().Require().Select(); 35 | 36 | Assert.AreEqual(expected: 2, actual: view.ElementAt(0).Component.x); 37 | Assert.AreEqual(expected: 4, actual: view.ElementAt(1).Component.x); 38 | } 39 | 40 | [Test] 41 | public void Exclude() { 42 | var world = new World(); 43 | world.CreateEntity().Attach(new NameComponent { name = "Bob" }).Attach(new IntComponent { x = 2 }); 44 | world.CreateEntity().Attach(new IntComponent { x = 1 }); 45 | world.CreateEntity().Attach(new NameComponent { name = "Susan" }).Attach(new IntComponent { x = 4 }); 46 | world.CreateEntity().Attach(new IntComponent { x = 3 }); 47 | 48 | var view = world.CreateView().Exclude().Select(); 49 | 50 | Assert.AreEqual(expected: 1, actual: view.ElementAt(0).Component.x); 51 | Assert.AreEqual(expected: 3, actual: view.ElementAt(1).Component.x); 52 | } 53 | 54 | [Test] 55 | public void Match() { 56 | var world = new World(); 57 | var entity = world.CreateEntity() 58 | .Attach(new NameComponent { name = "Bob" }) 59 | .Attach(new IntComponent { x = 7 }) 60 | .Entity; 61 | 62 | world.CreateEntity() 63 | .Attach(new NameComponent { name = "Susan" }) 64 | .Attach(new IntComponent { x = 90 }); 65 | 66 | var nameView = world.CreateView().Select(); 67 | var intView = world.CreateView().Select(); 68 | var bothView = world.CreateView().Select(); 69 | 70 | Assert.IsTrue(nameView.Any()); 71 | Assert.IsTrue(intView.Any()); 72 | Assert.IsTrue(bothView.Any()); 73 | 74 | Assert.AreEqual(expected: new NameComponent { name = "Bob" }, actual: nameView.FirstOrDefault().Component); 75 | Assert.AreEqual(expected: new IntComponent { x = 7 }, actual: intView.FirstOrDefault().Component); 76 | 77 | Assert.AreEqual(expected: new NameComponent { name = "Bob" }, actual: bothView.FirstOrDefault().Component1); 78 | Assert.AreEqual(expected: new IntComponent { x = 7 }, actual: bothView.FirstOrDefault().Component2); 79 | } 80 | 81 | [Test] 82 | public void Access() { 83 | var world = new World(); 84 | var entity1 = world.CreateEntity() 85 | .Attach(new NameComponent { name = "Bob" }) 86 | .Attach(new IntComponent { x = 7 }) 87 | .Entity; 88 | 89 | var entity2 = world.CreateEntity() 90 | .Attach(new NameComponent { name = "Susan" }) 91 | .Attach(new IntComponent { x = 90 }) 92 | .Entity; 93 | 94 | var entity3 = world.CreateEntity() 95 | .Attach(new IntComponent { x = -1 }) 96 | .Entity; 97 | 98 | var view = world.CreateView().Select(); 99 | 100 | Assert.IsTrue(view.Any(row => row.Entity == entity1)); 101 | Assert.IsTrue(view.Any(row => row.Entity == entity2)); 102 | Assert.IsFalse(view.Any(row => row.Entity == entity3)); 103 | 104 | Assert.AreEqual(expected: 7, actual: view.Table1.TryGet(entity1, out var comp1) ? comp1.x : 0); 105 | Assert.AreEqual(expected: 90, actual: view.Table1.TryGet(entity2, out var comp2) ? comp2.x : 0); 106 | } 107 | 108 | [Test] 109 | public void DestroyLoop() { 110 | var world = new World(); 111 | 112 | var view = world.CreateView().Select(); 113 | 114 | for (int index = 0; index < EntityAllocator.FreeMinimum; ++index) { 115 | world.CreateEntity().Attach(new IntComponent()); 116 | } 117 | 118 | Assert.AreEqual(expected: EntityAllocator.FreeMinimum, actual: view.Count()); 119 | 120 | foreach (var row in view) { 121 | world.Destroy(row.Entity); 122 | } 123 | 124 | Assert.AreEqual(expected: 0, actual: view.Count()); 125 | } 126 | 127 | // [Test] 128 | // public void Invalidate() { 129 | // var world = new World(); 130 | // world.CreateEntity().Attach(new IntComponent { x = 7 }); 131 | // var entity = world.CreateEntity().Attach(new IntComponent { x = 9 }).Entity; 132 | // world.CreateEntity().Attach(new IntComponent { x = 11 }); 133 | 134 | // var view = world.CreateView().Select(); 135 | 136 | // Assert.Throws(() => { 137 | // foreach (var row in view) { 138 | // world.Destroy(entity); 139 | // } 140 | // }); 141 | // } 142 | } 143 | } -------------------------------------------------------------------------------- /SimplecsTests/WorldTest.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using Simplecs; 3 | using System; 4 | using System.Linq; 5 | 6 | namespace SimplecsTests { 7 | public class WorldTest { 8 | [Test] 9 | public void Create() { 10 | var world = new World(); 11 | var entity = world.CreateEntity() 12 | .Attach(new NameComponent { name = "Bob" }) 13 | .Attach(new IntComponent { x = 7 }) 14 | .Entity; 15 | 16 | world.CreateEntity() 17 | .Attach(new NameComponent { name = "Susan" }) 18 | .Attach(new IntComponent { x = 90 }); 19 | 20 | var names = world.GetTable(); 21 | var ints = world.GetTable(); 22 | 23 | Assert.AreEqual(expected: 2, actual: names.Count); 24 | Assert.AreEqual(expected: 2, actual: ints.Count); 25 | } 26 | 27 | [Test] 28 | public void Destroy() { 29 | var world = new World(); 30 | var entity = world.CreateEntity() 31 | .Attach(new NameComponent { name = "Bob" }) 32 | .Entity; 33 | 34 | world.CreateEntity() 35 | .Attach(new NameComponent { name = "Susan" }) 36 | .Attach(new IntComponent { x = 90 }); 37 | 38 | var names = world.GetTable(); 39 | var ints = world.GetTable(); 40 | 41 | Assert.AreEqual(expected: 2, actual: names.Count); 42 | Assert.AreEqual(expected: 1, actual: ints.Count); 43 | 44 | world.Destroy(entity); 45 | 46 | Assert.AreEqual(expected: 1, actual: names.Count); 47 | Assert.AreEqual(expected: 1, actual: ints.Count); 48 | } 49 | 50 | [Test] 51 | public void ComponentsOn() { 52 | var world = new World(); 53 | var entity = world.CreateEntity() 54 | .Attach(new NameComponent { name = "Susan" }) 55 | .Attach(new IntComponent { x = 90 }) 56 | .Entity; 57 | 58 | Assert.AreEqual(expected: 2, world.ComponentsOn(entity).Count()); 59 | 60 | Type firstType = world.ComponentsOn(entity).ElementAt(0); 61 | Type secondType = world.ComponentsOn(entity).ElementAt(1); 62 | 63 | object first = world.GetComponent(entity, firstType); 64 | object second = world.GetComponent(entity, secondType); 65 | 66 | // Iteration order isn't guaranteed so be careful about assumptions here. 67 | // 68 | if (firstType == typeof(IntComponent)) { 69 | Assert.AreEqual(expected: typeof(NameComponent), actual: secondType); 70 | Assert.AreEqual(expected: 90, actual: ((IntComponent)first).x); 71 | Assert.AreEqual(expected: "Susan", actual: ((NameComponent)second).name); 72 | } else if (firstType == typeof(NameComponent)) { 73 | Assert.AreEqual(expected: typeof(NameComponent), actual: firstType); 74 | Assert.AreEqual(expected: "Susan", actual: ((NameComponent)first).name); 75 | Assert.AreEqual(expected: 90, actual: ((IntComponent)second).x); 76 | } else { 77 | Assert.Fail(message: $"Incorrect type retrieved by world.ComponentsOn: {firstType?.ToString()}"); 78 | } 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /omnisharp.json: -------------------------------------------------------------------------------- 1 | { 2 | "FormattingOptions": { 3 | "NewLinesForBracesInLambdaExpressionBody": false, 4 | "NewLinesForBracesInAnonymousMethods": false, 5 | "NewLinesForBracesInAnonymousTypes": false, 6 | "NewLinesForBracesInControlBlocks": false, 7 | "NewLinesForBracesInTypes": false, 8 | "NewLinesForBracesInMethods": false, 9 | "NewLinesForBracesInProperties": false, 10 | "NewLinesForBracesInObjectCollectionArrayInitializers": false, 11 | "NewLinesForBracesInAccessors": false, 12 | "NewLineForElse": false, 13 | "NewLineForCatch": false, 14 | "NewLineForFinally": false 15 | } 16 | } --------------------------------------------------------------------------------