Attempting to add cover system as next step, a bit stuck

Sorry, long complicated question!

I’ve gone into CodeMonkey’s Xcom game prototype to see how the cover system was done there, and am in the process of bringing it into this tutorial project to play around with it here. However, the prototype is setup slightly differently to this project, and I have a floor system entirely absent from the prototype, so the process is not entirely straightforward.

I’ve got as far as the LevelGrid changes in Awake, where I need the levelGrid to run raycasts at each gridPosition to see if there’s a cover object on it, and store the coverType on the gridPosition.

Here’s the prototype code I’m trying to borrow:

private void Awake() {
        Instance = this;

        grid = new GridXZ<GridPosition>(30, 30, 2f, Vector3.zero, (GridXZ<GridPosition> g, int x, int y) => new GridPosition(g, x, y));

        // Setup level obstacles
        LevelGrid.Instance.GetWidthHeight(out int width, out int height);
        for (int x = 0; x < width; x++) {
            for (int y = 0; y < height; y++) {
                Vector3 worldPosition = LevelGrid.Instance.GetWorldPosition(x, y);

                if (Physics.Raycast(worldPosition + Vector3.down * 5f, Vector3.up, out RaycastHit raycastHit, 10f, pathfindingObstaclesLayerMask)) {
                    // Something at this position, cover?
                    if (raycastHit.collider.TryGetComponent(out CoverObject coverObject)) {
                        grid.GetGridObject(x, y).SetCoverType(coverObject.GetCoverType());
                    }
                }
            }
        }
    }

grid seems to translate to gridSystem, with GridXY translating to GridSystem, if I’m understanding everything correctly. The GetWidthHeight() I think is a combination of GetWidth and GetHeight on my LevelGrid script. The constructor is also written in a different way, but I think it works the same.

Here’s what I have at the moment:

private void Awake() 
    {
        if (Instance != null)
        {
            Debug.LogError("There's more than one LevelGrid! " + transform + " - " + Instance);
            Destroy(gameObject);
            return;
        }
        Instance = this;

        gridSystemList = new List<GridSystem<GridObject>>();

        for (int floor = 0; floor < floorAmount; floor++) 
        {
            GridSystem<GridObject> gridSystem = new GridSystem<GridObject>(width, height, cellSize, floor, FLOOR_HEIGHT, 
                (GridSystem<GridObject> g, GridPosition gridPosition) => new GridObject(g, gridPosition));
                
            width = LevelGrid.Instance.GetWidth();
            height = LevelGrid.Instance.GetHeight();

            for (int x = 0; x < width; x++) 
            {
                for (int z = 0; z < height; z++) 
                {
                    Vector3 worldPosition = LevelGrid.Instance.GetWorldPosition(x, z);
                    if (Physics.Raycast(worldPosition + Vector3.down * 1f, Vector3.up, out RaycastHit raycastHit, 10f, obstaclesLayerMask)) 
                    {
                        // Something at this position, cover?
                        if (raycastHit.collider.TryGetComponent(out CoverObject coverObject)) 
                        {
                            gridSystem.GetGridObject(x, z).SetCoverType(coverObject.GetCoverType());
                        }
                        
                    }
                }
            }
            gridSystemList.Add(gridSystem);
        }
    }

The only two errors I have remaining relate to LevelGrid.Instance.GetWorldPosition(x, z) and gridSystem.GetGridObject(x, z). Both are error CS1501: “No overload for method (method) takes 2 arguments”. The script in CodeMonkey’s prototype does contain two arguments, but investigating the problem I realised the functions on the prototype work very differently, requiring two ints while mine require only a GridPosition, and filling it with gridPosition doesn’t work.

