Coder Social home page Coder Social logo

hunterdyar / daily-golf-project Goto Github PK

View Code? Open in Web Editor NEW
2.0 1.0 0.0 5.08 MB

Unity example project demonstrating modular project architecture

License: Creative Commons Zero v1.0 Universal

Mathematica 7.47% C# 46.93% ShaderLab 38.85% HLSL 6.75%
unity

daily-golf-project's Introduction

Daily Golf Project

Unity example project demonstrating modular project architecture. A tiny golf game on a procedural map.

The goal of this game is to use data-oriented design patterns and modular coding patterns to allow the different systems to exist independently, and be built up over time.

A gif of the island continuously regenerating

Game Architecture Patterns Being Used

  • Data-Oriented Design. Primarily through storing game data in ScriptableObjets. See the still-relevant 2017 Unite talk from Ryan Hipple.
  • Actions. Using events (static or on ScriptableObjects) to decouple dependencies. See my Event Systems in Unity page.
  • Extension Methods. Great for reusability, but also great for plain old readability. e.g. self-documenting code.
  • Delegates. Keep one system ignorant of the inner workings of another system using functions-as-parameters.

System Notes

Golf Ball Movement

'Stroke' is a POCO that describes a single hit on the golf ball. It's used to store previous hits and edited at runtime. (Caddy scriptable object). CurrentStroke is what the trajectory prediction system is using to figure out what force might get added, for example.

Scriptable Object Organization

"Active" scriptable objects (ActiveGolfConfiguration, InputReader) are objects used for storing references and accessing objects in the scene, instead of singletons, managers, or other such patterns to solve this problem. There is probably just one of these.

"Game Data" scriptable objects (Clubs), on the other hand, is just data. Settings, etc.

The Generator is a little of both. It's basically settings for generation, but it also stores a generation as an image texture sub asset.

Input

Uses new input system. InputActions have a c# class generated, and interfacing with the input actions is done entirely by an InputReader scriptable object. The rest of the project only interfaces with this, which provides convenient actions, process functions, and read-only properties. It keeps the rest of the code agnostic of which input system we are using. It lets us send 'fake' inputs in a non-jank way easily, either with inspector scripts or with public functions.

This technique is inspired from Unity's own 'modular game architecture with scriptableobjects' ebook (repo).

Basically, the input reader serves the role of the PlayerInput component.

Using a scriptableobject to "wrap" an inputactions has the further advantages.

  • A convenient place to store input settings.
  • A place to put custom inspector doodads to preview/read the data for debugging.
  • A way to fire off test actions from the inspector.
  • Easy handling of input state. We know there is a single source of truth for input (er, except for UI using the EventSystem).
  • Hard to misconfigure - more of the setup is handled automatically. Compare with the 'universal' PlayerInput component.

