July 4, 2023

The /r/roguelikedev challenge for Unity: Parts 0 and 1

By bluesoul

Part 1: Instantiating the @ symbol and moving it around.

Video

So far we’ve got, uh, the concept of a game that may one day exist, and a spritesheet. That’s a better start than it sounds like, but we’re going to get going in earnest now. Start by adding two new folders via Unity, right click in that empty space in your Assets folder and choose Create -> Folder. Name it Scripts, then repeat the process and name it Resources. If your Assets view looks like this, you’re good:

Drag your sprite sheet into Resources, and your GameManager into Scripts.

Now open up Resources. Your sprite sheet has a small Play button-looking arrow on the right side of it, hit it and it should expand to all the symbols that make up the sheet.

Find the @ symbol, which in my picture above is in the third row on the far left (the 0 above it is easily mistaken for it), and drag it into the hierarchy view below Main Camera. If you did it right, you should have a dejavu10x10_gs_tc_NoBackground_31 item below Main Camera.

In the Inspector on the right, at the very top of the menu, rename it from dejavu10x10_gs_tc_NoBackground_31 to Player.

If you did it right it will be renamed in the hierarchy view as well.

Now drag Player from the hierarchy view back into the resources folder. You can drop it pretty much anywhere in there even if it’s on top of another sprite. It’ll now be in Resources as a distinct Player object.

Delete Player from the hierarchy view by clicking it and either hitting the Delete key or right-click -> Delete.

Now click the Scripts folder and double-click GameManager.

We’re going to add some new code here, first a Start method that calls the Instantiate() method.

    void Awake()
    {
        ...
    }

    private void Start()
    {
        Instantiate(Resources.Load<GameObject>("Player")).name = "Player";
    }

