Cysharp

Stars and mentions

Last 30 days, Oct 5 – Nov 3

GitHub stars133
mentions11
05101520Oct 5Oct 19Nov 3
UTC days · repositories Cysharp owns6 repositories

Package downloads

Cumulative recorded downloads · since 2025-11-01

npm7.6K
01.9K3.8K5.7K7.6KOct 5Oct 19Nov 3
Nov 2npm: 1/3 packages observed

Across 3 known packages in repositories Cysharp owns. Cumulative totals include recorded downloads before the selected range. Missing observations leave gaps; totals may be incomplete.

Top repositories

Cysharp’s repositories ranked by new stars, as on trending.

Loading

Contributes to

Repositories outside Cysharp’s account where Cysharp opened or reviewed pull requests in the past 365 days.

Who noticed Cysharp

Everyone who starred or forked Cysharp’s repositories since the archive began in 2011.

Notable accounts

Ordered by the stars their own repositories have received.

Biggest fans

Accounts that starred the most of these repositories.

Breakout weeks

Each repository’s best week of new stars, with the release published around it.

Mentions

11posts
Nov 40 posts
  1. Unity3D for .NET Developers: How to get started As a C#/.NET developer, have you ever thought about getting into game development with Unity3D? After all, it uses the same language. Is it really the same, though? Can you simply copy code written for .NET into a Unity3D application and expect it to work without any issues? Let's take a closer look to find out what extra knowledge C#/.NET developers need to comfortably and effectively develop games. !1424_unity3d/image1.png > We published and translated this article with the copyright holder's permission. The author is Kekchpek. Compilation and execution First, we'll compare how our code is compiled and executed in .NET and Unity3D. In .NET, the process is fairly straightforward and transparent. The C# code is first compiled into IL, then packaged into DLL or EXE files, and finally executed by a virtual machine. However, this approach has one drawback that has long posed a major obstacle to cross-platform development for C# programmers: dependence on a virtual machine. Today's .NET is nothing like its predecessor, .NET Framework, which was exclusively Windows-based. Before .NET emerged, the most popular solution for cross-platform development was the Mono virtual machine, which could also execute IL code and served as an alternative to .NET Framework. Unity3D originally ran on the Mono virtual machine. Today, Mono remains one of the options for running C# code in Unity3D. The second option is IL2CPP, a newer technology developed by Unity Technologies, which is gradually replacing Mono. Code compiled with IL2CPP runs faster and doesn't require a pre-installed or bundled virtual machine. As for how IL2CPP works, its name speaks for itself. Roslyn first compiles C# into IL, which is then translated into C++. Finally, the C++ compiler converts the code into machine code. The C++ compiler varies depending on the target platform. In terms of C#, however, the code behaves much like it does in .NET. Unity3D uses a very similar garbage collector, standard library, and so on. So, although the compilation process differs, the average developer won't notice much difference. One drawback is that it's nearly impossible to predict how the code will compile on different platforms. IL2CPP bugs are certainly a possibility, but encountering them is as unlikely as running into bugs in .NET. Any C# DLL libraries that contains IL can be used in Unity3D. During compilation, Unity runs the code through IL2CPP and recompiles it into C++. However, things aren't that simple when it comes to C# versions. Unity uses C# 9, which is rather outdated, and even that version lacks support for certain features. You can read more about it here. Libraries and compatibility As I mentioned earlier, libraries written in C# can be compiled for different platforms using IL2CPP, but there are some limitations. Unity supports compatibility with .NET Framework and .NET Standard libraries. However, that doesn't mean libraries compiled for .NET won't work. They might work, but not all of them will. It depends on whether their IL code contains anything that Unity3D can't translate into C++. The standard library works in Unity3D, but with a few caveats. In other words, we have access to all the features of standard C#, including collections, concurrency, Tasks, LINQ, networking, and more. However, Thread and Task (when used to run separate threads), for example, won't work in WebGL builds (games that run in browsers). This is one of the most obvious limitations, but one may occasionally encounter others related to the specifics of a particular platform. From a coding perspective, Unity3D acts as an extensive library within the UnityEngine namespace. The UnityEditor namespace also exists that contains the editor API, which enables developers customize windows, builds, plugins, and more. How to organize code into modules In .NET, we're used to organizing code into separate modules via .sln and .csproj files. Unity3D has them too, but we can't work with them directly. Unity3D creates .sln and .csproj files, purely for the developers' convenience. This allows popular IDEs to provide proper syntax highlighting, error detection, and other useful features. Unity itself doesn't rely on the contents of .sln or .csproj files at any stage of the project build process. As a result, one common mistake beginners make is adding a library via their IDE. This only modifies the .csproj file, but Unity remains unaware of the change. So, when a dev tries to compile the project, Unity starts throwing errors even though the IDE shows no errors at all. To control how Unity3D splits code into modules and what properties those modules have, we need to use Unity's built-in tools. The main feature we'll work with here is Assembly Definition, Unity's equivalent of a .csproj file in .NET. To use Assembly Definition, we need to create an .asmdef file in one of the project folders. We can do this via the Unity graphical interface. Once we create the file, Unity treats all .cs files in that folder and its subfolders as a separate module. It generates a dedicated .csproj file for this code and enables us to specify dependencies required by that particular module. We can also use the module as a dependency. We configure everything using Unity3D's graphical interface, and Unity reflects those settings in the generated .csproj files. By default, we don't create any .asmdef files, Unity automatically divides code into four modules based on the names of the folders containing the .cs files: - Assembly-CSharp-Editor-firstpass: the "*/Standard Assets/**/Editor", "*/Pro Standard Assets/**/Editor" and "*/Plugins/**/Editor" folders - Assembly-CSharp-firstpass: the "*/Standard Assets", "*/Pro Standard Assets" and "*/Plugins" folders - Assembly-CSharp-Editor: the "*/Editor" folders - Assembly-CSharp: any other folder Using Assembly Definitions is generally considered a good practice, though not every project has them. The key point to keep in mind is the difference between the Editor and Runtime modules. Editor contains code that runs only in the Unity editor and doesn't make it into the game build. Developers usually use this code to create additional tools for game designers, artists, and programmers. We can specify in any Assembly Definition that it's intended only for the editor. How to integrate third-party libraries The easiest way to add a third-party library is to simply copy its DLL into the project folder. Unity automatically scans the project folder for DLLs and adds them as dependencies to the generated .csproj files. In the Assembly Definition settings, we can explicitly specify which DLLs to include as dependencies and which to exclude. We can also directly add source code instead of using DLLs. This might seem a little strange to a .NET developer, but it's actually a fairly common way to distribute plugins and SDKs for Unity3D. Unity even has a dedicated packaging mechanism called .unitypackage. These files simplify the distribution of assets that need to be imported into specific parts of a project. Usually, when we want to download some open-source SDK, we get a .unitypackage file from GitHub and open it in the Unity editor. The editor adds the necessary source files to the project or updates existing ones, first enabling a developer to review the package contents and choose which files to import. Some SDKs even offer to remove outdated files from previous versions after importing the .unitypackage. However, I'm not sure whether this feature is built into the Unity editor or if SDK developers implemented it themselves. A more convenient way to manage dependencies is the Unity Package Manager. It's a package manager similar to npm, with a graphical interface built into the Unity Editor. As with npm, each package used in the project has a package.json file that describes the package and its dependencies. Every package stores this file in its root directory. The project itself, however, uses manifest.json instead of package.json, unlike npm. Despite the different name, manifest.json serves a similar purpose: it lists the project's dependencies and the package registries from which Unity can retrieve them. Instead of specifying a version, we can provide a link to a GitHub repository or a specific directory within that repository from which Unity should download the dependency. We can also use links to private repositories after configuring Git Credential Manager. Unfortunately, we can specify GitHub links only for dependencies listed in manifest.json. In other words, we can't use GitHub links for transitive dependencies (dependencies of other dependencies). This limitation makes managing dependencies less convenient when building infrastructure for large-scale projects. One solution is to create our own package registries and host them in the cloud. These registries can be either private or public. OpenUPM (Open Unity Package Manager) deserves a special mention. It's an open package registry for the standard Unity Package Manager where anyone can publish their packages. Most packages distributed as .unitypackage files are also available on OpenUPM. There are also many NuGet packages available on OpenUPM. However, not every NuGet package developer—or developer of any other package—takes the time to publish their packages on OpenUPM. As a result, some packages listed on OpenUPM are just forks of the original projects, tweaked to work with OpenUPM. I'd still advise against dismissing these packages outright. If you're a company that takes its code supply chain seriously and carefully controls what goes into its projects, the best approach is to set up your own package registry. You can then validate incoming packages and download everything from that registry. But if you're developing a small game, a prototype, or if you simply lack the resources to maintain a robust infrastructure for your project, OpenUPM is an excellent solution. And since this article is aimed at .NET developers, I'd like to mention NuGet for Unity. It's a plugin that adds a user-friendly interface for managing NuGet packages in Unity. I wouldn't recommend it, despite having used it a lot myself. The main disadvantage of using NuGet is that it brings multiple sources and dependency managers into a project. This can quickly turn into a major headache in large projects with lots of dependencies. Managing package versions and resolving conflicts between transitive dependencies can be serious challenges. In short, I recommend using the Unity Package Manager alongside OpenUPM and other package registries wherever possible. Resort to other ways of adding dependencies only when the Unity Package Manager isn't an option. Unity3D architecture Like many game engines, Unity3D uses a tree-based structure to organize objects. An application contains multiple scenes (UnityEngine.Scene), and each of them contains numerous game objects (UnityEngine.GameObject) that act as containers for components (UnityEngine.Component). A component is a basic logical unit that contains executable code. Game objects (GameObject) can also be arranged into hierarchical structures. !1424_unity3d/image2.png Every GameObject has one mandatory component that can't be removed: UnityEngine.Transform. It controls the object's position in the scene and its place in the hierarchy compared to other GameObjects. We can add or remove other components either while working with the scene in the editor or at runtime through code. Entry point I believe we have enough context now to move on to the more hands-on topics. The application entry point is the best place to start. In Unity3D, we don't have access to the Main function. Instead, we have to work within Unity's architecture, which loads the first scene in the scene list when the application starts. The only option available to us is to create a custom component and then link it to an object in the starting scene. Once the scene has loaded, the code in that component will start executing. To create a component, we'll create a class that inherits from UnityEngine.MonoBehaviour. Here's a simple example of a custom component: using UnityEngine; public class MyComponent : MonoBehaviour { private void Start() { // do something on component start working } private void Update() { // do something every frame } } Now, we can use the editor to create as many instances of this class as needed and attach them to any GameObject in the scene. Each component's Start method will be called at the beginning of its lifecycle, while Update will run every frame. Unity orders all these method calls according to its Script Lifecycle, which is a key architectural element in Unity3D. When designing how code interacts with the engine's API, developers need to understand exactly where each code fragment should run in the cycle. This is where Unity3D starts getting quirky. Notice that Start and Update don't override any base methods; they aren't part of an interface, and aren't even public. Yet third-party code still calls them. We rarely see this approach in .NET, where such things usually rely on reflection. Unity, however, can afford to bend the language rules. Acting as a compiler, it integrates code into its event system without requiring the explicit language constructs normally used for this purpose. In other words, Unity binds methods by name, almost as if we were writing JavaScript. The methods can be private or public—it makes no difference. We can also use inheritance with virtual methods. For example, we can create components that inherit from MyComponent and override the necessary virtual methods. Unity3D's event system includes many other methods that Unity calls at various stages of the Script Lifecycle. We could probably call this reflection in terms of the concept, but the way it works is fundamentally different from using System.Reflection or similar runtime tools. All the magic happens at compile time, so it doesn't affect the performance of such calls. Unity provides a class called UnityEngine.UIBehaviour, which can also be inherited from to create components. It's a MonoBehaviour subclass whose main purpose is to provide a set of empty virtual methods related to Unity UI events. However, nothing stops us from inheriting directly from MonoBehaviour and defining all the necessary methods without overriding any virtual ones. This feels a little odd, as if even the Unity developers haven't quite settled on a consistent way to best implement Script Lifecycle functions. How to create components We've already encountered one unusual aspect of C# code in Unity, and there's more to come. As I mentioned earlier, we can add components either using the scene editor or at runtime via code. Let's start with adding them in the editor. This part is straightforward: we select an object in the scene, click the Add Component button in the Inspector window, choose MyComponent, and Unity adds it to the object. To understand how this works, it's worth taking a look under the hood. Each scene exists as a separate file that contains everything in that scene, including the entire object hierarchy and all its components. We create an instance of our class when we add a component. When we save the scene, Unity serializes that instance into the scene file. Once we build and run the game, Unity deserializes all our components as it loads the scene, creating new instances of their classes. If we apply the UnityEngine.SerializeField attribute to any of our fields, we can save a component with a specific value for that field. The editor even displays a UI control that enables us to edit the field on the component we've added. By default, Unity serializes public fields. Adding components at runtime is also quite straightforward. We use the GameObject.AddComponent<T>() method for this. It's an instance method, so we first need to obtain a GameObject instance. This isn't a problem, as the Unity API provides plenty of ways to retrieve a particular GameObject. The most common approach is to use the MonoBehaviour.gameObject property. By the way, the Unity Engine's API naming conventions may look unusual to .NET developers: properties and fields start with a lowercase letter. Still, in almost every project I've seen, developers stick to the naming conventions commonly used in .NET. Here's an example of a component that adds a copy of itself to the same GameObject three seconds after it starts executing: using UnityEngine; public class CopySelfComponent : MonoBehaviour { private float _passedTime; private void Update() { if (_passedTime > 3f) { // gameObject is a public property of MonoBehaviour gameObject.AddComponent<CopySelfComponent>(); } else { // Time.deltaTime is a static property that stores the delta between // the start of the previous and current Update phases // in the Script lifecycle _passedTime += Time.deltaTime; } } } At this point, an experienced developer might ask, "What about object dependencies?" It's no secret that OOP revolves around creating various classes and establishing clear, precise, and elegant relationships between them. In most programming languages, including C#, we use constructors to define a class's dependencies. I've already described two ways to create a component, but neither involves a constructor. The reason might blow your mind: constructors for MonoBehaviour subclasses are UB in C#. For those unfamiliar with C++ and the concept of undefined behavior, here's how I'd describe it: you can use it, but it won't end well. Whatever you're trying to achieve, it'll probably fail. In other words, we can't use constructors for components. There's a formal alternative: the Awake() method. It's the first method Unity calls when creating a component, with a few caveats we won't get into here. However, this method doesn't take any arguments either. A common workaround for dependency injection is to define a separate method, usually called Construct(...) or Initialize(...), and pass all the required dependencies to it. Many developers will immediately recognize the downsides of this approach: we end up with an object in an inconsistent state immediately after its creation. That's something we have to live with. The usual solution is to encapsulate object creation in factories or use dedicated DI frameworks with their own APIs for creating objects and components. Here's what a typical component might look like when written using one of the most popular DI frameworks, Zenject: using UnityEngine; using Zenject; public class MyComponent : MonoBehaviour { // Here, we can't even use `readonly` for dependencies, since they change after the object is created. private IService1 _service1; private IService1 _service2; private IService1 _service3; // An attribute that tells the DI framework that this is a method for dependency injection [Inject] public void Construct( IService1 service1, IService2 service2, IService3 service3) { _service1 = service1; _service2 = service2; _service3 = service3; } ... } Things aren't so bad when it comes to classes that don't inherit from MonoBehaviour. We can use them just as we would in .NET. In large projects, developers typically implement most of the application's business logic in regular classes and use components as an integration layer with the engine. The intricacies of writing game code Overall, I've covered the main differences in the language, but Unity3D development also has a few specific aspects. Remember that games are resource-intensive applications, mainly because of their graphics. However, we also need to handle code more carefully to avoid putting unnecessary strain on a device that's already working hard. So, here are a few things to keep in mind: Minimize calls to "expensive" APIs The Unity library offers plenty of ways to tank an application's performance. Almost all API calls that create or access entities are rather slow. Of course, "slow" in this context doesn't refer to the pace perceived by a human observer; rather, it's a measure of the speed in terms of CPU standards. Here's an example of code that gradually changes a UI image's color from one to another and back again: using UnityEngine; using UnityEngine.UI; namespace Components { public class MyBehaviour : MonoBehaviour { // When the scene loads, Unity prioritizes the serialized field value. // If the component is being created for the first time, Unity uses the initialization value instead. [SerializeField] private Color _color1 = Color.red; [SerializeField] private Color _color2 = Color.blue; private void Update() { var img = this.GetComponent<Image>(); // THIS CODE WILL RUN SLOWLY // Some simple math is involved here. I'll spare you the details. // You can look into it on your own if you're interested. img.color = Color.Lerp(_color1, _color2, Mathf.PingPong(Time.time, 1f)); } } } If the code accumulates enough of such calls, they'll significantly impact performance. A better approach is to cache Image in Awake() or declare it as a serialized field so we can assign its value in the editor. We can drag and drop many types derived from UnityEngine.Object into UI fields in the editor to assign references to specific instances like this: using UnityEngine; using UnityEngine.UI; public class MyBehaviour : MonoBehaviour { [SerializeField] private Color _color1 = Color.red; [SerializeField] private Color _color2 = Color.blue; private Image _image; private void Awake() { _image = GetComponent<Image>(); } private void Update() { _image.color = Color.Lerp(_color1, _color2, Mathf.PingPong(Time.time, 1f)); } } Or like this: using UnityEngine; using UnityEngine.UI; public class MyComponent : MonoBehaviour { [SerializeField] private Color _color1 = Color.red; [SerializeField] private Color _color2 = Color.blue; [SerializeField] private Image _image; private void Update() { _image.color = Color.Lerp(_color1, _color2, Mathf.PingPong(Time.time, 1f)); } } Here are some other commonly used—though not the only—methods that can cause performance issues: - GetComponent<T>() - GetComponents<T>() - GetComponentInChildren<T>() - GetComponentInParent<T>() - FindObjectOfType<T>() - FindObjectsOfType<T>() - Find(string) - FindWithTag(string) - FindGameObjectsWithTag(string) How to create objects Object creation is another aspect of Unity development that deserves careful attention. I'm referring to both Unity3D objects (GameObject and MonoBehaviour) and regular class instances. In short, the best approach for performance is to avoid creating objects altogether. This is why Unity3D developers extensively use object pooling. It's hardly a groundbreaking idea, and Unity3D already provides a built-in class for this purpose: UnityEngine.Pool.ObjectPool<T>. Why does this matter in the game development industry? There are different reasons for Unity3D objects and regular class instances. For regular class instances, the issue is straightforward: memory allocation. One of the biggest threats to application performance is the garbage collector. Just as in .NET, the garbage collector pauses code execution to manage memory. Creating large numbers of short-lived objects can trigger frequent garbage collection, causing FPS drops during those pauses. Developers may encounter this issue when their code creates various collections to process and transfer data, DTOs, and similar objects. Although they usually have a longer lifespan, memory allocation for GameObject instances and components is another concern. The issue once again lies with the slow API responsible for creating them. Sometimes, we need to transition from one game state to another while displaying large numbers of objects along the way. For example, we might load an environment, spawn a large number of units, or create bullets when firing a weapon, which is a now-classic use case. Our goal in these situations is to create game objects as quickly as possible to prevent FPS drops and loading screens that frustrate players. That's where object pooling comes in, ideally with the warm pool. However, we shouldn't forget about the balance between memory usage and performance. Pooling too many objects will cause the application to use too much memory, creating its own set of problems. So, we should make smart use of the device's available resources. Structs For the same performance reasons, it's a useful skill for any developer to know when to use a struct instead of a class, as this helps reduce the load on the garbage collector. I won't go into much detail here, since the rules are roughly the same as in .NET. It's also important to remember about boxing and avoid unnecessarily allocating structs on the heap. How to destroy objects When it comes to destroying objects, Unity has a small peculiarity. Regular classes depend on the garbage collector. However, things become more complicated when it comes to objects that are part of the engine. Let's take GameObject, for example. When we write new GameObject(), we create a new instance of the GameObject class. At the same time, Unity adds the object to the hierarchy of the currently open scene. It may seem obvious that if the code no longer contains any references to this instance, it will still be part of the scene and will not be removed by the garbage collector. To explicitly remove the object from the scene, we need to call the UnityEngine.Object.Destroy(UnityEngine.Object) static method and pass the instance as an argument. This may seem obvious for GameObject and MonoBehaviour subclasses. We know these objects are part of the scene, so it makes sense that we need to take explicit action to remove them. However, there are less obvious cases, too. For example, the Texture2D class is an abstraction of a texture, which is an image. We can create a Texture2D instance and assume that we're the only ones holding a reference to it, so once we lose all references, the garbage collector will remove it. Unfortunately, that's not how it works. The rule is simple: we should call UnityEngine.Object.Destroy(UnityEngine.Object) for all explicitly created subclasses of UnityEngine.Object. The reason is that C# objects in the Unity API are simply wrappers for native objects belonging to the engine's C++ code. If we lose all references to our object, only the C# wrapper is removed. However, the C++ object under the hood still occupies memory. On the other hand, calling UnityEngine.Object.Destroy(UnityEngine.Object) destroys only the C++ object. So, after calling UnityEngine.Object.Destroy(UnityEngine.Object), we still have the same C# object, but all of its methods will throw errors because the object that handles the logic has been destroyed. We need to be careful when comparing UnityEngine.Object subclasses to null. Unity overrides the equality operator for UnityEngine.Object. So, in addition to checking whether both operands are null, Unity also checks whether the C++ object has been destroyed. If it has, comparing the object to null will return true. This behavior can be confusing and makes comparisons to null relatively expensive. So, it's better to avoid them in frequently executed code as well. Linq LINQ extension methods are incredibly convenient. They're also highly effective in middleware-based architectures, where different layers of an application can process the same enumerable object in sequence without repeatedly iterating over its elements. However, there's one major drawback to using LINQ with Unity3D: its extension methods return objects that require heap allocations. This brings us back to the issue of unnecessary object creation we discussed earlier. The overhead is negligible on its own, but constantly using it in frequently executed code can create a problem. I recommend keep using LINQ if you follow the middleware-based approach. However, try to replace LINQ methods with regular for and foreach if you're using them within a single method. Also, be careful with methods that execute LINQ queries, such as ToList(), ToArray(), and so on, as they can significantly increase memory usage. Call them only when you actually need the query result as a concrete collection, or avoid them altogether. That said, I don't think things are any different here compared to .NET. Concurrency and asynchronous programming Concurrency in Unity3D can be tricky. As I mentioned earlier, code dispatched to another thread simply won't run at all on WebGL. I haven't encountered any concurrency issues with how threads behave at the OS level on other platforms. However, almost every Unity API call will throw errors if we try to make it from a thread other than the main one. Experienced Unity developers might point to the Job System, but as far as I know, it only provides limited access to Transform. On the one hand, this is nice. We don't have to struggle too much with synchronization, and neither do Unity3D developers. Many Unity developers don't even know about the lock keyword, which is considered one of the cornerstones of concurrency in C#. On the other hand, we may sometimes want to use multiple CPU cores to modify the properties of a large number of Unity objects. Still, Unity makes extensive use of concurrency under the hood, and its API provides asynchronous operations for loading data from disk, handling network communication, and performing some other resource-intensive tasks. That said, developers at Unity Technologies aren't particularly fond of the async/await syntax, so we often have to make do with callbacks and events. Of course, nothing stops us from wrapping these in a proper async API using TaskCompletionSource and a few other methods. Unity also offers a rather unconventional approach to asynchronous programming called coroutines. Basically, coroutines enable developers to pass an enumeration to the Unity3D engine via its API. Each element tells the engine when to retrieve the next one. When combined with the yield operator, this works as a makeshift version of async/await. It looks something like this: public IEnumerator Coroutine() { Debug.Log("Do something"); yield return new WaitForNextFrame(); Debug.Log("Do something in the next frame") yield return new WaitForSeconds(2f); Debug.Log("Do something after two seconds"); var t = Task.Delay(4000); yield return new WaitUntil(() => t.Completed); Debug.Log("Do something after 4 more seconds."); } ... someMonoBehaviourInstance.StartCoroutine(Coroutine()); ... You may have noticed that starting a coroutine requires calling a method on a specific object instance. This tight coupling to an object is one of the advantages of coroutines: Unity cancels a coroutine when we destroy its associated object using UnityEngine.Object.Destroy(obj). In every other respect, though, coroutines are simply a less convenient alternative to async/await. Unity3D also has its share of issues with Task. Firstly, it runs on the thread pool, so we run into the same problem: we can't use the Unity API from those threads. Secondly, Unity3D developers try to avoid allocating memory on the heap, and Task is a class that allocates memory on the heap. ValueTask helps reduce this overhead, as does UniTask, its Unity-specific counterpart. UniTask is an open-source plugin that offers an alternative to ValueTask. It provides additional optimizations and enhances the user experience when working with the Unity API. The library also includes various extension methods for Unity classes, providing convenient ways to use them with await. Unity3D also has its own implementation of SynchronizationContext. It slightly streamlines the use of Task and ensures that the code after await won't run on a different thread. If the object we're await-ing executes on Unity's main thread, the code after await will run as soon as the execution finishes. If we use concurrency instead, the code after await will run during an early stage in the Script Lifecycle. WPF uses a similar synchronization context. Covering code with automated tests Unity includes a built-in editor window for running and viewing tests. The test framework is based on NUnit, but there are a few differences. First, Unity supports two types of automated tests: PlayMode and EditMode, which are just regular NUnit tests. We can create usual C# objects, mock dependencies using a mocking library such as NSubstitute (also available through OpenUPM), perform various operations, and check the results. PlayMode tests, on the other hand, run the Script Lifecycle. They enable us to check how the MonoBehaviour subclasses behave over time, how they respond to various physics triggers in the Script Lifecycle, and so on. In my experience, relatively few projects have made extensive use of PlayMode tests. Most teams move as much business logic as possible into standard C# classes. This leaves the MonoBehaviour subclasses as a thin layer between the C# architecture and the Unity API. I think the main reason is that testing anything related to the Script Lifecycle can be challenging. The engine's physics and execution systems don't provide APIs that offer the level of fine-tuning often required when writing automated tests. One disadvantage of testing in Unity is the lack of support for asynchronous tests. This is where the coroutines mentioned above come in, replacing asynchronous operations. It looks something like this: [UnityTest] // This is a special attribute for such tests public IEnumerator AsyncTest() { // Arrange SomeService someSerivce = // create some service with mocked dependencies // Act Task<bool> asyncHandle = someService.SomeAsyncMethod(); yield return new WaitUntil(() => asyncHandle.IsCompleted); // Arrange Assert(true, asyncHandle.Result); } Conclusion Those were the main points to keep in mind when starting to write C# code in Unity3D. If you have any comments or notice any errors, please leave a comment on the article or open an Issue on GitHub. Want to learn more? The PVS-Studio team loves to publish valuable content for C# developers and folks working in game development. Subscribe to our newsletter to follow the articles from the PVS-Studio website. Here are some articles: - I found some bugs in Create mod and checked them in Minecraft - Game++. Part 2.2: Types of optimization - What's new in C# 15: Overview

  2. アプリから URL を開くとき、ルールに従ってブラウザーやプロファイルを切り替えるツールを公開しました。 Windows と macOS 両方でお使いいただけるのと、ルール設定ファイルを複数端末でも共有できるのが特徴です…! Cysharp/Preferis

    24 likes · 4 reposts · 1,452 views
  3. CSVからUnity・MagicOnion・Rustのマスターデータを一括生成するパイプラインを作った はじめに ゲーム開発では、アイテムやショップなどのマスターデータをCSVやスプレッドシートで管理し、それをクライアント(Unity)とサーバーの両方で使う場面が多いです。 このとき、次のような作業が手作業になりがちです。 CSVを、クライアント用とサーバー用の形式にそれぞれ変換する 列を足すたびに、C#のクラスとRustのstructを両方直す クライアントとサーバーで型の幅や解釈が食い違う そこで、YAMLでスキーマを一度定義すれば、CSVから各配置先向けの成果物を一括生成するパイプラインを作りました。この記事では、ツールの使い方と機能を紹介したあと、設計時に何を検討して何を捨てたかをまとめます。 chinpanGX/master-data-pipeline master-data-pipeline とは CSVのマスターデータを、次の3系統に変換します。CSVはGitで管理する前提ですが、Googleスプレッドシートから取り込むこともできます。 配置先 生成物 クライアント(Unity) 暗号化済みバイナリ masterdata.bytes(MasterMemory)と、C#のモデル・Enum・ローダー リアルタイムサーバー(MagicOnion) クライアントと同一の masterdata.bytes APIサーバー(Rust) *.rs / *.json クライアントとリアルタイムサーバーは同じ成果物を使うので、データの食い違いが起きません。 全体フロー (任意)Google Sheets ↓ download_sheets.py csv/*.csv ← 正本。Gitで管理する ↓ normalize_csv.py 先頭3行を除去。分割されたCSVはここで結合 ↓ resolve_enum_ids.py Enum列を 表示名 → id に変換 ↓ validate_common.py unique / check_relation の検証 out/normalized_csv/*.csv │ ├→ csharp-codegen(.NET, Roslyn) スキーマ → C# POCO / Enum / Loader ├→ csharp_converter(.NET) CSV + 生成C# → masterdata.bytes └→ server_codegen(Rust) スキーマ → *.rs / *.json [コピースクリプト] out/ の生成物を client / realtime_server / server へ配置 前処理(Python)、C#の生成と変換(.NET)、サーバー向け生成(Rust)と、言語ごとに得意なツールを使い分けています。 使い方 フォルダ構成 master-data-pipeline/ ├── schema/ │ ├── tables/ テーブルの定義(YAML) │ └── enums/ Enumの定義(YAML) ├── csv/ マスターデータのCSV(正本) ├── tools/ ツール本体(python / dotnet / rust) ├── out/ 生成物(実行すると作られる) ├── build.sh ツールのビルド ├── run.sh データの生成と配置 └── Makefile build.sh と run.sh を決まった順に呼ぶ入口 編集するのは schema/ と csv/ だけです。 ツールのビルドとデータの生成を分けています。 build.sh はツール本体のビルド、run.sh はデータの生成と配置です。スキーマやCSVを変えただけなら、ツールの再ビルドは要りません 生成とコピーも分けています。 生成物はいったん out/ に集まり、client / server / realtime_server の各プロジェクトへは、コピーで明示的に渡します。パイプラインの外にあるフォルダを、ツールが直接参照することはありません 1. スキーマを書く テーブルは schema/tables/*.yaml に定義します。 sample_item.yaml name: sample_item input_csv: sample_item_master fields: - name: item_id type: int primary_key: true - name: name type: string - name: rarity type: enum enum_type: SampleItemRarity - name: shop_id type: int - name: internal_memo type: string targets: [server] validate: unique: - item_id check_relation: - field: shop_id table: sample_shop target_field: shop_id Enumは schema/enums/*.yaml です。 sample_item_rarity.yaml enum: SampleItemRarity members: - id: 0 key: Common # 生成されるC#/Rustのenumメンバー名 name: コモン # CSVに入力する表示名 - id: 1 key: Rare name: レア CSVには「コモン」「レア」のように日本語の表示名を入力でき、パイプラインがidに変換します。定義にない値を入力するとエラーになります。 対応している型は int / long / string / bool / enum の5種類です。 2. CSVのレイアウト CSVは固定レイアウトで書きます。Googleスプレッドシートから取り込む場合も、シートを同じレイアウトにします。 行 内容 1-2行目 自由記述のコメント 3行目 型情報(コメント用。実際の型はYAMLが正) 4行目 ヘッダー(YAMLの fields[].name と一致させる) 5行目〜 データ 3. 生成する Makefileに入口をまとめています。 make tools # ツール本体のビルド(初回とツールのコード変更時のみ) make check # 前処理と検証だけ。生成物は変えない make generate # 全配置先を作り直す make download generate # スプレッドシートから取り込んでから生成 make generate は check → server → client → realtime の順に流れ、途中で失敗した時点で止まります。つまり、検証に失敗したデータが各プロジェクトに配られることはありません。 sample_item.yaml からは、次のコードが生成されます(生成物の先頭に付く DO NOT EDIT コメントなどは省略しています)。 クライアント向けには、MasterMemoryのテーブルとしてC#のクラスが生成されます。targets: [server] の internal_memo は含まれません。 SampleItemData.cs [MemoryTable("sample_item")] [MessagePackObject(true)] public partial class SampleItemData { [PrimaryKey] public int ItemId { get; set; } public string Name { get; set; } public SampleItemRarity Rarity { get; set; } public int ShopId { get; set; } } APIサーバー向けには、同じ定義からRustのstructが生成されます。こちらには internal_memo も含まれます。 sample_item.rs #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SampleItem { pub item_id: i32, pub name: String, pub rarity: SampleItemRarity, pub shop_id: i32, pub internal_memo: String, } 主な機能 検証(unique / check_relation) validate に書いた内容で、生成前にCSVを検証します。 unique: 列の値の重複チェック check_relation: 他テーブルの列に存在する値かどうかのチェック(外部キー相当) ショップが存在しないのにアイテムが shop_id で参照している、といったデータ不整合を、ゲームを起動する前に見つけられます。 targets による出力先の制御 targets は、client / server のどちらに出力するかを制御します。省略時は両方です。 フィールド単位: サーバーにだけ必要な列(内部メモなど)を targets: [server] にすると、クライアントのバイナリに含まれません テーブル単位: 他テーブルから参照されるだけの参照専用テーブルを targets: [server] にすると、クライアント向けのC#クラスと masterdata.bytes から除外されます。DB保存や check_relation の検証対象には残ります クライアントに配るデータは最小限にしたいので、この制御が効きます。 CSVの水平分割 行数の多いテーブルは、複数のシート(CSV)に分けて管理できます。input_csv に * を書くとglobとして扱われ、前処理で1つに結合されます。 name: sample_item input_csv: sample_item_master_* csv/ sample_item_master_01.csv sample_item_master_02.csv sample_item_master_03.csv 結合後は分割していないテーブルと区別がつかないので、クライアントやサーバー側の実装は変わりません。結合順はファイル名の昇順で固定し、出力が決定的になるようにしています。4行目のヘッダーが全ファイルで一致しない場合はエラーで止まります。 常時暗号化 masterdata.bytes は常に暗号化されます。パスワードには、スキーマ(schema/tables/.yaml + schema/enums/.yaml)から決定的に算出した content_hash(SHA-256)を使います。 ポイントは、CSVの実データはハッシュに含めないことです。 データだけを更新してもパスワードは変わらない スキーマを変えるとパスワードが変わり、古い masterdata.bytes は新しい MasterDataLoader で復号できなくなる スキーマが食い違ったバイナリを読み込もうとすると、読み込み時に確実に失敗します。「古いバイナリを新しいコードが静かに誤読する」という事故を防げます。 content_hash は、生成される MasterDataLoader.cs に定数として埋め込まれます。鍵をクライアントに同梱する以上、強度は「一般ユーザーの覗き見防止」レベルです。 生成物は触らない仕組み 生成されるファイルの先頭には DO NOT EDIT コメントを付けます 生成・コピーのたびに、書き込み先を削除してから作り直します。テーブルを削除・リネームしても古いファイルが残りません なぜこのような構成になったか ここまで紹介した完成形だけ見ると最初から決まっていたように見えますが、実際は「作ったものを削る」判断が多くありました。ここでは、設計時に何を検討して何を捨てたかを論点ごとにまとめます。 出発点 もともと次の環境がありました。 Unityクライアント。マスターデータはMasterMemoryで扱う方針 Rust(axum)製のAPIサーバー。起動時にJSONを読み込み、HashMapで保持する Python製のコンバーター(スプレッドシート → CSV → サーバー向けJSON / クライアント向け正規化CSV) ここへ、のちにMagicOnionのリアルタイムサーバーが加わることになりました。これを足がかりに、クライアントとサーバーで同じマスターデータを無理なく扱える形を探りました。 以降の判断の多くは、「クライアントはMasterMemoryを使う」という前提から順に決まっています。 MasterMemoryは使い続ける 構成を見直すにあたって、MasterMemoryをやめて「起動時にDictionaryへ展開する自作方式」にする案も検討しました。しかし、ゼロアロケーションと、Dictionaryを介さないプロパティアクセス(ジェネリック静的キャッシュ)を維持したかったことと、同等のものを自作する工数が大きいことから、当初の方針どおりMasterMemoryを使い続けることにしました。 MasterMemoryを包むRepositoryは作らない MasterMemoryを使うと決めたうえで、次に考えたのは、MasterMemoryを抽象化するRepositoryをクライアントに置くかどうかです。テーブルごとのインターフェースと実装、集約Repository、ジェネリックな GetTable<T>() と案を進めましたが、次の問題が出ました。 「RepositoryがRepositoryを持つ」という命名になる テーブルが増えるとインターフェースが膨らむ MasterMemoryが生成するメソッド(主キー検索や、将来使うかもしれないセカンダリキー検索)を、独自の抽象で薄めてしまう 結論は、MasterMemoryを抽象化するRepositoryもインターフェースのラッパーも作らず、MemoryDatabase を直接公開することでした。MasterMemoryがコンパイル時に解決している「型 → テーブル」の仕組みを、ラッパーで作り直しているだけだったからです。MasterMemoryを使い続けると決めた以上、その機能を薄める層を挟む理由はありませんでした。 クライアントはオニオンアーキテクチャですが、マスターデータは読み取り専用なので、Domain.MasterData アセンブリとして全層から参照できる例外扱いにしました。 bytesはPythonでは作れない 既存がPython製だったので、MasterMemoryが読む masterdata.bytes もPythonで作れないか考えました。結論は不可です。MasterMemoryのbytesは、MessagePack C#の生成フォーマッターとソート済みインデックスに依存していて、公開された安定フォーマットではありません。C#側で DatabaseBuilder を通す必要があります。 これが、C#の変換ツール(csharp_converter)を作る決め手になりました。 暗号化:公開するとパスワードが漏れる 暗号化は、自作の暗号ライブラリが持つAES-CBC + PBKDF2の実装を流用しました。パスワードは、当初は全出力(スキーマとCSVデータ)から算出した content_hash を使う方針でした。最終的には、「常時暗号化」で説明したとおり、スキーマだけから算出するようにしています。 ここで1つ問題が出ました。バージョン照合のために content_hash を公開すると、パスワードがそのまま漏れます。そこで、content_hash は外に出さず、生成する MasterDataLoader.cs に定数として埋め込むことにしました(DLCのバージョン照合は別トピックとして切り離しています)。 強度についての前提もはっきりさせています。鍵をクライアントに同梱する以上、これは一般ユーザーの覗き見を防ぐ程度のものです。この前提があるので、次の2つは見送りました。 テーブルごとに分割して暗号化する: 攻撃対象が小さくなり、秘匿性の面で不利になるため ファイル名をハッシュ化する: 防御効果が薄く、運用コストが増えるため テーブルの定義はクライアントとサーバーで1つにする スキーマの設計で一番大事にしたのは、1つのテーブルの定義を、クライアントとサーバーで同じファイルに書くことです。共通の列を2か所で管理すると、片方だけ直して型がずれる事故が起きるためです。 この方針を、どの形式で実現するかを検討しました。 C#のクラスを正にする案: MasterMemoryはC#のクラスでテーブルを直接定義できるので、クライアントはこれが一番素直です。ただし、サーバーはRustなので、C#のクラスからRustのstructを生成する仕組みが要ります。C#の構文を解析してRustへ変換するツールを作る実装コストを考えて、この案は除外しました 言語に依存しないYAMLを正にする案: YAMLを1つ書き、そこからC#のクラスとRustのstructの両方を生成します。生成器は2つ要りますが、どちらも「YAMLを読んでコードを書き出す」だけなので、言語間の変換より単純です 結論はYAMLを正にすることにしました。YAMLには、テーブルごとに列とその型、検証(列の値の一意性、他テーブルへの参照)を宣言的に書きます。特定の言語に寄せた書き方はせず、RustとC#の両方へ同じ定義から出力できるようにしています。 ファイルは、テーブルごとに1つです。当初は言語固有の列があるのでclient用とserver用でファイルを分けていましたが、上の方針に合わせて1ファイルに統合しました。出力先は列ごとの targets: [client, server] で制御します(未指定なら両方)。テーブルを1ファイルに集約しないのは、テーブル数が多く見通しが悪くなるためです。 使い方で示した sample_item.yaml では、internal_memo に targets: [server] を付けています。そのため、この列はRustのstructにだけ出力され、クライアント向けのC#クラスには出力されません。列を足すときはYAMLを1か所直すだけで、C#とRustの両方に反映されます。 C#の生成は、Roslyn(Microsoft.CodeAnalysis.CSharp)の構文木ベースにしました。学習目的と、生成コードの品質を重視した選択です。生成するのはPOCO、Enum、MasterDataLoader.cs で、Enumの生成もPythonからcsharp-codegenに統一しました。 スキーマ仕様は削る方向で決めた スキーマの仕様は、足すより削る判断が中心でした。 型は int / string / bool / enum / long の5種 default は削除。全列を入力必須にして、空欄はエラーにする 主キーは1テーブル1列のみ(複合キー非対応) セカンダリキーはスキーマから完全に削除。必要になった時点で再設計する boolの表記ゆれの正規化は、各コンバーターにハードコード nullableや複合キーがないのは、機能不足というより、最初に「必要になるまで作らない」と決めた結果です。 ただし、これはMasterMemoryの強みを手放す判断でもありました。MasterMemoryは、C#のクラスに属性を付けるだけで、次の機能を使えます。 複合主キー([PrimaryKey] の keyOrder) セカンダリキー([SecondaryKey])と、重複を許すインデックス([NonUnique]) インデックスを使った検索(FindBy... / FindRangeBy... / FindClosestBy...) しかし、このスキーマはRustのAPIサーバーとも共有します。YAMLに書けるのは、C#とRustの両方で実現できるものだけです。たとえばセカンダリキーをスキーマに書くなら、Rust側にも同じインデックスを自分で実装しなければなりません。言語をまたいでスキーマを共有する以上、スキーマの表現力は2つの言語の共通部分に揃えるしかありませんでした。 つまり、スキーマ仕様を削ったのは「必要になるまで作らない」という方針の結果であると同時に、マルチ言語構成を選んだことの代償でもあります。 Enumは入力では名前、以降はid Enumの扱いは、2案から選びました。 共通の前処理で、コンバーターが name → id を解決する nameの文字列のまま出力して、各言語のデシリアライザで変換する 結論は、CSVにはnameで入力し、前処理でidに変換することです。以降のツールは数値として扱います。解決のロジックをC#とRustで二重実装せずに済み、CSVのフォーマットもクライアントとサーバーで揃えられるからです。 Enumの型は、共通のEnum定義YAML(id / name / enum)から、C#のenumとRustのenumの両方を生成します。intのままでは型安全性に問題があるためです。 CSVの書式は固定する CSVの書式は固定ルールにしました。1〜2行目はコメント、3行目は型情報(人間向けで、ツールは読まない)、4行目はカラム名、5行目以降がデータです。 結果として、型情報の一次ソースはYAMLに一本化され、header_start_row のような設定もYAMLから消えました。運用の担保はREADMEへの明記です。 検証は1回だけ 前処理は、先頭3行の除去 → Enumのname → id解決 → unique / check_relation の検証、の順です。 検証はC#とRustの各コンバーターには持たせず、共通の前処理で1回だけ実行します。クライアントとサーバーで結果が変わらない処理を、二重に実装したくなかったからです。 リアルタイムサーバーへの配布方法 リアルタイムサーバー(APIサーバーとは独立)を追加するとき、次の案を検討しました。 リアルタイムサーバー向けは暗号化なしにする コンバーターを環境ごとに分ける 暗号化をオプションにする 結論は、常に暗号化し、コンバーターは csharp_converter の1本、出力ファイル名は masterdata.bytes に統一です。分岐を作るより、全環境で同一の成果物にするほうが、確認すべき組み合わせが減ります。 Loaderも、復号と MemoryDatabase の構築を共通化しました。同じ生成物をUnityとMagicOnionの両方に配置し、bytesの取得方法だけが環境ごとに異なります。 この過程で、共有のClass Library(MasterDataModels)と CsvSourceAttribute は廃止しました。コンバーターがYAMLを直接読み、生成コードを Compile Include で直接参照します。 現在の制約 次の点は未対応です。 nullable型がない(NONE のようなセンチネル値で代替する) 複合主キー・複合UNIQUEがない(代理キーの列を1つ追加する) 列名にRustの予約語(type など)は使えない(r# エスケープ未対応) どれも、スキーマをC#とRustで共有していることから来る制約です。 複合主キーは、MasterMemoryなら keyOrder で書けます。しかし、Rust側にも同じ仕組みを用意しないとスキーマに書けません nullableも、C#、Rust、DBのそれぞれで「値なし」の扱いを揃えて実装する必要があります 列名の予約語は、Rustのコードを生成していなければ起きない問題です 運用で回避できる範囲なので、まずは必要になったときに足す方針です。ただ、どれを足すにもC#とRustの両方に実装が要ります。この点が、後に構成を見直すきっかけになりました。 おわりに スキーマを一度書けば、Unity・リアルタイムサーバー・APIサーバーの成果物が、検証つきで同じ元データから生成されるようになりました。スキーマの食い違いがバイナリの復号失敗として早期に表に出るのも、運用してみて安心できる点です。 実際に使っているゲーム(Atlas)では、テーブル10個、Enum 4個の規模で運用しています。Googleスプレッドシートからの取り込みは作りましたが、実際の運用では使っていません。CSVをGitで管理し、Claude Codeで直接編集して、make check で検証する流れに落ち着きました。 その後 このパイプラインを運用するうちに、「スキーマ仕様は削る方向で決めた」で手放したものが効いてきました。Rust側ではMasterMemoryのインデックスやLINQの恩恵を受けられず、C#側でも、スキーマがRustとの共通部分に縛られるため、セカンダリキーや複合キーを使えません。 次のプロジェクトではAPIサーバーもC#に統一し、スキーマをYAMLではなくC#のクラスで書く構成に移っています。MasterMemoryの属性をそのまま書けるので、セカンダリキーや複合キーもスキーマで表現できます。このツール自体はそのまま残していて、C#統一向けの仕組みをつくっていますので、別の機会に紹介をしようと思っています。

  4. ZLinq v1.6.0出しました!.NET 11の新規メソッド対応(FullJoinなど)と全メソッドへのIntelliSense付与、などなどです! Cysharp/ZLinq

    29 likes · 7 reposts · 1,445 views
  5. ConsoleAppFramework v5.8.0出しました!小さい使い勝手の改善を幾つか、になります……! Cysharp/ConsoleAppFramework

    29 likes · 4 reposts · 1,595 views
  6. ZLinq exists: Cysharp/ZLinq > Zero allocation LINQ with LINQ to Span, LINQ to SIMD, and LINQ to Tree (FileSystem, JSON, GameObject, etc.) for all .NET platforms and Unity, Godot.

  7. YetAnotherHttpHandler 1.11.7 をリリースしました。 このリリースではバンドルされたルート証明書の更新を行なっています。eMudhra の証明書に関する問題に対応となっていますのでご利用中の方はアップデートをお願いします。 github.com/Cysharp/YetAnotherHttpHandler/releases…

    14 likes · 6 reposts · 817 views
  8. NativeCompressions v1.0.0をリリースしました!LZ4/Zstandardという高速・スタンダードな二種の圧縮ライブラリをネイティブバインディングで提供することで、パフォーマンスとセキュリティ両面を担保しています! Cysharp/NativeCompressions t.co

    40 likes · 10 reposts · 2,424 views

Achievements

Awards earned by the repositories Cysharp owns. Select an award to embed its badge.

No awards yet

Repositories earn awards by reaching the top three in a completed trending day or week.

A repository earns an award for finishing first, second or third in a completed trending day or week, ranked by new stars, new forks or pull request authors. Awards follow a repository through renames and transfers and count toward its current owner. Weeks run Monday to Sunday, UTC.