Disadvantages

  • It's overkill for this project.
    • A public reference to InputAction assets would be fine for a game of this scale.
    • We could do it the same, but provide the data as static actions and floats! I don't use statics because it lets me use multiple scriptable objects to store different settings instances for testing and swapping out easily. Especially useful for a project in source control.
  • Another thing to remember and configure.
  • When we create actions, we have to implement them in the ScriptableObject (since it's an interface for the binding), even ones we don't use.

Really, I just like this method for the sake of the rest of the code base. Doing a lot of XR development, it's always useful to have test buttons in the scene (although not needed for a project like this one). I like completely compartmentalizing input away - it tends to be a place where complexity grows over time, and lots of little input handling scripts has always been a headache.

UI

HUD reads from the 'caddy'. With the scriptable object, it is completely independent from any other element. in-scene UI is handled by the trajectory prediction system.

Trajectory Prediction

Basically entirelly in a single script/child of the player, GolfHitPreviewLine.cs.

Uses multi-scene physics to simulate the balls path and draw a line for each tick of that simulation. See the TNTC video for a breakdown of the technique.

Preview trajectory updating in real time

Camera Control

CameraSystem is a state machine. GolfCamera is the base class for a state. Actual camera switching is done via Cinemachine, changing the priority of the cameras, to use their blends.

I'm not very happy with the system right now, need to take advantage of cinemachine more - blending to a tee camera when the ball is close to a tee, blending to an overview when the ball is high; but using our own script for when aiming. The plan is some appropriate mix of high level custom state machine with lower level cinemachine.

Custom Attributes

[ReadOnly] and [Layer] are not attributes that are built into Unity (although they should be). I implemented these as custom attributes, see the utilities folder. Each one is in it's own folder/namespace because I imagine you may want to directly copy them into your own projects. Go for it. That's what I do.

Map Generation

Map generation happens in a Generator scriptable Object (generator.cs). This creates a random level and saves it as a sub-asset, as a Texture2D. So anything can just read those files. We run a series of processes on the grid of pixels. See This RedBlogGames article for more information, and my own 2DRoguelikeLevelGenerator package.

Tee positions (and player spawn) are done by trying to randomly place non-overlapping circles onto the map on valid locations. We keep trying while decreasing the radius of the circle. It's poisson disk sampling with a search, and I would describe it as "good enough for now". It's slow and could have a tee spawn on an island of just one square.

MapGenerator.cs is a simple script to listen for generation (we can regenerate levels at runtime clicking a button in the inspector, very helpful for testing) that spawns in cubes on a grid.

Map Tileset

There are two major tricks on display in the MapTileset. The first is obvious, the custom inspector. It's neat, but really just enough to do the job. Architecturally, the main trick used for modular development is a delegate. The Map Tileset's job is to tell us which prefab to spawn as a convenient ScriptableObject. To do this, it would need to know how we are saving map data, so it give it the map, and it can parse that to find the neighbors.

Instead, we use a delegate, a function we can pass in as an argument. This one is called 'NeighborTest'. So the tileset object here is written independently from how the map data is stored. We could re-write the way map data is stored entirely without having to worry about this prefab picking system at all.

Map Tilset custom inspector screenshot

The way the MapTileset works is as a series of rules, which it checks in order, looking for the first appropriate one. The rule describes what the spaces neighbors can be.

Skybox

Not an architecture note, but for the curious:

I basically used this tutorial: https://medium.com/@jannik_boysen/procedural-skybox-shader-137f6b0cb77c. I had to turn off "Cast Shadows" in the graph inspector to get the UV mapping to behave as it does in the older version of Unity used in that tutorial.

daily-golf-project's People

Contributors

hunterdyar avatar

Stargazers

 avatar  avatar

Watchers

 avatar

daily-golf-project's Issues

Surface reaction/friction system

Completely independent system that acts as a trigger area (but is actually a raycast down from ball onto some kind of surface-encoded data? material? then a material to friction lookup?)

Or we could make a bunch of appropriate-shaped trigger areas in the prefabs, which might be simpler in the long run, but... procedural?

Toon shader material pass.

Use ShaderGraph for custom (simple) flat approach. Lots of good examples out there, it doesn't need to be sophisticated.

Combined with slightly rounded corners on the tiles, I think it will look pretty good.

Skybox?

Camera Switcher

Camera system, like UI, needs to read from the Caddy to get the current stroke and check it. Need to make sure game events are getting consolidated correctly for the state switches and such.

As well as the separate (?) game manager -ey list of past strokes and scorecard thing, #15

camera system listens to events, and it knows it has certain children to activate.

Ball-Collision location decal

Get the location of the balls first contact point, and set a gameobject to that position on preview.

Then use a downward decal feature to make the spot visible, and only visible when aiming.
Change circle size depending on club being used, although we don't have randomness yet.

Cloth sim flag for pole

Will need a wind zone, and have that be accurate to the physics of the object.
Will need a double-sided flag cloth.

Failed Stroke (in water)

Detect failure (y position)

Return to failed stroke start point and create new stroke.

Add 'failed' to strokestatus enum

Generate Tees and spawn positions

Basically keep poisson disk sampling the island to find tees+1 spots. Shuffle them if needed. then put the player at a random spot and the tee's down. Do a very minor/blurry height stamp around each tee. Note the spots with a different color.

We want to find the largest possible disks to sample with, so we will try with a disk the size of the map that we know won't work, then decrease it, try x random times to fit y points, repeat. There are better ways where we don't resample everything but do a maze/tree search at each successful point with recursion. That sounds annoying and this will probably be fast and will eventually find something.

Explore camera & input setup

Need design doc on how setup should work.
Feels like input and camera are deeply linked - aiming. ... are they?

research non-mobile golf games. Alignment and exploration cameras are clear, but how transition?

Cinemachine camera is pretty close, but I think the distance/ring should be determined by the club.
State to look around separate from ball (explore)
State for camera to follow ball without moving from original location (future possible fancy camera switch clearshot system)

I think starting with mouse and gamepad modes is probably all I want for now input wise.

Remove Camera System, replace with MixingCamera

Then a manager that sets the various weights depending on events, stroke state, etc).

Basically the same, but use the MixingCamera, since that's what we ultimately want, to control the states. And a single script to edit things, unrelated scripts to do things like control aim.

  • Aim camera
  • Flight camera (clear shot?)
    • procedurally add clear shot options to the tee's and around the map?
  • Distance from Tee, tee cam.

Blend/switch between them automatically.

Create Pattern-Match on grid of bools for prefab selection/replacement

So:
ooo
oxo
ooo

would get replaced with a tile that represents only a single isolated square (in 2D).

We can create patterns for corners, inside and outside edges, or not.
Then we have to be sure to apply them in the correct way.

A scriptableObject serves as the 'give me pattern i'll give you gameObject', but we need some way to define the map. Our texture2D needs to become a 3 dimensional array of bools (or colors? enums? ScriptableObject "VoxelType"'s? as enum?). That will be easy enough, that's what we are already doing.

Then we just pass along a coordinate, and run all the patterns against it in order, with options like "match first" or "match last".

patterns are a sorted list or given priority overrides? lets say manually sorted for now.
custom inspector (property drawer) for the pattern definition, which is a lot like our current 27 slice, I guess. Neighbors in x directions?

Add Bounces to Preview

We need it for the putter, (bug fix for putter preview) but we can do a bounceCountForPreview in the club properties.

also be for future decals, settings to control that.

This means copying (just?) colliders into simulation, and changing their layer to simulation.
it ALSO means dealing with some OnWorldGenerationComplete action.

Toon Skybox.

Can this be done with shadergraph? basically just a gradient?

Buttons for the InputReader

Fire off 'fake' events from the inspector.

Button attribute would be nice, and good for that other repo I do.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.