{"id":13663232,"url":"https://github.com/seanmoakes/ClassicToHybrid","last_synced_at":"2025-04-25T13:32:18.167Z","repository":{"id":87850431,"uuid":"126287708","full_name":"seanmoakes/ClassicToHybrid","owner":"seanmoakes","description":"A look into how to implement Hybrid ECS in a classic Unity Project","archived":false,"fork":false,"pushed_at":"2022-02-11T14:41:24.000Z","size":3294,"stargazers_count":40,"open_issues_count":0,"forks_count":10,"subscribers_count":5,"default_branch":"master","last_synced_at":"2024-11-10T19:34:33.916Z","etag":null,"topics":["classic","ecs","hybrid","unity"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/seanmoakes.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-03-22T06:06:18.000Z","updated_at":"2023-07-28T18:10:59.000Z","dependencies_parsed_at":null,"dependency_job_id":"cbc4a4da-addd-41a7-af8c-4b394a9f114f","html_url":"https://github.com/seanmoakes/ClassicToHybrid","commit_stats":null,"previous_names":[],"tags_count":10,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/seanmoakes%2FClassicToHybrid","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/seanmoakes%2FClassicToHybrid/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/seanmoakes%2FClassicToHybrid/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/seanmoakes%2FClassicToHybrid/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/seanmoakes","download_url":"https://codeload.github.com/seanmoakes/ClassicToHybrid/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250825059,"owners_count":21493390,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["classic","ecs","hybrid","unity"],"created_at":"2024-08-02T05:02:21.910Z","updated_at":"2025-04-25T13:32:13.114Z","avatar_url":"https://github.com/seanmoakes.png","language":"C#","funding_links":[],"categories":["C\\#"],"sub_categories":[],"readme":"# Using ECS On Existing Unity Projects - Please Note a lot of this project is now out of date\n\n## Introduction\n\nIn this project I will show you a way of progressively integrating Unity's ECS in existing projects, using the ECS TwinStickShooter Sample projects for demonstration.\n\nIn the sample projects for the ECS, Unity include three versions of a TwinStickShooter project.\n\n- Classic: How the project would be implemented without ECS.\n- Hybrid: Make use of ECS systems while holding on to GameObjects etc.\n- Pure: Makes full use of ECS, so no GameObjects in sight.\n\nThese projects are great as examples of what might be achieved, but they do very little to show how a developer might implement use of the ECS in an existing project.\n\n## Requirements\n\n- You need to be using at least Unity 2018.1.0b12 to work with the ECS.\n- [Unity's Sample projects for the ECS](https://github.com/Unity-Technologies/EntityComponentSystemSamples)\n\n## What We Will Do\n\n- We are going to take the 'Classic' TwinStickShooter project use the 'Hybrid' project as a goal to work towards.\n- We will do this in steps, keeping the game working after each step.\n\n## How We Will Do This\n\n- Examine the 'Classic' Project to identify the existing components and systems, their behaviours, and how they relate to the GameObjects.\n- Design the systems we will use to replace the behaviours.\n- Create and implement the systems one at a time. Ensuring we don't break the game at each step.\n\n## The Classic Project\n\n### Scripts\n\n/Assets/GameCode\n\n- EnemySpawnSystem.cs\n- ShotSystem.cs\n- TwoStickBootstrap.cs\n- TwoStickExampleSettings.cs\n- UpdatePlayerHUD.cs\n\n/Assets/GameCode/Components\n\n- Enemy.cs\n- EnemyShootState.cs\n- Faction.cs\n- Health.cs\n- MoveSpeed.cs\n- Player.cs\n- Shot.cs\n- Transform2D.cs\n\n[A more in depth look at the Scripts](./script_details.md)\n\n### Prefabs\n\n- Enemy\n- EnemyFaction\n- EnemyShot\n- Player\n- PlayerFaction\n- PlayerShot\n\nLooking at each prefab, we can make a table to show the common components amongst the prefabs.\n\n| Component             | Enemy | EnemyFaction | Enemy Shot | Player | PlayerFaction | PlayerShot |\n| --------------------- |:-----:|:------------:|:----------:|:------:|:-------------:|:----------:|\n| Transform             |x      |x             |x           |x       |x              |x           |\n| MeshRenderer          |x      |              |x           |x       |               |x           |\n| Mesh_Filter           |x      |              |x           |x       |               |x           |\n| Enemy                 |x      |              |            |        |               |            |\n| EnemyShootState       |x      |              |            |        |               |            |\n| Faction               |x      |x             |x           |x       |x              |x           |\n| Health                |x      |              |            |x       |               |            |\n| MoveSpeed             |x      |              |x           |        |               |x           |\n| Transform2D           |x      |              |x           |x       |               |x           |\n| Shot                  |       |              |x           |        |               |x           |\n| Player                |       |              |            |x       |               |            |\n\n### Scene Objects\n\n- Main Camera\n- Directional Light\n- Settings - TwoStickExampleSettings.cs - default values for numerical data, prefab values need to be linked to the appropriate prefab.\n- EnemySpawner - EnemySpawnSystem.cs\n- StarField\n  - part_starfield\n  - part_starfield_distant\n- Canvas\n  - HealthText\n  - NewGameButton\n    - Text\n- EventSystem\n- HUD - UpdatePlayerHUD.cs data members linked to UI objects in Canvas.\n\n## System Design\n\nThe Systems used in the hybrid project follow a rough template.\n\n```C#\nusing Unity.Entities;  // Gives access to the ECS\nusing UnityEngine;\n\npublic class MySystem : ComponentSystem\n{\n    // One or more Structs of required components\n    struct Data\n    {\n        // The required Components\n    }\n\n    // Update to be run on all matching Entities.\n    protected override void OnUpdate()\n    {\n        // The Behavior\n    }\n}\n```\n\n### Required Component Structs\n\nWe can declare these simply as a [list of the components needed by the system and access them via the GetEntities method](https://github.com/Unity-Technologies/EntityComponentSystemSamples/blob/master/Documentation/content/getting_started.md#componentsystem---a-step-into-a-new-era), or we can [inject the data into a Component Group which can be iterated over to access the required Component types](https://github.com/Unity-Technologies/EntityComponentSystemSamples/blob/master/Documentation/content/ecs_in_detail.md#component-group-injection).\n\n## Identifying the Systems\n\nAs we are working towards the Hybrid Project, it makes sense to get the list of Systems from there. Obviously this can't be done for other projects, in those cases I would suggest that you create Systems by identifying the behaviours as we did in [the depth look at the Scripts](./script_details.md) and determine which behaviours you want to keep together, and which ones you want to split into seperate Systems.\n\nFrom the Hybrid Project we can see that the systems are:\n\n- DamageSystem\n- EnemyShootSystem\n- EnemySpawnSystem\n- MoveSystem\n- PlayerInputSystem\n- PlayerMoveSystem\n- RemoveDeadSystem\n- ShotDestroySystem\n- ShotSpawnSystem\n- SyncTransformSystem\n- UpdatePlayerHUD\n\nWe can also see some other noteworthy changes to the project:\n\n- There are additional component classes\n  - EnemySpawnSystemState - The data previously in EnemySpawnSystem.cs.\n  - Position2D - replaces the Position value from Transform2D.\n  - Heading2D - replaces the Heading value from Transform2D.\n- Transform2D has been removed.\n- The following have been renamed:\n  - Player - PlayerInput.\n  - ShotSystem - ShotSpawnSystem.\n- There is an additional EnemySpawnState prefab.\n\nNow we have a target to get to, let's get started.\n\n## Getting Started\n\nBefore making any systems, the easiest change to make is to rename the files as above. Then we can create the files EnemySpawnSystemState.cs, Position2D.cs and Heading2D.cs. Position2D and Heading2D should just contain a float2 called Value. Attach both Position2D and Heading2D to all gameObjects which have the Transform2D component.\n\nNext we want get ready to remove Transform2D.\n\n### SyncTransformSystem.cs\n\nWe know that Transform2D is responsible for updating the transform.position and transform.rotation of any gameObjects it is attached to, so before we can get rid of it, we need to replace the behaviour. We will do this in our first system, SyncTransformSystem.\n\nLook at the behaviour in Transform2D\n\n```C#\ntransform.position = new float3(Position.x, 0, Position.y);\ntransform.rotation = Quaternion.LookRotation(new Vector3(Heading.x, 0f, Heading.y), Vector3.up);\n```\n\nIn this we need access to the transform component, and the Transform2D component, so the required Components struct will look like this.\n\n```C#\n// The required Component struct: Data\npublic struct Data\n{\n    // The Transform2D Component, declared as ReadOnly as it will not be mutated.\n    [ReadOnly] public Transform2D FromTransform;\n    // The transform Component.\n    public Transform Output;\n}\n```\n\nThe behaviour will be in a method called OnUpdate.\n\n```C#\nprotected override void OnUpdate()\n{\n    // Perform the behaviour for all entities that have the required Components.\n    foreach (var entity in GetEntities\u003cData\u003e())\n    {\n        // Access the components via entity.\"Component Name\"\n        float2 p = entity.FromTransform.Position;\n        float2 h = entity.FromTransform.Heading;\n\n        //transform.position = new float3(Position.x, 0, Position.y);\n        entity.Output.position = new float3(p.x, 0, p.y);\n\n        //transform.rotation = Quaternion.LookRotation(new Vector3(Heading.x, 0f, Heading.y), Vector3.up);\n\n        // Only Apply if there is a heading input\n        if (!h.Equals(new float2(0f, 0f)))\n            entity.Output.rotation = Quaternion.LookRotation(new float3(h.x, 0f, h.y), new float3(0f, 1f, 0f));\n    }\n}\n```\n\nAs you can see, I first copied in the behaviour from Transform2D, then comment it out and replicate for the current context. You can also note that references to Vector3 are replaced with float3.\n\nThe completed file.\n\n```C#\nusing Unity.Collections;\nusing Unity.Entities;\nusing Unity.Mathematics;\nusing UnityEngine;\n\nnamespace TwoStickClassicExample\n{\n    public class SyncTransformSystem : ComponentSystem\n    {\n        public struct Data\n        {\n            [ReadOnly] public Transform2D FromTransform;\n            public Transform Output;\n        }\n\n        protected override void OnUpdate()\n        {\n            foreach (var entity in GetEntities\u003cData\u003e())\n            {\n\n                float2 p = entity.FromTransform.Position;\n                float2 h = entity.FromTransform.Heading;\n                entity.Output.position = new float3(p.x, 0, p.y);\n                if (!h.Equals(new float2(0f, 0f)))\n                    entity.Output.rotation = Quaternion.LookRotation(new float3(h.x, 0f, h.y), new float3(0f, 1f, 0f));\n            }\n        }\n    }\n\n}\n```\n\nTest this by comment out the LateUpdate function in Transform2D. If the game still works, then you know that your first component system is now up and running.\n\nFollowing this, I substituted all references to Transform2D  with Position2D and Heading2D. Tested the build, then removed the Transform2D Component from all prefabs. Tested the build once more and finally deleted Transform2.cs.\n\n## The Workflow\n\nIn implementing this first System, we have a potential workflow to use for the remaining systems.\n\n- Identify the behaviour(s).\n- Create the required component groups.\n- Copy behaviour into the System and adapt as necessary.\n- Comment out source behaviour to test. Delete when successful.\n- Push the successful build to VCS.\n\n## Moving Forward\n\nIf you want to challenge yourself then I would stop reading now and go and see if this workflow works for you.\n\nI [documented the process I followed in creating the systems](./SystemDesign.md), but I thought it might be useful to highlight some things I found interesting along the way.\n\n## Notes\n\n### Multiple Required Component Groups\n\nThe following systems use more than 1 required Component structs.\n\n- DamageSystem.cs\n  1. ReceiverData\n  2. ShotData\n- EnemyShootSystem.cs\n  1. Data\n  2. PlayerData\n- RemoveDeadSystem.cs\n  1. Entities\n  2. PlayerCheck\n- ShotDestroySystem.cs\n  1. Data\n  2. PlayerCheck\n\nIf we look at ShotDestroySystem.cs, we can see that it is possible to use both Types of Component Group array in the same System.\n\n### Use Length in injected Component Groups\n\nAdd \"public int Length;\" to Component Group Arrays that use Injection. When this is included, it is assigned the length of the return array, making it easy to iterate over the array's content.\n\n### Common Efficiency Improvements\n\nMany of the systems are made more efficient by moving functions outside of loops, this is often done with Time.deltaTime. Null checks are also added to the start of many Systems' OnUpdate function to abort at the first hurdle.\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fseanmoakes%2FClassicToHybrid","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fseanmoakes%2FClassicToHybrid","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fseanmoakes%2FClassicToHybrid/lists"}