I wondered if an issue is the for (int x = 0; x < with; x++) stuff, as I copied that over but I’m not 100% sure if my project can use it. The other thought I had was whether I was right to put most of the ocde inside for (int floor = 0; floor < floorAmount; floor++) {. Regardless, I’m beginning to get to the limits of my understanding here.

Can anyone tell what’s wrong with this? Also any advice on how to reduce the code down a bit would be appreciated - it’s quite hard to read now with nested fors, ifs and brackets everywhere.

I haven’t looked at the XCom proto, but I might be able to help with a few conversions…

For example: LevelGrid.Instance.GetWidthHeight(out int width, out int height);
can be changed in the course project to

int width = LevelGrid.Instance.GetWidth(); //Edit, I see further down the post, you figured this out
int height = LevelGrid.Instance.GetHeight();

Alternatively, you could add the GetWidthHeight method to LevelGrid.cs This may help adapt other XCom code into the course project.

public void GetWidthHeight(out int width, out int height)
{
     width = this.width;
     height = this.height;
}

Two fixes for this as well:

Vector3 worldPosition = LevelGrid.Instance(GetWorldPosition(new GridPosition(x,z));
gridSystem.GetGridObject(new GridPosiiton(x,z)).SetCoverType(//

Or, again, you could create overloads for these methods. In LevelGrid, this method is handy:

public Vector3 GetWorldPosition(int x, int z) => GetWorldPosition(new GridPosition(x,z));

and in GridSystem.cs

public TGridObject GetGridObject(int x, int z) => GetGridObject(new GridPosition(x, z));

Long post warning

Thanks - I’ve got a bit further thanks to this.

I stopped the errors for the GetWorldPosition() and GetGridObject() functions, and then got stuck again implementing the SetCoverType() function for a while.

In the prototype, there is no GridPosition struct script or GridObject constructor script - the GridPosition class is contained within LevelGrid, and I think the GridObject functionality is handled as part of the GridXY script (the prototype’s equivalent of GridSystem) but it’s hard to understand.

Anyway, realising this, I moved the SetCoverType(CoverType coverType), GetCoverType() and GetCoverTypeAtPosition(Vector3 worldPosition) functions to the GridObject script, but got errors.

The first two stated “GridObject does not contain a definition for ‘coverType’ and no accessible extension method accepting a first argument of type ‘GridObject’ could be found”.

The latter error was "Argument 1L cannot convert from ‘UnityEngine.Vector3’ to ‘GridPosition’. I tried to fix this by making an overload (I think it’s called?) for GetGridObject(Vector3 worldPosition), which is designed to convert the worldPosition into a gridPosition which the original GetGridObject(GridPosition gridPosition) can use:

public TGridObject GetGridObject(Vector3 worldPosition)
    {
        GridPosition gridPosition = new GridPosition();
        gridPosition = GetGridPosition(worldPosition);
        return GetGridObject(gridPosition);
    }

…but all I succeeded in doing was changing the second error to match the first (it couldn’t find a definition for GetCoverType)

After getting stuck for the better part of an hour trying to figure this out, I realised I’d put private CoverType covertype at the top of the script. I had not uppercased the T in coverType. So that was annoying. Only thing I’m worried about here now - should there be something within the brackets after new GridPosition ?

I then went back in the CoverVisualTest script, which controls an object which moves to the mouse position and shows what cover is at that worldPosition. I’ve adjusted the script I brought in from the prototype to reflect the differences between the two projects, and the relevant part currently reads:

private void Update() 
    {
        Vector3 worldPosition = MouseWorld.GetPositionOnlyHitVisible();
        GridPosition gridPosition = LevelGrid.Instance.GetGridPosition(worldPosition);
        if (LevelGrid.Instance.IsValidGridPosition(gridPosition)) {
            Vector3 snappedWorldPosition = LevelGrid.Instance.GetWorldPosition(gridPosition);

            transform.position = snappedWorldPosition;
            GridPosition testGridPosition = gridPosition;
            
            CoverType coverType = LevelGrid.Instance.GetCoverTypeAtGridPosition(testGridPosition);
            switch (coverType) {
                default:
                case CoverType.None:
                    foreach (SpriteRenderer spriteRenderer in spriteRendererArray) {
                        spriteRenderer.enabled = false;
                    }
                    break;
                case CoverType.Half:
                    foreach (SpriteRenderer spriteRenderer in spriteRendererArray) {
                        spriteRenderer.enabled = true;
                        spriteRenderer.sprite = halfCoverSprite;
                    }
                    break;
                case CoverType.Full:
                    foreach (SpriteRenderer spriteRenderer in spriteRendererArray) {
                        spriteRenderer.enabled = true;
                        spriteRenderer.sprite = fullCoverSprite;
                    }
                    break;
            }
        }

    }

I also made two new functions in LevelGrid to help:

public CoverType GetCoverTypeAtGridPosition(GridPosition gridPosition)
    {
        GridObject gridObject = GetGridSystem(gridPosition.floor).GetGridObject(gridPosition);
        return coverType;
    }

    public void DestroyCoverAtGridPosition(GridPosition gridPosition, CoverType coverType)
    {
        GridObject gridObject = GetGridSystem(gridPosition.floor).GetGridObject(gridPosition);
        gridObject.SetCoverType(CoverType.None);
    }

All this successfully fixed all the errors on VS Code, and I was feeling very proud of myself until I tried the game again, where everything broke and I was flooded with these errors:
“ArgumentOutOfRangeException: Index was out of range. Must be non-negative and less than the size of the collection.
Parameter name: index
System.Collections.Generic.List`1[T].get_Item (System.Int32 index) (at <605bf8b31fcb444b85176da963870aa7>:0)”

They all seem tied to this in the LevelGrid script:

private GridSystem<GridObject> GetGridSystem(int floor)
    {
        return gridSystemList[floor]; // Error flagged as being here
    }

It says "LevelGrid.GetGridSystem (System.Int32 floor) before the error line is named. The other code lines highlighted in the errors vary (it’s not a single repeating one, until it gets to the CoverVisualTest as that is tied to update on the mouse position). One is tied to GetWidth and Awake, the others are tied to GetGridPosition() and a whole bunch of object scripts.

I did notice that deleting the below from LevelGrid Awake seems to make no difference, and I’m wondering if I even need it:

    width = LevelGrid.Instance.GetWidth();
    height = LevelGrid.Instance.GetHeight();

I also tried replacing it with a GetWidthHeight(out in width, out int height) function, but the editor freaked out because width and height are already used earlier in Awake when the grid is built. Frankly, that made me wonder why I didn’t have problems before.

I’m not sure what to do about this and think I’m getting to the limit of my already limited talent now. Do you have any ideas?

Without more knowledge of @CodeMonkey’s XCom prototype and the differences, I’m mostly guessing (While I know the course codebase very well, I haven’t had time to look at his XCom stuff).

You’ll have to find that in Hugo’s XCom and add it in, we don’t have a Cover at all in the course.

Looks like you need to add some sanity checks for positions that aren’t within the width and height

public bool IsValidGridPosition(int x, int z)
{
    return x>=0 && x<width && z>=0 && z<=height;
}

public bool IsValidGridPosition(GridPosition gridPosition) => IsValidGridPosition(gridPosition.x, gridPosition.y);

Yeah sorry I went a bit overboard with the post length here. I kept writing stuff to ask for help, and figuring it out before I’d finished.

With the sanity checks, don’t I have them already? This in the levelGrid start:

for (int x = 0; x < width; x++) 
            {
                for (int z = 0; z < height; z++) 
            {

EDIT:

I’ve tried making an overload for GetWorldPosition:

public Vector3 GetWorldPosition(int x, int floor, int z)
    {
        return new Vector3(x * cellSize, floor * FLOOR_HEIGHT, z * cellSize);
    } 

That stops the errors, but the coversystem still doesn’t work. (I thought performance had tanked, but turns out the cause was the Debug Logs I had running)

Okay I’ve done a lot more Debug testing. It looks like my coverVisualTest script is working just fine, the issue is the setup on the LevelGrid is not assigning coverType to GridPositions.

It’s supposed to do this with Raycasts, but this Raycast isn’t doing its job:

Vector3 worldPosition = LevelGrid.Instance.GetWorldPosition(x, z, floor); // problem code
if (Physics.Raycast(worldPosition + Vector3.down * 1f, Vector3.up, out RaycastHit raycastHit, 10f, obstaclesLayerMask)) 
{   Debug.Log(raycastHit);
    // Something at this position, cover?
    if (raycastHit.collider.TryGetComponent(out CoverObject coverObject)) 
    {
        gridSystem.GetGridObject(new GridPosition(x, z, floor)).SetCoverType(coverObject.GetCoverType());
    }
    
}

I did a Debug.Log on the rayCastHit, and it’s only getting 3 hits on quite a large level. Running a DebugLog on WorldPosition found them (I think), which revealed only 3 were fired, all three hit a wall and none of them recognised it as a coverObject, despite it being defined as such.

(Rays were fired at (20, 0, 0), (20, 0, 2) and (20, 0, 4). I have no idea why only those).

Raycasts were fired on each location, but the only gameobjects that were on the obstaclesLayerMask defined layers were on those three locations. You can test this by adding

else 
{
   Debug.Log($"Raycast at {x},{z} did not have any hits");
}

Is the CoverObject on the root of the wall tiles in question?

Ah you’re right, it is firing at the other positions but not hitting anything, though that’s rather confusing. The wall it has hit is about 13 GridPositions in length, yet was only hit 3 times.

Sorry - CoverObject is actually a script attached to all the obstacle prefabs. It contains this:

public class CoverObject : MonoBehaviour
{
    [SerializeField] private CoverType coverType;

    public CoverType GetCoverType() 
    {
        return coverType;
    }

}

public enum CoverType 
{
    None,
    Half,
    HalfFlimsy,
    Full,
    FullFlimsy,
}

I specify what type of cover each object has on the SerializedField of the script on each prefab.

Alright update.

I realised that I have a very similar raytracer in place already which is working - the pathfinding script. Looking at that, I realised that I needed to build the levelgrids completely seperately, and then cycle through the floors along with the x and z to assign cover to the gridObjects - turns out that was where all the out of range exceptions were coming from.

By imitating the Pathfinding system (making some changes to make it work with GridObjects instead of PathNodes), I’m pretty sure the raytracing is finally working. It’s detecting all the obstacles and getting their cover type. However for whatever reason, the cover type is not being recorded on the GridObjects.

if (raycastHit.collider.TryGetComponent<CoverObject>(out CoverObject coverObject))
{
    GridObject gridObject = gridSystem.GetGridObject(gridPosition);
    gridObject.SetCoverType(coverObject.GetCoverType());
}

Here’s that last bit of code on the LevelGrid setup. I’ve run Debug.Log(gridObject); and Debug.Log(gridObject.GetCoverType()); at the end, and both return exactly what I would expect.

Running the below code (with the debug) on my CoverVisualTest script shows none of the GridObjects are recording coverType, however:

private void Update() 
    {
        Vector3 worldPosition = MouseWorld.GetPositionOnlyHitVisible();
        GridPosition gridPosition = LevelGrid.Instance.GetGridPosition(worldPosition);
        if (LevelGrid.Instance.IsValidGridPosition(gridPosition)) {
            Vector3 snappedWorldPosition = LevelGrid.Instance.GetWorldPosition(gridPosition);

            transform.position = snappedWorldPosition;
            GridPosition testGridPosition = gridPosition;

            CoverType coverType = LevelGrid.Instance.GetCoverTypeAtGridPosition(testGridPosition);
            Debug.Log(coverType);
            // Some code for visuals

I tried adding coverType as a requirement for the public GridObject constructor in the GridObject script, but as far as I can tell, it makes no difference.

I think I’ve almost cracked it, though

EDIT:

Really confused now

image

I adjusted the GridDebugObject so it can show cover at the position, and it appears the coverType is being recorded. So I’ve got no idea why the CoverVisualTest script won’t recognise it

Finally, I worked it out. Here’s the culprit:

public CoverType GetCoverTypeAtGridPosition(GridPosition gridPosition)
    {
        GridObject gridObject = GetGridSystem(gridPosition.floor).GetGridObject(gridPosition);
        return gridObject.GetCoverType();
    }

This used to read return coverType on the bottom line, so essentially I was finding a gridPosition, and doing nothing with it. Now it all works perfectly.

Hopefully this mess of a thread helps someone else struggling to do this.

Edit: Note - I can only mark one post as the solution, but this is just the solution to the last issue I encountered! It’s been a long process to get this system working

1 Like

Whew! I’m glad I was able to help some, despite not knowing anything about the XCom project (I will take a look at that at some point, but I have to devote most of my time to dealing with the courses I TA directly, like this one).

This topic was automatically closed 24 hours after the last reply. New replies are no longer allowed.

Privacy & Terms