/UniMUD

Primary LanguageC#MIT LicenseMIT

UniMUD

Package for interacting with the MUD framework in Unity.

unimud_header

Prerequisites

  1. git (download)
  2. foundry (forge, anvil, cast) (download, make sure to foundryup at least once)
  3. node.js (v16+) (download)
  4. pnpm (after installing node: npm install --global pnpm)
  5. Unity (download)
  6. The .NET SDK (7.0) (download)

If you are using Windows:

  1. Install Git Bash (gitforwindows.org)
  2. Install nodejs, including “native modules” (nodejs.org/en/download) (re native modules: just keep the checkmark, it’s enabled by default in the installer)
  3. Use Git Bash for all terminal commands

Tankmud Written Tutorial

Follow this guide to make a basic MUD game with Unity.

tankmud_gif

Tadpol Video Tutorial

thumbnailGit

Quickstart (with template)

  1. git clone https://github.com/emergenceland/UniMUD.git
  2. Open terminal and enter the project root cd UniMUD/templates/counter
  3. Install MUD pnpm install
  4. Enter contract folder cd packages/contracts
  5. Deploy contracts locally pnpm dev
  6. Open a second terminal window to contracts cd UniMUD/templates/counter/packages/contracts
  7. Run codegen for C# scripts and link to the deploy pnpm dev:unity
  8. Open the project in Unity Hub UniMUD/templates/counter/packages/client
  9. Enter play mode!

Example Usage

Fetching a value for a key

To fetch a table by key, use GetTable on MUDTable:

MonsterTable monstersTable = MUDTable.GetTable<MonsterTable>(key);

Making transactions

using IWorld.ContractDefinition;
using mud;

async void Move(int x, int y)
{
	// The MoveFunction type comes from your autogenerated bindings
	// NetworkManager exposes a worldSend property that you can use to send transactions.
	// It takes care of gas and nonce management for you.
	// Make sure your MonoBehaviour is set up to handle async/await.
	await NetworkManager.World.Write<MoveFunction>(System.Convert.ToInt32(x), System.Convert.ToInt32(y));
}

Representing State

UniMUD caches MUD v2 events in the client for you in a "datastore." You can access the datastore via the NetworkManager. The datastore keeps a multilevel index of tableId -> table -> records

class RxRecord {
	public string TableId { get; set; }
	public string Key { get; set; }
	public Property RawValue { get; set; }
}

For example, records for an entity's Position might look like:

[
  {
	"tableId": "Position",
	"key": "0x1234",
	"x": 1,
	"y": 2
  },
  {
	"tableId": "Position",
	"key": "0x5678",
	"x": 3,
	"y": 4
  }
]

Queries

For queries that are useful in an ECS context, you can use the Query class to build queries.

Get all records of entities that have Component X and Component Y

RxTable Health = ds.tableNameIndex["Health"];
RxTable Position ds.tableNameIndex["Position"];

var hasHealthAndPosition = new Query().In(Health).In(Position)

// -> [ { table: "Position", key: "0x1234", value: { x: 1, y: 2 } },
//      { table: "Health", key: "0x1234", value: { health: 100 } },
//      { table: "Position", key: "0x456", value: {x: 2: y: 3} }, ...]

Get all records of entities that have Component X and Component Y, but not Component Z

var notMonsters = new Query().In(Health).In(Position).Not(Monster)

Get all records of entities that have Component X and Component Y, but only return rows from Component X

var allHealthRows = new Query().Select(HealthTable).In(Position).In(HealthTable)
// -> [ { table: "Health", key: "0x1234", value: { health: 100 } } ]

Get all monsters that have the name Chuck

var allMonstersNamedChuck = new Query().In(MonsterTable).In(MonsterTable, new Condition[]{Condition.Has("name", "Chuck")})
// -> [ { table: "Monsters", key: "0x1234", value: { name: "Chuck", strength: 100 } } ]

Make sure you actually run the query after building it, with NetworkManager.Datastore.RunQuery(yourQuery)

using mud;

void RenderHealth() {
  var hasHealth = new Query().Select(Health).In(InitialHealth).In(Health).In(TilePosition);

  var recordsWithHealth = NetworkManager.Datastore.RunQuery(hasHealth); // don't forget

  foreach (var record in recordsWithHealth) {
    DrawHealthBar(record.value["healthValue"]);
    // assumes the health table has an attribute called "healthValue"
  }
}

Reacting to Updates

You can do reactive queries on the datastore, with the MUDTable.GetUpdates<YourTable>() method.

using System;
using UniRx;
using mud;
using UnityEngine;

public class Counter : MonoBehaviour {
    private IDisposable _disposable = new();

    void Start() {
        net = NetworkManager.Instance;
        net.OnNetworkInitialized += SubscribeToCounter;
    }

    private void SubscribeToCounter(NetworkManager _) {
        _counterSub = MUDTable.GetUpdates<CounterTable>().ObserveOnMainThread().Subscribe(OnIncrement);
    }

    private void OnIncrement(RecordUpdate update) {
        if (update.Type != UpdateType.DeleteRecord) {
            var currentValue = update.CurrentRecordValue;
            if (currentValue == null) return;

            Debug.Log("Counter is now: " + JsonConvert.SerializeObject(currentValue));
            Instantiate(prefab, Vector3.up, Quaternion.identity); 
        }
    }
}

Deploying to a testnet

Select the Testnet NetworkType on the NetworkManager or create your own NetworkData ScriptableObject in the project window and link it.

Limitations

  • Does not support setters for individual table values on contracts (must set the entire table every time)
  • Does not support pop or push array functions on contracts

Future work

  • Indexer
  • Caching/persistence

License

MIT