A production-ready Unity 3D game template by punkfuncgames featuring modern architecture patterns, dependency injection, reactive programming, and comprehensive game systems.
This is a Unity 3D project built on the URP (Universal Render Pipeline) template, designed as a scalable foundation for 3D games. Built with a modular UPM package architecture -- 26 self-contained packages organized in dependency tiers. Include only the modules you need; remove the rest with zero compilation errors thanks to #if PUNKFUNC_<MODULE> guards.
Current Version: 0.1.0
Unity Version: 6000.0.x (Unity 6)
Render Pipeline: Universal Render Pipeline (URP)
Company: punkfuncgames
- Modular UPM Packages - 26 packages in 6 dependency tiers, mix and match per project
- Dependency Injection - VContainer for clean, testable service architecture
- Reactive Programming - R3 (Reactive Extensions) for responsive data flows
- Event-Driven Communication - MessagePipe for decoupled system messaging
- Async/Await - UniTask for efficient asynchronous operations
- MVC Pattern - Clean separation of Model, View, and Controller
- Modular Bootstrap - Phase-based initialization system
- Conditional Compilation -
#if PUNKFUNC_<MODULE>guards for safe package removal
- Game State Management - Complete state machine (Menu, Playing, Paused, GameOver, etc.)
- Scene Management - Async scene loading with progress tracking
- Save System - Binary serialization with encryption and cloud sync support
- Audio Management - Music, SFX, and volume control with crossfading
- Input System - New Input System with action-based controls
- Object Pooling - Performance-optimized spawn management
- UI Framework - Panel management with animations and transitions
- Steam Integration - Steamworks.NET for achievements, leaderboards, and multiplayer
- Addressables - Scalable asset management and DLC support
- Localization - Multi-language support framework
- Analytics - Unity Analytics integration
- Edit Mode Tests - Unit testing for core logic with NSubstitute mocking
- Play Mode Tests - Integration testing for gameplay systems
| Package | Version | Purpose |
|---|---|---|
| Unity | 6000.0.x | Game Engine |
| URP | 17.3.0 | Universal Render Pipeline |
| VContainer | 1.17.0 | Dependency Injection |
| MessagePipe | 1.8.1 | Event System |
| R3 | 1.3.0 | Reactive Extensions |
| UniTask | Latest | Async/Await |
| Cinemachine | 3.1.5 | Camera Control |
| Input System | 1.16.0 | Input Handling |
| Addressables | 2.7.6 | Asset Management |
| Localization | 1.5.9 | Multi-language Support |
| Package | Purpose |
|---|---|
| Steamworks.NET | Steam API Integration |
| DOTween | Animation/Tweening |
| NuGetForUnity | Package Management |
| NSubstitute | Mocking framework for tests |
| Package | Purpose |
|---|---|
| Rider/VS Editor | IDE Integration |
| Test Framework | Unit Testing |
The template is split into 26 UPM packages organized in dependency tiers (0-5). Each package:
- Has its own assembly definition and version define (
PUNKFUNC_<MODULE>) - Can be added or removed independently
- Uses
#if PUNKFUNC_<MODULE>guards so the project compiles cleanly with any subset
- Copy the package folder into
Packages/ - Unity auto-detects it -- the version define activates automatically
- Add the module's installer to your
ProjectLifetimeScopeinside a#ifguard
- Delete the package folder from
Packages/ - All guarded references compile away cleanly
- Remove any installer calls or serialized SO references from your scopes
See docs/MODULE_CATALOG.md for the full package list, tiers, and dependencies.
- Unity 6000.0.x or later
- Git LFS (for large files)
- Steam Client (for Steam features)
-
Fork the template
git clone <repository-url> cd 3d-project-base
-
Customize modules (optional)
- Review
docs/MODULE_CATALOG.mdfor available packages - Remove any packages you do not need from
Packages/ - The project compiles cleanly with any subset of packages
- Review
-
Open in Unity
- Open Unity Hub
- Add project from cloned directory
- Open with Unity 6000.0.x+
-
Configure Steam (Optional)
- Open
steam_appid.txt - Replace
480with your Steam App ID - Or keep
480for testing with Spacewar
- Open
-
Build Scenes Ensure these scenes are in Build Settings:
[BOOTSTRAP]- Initial loading scene[MAINMENU]- Main menu[GAMESCENE]- Gameplay scene
-
Play
- Enter Play Mode from
[BOOTSTRAP]scene - Or use
Ctrl+Shift+P(Windows) /Cmd+Shift+P(Mac) for Play Mode from first scene
- Enter Play Mode from
Packages/ # UPM packages (module code)
├── com.punkfuncgames.core/ # MVC, composition, interfaces
├── com.punkfuncgames.math/ # BigDouble math library
├── com.punkfuncgames.log/ # Centralized logging
├── com.punkfuncgames.wallet/ # Currency management
├── com.punkfuncgames.idle/ # Idle game mechanics
├── com.punkfuncgames.save/ # Save/load with encryption
├── com.punkfuncgames.audio/ # SFX, music, volume
├── com.punkfuncgames.ui/ # Panel management, dialogs
├── com.punkfuncgames.steam/ # Steamworks.NET integration
├── ... (26 packages total) # See docs/MODULE_CATALOG.md
└── com.punkfuncgames.<module>/
├── package.json
├── Runtime/PunkFuncGames.<Module>/ # Production code
│ ├── Config/ Event/ Installer/
│ ├── Model/ Service/
│ └── <Module>.asmdef
└── Tests/ # Per-package tests
├── EditMode/
└── PlayMode/
Assets/
├── Scripts/PunkFuncGames/GameTemplate/ # Template shell
│ ├── Runtime/
│ │ ├── Scope/ # VContainer lifetime scopes
│ │ ├── Bootstrapping/ # Phase-based init
│ │ └── Gameplay/ # Game-specific logic
│ └── Tests/ # Template-level tests
├── Scenes/
│ ├── [BOOTSTRAP].unity
│ ├── [MAINMENU].unity
│ └── [GAMESCENE].unity
├── Art/ Audio/ Prefabs/ ScriptableObjects/
├── Input/ Localization/ Resources/ Settings/
└── ThirdParty/
For the complete list of all 26 packages with dependency tiers, see docs/MODULE_CATALOG.md.
This project strictly adheres to the Cysharp Modern Stack:
| Library | Role | Usage |
|---|---|---|
| VContainer | Dependency Injection | builder.Register<>(), Constructor Injection for pure classes, [Inject] for MonoBehaviours |
| UniTask | Async/Await | UniTask, UniTaskVoid, .Forget() for fire-and-forget |
| R3 | Reactive Extensions | ReactiveProperty<>, Observable, Subject |
| MessagePipe | Messaging | IPublisher<T>, ISubscriber<T> for Pub/Sub |
The following patterns are explicitly rejected:
- ❌ Coroutines (
IEnumerator,StartCoroutine) - ❌ Zenject (
Container.Bind) - ❌ Standard .NET Tasks (
Task.Delay,Task.Run) - ❌ UniRx (use R3 instead)
- ❌
async void(useasync UniTaskVoid)
ProjectLifetimeScope (Singleton)
│
├── BootstrapOrchestrator (IAsyncStartable)
│ └── IBootstrapPhase[] (ordered initialization)
│
├── Core Services (Singleton)
│ ├── GameStateService (IGameStateService)
│ ├── AssetService (IAssetService)
│ ├── AudioService (ISFXService, IMusicService, IVolumeService)
│ ├── SaveService (ISaveService)
│ ├── InputService (IInputService)
│ ├── UIService (IPanelService, IDialogService, etc.)
│ ├── PoolService (IPoolService)
│ └── CameraService (ICameraService)
│
└── GameplayLifetimeScope / MainMenuLifetimeScope (Scoped)
└── Scene-specific systems
-
ProjectLifetimeScope - Persists across all scenes
- Core services
- Steam integration
- Save system
- Audio service
-
MainMenuLifetimeScope - Scoped to main menu scene
- Menu UI controllers
- Settings panel
-
GameplayLifetimeScope - Scoped to gameplay scene
- Gameplay controllers
- Level management
- Player systems
All code follows the PunkFuncGames AI Instructions standards:
- Sealed Classes: All concrete service and controller classes are
sealedto prevent inheritance issues - No XML Comments: Code is self-documenting via clear naming conventions
- Explicit Visibility: All members have explicit access modifiers (
private,public, etc.) - Class Layout Order: Constants → Fields → Properties → Constructors → Lifecycle → Event Listeners → Public Methods → Private Methods → Cleanup → Nested Types
public sealed class GamePresenter : IStartable, IDisposable
{
private readonly ISaveService _saveService;
private readonly ISubscriber<GameStartEvent> _subscriber;
private readonly CompositeDisposable _disposables = new();
public GamePresenter(ISaveService saveService, ISubscriber<GameStartEvent> subscriber)
{
_saveService = saveService;
_subscriber = subscriber;
}
public void Start()
{
_subscriber.Subscribe(x => HandleGameStartAsync(x).Forget())
.AddTo(_disposables);
}
private async UniTaskVoid HandleGameStartAsync(GameStartEvent e)
{
await _saveService.LoadAsync(e.ProfileId, CancellationToken.None);
}
public void Dispose() => _disposables.Dispose();
}public sealed class MainMenuPanel : ViewBase
{
private ISceneService _sceneService;
private IPanelService _uiPanelService;
[Inject]
private void Construct(ISceneService sceneService, IPanelService uiPanelService)
{
_sceneService = sceneService;
_uiPanelService = uiPanelService;
}
private void OnPlayClicked()
{
PlayGameAsync().Forget();
}
private async UniTaskVoid PlayGameAsync()
{
await _sceneService.GoToGameplayAsync();
}
}public async UniTask LoadDataAsync(CancellationToken ct = default)
{
await UniTask.Delay(1000, cancellationToken: ct);
}
private async UniTaskVoid ShowSettingsAsync()
{
await _uiPanelService.ShowPanelAsync<SettingsPanel>(
nameof(SettingsPanel),
UILayer.Popup,
cancellationToken: DestroyCancellationToken);
}public sealed class MyService : IDisposable
{
private readonly CompositeDisposable _disposables = new();
public MyService(IGameStateService gameState)
{
gameState.CurrentState
.Where(state => state == GameState.Playing)
.Subscribe(_ => OnGameStarted())
.AddTo(_disposables);
}
public void Dispose()
{
_disposables.Dispose();
}
}public sealed class AudioEventHandler : IAsyncStartable, IDisposable
{
private readonly IMusicService _musicService;
private readonly ISFXService _sfxService;
private readonly CompositeDisposable _disposables = new();
public AudioEventHandler(
IMusicService musicService,
ISFXService sfxService,
ISubscriber<PlaySFXRequest> sfxSubscriber)
{
_musicService = musicService;
_sfxService = sfxService;
}
public UniTask StartAsync(CancellationToken ct)
{
_sfxSubscriber.Subscribe(OnPlaySFXRequest).AddTo(_disposables);
return UniTask.CompletedTask;
}
public void Dispose() => _disposables.Dispose();
}Manages game state transitions with reactive notifications.
public sealed class MyComponent : ComposableBehaviour
{
[Inject] private IGameStateService _gameState;
protected override void OnInjected()
{
_gameState.CurrentState
.Where(state => state == GameState.Playing)
.Subscribe(_ => OnGameStarted())
.AddTo(Disposables);
}
}States: None, Initializing, MainMenu, Loading, Playing, Paused, GameOver, Victory
Implements segregated interfaces for better modularity:
[Inject] private ISFXService _sfx;
[Inject] private IMusicService _music;
[Inject] private IVolumeService _volume;
// Play sound effect
_sfx.PlaySFX("explosion");
_sfx.PlaySFXAtPosition("footstep", position);
// Play music with crossfade
await _music.PlayMusicAsync("battle_theme", crossfadeDuration: 1f);
// Control volume
_volume.SetVolume(AudioChannel.Music, 0.8f);Reactive input handling with the new Input System:
[Inject] private IInputService _input;
protected override void OnInjected()
{
_input.Movement
.Subscribe(dir => Move(dir))
.AddTo(Disposables);
_input.OnPrimaryAction
.Subscribe(_ => Attack())
.AddTo(Disposables);
}Persistent data storage with binary serialization, encryption, and reactive observation:
[Inject] private ISaveService _save;
// Save data
_save.Set("player.health", 100);
_save.Set("player.gold", 500);
// Load data
int health = _save.Get<int>("player.health", defaultValue: 100);
// Observe changes
_save.Observe<int>("highscore")
.Subscribe(score => UpdateHighScoreUI(score))
.AddTo(Disposables);
// Manual save
await _save.SaveAsync();[Inject] private IPublisher<DamageDealtEvent> _damagePublisher;
_damagePublisher.Publish(new DamageDealtEvent(
target, source, damage, DamageType.Fire, hitPoint, isCritical: true
));[Inject] private ISubscriber<DamageDealtEvent> _damageSubscriber;
_damageSubscriber.Subscribe(evt => {
ShowDamageNumber(evt.Damage, evt.HitPoint);
}).AddTo(Disposables);Right-click in Project window:
- Create > GameTemplate > Game Settings
- Create > GameTemplate > Audio Settings
- Create > GameTemplate > Level Config
Edit steam_appid.txt:
480 # Replace with your Steam App ID
For testing without Steam:
- Keep
480(Spacewar - Valve's test app) - Or set to
0to disable Steam
- Open Window > General > Test Runner
- Select Edit Mode or Play Mode tab
- Click Run All
[TestFixture]
public sealed class MyServiceTests
{
private MyService _service;
private IPublisher<MyEvent> _mockPublisher;
[SetUp]
public void SetUp()
{
_mockPublisher = MockFactory.CreatePublisher<MyEvent>();
_service = new MyService(_mockPublisher);
}
[Test]
public void MyMethod_DoesSomething()
{
// Act
_service.MyMethod();
// Assert
_mockPublisher.Received().Publish(Arg.Any<MyEvent>());
}
}[TestFixture]
public sealed class MyIntegrationTests
{
[UnityTest]
public IEnumerator LoadScene_CompletesSuccessfully()
{
// Act
var task = _sceneService.LoadSceneAsync("TestScene");
// Wait for completion
while (!task.IsCompleted)
{
yield return null;
}
// Assert
Assert.That(SceneManager.GetActiveScene().name, Is.EqualTo("TestScene"));
}
}- Create the interface:
public interface ICustomService
{
void DoSomething();
}- Implement the service:
public sealed class CustomService : ICustomService, IDisposable
{
private readonly Dictionary<string, int> _items = new();
public void DoSomething() { }
public void Dispose() { }
}- Register in ProjectLifetimeScope:
builder.Register<CustomService>(Lifetime.Singleton)
.As<ICustomService>();// Define event (record struct for immutability)
public readonly record struct PlayerLevelUpEvent(int NewLevel);
// Publish
[Inject] private IPublisher<PlayerLevelUpEvent> _publisher;
_publisher.Publish(new PlayerLevelUpEvent(5));
// Subscribe
[Inject] private ISubscriber<PlayerLevelUpEvent> _subscriber;
_subscriber.Subscribe(evt => {
ShowLevelUpEffect(evt.NewLevel);
}).AddTo(Disposables);| Platform | Status | Notes |
|---|---|---|
| Windows | ✅ Full | Primary target |
| macOS | ✅ Full | Supported |
| Linux | ✅ Full | Supported |
| Steam Deck | UI scaling needed | |
| Android | 🔄 Planned | TBD |
| iOS | 🔄 Planned | TBD |
| Consoles | 🔄 Planned | TBD |
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
This project is proprietary to punkfuncgames. See LICENSE file for details.
- Cysharp - UniTask, R3, MessagePipe libraries
- hadashiA - VContainer DI framework
- rlabrecque - Steamworks.NET
- Demigiant - DOTween
For issues, questions, or contributions:
- Create an issue in the repository
- Contact: [your-email@example.com]
- Mobile input support
- Online multiplayer framework
- Mod support via Addressables
- Console platform ports
- Achievement system UI
- In-game analytics dashboard