Skip to content

punkfuncgames/tetris-clone

Repository files navigation

3D Project Base

A production-ready Unity 3D game template by punkfuncgames featuring modern architecture patterns, dependency injection, reactive programming, and comprehensive game systems.

🎮 Project Overview

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


✨ Key Features

🏗️ Architecture

  • 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 Systems

  • 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

🌐 Platform Features

  • 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

🧪 Testing

  • Edit Mode Tests - Unit testing for core logic with NSubstitute mocking
  • Play Mode Tests - Integration testing for gameplay systems

📦 Dependencies

Core Packages

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

Third-Party

Package Purpose
Steamworks.NET Steam API Integration
DOTween Animation/Tweening
NuGetForUnity Package Management
NSubstitute Mocking framework for tests

Development Tools

Package Purpose
Rider/VS Editor IDE Integration
Test Framework Unit Testing

📦 Modular Package Architecture

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

Adding a Package

  1. Copy the package folder into Packages/
  2. Unity auto-detects it -- the version define activates automatically
  3. Add the module's installer to your ProjectLifetimeScope inside a #if guard

Removing a Package

  1. Delete the package folder from Packages/
  2. All guarded references compile away cleanly
  3. Remove any installer calls or serialized SO references from your scopes

See docs/MODULE_CATALOG.md for the full package list, tiers, and dependencies.


🚀 Quick Start

Prerequisites

  • Unity 6000.0.x or later
  • Git LFS (for large files)
  • Steam Client (for Steam features)

Setup

  1. Fork the template

    git clone <repository-url>
    cd 3d-project-base
  2. Customize modules (optional)

    • Review docs/MODULE_CATALOG.md for available packages
    • Remove any packages you do not need from Packages/
    • The project compiles cleanly with any subset of packages
  3. Open in Unity

    • Open Unity Hub
    • Add project from cloned directory
    • Open with Unity 6000.0.x+
  4. Configure Steam (Optional)

    • Open steam_appid.txt
    • Replace 480 with your Steam App ID
    • Or keep 480 for testing with Spacewar
  5. Build Scenes Ensure these scenes are in Build Settings:

    • [BOOTSTRAP] - Initial loading scene
    • [MAINMENU] - Main menu
    • [GAMESCENE] - Gameplay scene
  6. Play

    • Enter Play Mode from [BOOTSTRAP] scene
    • Or use Ctrl+Shift+P (Windows) / Cmd+Shift+P (Mac) for Play Mode from first scene

📁 Project Structure

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.


🏛️ Architecture Overview

The Cysharp Modern Stack

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

❌ Forbidden Patterns

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 (use async UniTaskVoid)

Dependency Injection Flow

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

Lifetime Scopes

  1. ProjectLifetimeScope - Persists across all scenes

    • Core services
    • Steam integration
    • Save system
    • Audio service
  2. MainMenuLifetimeScope - Scoped to main menu scene

    • Menu UI controllers
    • Settings panel
  3. GameplayLifetimeScope - Scoped to gameplay scene

    • Gameplay controllers
    • Level management
    • Player systems

Coding Standards

All code follows the PunkFuncGames AI Instructions standards:

  • Sealed Classes: All concrete service and controller classes are sealed to 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

✅ Constructor Injection (Pure C# Classes)

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();
}

✅ Method Injection (MonoBehaviours)

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();
    }
}

✅ Cancellation Tokens

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);
}

✅ R3 Observables with Disposal

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();
    }
}

✅ Entry Points (Pure C# Controllers)

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();
}

🎯 Core Systems

Game State Service

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

Audio Service

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);

Input Service

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);
}

Save Service

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();

MessagePipe Events

Publishing Events

[Inject] private IPublisher<DamageDealtEvent> _damagePublisher;

_damagePublisher.Publish(new DamageDealtEvent(
    target, source, damage, DamageType.Fire, hitPoint, isCritical: true
));

Subscribing to Events

[Inject] private ISubscriber<DamageDealtEvent> _damageSubscriber;

_damageSubscriber.Subscribe(evt => {
    ShowDamageNumber(evt.Damage, evt.HitPoint);
}).AddTo(Disposables);

📝 Configuration

Creating Settings Assets

Right-click in Project window:

  • Create > GameTemplate > Game Settings
  • Create > GameTemplate > Audio Settings
  • Create > GameTemplate > Level Config

Steam Configuration

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 0 to disable Steam

🧪 Testing

Running Tests

  1. Open Window > General > Test Runner
  2. Select Edit Mode or Play Mode tab
  3. Click Run All

Test Patterns

EditMode Tests (Unit Tests)

[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>());
    }
}

PlayMode Tests (Integration Tests)

[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"));
    }
}

🎨 Customization

Adding a New Service

  1. Create the interface:
public interface ICustomService
{
    void DoSomething();
}
  1. Implement the service:
public sealed class CustomService : ICustomService, IDisposable
{
    private readonly Dictionary<string, int> _items = new();

    public void DoSomething() { }
    public void Dispose() { }
}
  1. Register in ProjectLifetimeScope:
builder.Register<CustomService>(Lifetime.Singleton)
    .As<ICustomService>();

Creating Custom Events

// 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 Support

Platform Status Notes
Windows ✅ Full Primary target
macOS ✅ Full Supported
Linux ✅ Full Supported
Steam Deck ⚠️ Partial UI scaling needed
Android 🔄 Planned TBD
iOS 🔄 Planned TBD
Consoles 🔄 Planned TBD

🤝 Contributing

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

📄 License

This project is proprietary to punkfuncgames. See LICENSE file for details.


🙏 Acknowledgments

  • Cysharp - UniTask, R3, MessagePipe libraries
  • hadashiA - VContainer DI framework
  • rlabrecque - Steamworks.NET
  • Demigiant - DOTween

📞 Support

For issues, questions, or contributions:


🗺️ Roadmap

  • Mobile input support
  • Online multiplayer framework
  • Mod support via Addressables
  • Console platform ports
  • Achievement system UI
  • In-game analytics dashboard

About

Tetris clone — MCP-driven game development

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors