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
| Element | Convention | Example |
|---|---|---|
| Classes, Structs, Enums | PascalCase | PlayerAccount, WalletState |
| Interfaces | IPascalCase | IWalletService, IRpcClient |
| Methods | PascalCase | ConnectWallet(), GetBalance() |
| Properties | PascalCase | IsConnected, WalletAddress |
| Public Fields | PascalCase | MaxRetries, DefaultTimeout |
| Private Fields | _camelCase | _walletService, _isConnected |
| Static Fields | s_camelCase | s_instance, s_defaultConfig |
| Parameters | camelCase | walletAddress, tokenAmount |
| Local Variables | camelCase | balance, transactionResult |
| Constants | PascalCase | MaxConnections, 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
.metafiles automatically - When creating files/folders, let Unity generate the
.meta - Include
.metafiles 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
.editorconfigbefore writing code - Never manually create
.metafiles (Unity generates them)