A project born in Breackeys Game jam 2024 :). The game is playable on itch.io
Do not push any commits to the
main
anddevelop
branches directly. Instead, you should create a new branch and a pull request for merging.
2022.3.15f1
Should always keep the same version with your team.
This project follows GitFlow version control rules, but not that strict.
Hereby a brief guide:
- For the
main
branch, you cannot push commits into it directly. It can only be merged from anyrelease
branch with a pull request. We need to make sure that every version of themain
branch is the latest production-ready game. - For the
develop
branch, you cannot push commits into it directly. It can only be merged from anyfeature/xxx_xxx
branch with a pull request. We need to make sure that every version of thedevelop
branch is a runnable game. - If you want to add a new feature or maybe refactor any code structure, you should create a branch named
feature/the_name_of_feature
from the develop branch. When the branch is finished, you can create a pull request for updating thedevelop
branch. - When a
develop
branch is ready to be published, you can create a pull request to create arelease/vx.x.x
branch. The name of a branch could berelease/v0.0.1
orrelease/alpha1
.
Installing Mono and .NET core beforehand is necessary in order for VScode to work properly.
For more information see this offical article.
Open Unity -> Edit
> Preferences
> External Tools
: Modify External Script Editor
to Visual Studio Code
If it works well, you will see references of any class, function and property.
You can start the demo by opening the System
scene in Unity and press the Play button. After then, you will start in the Menu
scene which present a menu panel. The purpose of System
scene is to initialize system managers like UIManager and EventManager and more before the game starts.
public class CharacterStatus
{
// private values should with a prefix _
private int _hp;
private int _physicalDefence;
private int _magicDefence;
// treat getters as functions, so should start with capital letter
public int HP { get => _hp; }
public int PhysicalDefence { get => _physicalDefence; }
public int MagicDefence { get => _magicDefence; }
public string name;
public void TakeDamage(int damage)
{
_hp -= damage;
}
}
Assets/
├─ DynamicAssets/ /* Dynamic assets usually means that the assets is
│ not always with the game itselfs. It might be
│ downloaded externally. Like DLCs(?) */
├─ Plugins/ /*For third party and custom plugins, libararies */
├─ Presets/
├─ Samples/ /* Generated by ProBuilder, you shouldn't touch it
├─ Resources/ /* generated by DOTween, you shouldn't touch it */
├─ Scenes/ /* System scenes */
├─ StaticAssets/ /* Normally put your assets here */
│ ├─ Materials/
│ ├─ Scenes/ /* This is the folder for level scenes */
│ ├─ Audios/
│ ├─ Sprites/
├─ TextMesh Pro/ /* Generated by TextMesh Pro */
├─ ThirdpartyAssets/ /* If you buy any pack of models, sprites,
│ sounds, put it here */
├─ URP/ /* URP, post processing data */
Before adding a scene, you should know the scene structure of this template. Initially, there are three scenes: System
, Menu
, and MainGame
.
System
is the scene for game objects that need to persist or be excluded from levels. For example, game manager, UI manager, and UI canvas.
Menu
and MainGame
are switchable scenes. While System
is persistent, these two scenes could be loaded/unloaded with the SceneLoader class.
Basically, you can add as many scenes as you want. Please keep in mind that the System
scene should always persist and should not be unloaded.
To add a new scene, hereby following steps:
/* Scripts/SceneLoader/AvailableScene.cs */
public enum AvailableScene
{
Menu,
MainGame,
NewScene
}
/* Scripts/Resources/SceneResources.cs */
public class SceneResources : ScriptableObject
{
[Header("Scenes")]
...
[Required("Must link a scene asset")]
[Scene]
public string NewScene;
}
/* SceneLoader.cs */
public class SceneLoader
{
...
public string GetSceneName(AvailableScene scene)
{
switch (scene)
{
...
case AvailableScene.NewScene:
return ResourceManager.instance.SceneResources.NewScene.GetSceneNameByPath();
}
}
}
Bind the scene object into StaticAssets/ScriptableObject/Resources/SceneResources
Now, you can load scenes programmally by calling game manager.
GameManager.instance.SwitchScene(AvailiableScene.NewScene);
For editing UIs, you can open the UIEditing
scene to do it.
To create an UI panel object, right click the canvas and find:
UI/Custom Components/EmptyPanel
or UI/Custom Components/EmptyPanel(with background)
.
The figure in the last step shows some UI components available. Objects in Custom Components
are some useful components made by me.
To add UI images, I highly recommend using Procedural Image instead of the built-in Image component in Unity.
With existing UI components, you are already able to create many kinds of UI panels. There are already some examples in this template.
Remember to create a folder for your panel. It will be easier to find when UI panels grow.
/* Scripts/UI/AvailableUI.cs */
public enum AvailableUI
{
MenuPanel,
GameHUDPanel,
NewPanel
}
/* Scripts/Resources/UIPanelResources.cs */
public class UIPanelResources : ScriptableObject
{
[Header("UI Panels")]
[Required("Must link a ui panel asset")]
[AssetsOnly]
public GameObject MenuPanel;
[Required("Must link a ui panel asset")]
[AssetsOnly]
public GameObject GameHUDPanel;
[Required("Must link a ui panel asset")]
[AssetsOnly]
public GameObject MewPanel;
}
/* Scripts/UI/UIManager.cs */
public class UIManager : Singleton<UIManager>
{
...
public GameObject GetUIPrefab(AvailableUI ui)
{
switch (ui)
{
...
case AvailableUI.NewPanel:
return ResourceManager.instance.UIPanelResources.NewPanel;
default:
return null;
}
}
}
Save it to Scripts/UI/Panels
public class NewPanel : UIPanel
{
public override AvailableUI Type { get => AvailableUI.NewPanel; }
public override void Open()
{
gameObject.SetActive(true);
}
public override void Close()
{
gameObject.SetActive(false);
}
public override void CloseImmediately()
{
gameObject.SetActive(false);
}
}
Bind the panel object into StaticAssets/ScriptableObject/Resources/UIPanelResources
Now, you can open UI panels programmally by calling UIManager
.
NewPanel newPanel =
UIManager.instance.OpenUI(AvailableUI.NewPanel) as NewPanel;
We sometimes need global events for the implementation, such as achievement system, eventbrodcasting. EventManager can send global events to subscribers to reach this problem.
Before we can send an event, we need to declare it in EventManager.
public class EventNames
{
/*
args
{
int gold
}
*/
public static EventName EarnedGold = new EventName("EARNED_GOLD", false);
}
public class AchievementSystem
{
private Subscription _earnedGoldEventSub;
private void Start()
{
_earnedGoldEventSub =
EventManager
.Subscribe(
IDENTITY,
EventNames.EarnedGold,
(e) =>
{
int gold = (int)e.args[0];
Debug.Log($"Player earned {gold} of gold");
// check if it unlocks "Gain 10000000 gold" achievement
});
}
// remember to cancel subscription on destory, if this gameobject
// usually destroy with the game, then this might not be mandatory
private void OnDestroy()
{
EventManager.CancelSubscription(
EventNames.EarnedGold, _earnedGoldEventSub);
}
}
EventManager.Publish(
EventNames.EarningGold,
new Payload()
{
args = new object[] { 10000 }
}
);
When setting up a new Unity project, we usually spend a lot of time working on scene management. This feature makes the process easier. In addition, if you want to know when the scene has been loaded, you can use an async await function to wait for it.
public async void SwitchScene()
{
// Do something before loading scene
await sceneLoader.SwitchScene(scene);
// Do somethign after loaded
}
However, you do not actually using the above function because it is already handled by GameManager
. :)
GameManager.instance.SwitchScene(AvailableScene.Menu);
While writing a script, it is often annoying to bind the script to a GameObject. Sometimes you even do not use the functions of the UnityEngine namespace.
Alternatively, you might want to use singletons. But you sometimes run into a problem: these Singleton classes have strong relatations, for example, the achievement system might want to be initialized after event system. Two Singletons will not be able to know each others status easily. The situation is getting worst when we have more Singletons!
Therefore, it would be a good idea to have only one object handling these classes. That's why we need DIContainer.
To add an object to DIContainer, you just need to register it in the SetUp()
function.
public partial class DIContainer : Singleton<DIContainer>
{
protected void SetUp()
{
Register<EventManager>(() => new EventManager());
Register<AchievementSystem>(() => new AchievementSystem(
GetObject<EventManager>()
));
// nice :)
}
}
When you want to use an object from DIContainer, you can obtain it like the following:
public class WorldLog : Monobehaviour
{
private Lazy<EventManager> _eventManager = new Lazy<EventManager>(
() => DIContainer.instance.GetObject<EventManager>(), true);
protected EventManager EventManager { get => _eventManager.Value; }
private Subscription _bossKilledEventSub;
private void Start()
{
EventManager
.Subscribe(
IDENTITY,
EventNames.BossKilled,
(e) =>
{
string bossName = (string)e.args[0];
Log($"{bossName} has been killed!!!!!");
});
}
}
Breiflly say, it is for UI Management. The panels are handling in a stack. Interfaces could be push into or pop from the stack.
public class MenuScene : GameScene
{
void Start()
{
MenuPanel manuPanel =
UIManager.instance.OpenUI(AvailableUI.MenuPanel) as ManuPanel;
}
}
public class MenuPanel : UIPanel
{
void Start()
{
btnClose
.ButtonDidClick
.ObserveOnMainThread()
.Subscribe(_ => UIManager.instance.Prev())
.AddTo(this);
}
}
public class GameManager : Singleton<GameManager>
{
void Start()
{
sceneLoader
.LoaderWillLoadScene
.ObserveOnMainThread()
.Subscribe(_ => UIManager.instance.CloseAllUI())
.AddTo(this);
}
}
In order to better control Unity UI components, I suggest writing your own components instead of using Unity's built-in components directly because it is so messy.
However, it does not means that you need to write the UI components from scratch. It is just extending functions from the built-in components.
For that, I already wrote some basic components for fast editing: WDText
, WDButton
, and WDTextButton
.
Here is some short snippet for the code:
public class WDTextButton : WDButton, IWDText
{
public WDText text;
public void SetText(string t)
{
text.SetText(t);
}
public void SetTextColor(Color32 color)
{
text.SetTextColor(color);
}
}
Basically, WDButton
handles hover animation and on click listener, WDTextButton
extends it then add the text&color editing functions.
Additionally, you can also implement an icon button:
public class IconButton : WDButton
{
public Image icon;
public void SetIcon(Sprite sprite)
{
icon.sprite = sprite;
}
}
DOTween - A fast, efficient, fully type-safe object-oriented animation engine for Unity. Usually used for UI animations in this project. It is also useful in gameplay
UniRx - Reactive Extensions for Unity, usually be used for UI Programming in this template
UniTask - Provides an efficient allocation free async/await integration for Unity
For now I only made a singleton class. It is very easy to use.
Note that it is generally not a wise idea to have too many singletons in a game. The code would be a mess.
using WillakeD.CommonPatterns;
public class GameManager : Singleton<GameManager>
{
// your code
}
It is a plugin for showing scene object on inspector, so that users can drag and drop on that.
using WillakeD.ScenePropertyDrawer;
public class SceneLoader : MonoBehaviour
{
[SerializeField]
[Scene]
private string _menu;
public string Menu { get => _menu; }
}