C# Patterns for Solana Game Development

January 30, 2026 · View on GitHub

Coding standards and patterns for Unity/C# development with Solana integration.


Naming Conventions

ElementConventionExample
Classes, Structs, EnumsPascalCasePlayerAccount, WalletState
InterfacesIPascalCaseIWalletService, IRpcClient
MethodsPascalCaseConnectWallet(), GetBalance()
PropertiesPascalCaseIsConnected, WalletAddress
Public FieldsPascalCaseMaxRetries, DefaultTimeout
Private Fields_camelCase_walletService, _isConnected
Static Fieldss_camelCases_instance, s_defaultConfig
ParameterscamelCasewalletAddress, tokenAmount
Local VariablescamelCasebalance, transactionResult
ConstantsPascalCaseMaxConnections, DefaultRpcUrl

Boolean Naming

Prefix booleans with verbs indicating state:

// Good
public bool IsConnected { get; }
public bool HasPendingTransaction { get; }
public bool CanSign { get; }
private bool _wasInitialized;

// Avoid
public bool Connected { get; }
public bool Pending { get; }

File Organization

// 1. Using statements (sorted)
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Solana.Unity.SDK;
using UnityEngine;

// 2. Namespace (matches folder structure)
namespace MyGame.Blockchain
{
    // 3. One public class per file
    public class WalletManager : MonoBehaviour
    {
        // 4. Constants
        private const int MaxRetries = 3;

        // 5. Static fields
        private static WalletManager s_instance;

        // 6. Serialized fields
        [SerializeField] private WalletConfig _config;

        // 7. Private fields
        private bool _isConnected;
        private Account _account;

        // 8. Properties
        public static WalletManager Instance => s_instance;
        public bool IsConnected => _isConnected;

        // 9. Events
        public event Action<Account> OnConnected;

        // 10. Unity lifecycle methods
        private void Awake() { }
        private void Start() { }
        private void Update() { }
        private void OnDestroy() { }

        // 11. Public methods
        public async Task<bool> Connect() { }

        // 12. Private methods
        private void HandleConnection() { }
    }
}

Project Structure (Unity)

Assets/
├── _Game/                          # Game-specific code
│   ├── Scenes/
│   │   ├── Boot.unity              # Initial loading
│   │   ├── MainMenu.unity          # Menu with wallet connect
│   │   └── Gameplay.unity          # Main game scene
│   ├── Scripts/
│   │   ├── Runtime/
│   │   │   ├── _Game.asmdef        # Main assembly
│   │   │   ├── Core/               # Managers, state
│   │   │   ├── Blockchain/         # Solana integration
│   │   │   ├── UI/                 # UI components
│   │   │   └── Gameplay/           # Game mechanics
│   │   └── Editor/
│   │       └── _Game.Editor.asmdef
│   └── Tests/
│       ├── EditMode/
│       │   ├── _Game.Tests.asmdef
│       │   └── TestDoubles/
│       └── PlayMode/
│           ├── _Game.PlayMode.Tests.asmdef
│           └── TestDoubles/
├── Packages/                        # UPM packages
└── Plugins/                         # Native plugins

Unity-Specific Patterns

Serialized Properties

// Use field: target for Unity attributes on auto-properties
[field: SerializeField]
public int Health { get; private set; } = 100;

[field: SerializeField]
[field: Range(0, 100)]
[field: Tooltip("Maximum health points")]
public int MaxHealth { get; private set; } = 100;

// MonoBehaviour in file must match filename
// File: PlayerController.cs
public class PlayerController : MonoBehaviour { }

.meta File Rules

CRITICAL: Never manually create .meta files.

  • Unity generates .meta files automatically
  • When creating files/folders, let Unity generate the .meta
  • Include .meta files in version control
  • For asset creation, use temporary Editor scripts:
using UnityEditor;
using UnityEngine;

public static class AssetCreator
{
    [MenuItem("Tools/Create My Asset")]
    public static void CreateAsset()
    {
        var asset = ScriptableObject.CreateInstance<MyScriptableObject>();
        AssetDatabase.CreateAsset(asset, "Assets/MyAsset.asset");
        AssetDatabase.SaveAssets();
    }
}

Design Patterns

Early Return

// Good - early return
public async Task<bool> ProcessTransaction(Transaction tx)
{
    if (tx == null)
        return false;

    if (!IsConnected)
        return false;

    var result = await SendTransaction(tx);
    return result.IsSuccess;
}

// Avoid - nested conditions
public async Task<bool> ProcessTransaction(Transaction tx)
{
    if (tx != null)
    {
        if (IsConnected)
        {
            var result = await SendTransaction(tx);
            return result.IsSuccess;
        }
    }
    return false;
}

Async/Await

// Always use ConfigureAwait(false) in library code
public async Task<Balance> GetBalanceAsync()
{
    var result = await _rpc.GetBalanceAsync(_address).ConfigureAwait(false);
    return result.Value;
}

