Simple Guide to Unity Package Management
By: Paul Harwood
Tags: Upm, Unity, Openupm, Packaging

Creating your own Unity Package

Photo by JESHOOTS.COM on Unsplash

Packaging code up into components, modules or packages is an important tool for code and version management for any project.

This has not always been easy for Unity Projects, and still remains an area of complexity and possibly an area where Unity is deficient. This will be a brief discussion of what to do and how to do it.

This article is part of a larger set about componentisation of Unity projects.

A Brief Detour about Unity

I am not going to provide much introduction to Unity, since if you do not know what it is then this article is probably not of much interest.

But in case you are interested, the least you need to know is that Unity is a development environment and compiler for Virtual Reality applications and consumes a number of types of assets such as C# scripts, 3D objects, shaders etc. Unity scripting is based on Mono.

Unity Packaging Concepts

There are two types of Unity package management provided out of the box:

There are also a couple of types of package management that are not available out of the box:

NuGet

Conda

Unity Asset Packages

Unity Asset Packages are bundles of Unity assets compressed into a .unitypackage file. The bundle is easy to distribute as a single file and is relatively easy to import into the project using a single command. When imported, the assets are imported into the asset folder system for the project.

This is the package format used by the Unity Asset Store.

The value of this format is that it is easy to create and easy to use. It works well for sets of icons, UIs, and template projects.

However, it does not include any concept of dependencies and since the assets are imported into the project file structure itself they then become part of the version control of the project making package version control much more difficult.

This is particularly a problem for code-based assets, making independent version control for the package code almost impossible.

Unity Package Manager

Unity Package Manager (UPM) is a more recent development than asset packages. It is a “proper” package management system based on NPM and has all of the dependency management that you would expect. The packages are loaded into a separate folder structure, not the project assets folder, so they do not get rolled into the version management system for the project, thus making package version management and updates a lot easier.

UPM is what Unity use for their own packages and components.

The value of UPM is that it allows the separation of component assets from the project assets, as well as the improved ability to do version and dependency management.

The only problem of using UPM is that it is a lot harder to create and distribute your own packages and each package (if it contains code) has to be in its own assembly (more about this later).

NuGet

NuGet is the package manager for .NET, built and run by Microsoft. Since Unity is built on Mono and this .NET, you would probably expect there to be close links between them.

However, that is not true. There is no out-of-the-box integration between them and using NuGet packages in Unity was, traditionally, difficult.

However, there is a community integration — NuGetForUnity. I have used this and it seems to work well.

That said, it is almost always better, where you can, to get the C# source for a library and turn that into a UPM package, thus letting Unity compile it into the DLL since you are much less likely to get version issues in the dependencies that way. That is what I did for a C# library called Geometry3Sharp for instance.

However, where that is not practical then you can use NuGetForUnity to install the NuGet DLL. This is, for instance, what I do for System.Reactive

Conda

Conda is a multi-platform and multi-language package and environment management system. Not surprisingly, there is no integration with Unity.

It may seem strange to raise it here but there are many libraries available through Conda that make excellent native plugins for Unity projects. There is considerable value to get from integrating Conda into Unity. This is explored in considerable more detail in this article:

About UPM Dependency Management and Registries

As we said above, UPM is based upon NPM and thus has many of the same capabilities. The packages are defined in a package.json which is similar to the npm version.

The PM client is the Unity Editor, and the package dependencies are defined in a manifest.json .

Packages are obtained from registries which can be:

The default registry from Unity,

A Scoped Registry,

A tarball,

A Git repo (for instance Github), or

The local disk.

A Scoped Registry is the same idea as the NPM scope and is your own repository. You can either set up your own server, as discussed in this article:

or you can add your packages to a public registry such as npmjs.com or OpenUPM.com. The later is my preference because of the automated build process.

There are some limitations around scoped registries. Particularly, they can only be defined in the parent project. You cannot define scoped registries in a package, although you can reference them.

You can also link to Github repos, as discussed in this article (including the associated problems)

Note that if you load a package from a git, you cannot then have dependencies on other git based packages. This can be a significant restriction.

Also, you can add the package from the local disk (by pointing at the relevant package.json file). When set up like this Unity will pick up changes to the scripts and automatically reload and recompile, so it is useful for developing and testing packages.

Building a UPM Package

As part of the ViRGiS Project, we wanted to create the core functionality as an installable package that is itself dependent on other 3rd party libraries, including C# and C++ libraries (such as the geometry3Sharp library mentioned above and GDAL etc).

The resulting package can be seen here.

Since the prime objective of doing this was to simplify version control across multiple projects, with each component only defined in one place, it was obvious that we needed to use UPM and not Asset Packages.

Also, since we have hierarchies of packages, we need a scoped registry. As part of the ViRGiS project, we want the build process to be as automated, preferably through Github, as much as possible since that reduces the projects Bus Factor. There is an open scoped registry called OpenUPM. This is based on Github Actions and other CI to automatically build the UPM packages from GH repos — in a very similar way to Conda Forge although possibly not as mature and polished yet. Publishing a new release in your package GH repo will automatically create a new release of the linked OpenUPM package (more details in the docs). We decided to use this scoped registry for all Unity packages.

Creating the Package

The workflow for creating a package that we have settled on works pretty much like this:

Create some working functionality, Turn that into a Unity Package on the local disk, Turn that into a working Unity Package in a GH Release, and Publish that on OpenUPM.

Create Working Functionality

It is almost always better to create the functional components in a Unity project and then create a package from that. This makes the early stages of development and debugging much easier. Usually, you will create a toy project to developed and test the functionality you want to package and get it all working to your satisfaction.

It is a really good idea at this stage to place the C# classes that you want to include in the package in their own namespace. This will mean that the scripts that make up the testing and development project, but not part of the package, will need to import the package classes with a using statement.

Note that:

If the classes you plan to include have or will have dependencies on classes provided by other UPM packages, those packages must exist as UPM packages before you can create this package,

they can have dependencies on the standard UnityEngine and UnityEditor libraries,

but you cannot have dependencies on the parent project, which does make sense,

and this does mean therefore that you cannot have UPM packages that are dependent on Unity Asset Packages.

Create a Local Package

At this point, you should create a new local folder, which is published on GitHub as the long term home of the package and that contains a copy of the Asset folder from the development project.

Unity publishes a set of folder structure conventions for Unity Packages. These are not enforced in any way — you can ignore them and the package works — but at this stage, it is a good idea to rename the Asset folder to Runtime and copy any Editor scripts into an Editor folder and the same for Tests etc.

You need to remove all Assets that are purely part of the Development and Test project and not part of the package — like scenes etc. You definitely need to make sure that you have NOT included anything from any Asset Packages that you might have been using.

Finally, go back to the actual development and testing project and delete all of the assets that are now part of the package. Other scripts that import the namespace that is now part of the package will eventually work, but at this stage, they will start to show an error message. Don’t worry about that.

You then need to create two things:

The package.json file, and

file, and The assembly definition file.

First, create a file called package.json at the root of the package repo. Add the contents as specified:

This is all fairly clear. The dependencies are written in the same syntax as for the manifest.json file — so if there are any dependencies that you want to link to the new package (rather than the project) they can be copied across (and, of course, deleted from the dev project). The SemVer version included here is the version that will appear in the Package Manager window and — if it is less than 1.0.0 then Unity appears to assume it is a Preview package and will show as such in the Editor Window.

As we will find out below, when we publish the package we will only be able to increase the SemVer. Therefore, the first time through this process it is a good idea to make the SemVer a beta version of the version number you want. When it is all working, you can then publish it again very easily as the final version

Note that, as discussed above, Unity will ignore any scoped registry definitions that are included here (at least as of 2020.1 — who knows about the future). If you look at the example I provided, i.e. here, this does include scoped registry definitions. These are totally ignored by Unity and we have only included them as a useful way of documenting them. For the package to work, these definitions must be copied to or included in the project manifest.json file.

Next, add the local package to the development project. With the dev project loaded in the Editor, go to the Package Manager window and select “+” and “Add from Disk” and then select the package.json file you have just created. Unity will add the package as per the package manifest, sort out the dependencies and, since the package is local, add the .meta files.

The package will be loaded into the Editor Inspector in the “Packages” virtual folder.

At this point, you need to add the assembly file.

Assemblies in .NET simply define the scope of the binaries — i.e. DLLs. One assembly is one DLL and is defined by the existence of an assembly definition file .asmdef . The assembly definition is used by the compiler to assemble the DLL but also to understand and follow the linkages between assemblies. Thus without the correct .asmdef file the package scripts will not be accessible.

To create the .asmdef file, open the Runtime folder of the Package in the Inspector and “Create” “Assembly Definition”. Rename the file to something that makes sense.

If your package has no dependencies, then just accept the defaults. However, if your package does have dependencies on other UPM packages then you also need those dependencies in the .asmdef .

Open the file in the Inspector and look for the Assembly Definition References. Unselect the “Use GUIDs” — you more flexibility and better debug info without it. Finally, for each dependency add an entry into the list and drag and drop the .asmdef file for the dependency onto the entry.

This article provides more depth on the asmdef file.

Create a GitHub Package

Once this package has been pushed to GitHub, it can be used as a UPM just be using the .git URL. Unload the local version of the package from Unity and then add the package using the .git URL to make sure that it is working without error messages.

However, that approach does not implement version control. The next step is to create a GitHub release, using the SemVer version from the package.json as the name of the release.

Unload the .git version of the package and add it again using the URL for the release. It should work.

At this point, you should make sure that the LICENSE and README.md are meaningful and up to date — we will ingest them in the next step.

Publish the Package on OpenUPM

OpenUPM has a simply, form based, ingestion process for packages, based around GitHub and GitHub actions.

As long as you have followed the steps above, you should have no problems. The end result of the form is a pull request that will automatically provision the package into the OpenUPM registry. The provision is based upon the GitHub release, and any new releases into the package GH repo will automatically provision a new version into OpenUPM.

Version numbers become important here. OpenUPM implements SemVer as per the standard and, like most package managers, does not let you delete or change a published version and by default servers the highest SemVer.

However, unlike some package managers (e.g. Conda) it does NOT have the concept of a “build number” which would allow you re-release a SemVer version (with a higher build number) if only the metadata has changed.

This means that you have to be careful about version numbers until you are sure that the build is working and the metadata is all correct. If you are entirely in control of the version number, then you can just keep bumping the SemVer until the metadata is correct but it is not always ideal. If you are packaging something that is cross platform then maintaining different version number per platform can be a quagmire. The next solution is to publish the package using beta SemVers (e.g 3.1.2-b4) until you are sure that everything is working correctly and then bump to the final SemVer. This was something we were badly burned by when first published a package.

The version number used by OpenUPM is the version number in package.json . When creating the GH release, we tend to tag the release with the same SemVer to avoid confusion but this is not the SemVer used by OpenUPM — if you correctly change the release stage but forget to change package.json the build will fail.