So, I’ve had a lot to deal with this week and didn’t have as much time to work on Last Day To Live as I’d hoped, but I still got quite a bit done. Here’s a list of what’s new since last week, in the order I worked on them (more or less, though I didn’t do them strictly sequentially):
- Respawning enemies
- New cursors
- Bugfix: character no longer sometimes runs off instead of attacking
- NPCs and dialogue
You can play the most recent build of the game here, if you’re so inclined.
(Incidentally, I realized that in the previous build, I’d had the player’s jump strength set way too high. The reason for this is because I’d temporarily increased it while trying to fix a bug—I was unable to jump using the space bar while the mouse button was held down to keep the player moving. And, of course, I’d forgotten to set the jump strength back to its previous value before building the game. The bug, incidentally, turned out not to be an issue with the code; it was just an issue with my laptop—the laptop doesn’t register a keypress while the mouse button below the trackpad is held down. Oh well.)
So let me go over the details of each item from the last week.
Respawning Enemies:
So, in the Create with Code course, one of the suggested steps in creating the personal project is to create a SpawnManager to spawn prefab enemies and items. Now, given the nature of Last Day to Live, I hadn’t planned on having enemies randomly spawn. (Items… well, maybe. But those haven’t been implemented yet anyway. More on that later.) So I skipped that step. But then it occurred to me that, even if I didn’t want enemies to spawn randomly, I might want at least some enemies to respawn after they were defeated. I decided to deal with that before tackling the dialogue system because I figured it would be easy to do.
Well, it was, but it wasn’t without some challenges. I wanted the enemies to check that the player wasn’t too close before respawning, so an enemy didn’t unfairly appear right next to the player. That wasn’t hard to do—just call a coroutine that checks the player’s distance at intervals, respawning the enemy and exiting the coroutine if the player is far enough away.
The tricky part was eliminating the enemy in a way to make it possible to bring the enemy back again later. I didn’t want to just spawn an enemy from a prefab, because it wouldn’t preserve the enemy’s waypoints—and besides, I might want to customize an individual enemy and have it respawn with customizations intact. So I tried just setting the enemy to inactive instead. The problem with this was that while the inactive enemy wasn’t visible and its Update function didn’t run, it was still seen as an acceptable target by the combat script, so the player would continue to attack the spot where the enemy was after it was gone (and, because of the way the health indicator was implemented, the heart showing the enemy’s health would keep reappearing at full strength every time it was depleted). So I hoped to figure out a way to delete the enemy while still making it possible to bring it back as it was. I tried passing the enemy GameObject as a parameter to the function in the GameManager that did the spawning, but the GameObject was passed as a parameter, which meant that when the enemy was deleted the reference that the GameManager held was no longer valid. Finally I just did the obvious: I changed the relevant conditions in the combat script from “(target != null)” to “(target != null && target.isActiveAndEnabled)”. I’m not sure why I hadn’t just done that in the first place.
New Cursors:
So, I said in the last post I’d need at least two new cursors, one for talking to NPCs and a pointer for UI. So, well, I don’t have much to say about those, except they there they are, once again blurrily enlarged to four times their (linear) actual size:
Bugfix:
So, I mentioned in an earlier post that there was an issue where sometimes when the user clicked on an enemy the player would rush off toward nowhere in particular. Since this only happened occasionally and I couldn’t find a way to reproduce it, it seemed like the cause of this bug was going to be hard to track down. Well, while implementing the new player talk state, I stumbled along the explanation of what was going on.
The culprit turned out to be the script for the player move script. But explaining what was going on requires going into a bit of detail as to how the player controls were implemented. As I described before, clicking on a walkable location made the player pathfind its way there (using Unity’s NavMesh system), while clicking and holding made the player go straight there (not using pathfinding, so the player could run off cliffs or into obstacles). The way this is implemented is that the Update function in the PlayerController script checks to see if the mouse button has been pressed; if so, it calls a function on the current player state called Move, which, unless overridden by a particular state, simply changes the player’s state to the move state. (As also described before, the player code uses a finite state machine, currently with seven states: Attack, Dead, Idle, Jump, Move, Navigate, and Talk.) Then, in the Update function of the PlayerMoveState script, the script checks to see if the mouse button has been released. If so, it checks how much time has passed since the button was pressed. If it’s been more than half a second (a time held in a variable and so easily modified if I decide to change it), the player’s state changes to Idle. Otherwise, it counts as a mouse click, and the result depends on what the player clicked on: it changes to the Navigate state if the mouse is over a walkable area, to the Attack state if the mouse is over an enemy, or to the Talk state if the mouse is over an NPC.
The problem is that if none of those conditions applied, then, well, nothing happened. The player stayed in the Move state, even though the mouse button had been released—and therefore kept moving toward the cursor. (So, yeah, apparently the player’s haring off hadn’t been random after all; I just hadn’t noticed it was directed toward the cursor location—if I had noticed that, I probably would have figured out the cause of this bug sooner.) So all I had to do was put in an “else” statement to put the player in the Idle state when the mouse is released if none of the previous conditions applies, and the problem seems to have been fixed—at least, I haven’t seen it happen since.
(Hm… it just occurred to me as I was writing this that I think I know how to resolve another minor issue. As it stands, it can be a little hard to click on small targets because the player starts moving as soon as the mouse button is pressed—not to mention that in the case of an enemy or NPC, the target itself may be moving—, so unless you lead the target or click very quickly the cursor may no longer be over the target when the mouse is released. But this issue would disappear and the controls would be much more natural and useable if I just check what the cursor is over when the mouse button was pressed, not when it’s released. Yes, when I write that out it sounds… really obvious and I definitely should have thought of it before.)
Oh, yeah, there’s one more minor glitch I think I resolved. The player did this weird jerky motion when exiting a fight or conversation. Turned out that was just the player settling into position when the NavMeshAgent was enabled, because the height that NavMeshAgent set the player to was different from the height the player would settle to when the NavMesh system wasn’t working. Changing the Base Offset of the player’s NavMeshAgent seems to have fixed this.
NPCs and Dialogue:
Okay, of all the things I implemented since the last post, this was the one that took up the most time. Implementing the NPCs themselves and the player Talk state was relatively straightforward, especially since a lot of the code could be shared with the enemy script and the Attack state, respectively. What wasn’t so straightforward was implementing the conversation system itself.
There were two main challenges in implementing this system: figuring out a structure to hold the conversation data, and actually displaying the conversation elements onscreen. I had a pretty good idea how to do the former. I mentioned in the previous post that the Unity Learn Beginner Programming Course had a section on something called ScriptableObjects that seemed potentially useful, but that I didn’t necessarily see any immediate use for yet. Well, a use occurred to me soon after posting that—ScriptableObjects could work very well for storing the data for the dialogue! (Also for storing information about different items and monster types, but that’s further down the line.) And, indeed, it did. I created four different classes of ScriptableObjects for the dialogue—well, three different classes, plus a parent class that two of the classes derived from. There was a PlayerDialogueNode to hold a dialogue option that the player could select, with a reference to the NPC dialogue node that came next. There was an NPCDialogueNode for, well, NPC dialogue, and two classes that derived from it: an OptionDialogueNode for dialogue followed by the player choosing from various dialogue options (with references to the PlayerDialogueNodes representing these options), and a FunctionDialogueNode for dialogue that would choose the next node based on the results of some function.
And all of this ended up working okay and being not too difficult to implement—except that last bit. The FunctionDialogueNode. The problem was, I needed some way to be able to specify the function the node called, and the parameters to pass to it. And it wasn’t easy to figure out how to do that.
Oh, one more thing, too… I had to figure out a way to map the possible return values of the function to the node that would be called based on that return value. The first obvious choice seemed to be a Dictionary (another data type I’d learned about in the Beginner Programming course), mapping objects to nodes. The problem was that it turned out dictionaries didn’t serialize; they didn’t show up in the Inspector in the Unity Editor, so I couldn’t set their values. All right, so I could use two Lists, one for the keys (the possible return values from the function) and one for the Values (the nodes), and deal manually with finding the Value with the same index as a given Key. (Actually, though this didn’t occur to me till later, there was really no reason to use a List for this; since nothing was going to be added to or deleted from it after it was created, an array could have done the job just as well.) Another problem, though. The List of nodes showed up in the Inspector just fine, but the List of values didn’t—only certain data types could be serialized and show up in the Inspector, and arbitrary objects weren’t one of them. I finally just decided I’d have to narrow things down and have all the dialogue branch functions return integers—and if I was going to do that, I might as well just use a single array and use the return value as the index.
Okay, but that still left the function itself. And that was much harder to deal with than I expected. I wanted to figure out a way to be able to select a function within the Inspector to associate with the given FunctionDialogueNode. But nothing I tried worked. I thought I could pass the function as a delegate (yet another data type I learned about from the Beginner Programming course), but delegates, like Dictionaries, didn’t show up in the Inspector. After struggling for some time to figure out a way around this, I finally resorted to creating a DialogueScript class of ScriptableObjects, containing a ChooseOption function, and then creating a new class derived from this DialogueScript for each different dialogue branch function, overriding the ChooseOption function of the parent class. (Within that parent class, I had to define the ChooseOption function as “virtual” to make this overriding possible. Abstract functions and overrides, incidentally, were yet another thing covered in the Beginner Programming course… I’d used subclasses and overridden functions before in C++, but either they work a little differently in C#, or it’s been long enough that I’d forgotten some of the details of how they worked. Anyway, if you want to write code in Unity and you’re not already very familiar with C# techniques and data structures, I highly recommend that course—I’ve done a lot of programming before in C++, and I still learned a lot from it.) Creating a new ScriptableObject for each different function I wanted to call from a FunctionDialogueNode was maybe not as elegant a solution as I’d hoped for, but it worked… I’m still wondering if there is a way to be able to assign a function to a field within the Inspector, but if so I couldn’t find it.
That still left the problem of the parameters. Just as I’d wanted the function to be able to return a value of any type, I wanted to be able to pass parameters of any type, as well. Now, it turns out there is a way in C# to allow a function to take arbitrary parameters: you can just specify the function’s parameters as “params object[] parameters” (or another variable name of your choice). The problem was that “object[]” part… once again, you can’t set arbitrary objects in the Inspector. And while I could settle for requiring the branch function to return an integer value, it seemed too restrictive to allow it to take only integer parameters, as well. I could foresee situations where it might be desirable to pass integers, or strings, or GameObjects, at the very least, and there’s no serializable object type that encompasses all of those. What I finally did was define a type of struct that included an array of integers, an array of strings, and an array of GameObjects; that struct was serialized in the Inspector, and I could pass it to the branch function. For the NPC in the current version of the game who chooses a number between 1 and 3, for instance, the relevant FunctionDialogueNode calls a DialogueScript that looks at the first element of the integer array in the passed struct, and chooses a random number between 0 and one less than that number. That way if I want to later have another dialogue node choose randomly between a different number of options, I don’t have to create a new DialogueScript for it; I can just call the same script with a different parameter.
(Structs, by the way, are a type of data structure that as far as I can recall was not covered in the Beginner Programming class—but that I was already familiar with thanks to my experience with C and C++. Dictionaries, delegates, and ScriptableObjects were all new to me, but I’ve made plenty of use of structs in the past.)
Anyway, as I said, there was also the matter of displaying the dialogue onscreen. Now, I had a pretty good idea for how I wanted it to look, and how I wanted the background graphic to resize itself to match the size of the text, and I was worried that getting the corners and edges to look right would involve some messy coding. Fortunately, I discovered that sprites in Unity supported a technique called “9-slicing” that did most of the hard work for me as far as that was concerned.
Getting the background image to the correct size was a bit harder, though. To set its size, I had to know the size of the text in front of it—and there was no immediately obvious way to find that, or rather, none of the ways that seemed obvious worked. I tried checking for the height of the RectTransform component of the text, but I kept getting zero. I found out about a component called a Content Size Fitter, and I put it on the text’s parent object, but that didn’t seem to help. Some websearching led me to the conclusion that the text’s size wasn’t actually being set until its next Update, which is why I was getting a height of zero calling it in the Start function of the script attached to the background image… but that was where I needed to call it. I finally finagled a kludgy way around the problem by calling a coroutine that kept checking the text’s height until it was no longer zero, and then setting the background image’s height accordingly. It wasn’t pretty, but it worked… at least for that specific use.
But I also needed to be able to find the height of the dialogue boxes so I could space them out correctly—I needed to be able to leave equal gaps between the NPC dialogue and each player dialogue option. And since I could have any number of player dialogue options, shunting this off to a coroutine didn’t really seem practical. Well, fortunately, it turned out there was a very simple solution, though it took me a lot of trial and error and reading through documentation to find it. There’s a function called GetPreferredValues that gives the “preferred” width and height of a TextMeshPro object, and all I had to do was call “dialogueText.GetPreferredValues().y” instead of “dialogueText.transform.GetComponent().rect.height”.
Oops… I just realized something. There may have been an even simpler solution to the problem, a solution which I’d already mostly carried out but of which I’d left a crucial step undone. That Content Size Fitter? I’d had its fit set to “Unconstrained”… which meant it wasn’t actually doing anything. If I’d set its fit to “Preferred Size”, then I might have been able to get the sizes without going through all that rigmarole. Wow. I’m an idiot. But then, I already knew that.
Wrap-Up:
So… that’s it for this week. What remains to be done? Well… a lot. I think I got way too ambitious choosing my personal project. Among the key gameplay features that remain to be implemented are:
- Falling. I mean, okay, as it stands the player is subject to gravity and does fall if they run off a cliff, but the game doesn’t know they’re falling, which means that moving by holding the mouse button down is still processed, and, importantly, the player isn’t damaged on hitting the ground. Because I have some puzzles and gameplay elements in mind where they player might actually want to jump off a cliff and die, I need to implement falling damage, which means I need to detect whether the player is on the ground…
- Items and inventory. There are a few rudiments of this system implemented: there’s a cursor for interacting with objects, and there’s an inventory icon in the UI. But right now, neither of those actually does anything.
- Ultimately, I want to have a large gameworld, which means, as I mentioned in a previous post, that it probably won’t be feasible to have it all in memory at once; I’ll have to look into how to have big open worlds and dynamically load sections of them as needed.
All those items, however, while definitely things I want to ultimately include in the game, are things I’m definitely not going to have done in time for the Graduation Play-a-thon next week. I’ll worry about them after that. There are a few, somewhat simpler fixes that I may be able to get to before the Play-a-thon:
- A few tweaks to the dialogue system. I need to add a reference to a script to the PlayerDialogueNode, as well, to make it possible for a particular dialogue option to only appear under certain conditions. This shouldn’t be hard to do, though; I just haven’t done it yet. Also, right now the dialogue just takes up too much space… it’s fine for short dialogue lines with only a couple of player options like the ones that are there now, but once I have longer dialogues and more player options they won’t fit on the screen at this side. Again, this should be a relatively easy fix; I just haven’t done it yet.
- A main game menu. Again, the icon is there; it just doesn’t do anything yet. I don’t expect to have much there before the Play-a-thon; maybe just volume controls. I can add more to the game menu later. But I hope to at least have something by next week.
- Music and sound effects. And maybe some particle effects, too.
- Oh, and a title screen.
But my main priority for now, and the next thing I’m going to work on, is… finally swapping out those simple primitives for some better-looking assets. Oh, and also working on the terrain to make a more interesting game map. I took advantage of some time when I was working on set without an internet connection and unable to do much else to sketch out a rough map of what I want the beginning area of the game to look like:
So, if nothing else, by next week I want to have the spheres and cubes and capsules replaced by more attractive models, and to have a more interesting game map to explore. I want the game to look nice for the Play-a-thon, at least, even if not all the planned features are there yet. Which I guess means making that model for the main character, if I’m really going to try to do that… hm.
Speaking of which, I did finish the Unity Learn Beginner Design course, and started the Beginner Art course… and the first project in that course is on game character concept design! At this point, though, I’m thinking maybe I’ll hold off on doing any more Unity Learn courses until after the Graduation Play-a-thon (not counting the Unity Learn Live courses I’m already registered for). I want to get as much done on the game as possible before the Play-a-thon, and I’ve now got only about a week left to do it…