Let’s take a longer look than the video about what we’re doing here. (I say “we” but when I do something like this, it’s honestly for my own benefit primarily.) We can find Instantiate() in the Unity docs, as a method of UnityEngine.Object. In the form that we’re using with a single Object argument (which is what’s being returned by Resources.Load<GameObject>(), it creates a copy of that GameObject and returns the clone. The GameObject class is also in the docs as the base class for every entity in Unity scenes, so that Player business we did dragging it in and out of the view was making it into a GameObject, and it’s looking for the name we set for it. The .name = "Player" indicates that we’re actually naming it Player instead of Unity’s default behavior which would be something like Player (clone).

Now, we’re going to add a couple of properties to the GameManager class as well as a getter for one of them.

public class GameManager : MonoBehaviour
{
    public static GameManager Instance;

    [SerializeField] private float time = 0.1f;
    [SerializeField] private bool isPlayerTurn = true;
    
    public bool IsPlayerTurn => isPlayerTurn;

The [SerializeField] is another bit of Unity’s doing, the docs indicate that by default, Unity will not serialize private fields, but when you do want them serialized, preface the variable declaration with [SerializeField]. Since we’re handling turns with a private var, and serialization is used to load game state from memory, we need this. We also need the getter because presumably somewhere in the future we’ll need to know if it’s the player’s turn from other classes, like a movement class.

Now we’re going to add a couple more methods, one to end the player’s turn and one to wait for the NPCs to move, we’re just going to stub these out for now because there’s no NPCs.

    private void Start()
    {
        Instantiate(Resources.Load<GameObject>("Player")).name = "Player";
    }

    public void EndTurn()
    {
        isPlayerTurn = false;
        StartCoroutine(WaitForTurns());
    }
    
    private IEnumerator WaitForTurns()
    {
        yield return new WaitForSeconds(time);
        isPlayerTurn = true;
    }

Looking at these one by one, the EndTurn() method is pretty straightforward. It sets the player turn var to false, and then starts a coroutine which can be interrupted by use of the yield keyword, which is present in our next method. The IEnumerator type shows that this is a way to count any non-generic object, so we’ll presumably eventually be enumerating a list of monsters.

Now, back in Unity, we’re going to create a new file in Scripts. Create -> Input Actions (at the very bottom) and name it Controls.

Select it and in the Inspector, check the Generate C# Class box and hit Apply, then click Edit Asset. You should have a new Controls (Input Actions) window open up. Under the No Control Schemes drop down, choose Add Control Scheme... Call it Arrow Keys, hit the Plus and select Keyboard, hit the plus again and select Mouse -> Mouse, then set Mouse to optional, and hit Save.

Now it should say Arrow Keys on the drop down. Hit the plus to the right of Action Maps and call your new action map Player.

Right-click that New action action and hit Rename, name it Movement. Now on the action properties, set the Action Type to Value and the Control Type to Vector 2, which I’ll get into more a little further down this page.

Now click the + next to Interactions and click Press, then click the + next to Processors and click Normalize Vector 2.

Now expand the Movement action, delete the binding under it with a right click -> Delete, then click the + next to Movement and choose Add Up/Down/Left/Right Composite. For each one of the bindings that just spawned, change the Binding -> Path value to the appropriate arrow key.

Now make one more Action called Exit, set the path to Escape, and check the box to use it in the Arrow Keys control scheme.

Now hit Save Asset.

Make another C# Script in your Scripts folder and call it Player and double-click it. We make a number of changes that net out to this being your starting point for the file:

using UnityEngine;
using UnityEngine.InputSystem;

public class Player : MonoBehaviour
{

}

Now we’re going to add a couple of properties.

public class Player : MonoBehaviour
{
    private Controls controls;
    [SerializeField] private bool moveKeyHeld;

We’re making the control system a part of the player class and taking a true/false of if a movement key is held. So far so good.

public class Player : MonoBehaviour
{
    private Controls controls;
    [SerializeField] private bool moveKeyHeld;
    
    private void Awake() => controls = new Controls();

I didn’t mention it before, but the Awake() method fires when the script instance is first loaded. So when the Player instance is loaded, we instantiate a new Controls object.

    private void Awake() => controls = new Controls();

    private void OnEnable()
    {
        controls.Enable();

        controls.Player.Movement.started += OnMovement;
        controls.Player.Movement.canceled += OnMovement;
        
        controls.Player.Exit.performed += OnExit;
    }
    
    private void OnDisable()
    {
        controls.Disable();
        
        controls.Player.Movement.started -= OnMovement;
        controls.Player.Movement.canceled -= OnMovement;
        
        controls.Player.Exit.performed -= OnExit;
    }

Depending on your IDE, you might have it start barking at you around now, but there’s interesting things going on. Our controls.Enable() and Disable() methods resolve correctly as part of the IInputActionCollection2 class methods, and note we’re using the same names Movement and Exit that we defined in our input actions file. The OnMovement and OnExit vars don’t exist yet but we’re going to do that now.

    private void OnDisable()
    {
        ...
    }

    private void OnMovement(InputAction.CallbackContext ctx)
    {
        if (ctx.started)
            moveKeyHeld = true;
        else if (ctx.canceled)
            moveKeyHeld = false;
    }
    
    private void OnExit(InputAction.CallbackContext ctx)
    {
        Debug.Log("Exit");
    }

We can find some documentation about InputAction.CallbackContext and see that it’s defining some properties that we make use of with started and canceled. We’re going to refactor this pretty early on, so don’t get too hung up on it for the time being if you know better. Note the OnExit method doesn’t actually close the application yet, that will also change as we go further along.

    private void OnExit(InputAction.CallbackContext ctx)
    {
        Debug.Log("Exit");
    }

    private void FixedUpdate()
    {
        if(GameManager.Instance.IsPlayerTurn && moveKeyHeld)
            MovePlayer();
    }

    private void MovePlayer()
    {
        transform.position += (Vector3)controls.Player.Movement.ReadValue<Vector2>();
        GameManager.Instance.EndTurn();
    }
}

It’s particularly important here to name the FixedUpdate() method as such, because we’re overriding a parent method that does frame-independent calculations. If you get cute here, your game is going to feel really weird until you fix it. That method’s pretty readable, if it’s your turn and you’re holding down a movement key, move. The second method to actually do the moving talks about vectors, and you’ll see Vector3 and Vector2 a lot as we continue. I’m not a math guy, really. I’m not a graphics guy either. I looked at the docs and there’s even a Vector4. So the number here is referring to dimensions, so Vector2 makes use of (x,y), Vector3 makes use of (x,y,z), and Vector4 is for Time Lords I assume.

You might go, “We’re making a 2D roguelike, why would I ever want Vector3? That’s fair. Unity is a 3D engine first and foremost, and even 2D calculations get translated to 3D ones with the Z-axis set to 0, and some of those methods will only take Vector3. So we’ll make use of Vector3 as we go primarily for the convenience of not having to cast Vector2 objects to Vector3 constantly.

We’re actually pretty close to wrapped-up for this section. Go back to Unity now and under Resources, find your Player object (the @ symbol). In the inspector, click Add Component towards the bottom, and type in Player and choose the Player script.

My inspector differs from the video in that there’s now a “Move Key Held” option, unchecked. Leave it there. Now under the Main Camera in the hierarchy view, right click anywhere in that space and choose Create Empty, then name it GameManager. Select it and in the inspector, Add Component and choose the Game Manager script. Click the Main Camera object in the hierarchy, in the inspector set the Background to black.

Save and press Play at the top center of Unity. You should at this point be able to use the arrow keys to move the @ around the screen. On a lower-end rig, this is going to be very disheartening as it might run at around 7-8FPS if you click the Stats button, but we’ll figure that out as we go. If your arrow keys aren’t moving your @ around, double-check both the code samples and the Unity instructions.

Hey, we’re done with Part 1. Nice work. Commit and push your changes and let’s move on to the next one.

Pages: 1 2 3