// In Unity MonoBehaviours, stay on main thread (no ConfigureAwait)
public async void OnConnectClicked()
{
    var success = await _walletService.Connect();
    _statusText.text = success ? "Connected" : "Failed"; // UI update on main thread
}

Null Handling

// Use null-conditional and null-coalescing
var balance = account?.Balance ?? 0;
var address = wallet?.Address?.ToString() ?? "Not connected";

// Use pattern matching for null checks
if (result is { IsSuccess: true, Value: var value })
{
    ProcessValue(value);
}

Events

// Use System.Action for events
public event Action OnDisconnected;
public event Action<Account> OnConnected;
public event Action<double> OnBalanceChanged;

// Invoke safely
private void RaiseConnected(Account account)
{
    OnConnected?.Invoke(account);
}

// Handler naming: Subject_Event
private void WalletService_OnConnected(Account account)
{
    UpdateUI();
}

Blockchain-Specific Patterns

Transaction Building

// Use builder pattern
var transaction = new TransactionBuilder()
    .SetRecentBlockHash(blockHash)
    .SetFeePayer(payer)
    .AddInstruction(instruction1)
    .AddInstruction(instruction2)
    .Build(signers);

Error Handling

// Wrap blockchain calls with specific error handling
public async Task<TransactionResult> SendTransaction(Transaction tx)
{
    try
    {
        var signature = await _wallet.SignAndSendTransaction(tx);
        return TransactionResult.Success(signature);
    }
    catch (RpcException ex) when (ex.Message.Contains("insufficient funds"))
    {
        return TransactionResult.Failure(TransactionError.InsufficientFunds);
    }
    catch (TimeoutException)
    {
        return TransactionResult.Failure(TransactionError.Timeout);
    }
    catch (Exception ex)
    {
        Debug.LogError($"Transaction failed: {ex.Message}");
        return TransactionResult.Failure(TransactionError.Unknown);
    }
}

Account Deserialization

// Use explicit offset tracking
public static PlayerData Deserialize(ReadOnlySpan<byte> data)
{
    var offset = 8; // Skip discriminator

    return new PlayerData
    {
        Owner = new PublicKey(data.Slice(offset, 32)),
        Score = BinaryPrimitives.ReadUInt64LittleEndian(data.Slice(offset += 32, 8)),
        Level = BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(offset += 8, 4)),
    };
}

Modern C# Features (C# 12/13)

// Primary constructors
public class WalletService(IRpcClient rpc, ILogger logger)
{
    public async Task<Balance> GetBalance() => await rpc.GetBalanceAsync();
}

// Collection expressions
List<int> numbers = [1, 2, 3, 4, 5];
int[] array = [..existingList, 6, 7];

// Pattern matching
if (result is { IsSuccess: true, Value: var value })
{
    Process(value);
}

// File-scoped namespaces
namespace MyGame.Blockchain;

public class TransactionBuilder { }

XML Documentation

/// <summary>
/// Connects to a Solana wallet using the specified adapter.
/// </summary>
/// <param name="adapterType">The type of wallet adapter to use.</param>
/// <returns>True if connection succeeded, false otherwise.</returns>
/// <exception cref="WalletException">Thrown when wallet is unavailable.</exception>
public async Task<bool> Connect(WalletAdapterType adapterType) { }

// For interface implementations, use inheritdoc
/// <inheritdoc/>
public async Task<bool> Connect(WalletAdapterType adapterType) { }

Comments Best Practices

  • Write comments in English
  • Explain "why not" - if other implementations seem possible, explain why they weren't chosen
  • Update comments when code changes
  • Delete unnecessary comments proactively
private List<Player> _activePlayers = new List<Player>();
// Using List instead of Dictionary<int, Player>:
// Small player count with infrequent lookups prioritizes
// memory efficiency and iteration speed.

Performance Patterns

// Cache frequently accessed data
private PublicKey _cachedAddress;
public PublicKey Address => _cachedAddress ??= DeriveAddress();

// Use object pooling for frequent allocations
private readonly Queue<NFTCard> _cardPool = new();

// Avoid allocations in Update loops
private readonly List<Enemy> _tempEnemyList = new(); // Reuse list

void Update()
{
    _tempEnemyList.Clear();
    GetActiveEnemies(_tempEnemyList); // Fills existing list
}

Avoid These Patterns

// Don't use regions
#region Bad Practice
#endregion

// Don't use var for unclear types
var x = GetSomething(); // What type is x?

// Use var only when type is obvious
var balance = 100.0; // Clearly a double
var accounts = new List<Account>(); // Clearly a List

// Don't ignore async warnings
public void BadAsync() // Should be async Task
{
    _ = SomeAsyncMethod(); // Fire and forget is dangerous
}

// Don't block on async code
var result = GetDataAsync().Result; // Can deadlock

// Do await properly
var result = await GetDataAsync();

Principles

  • KISS: Keep It Simple, Stupid
  • SOLID: Especially Single Responsibility, Interface Segregation, Dependency Inversion
  • Read .editorconfig before writing code
  • Never manually create .meta files (Unity generates them)