[MUSIC PLAYING]
PATRICK MARTIN: Welcome, game developers,
to another Firecast.
Have you been staring longingly at Cloud Firestore for Firebase
and waiting for that one perfect tutorial
to help you integrate it into a Unity game?
Well, welcome, friend.
Let's get started.
So I'm a fan of tabletop RPGs, and I
figured it would be fun to create an RPG character
system in Unity and sync it to Firestore.
Maybe this will be the basis for an awesome new Firebase
adventure game, or just a clever hack
to avoid forgetting my character sheet again.
But first, let's talk about why Firestore, other than this
being some elaborate MacGuffin.
It's not the only Firebase database in town, after all.
[WHOOSH]
Firestore is a NoSQL database that
lets me view changes in near-real time
and is paired with a powerful query system.
Firestore scales up to over a million concurrent users,
so I'm not going to have to think about manually sharding
or balancing my player base.
This means that unless I decide to pivot to a real-time action
RPG, I can iterate quickly as I prototype
and have a good amount of certainty
that my game will be shippable as is when I'm done,
not that I'd ever ship code I wrote whilst prototyping.
[WHOOSH]
It's great for that meta gameplay
I might want to ship in my game as well--
maybe guild progress towards some shared goal,
seasonal leaderboards, even a backing for a battle
pass when I consider monetization.
Firestore is ultra robust and fast enough
to be the core of any asynchronous multiplayer
mechanics I might consider.
I could build to a board game, card game,
or even a multiplayer stat-based trivia game
entirely on Firestore.
And I'll be able to rest assured that with 99.999% uptime,
my players will be able to play when and where they want.
[WHOOSH]
Firestore is one of two databases available to Firebase
game developers.
See this page for more information
on choosing a database.
Link in the description below.
Generally, you'll want to use real-time database
for small amounts of rapidly changing data,
whereas Firestore is great for lots of data
that changes less frequently.
But this decision could be a video into itself,
and you can use both if you have different needs
for different features.
If you're like me, you're probably already a little bored
and just want to jump to writing some C Sharp.
But I need to talk a little bit about how Firestore
stores and accesses data, since this
will inform my code structure.
Firestore is a schemaless, NoSQL database.
This may take a bit of adjustment
if you're not used to it, but it will
feel familiar to any existing Realtime Database or Mongo
users.
[WHOOSH]
Firestore is different than Realtime Database,
though, so don't skip to the next chapter just yet.
Firestore has two kinds of objects--
collections and documents.
Documents roughly map to a struct in C Sharp,
so that's how I'll plan to use them.
A collection is just a group of these documents.
In this architecture, any single character sheet
is a document, since they'll probably
have a name, stats, and whatever other gameplay data I
need to place there.
To hold a bunch of these character sheets
in my database, I'll put them in a collection
called character sheets.
[WHOOSH]
The only other rules to keep in mind
is that the root of the database has to be a collection,
and the documents themselves can't contain other documents.
But they can certainly have child collections
that contain other documents.
So if I wanted to add a player inventory,
I could look at doing that by adding a inventory
subcollection to each character document.
This means that, in general, I'll
be zigzagging through my data with a collection, then
a document, then a subcollection, then a document,
as far down as I need to go.
Also note that subcollections don't actually
live in documents, as in there won't be a field in a character
document called Inventory.
So you don't have to worry about what kind of C Sharp collection
you add in Unity.
Now, it's time to fire up Unity and throw something together.
As I said at the start, I'm building a character creation
system as the first step toward some RPG project.
[WHOOSH]
I have already installed and set up Firebase
after following this Getting Started tutorial.
Link in the description below.
[WHOOSH]
This means that I have my Google Services configuration
files in my game.
I'm verifying that I have my dependencies.
And in this case, I'm loading a scene
after Firebase initializes.
Next, I'll add Firestore to my Firebase project
by opening my Firebase dashboard.
I'll go here and click Create database.
I'm going to create this database in test mode, which
sets the security rules so that it will block
reads and writes entirely for 30 days.
So a quick security timeout--
rules are, of course, very important.
The Google Services configuration
files you downloaded in the Getting Started video are
trivially retrievable by anyone you send your game to,
even on the app stores.
This means that just downloading your game
would give someone enough information
to read or write anything anywhere in your database
if you're to leave it open.
So the only way to really ensure that enterprising players
aren't poking around where they don't belong
is to define strict access rules, only allowing players
to write their own data, and only allowing them to read
what's shared with them.
Anything short of that is just asking
for trouble in the future.
[WHOOSH]
To find out more about rules, see this video.
Link in the description below.
With the test mode rules, your database
will be fully open for 30 days to let
you experiment before locking down,
preventing any reads or writes.
So you have some time to experiment,
but don't waste it all, and be a little cautious
with whom you share your project with during this time.
Back to the screencast.
[WHOOSH]
I'll also choose a location.
There are different pros and cons to each one.
But generally, you want the one that's closest to your players.
It's probably also better to choose a multiregion location,
like nam5 if you want those five nines
of uptime, which I'm doing.
There are some other considerations.
For example, choosing a multiregion location
favors reliability over speed, but I'll
include relevant links in the description below to help out.
You only get one Firestore database per Firebase project,
and I live in the US, so nam5 is a good, safe bet for me.
So now let me import the Firestore Unity
package from my downloaded Firebase SDK.
I'm using the version in the dotnet4 folder, which
is the package you should use with any recent version
of Unity.
I'll start by defining the data that
represents a character in a struct called CharacterData.
Remember how I mentioned that Firestore documents are
basically structs?
I literally mean that.
I just have to tag this struct with the FirestoreData
attribute for this to work.
You could theoretically use a MonoBehaviour or just
deal with the document as a C Sharp dictionary,
but I find that this is the safest
and easiest representation.
Next, let me define what's in a player.
These have to be properties rather than fields.
That is, they need a getter and a setter.
If it wasn't for this requirement,
I might just choose to send a MonoBehaviour to Firestore
rather than mediating through a struct
to make it easier to edit game data in the Unity Editor
itself.
So I'll add a FirestoreProperty annotation and create a string,
Name, with a get and a set, also,
a string description for some cool back story.
No character is complete without a lengthy novella
accompanying them.
[BLING]
And finally, I figure that, at some point,
I'll have something like stats.
Property for attack and defense seems like a good first step.
I want to hook these up to the Unity UI,
so I'll just create a SetCharacterData class.
First, I'll have a serialized string for the _characterPath,
where this lives in Firestore.
I'll put a document named one_cool_dude in the collection
character_sheets for now, but it will
be configurable in the Editor.
Then I'll add an input field for each property in my player
data, so name, description, attack, and defense.
Oh, and the Submit button.
[BLING]
When you click on the button, I'll create a characterData
to upload.
Grab the Name, Description, Attack, and Defense,
then upload it to Firestore at the _characterPath I defined.
Note that by default, this replaces the document with
the data you pass into SetAsync().
You can pass an optional SetOptions to merge the data
or only merge specific fields.
This doesn't make sense for my game right now,
so I'll just delete it.
Let me also build a UI real fast.
Then I'll hook up Name, Description, Attack Defense,
and the Submit button.
After all that, I can click Run, enter a name like Vizzini,
a quick description like "Have you heard of Plato?
Aristotle?
Socrates?
Amateurs!"
Some stats attack 10, and defense 0, and hit Submit.
Now, if I jump back over to my Firebase Console,
I might have to hit Refresh real FAST
but now I should see all my data.
This will also update in real time after that first refresh.
So if I think Defense is a little mean,
I can up it to 1 or even 2.
There is, of course, a bit more to do here for a shipping game.
I don't sanitize inputs at all, either locally or via security
rules on my Firestore database, and I can only
create one character right now.
But that's a problem for post-production Patrick.
Pre-production Patrick needs to write a quick character
viewer to make sure that this data can get back
into the game.
So I'll create a class named GetCharacterData.
I need the database path, so let me
copy paste it from the Set behavior.
Then to make this video easier to follow,
I'll create a bunch of text fields to write into.
Normally, I'd use UnityEvents to make
this a little more pluggable.
[BLING]
Next, I'll create a public function
that can be called on a button press
that I set up in the Editor.
I'll get the Firestore document and my _characterPath and ask
for a snapshot asynchronously.
It's very important that I continue
this on the main thread, since I'll be calling functions
on Unity objects.
Not doing this can result in some confusing threading
exceptions.
I'll just assert that there's no exception for now.
You'll want to check this and act on it in the future.
And finally, I'll grab the character data just
by saying, task.Result.ConvertTo characterData.
That's it for reading from the database.
Let me just complete this bookkeeping task of updating
all the text fields.
I'll create a UI real fast, then add
the text fields Name, Description, Attack, Defense,
and hook them up.
Let's hit Go and see what happens.
So you can see that I can get the last character
data I uploaded, and I can also change it here and sync it
down.
[WHOOSH]
But this isn't the best way to keep data in sync
with Firestore.
GetData() by default makes a call out to the Firestore
server.
No matter how fast this connection
is, the reality of network latency,
the fact that players may lose internet connectivity
between subway stops--
in that 30 seconds after walking out
the door when your phone can't decide whether it wants
your Wi-Fi or the cell tower means that your game may never
be perfectly in sync.
Every time I've shipped an online feature,
whether a real-time competitive multiplayer
mode or asynchronous metagameplay,
I've had to build a large amount of potentially
buggy latency compensation code to predict
the effects of a player's action,
update the game accordingly, then rectify my prediction
if it turns out to be wrong.
Listen() is probably the best possible generic solution
to this problem.
Rather than waiting for a network request
and returning data based on that outcome,
it hits a local cache containing the last known state
of the document.
If the cache is up to date, the callback fires once,
and the listener sticks around in case
the document's updated again.
But if the cache is out of date, the player
will get a second update seconds to minutes later,
depending on the quality of their network connection.
So you can give users some feedback immediately.
But just remember that you need to gracefully update your game
state again when it changes.
But because of this, you get a bunch
of other cool side effects.
For example, if you write data to a document,
you actually update the cache before you update the server
data, and any listeners can get your local update immediately.
You could feasibly have an entire game play out offline
during a subway ride and just sync the data up afterwards.
Just remember that when it does sync,
and the transactions fail, you'll
get another update undoing the gameplay
that you'll also have to gracefully handle.
Like I said, it's not a perfect solution,
but probably still automating a ton of code
that you'd be writing manually anyway.
So let's see how this works.
[WHOOSH]
I'll create a Start() function, copy over my GetSnapshot line,
and change this to Listen().
I'll also cache the _listenerRegistration.
Then I'll create an OnDestroy() callback, before I forget,
and call Stop() on this _listenerRegistration.
In Unity, this is very important if you
want to avoid potential NullReferenceExceptions.
Now, I'll just copy the rest of the body of GetData() out
and delete the old function.
I don't need it anymore.
I don't need the Get button anymore,
so I'll just delete that.
Then click Launch.
Note that I already have the data.
If I set new character info, it just syncs automatically.
[WHOOSH]
That should get you set up with the basics,
but this is only the start.
First and foremost, start thinking about security.
Before you share your Google Services data
outside of your team, either by sending it to testers
or making it open source, you'll want to create robust security
rules.
Next, I'd love to see you all build on this framework.
Maybe you can create a simple multiplayer deck-battling game,
or perhaps a multiplayer co-op RPG, where you and some friends
have to make cool characters to fight against some epic boss.
You could even use this to create a game manager
for your next tabletop RPG session.
I may be doing that myself to slim down my binder
full of character sheets.
Don't forget to like and subscribe
if you love these Firebase games tutorials.
Let me know what you're building or what
you want to hear me talk about next in the comments below.
And of course, follow me on Twitter at @pux0r3.
So long and have a wonderful time.
[MUSIC PLAYING]