Skip to content

Programming Overview

ketexon edited this page Jan 26, 2024 · 5 revisions

Conventions

Use C# naming conventions. Briefly: camelCase for local vars and non-public member vars, PascalCase for types, public member vars, properties. Make sure code is formatted using default VS formatter (on windows, Ctrl+K Ctrl+D). You can also name private variables with an underscore, notably for properties.

Name ScriptableObjects with "NameSO".

ScriptableObjects

Use ScriptableObjects wherever you need a data structure that needs to be instantiated by hand and/or a data structure that is used across multiple classes. ScriptableObjects lets you modify the data structure in the editor, which is friendlier and does not require modifying code, and allows you to easily pass around the data without any code coupling.

ScriptableObjects are preferred to static variables, since static variables require coupling. However, this is not as enforced.

Another use of ScriptableObjects is to get a referentially unique ID. This is useful for identifying things. For example, instead of using strings to identify things, you can use a ScriptableObject. For example, to check if the player is holding a certain fish, compare the FishSO instead of the fish's name.

You can also use ScriptableObjects to simplify components by moving non-scene parameters to the Project pane instead of the scene. This makes it so you can modify these settings without commiting any scene files. This is completely optional and shouldn't be overdone.

Architecture

Items

Items are anything the player can hold, and currently includes:

  • Fishing Rod
  • Fish Item

Items are described by an ItemSO asset, which contains the item's name and its prefab (at this point). The prefab must have a script deriving Item on it.

Implementation note: ItemSOs are referentially unique (as compared to Prefabs, which are usually duplicated at runtime), so you can do comparisons with them (eg. if (playerItem.EnabledItem == fishingRod), which would not work if we were comparing a prefab to the instantiated GameObject).

Items are managed by the PlayerItem script (on the Player). This is what manages what items are owned by the player, switching between items, and activating items. When an item is enabled, the current item's GameObject is destroyed and the new item's prefab is instantiated.

The player holds some items in their "inventory" (represented by an ItemSO list). You can cycle the items using the scroll wheel.

All items that aren't in the item wheel are called temporary items, and can be enabled programmatically (eg. the Fishing Rod enables the Fish Item as a temporary item when you collect a fish).

Since the PlayerItem script instantiates a new item immediately but destroys the old item next frame (that's how Destroy works), the old item can interact with the new item even after the items get swapped. For example, the Fishing Rod enables the Fish Item and then calls the Fish Item's SetFish function to set which fish is being held, which would not be possible if the Fishing Rod got destroyed immediately.

There are some utility functions for Items, seen in the Item base class. Notably, a callback for when the item is used (OnUse, spacebar), a callback for immediately after the item is Destroyed (OnStopUsingItem), and an initialization function (Initialize) that stores protected member values for the player GameObject (player), the PlayerInput (playerInput), and the PlayerItem (playerItem).

Input

PlayerInput sends events that you can configure in the editor. Since items are created at runtime, you cannot hook up input events to them in the editor. Thus, they have to be done by hand using InputActions (see Input System's docs for Responding to Actions)

The input system has an InputActionReference class which allows you to reference a specific input action (eg. UseItem, Move) in the editor, which you can then access the InputAction via InputActionReference.action. You can then register callbacks to the InputAction by creating a function with the signature void (InputAction.CallbackContext ctx) (eg. void OnMove(InputAction.CallbackContext ctx)) and subscribing to the InputAction.started, InputAction.performed, and/or InputAction.canceled callbacks by doing inputActionAsset.asset.performed += MyFunction. Usually, you subscribe to performed for buttons and performed and canceled for values. Importantly, once your object gets destroyed (in OnDestroy), you must unregister from these events, or else there will be null reference exception, by doing inputActionAsset.asset.performed -= MyFunction.

Check the FishingRod Initialize and OnDestroy for an example of subscribing and unsubscribing to input actions.

Fish

There are a lot of types of fish instances, for example, the fish in the water, the fish in your hand, the fish in the notebook. These are all gathered into one FishSO asset.

The FishSO asset currently holds:

  • The Fish's name
  • The Fish's in-water prefab
  • The Fish's in-hand prefab

Implementation note: you cannot swap animation controllers at runtime. If there are two animators for a fish, the only way to swap between them is to have two separate prefabs. The alternative is to use separate state loops for the in-water and in-hand.

State

We want to have global state mainly managed by ScriptableObject assets, since they can be referenced in the editor.

Global parameters are in the GlobalParametersSO class.

Data for many objects, including fish, items, fishing zones, etc. are stored in respective ScriptableObjects next to their scripts.

Utility Functions

See Utility Functions

Clone this wiki locally