Disclaimer
I myself am a noob working on my first Unity game. However, like you, mine is a 2D game with no aspirations for a 3rd dimension (a typical puzzle game).
Use the UI
Like you, I struggled to understand whether it is better to use the world space or the UI space. Everyone said to use the world, so I started out with that. After playing around with things a fair amount, especially trying to make things pixel-perfect, I realized that the Unity world is really 3D, whether you pretend it is 2D or not, and this is simply more firepower than you need for purely 2D apps. On the one hand, setting the game scale to world units and setting the world unit to be the size of your tile makes for very nice, very convenient coordinates. On the other hand, if you want things pixel-perfect, this route will make you pull your hair out. That's because if you end up drawing a line between two pixels, Unity will very helpfully interpolate for you, drawing the line two pixels wide and adjusting the color accordingly (I want to call it "anti-aliasing", but this effect happens even if you explicitly turn anti-aliasing off, so I think that refers more to curves than simple straight rectilinear lines).
What I did is switch entirely to UI elements: Panels
, Buttons
, ScrollView
, etc. This means that everything you do is now in screen coordinates, which is both good and bad. The good thing is, it is trivial to make things pixel-perfect. The bad thing is, screen resolutions. If you only want to target a single resolution, life is easy. Of course, if you were going to do that, why are you using Unity? So, the best way to go is to create a Canvas
set to "Screen Space - Overlay" and set the Scaler
to "Scale With Screen Size". I also set the scaling to 0.5, so it accounts for both horizontal and vertical changes evenly. This will mostly make your game scale appropriately from tiny phones up to massive 4K TV displays.
Custom Scaling
My situation is unique in that I want the puzzle board to remain close to a fixed physical size (so zoomed in on tiny displays and zoomed out on big ones), which implies "Constant Physical Size". However, I also want to be able to zoom in and out under user control. So I removed the Canvas Scaler
entirely and I simply resize the main Panel
to zoom. Note that the Camera is almost completely irrelevant with this style, and you should basically ignore it.
The next issue is that I have menus which I do want to scale to the screen size. So I created a second Canvas
which has the Scaler
set as mentioned above. This causes my menus to always be visible at an appropriate size for the display, while letting the puzzle board expand and contract as necessary.
In this style, there are no "world coordinates". There are just screen coordinates, determined by your Canvas Scaler
or your Canvas
. Layering is sometimes achieved by setting the Z order, but mostly by ordering your GameObjects
(rendering goes from top to bottom, so the last object drawn will always be "on top"). Of course, my Canvases also render in list order, so the Menu Canvas is drawn last, and overlays everything else.
Widgets
I suspect you can get far by using Panels
for most of your controls. A button provides a very convenient OnClick()
event to hook into, but I use OnPointerClick()
from the EventSystem, and find out which panel the mouse is over via eventData.pointerEntered. This way, you have a single entry point for click handling, and you can get at the object clicked without having to compute its coordinates on the screen. If you are generating your tiles programmatically, I don't see a need for a GridLayout
(although, I haven't used it, so take that with a grain of salt).
It looks like Area 1 on your drawing is a "game map", and Area 2-5 are what I would call the "UI" proper. If you want the game map to be scrollable, you can probably just create a ScrollRect
and lay down the entire map at once, using the ScrollRect
to mask all but the visible area. You should be able to lay out all the UI elements statically in the designer, so you can get the controls pixel-perfect. Note that "Pixel Perfect" is actually an option on your Canvas
, which will snap objects to integer screen coordinates so you don't get the anti-aliasing problem I mentioned above. Definitely check this box if you want crisp edges.
Every widget that you create more than 1 of, you should create as a Prefab first. Make the Prefab have the properties which will be most common, and then create all the instances from the Prefab by dragging the Prefab onto your object tree. This will make all your tiles have the same size, anchors, etc. However, note that once you override a Prefab, it will stop auto-updating when you change the Prefab properties. So, you can make each object constructed from a Prefab different from the Prefab (reflected as overrides), but there is the cost to doing so, which is that it disconnects the object from Prefab updates.
Scripts
Yes, all your game logic should live in scripts. There should be [at least] one for your map, one for your characters, one for game items, one for menu actions, etc. The scripts can use dependency injection by declaring public data members which you bind in the Unity Editor (drag a game object onto the field in the property page). This makes it easy to refer to other Unity objects inside your script without making global references to the object hierarchy.
You'll notice that every script gives you a Start()
and Update()
method for free. This is a reminder to NOT USE THE CONSTRUCTOR. The c'tor will get called multiple times because of serialization. You want initialization to be done in Awake()
or Start()
. This is unfriendly to #nullable enabled
code, unfortunately, but such is life.