More Player Feedback on Placeables

When players were placing the objects from the UI, the object would not always spawn where the icon was, leaving the player confused.  To solve this issue, we decided to spawn the object into the scene the moment the icon is no longer above the UI.  Additionally, we wanted to make it clear to the player that the object they are dragging is not in the scene.  To convey this we wanted dragged objects to be transparent.  Finally, to let players know when they were putting the object in an illegal spot, say on a Spawner, Furnace, or outside the level, we would highlight the object red.  The two videos below show our desired effect.

To tackle this problem I took a look at what systems were needed code wise.  

- A system to turn objects transparent

- A system  to turn objects red. 

- A system to check for invalid spaces: Floors, Furnaces, Spawners, Collectors

- A system to check when the object is outside the level.

Because turning objects red and making them transparent would require similar functionality, I decided to make them into one class.  I started by making a class that would change the color and/or transparency of the Placeable.  I quickly found out that our current shader does not support transparency.  I talked to our artist, our shader will not be changing, so that option was out.  Okay, so I needed to pick a transparent shader for my code to store. I choose "Transparent/VertexLit" because it was the least detailed.  I stored our original shader as well so I can easily switch between the two.  All I needed to do was loop through each object with at least one material and change all their materials to transparent.  This same method was applied to a change color function when changing the object to red.  Finally, just for readability's sake, I made the ChangeShader function private and two public functions that could be called to make the object transparent or opaque. 

The final code is shown below.

public void MakeTransparent(){ChangeShader(mTransparentShader, false);}
    public void MakeOpaque(){ChangeShader(mNormalShader, true);}

    void ChangeShader(Shader incomingShader, bool state){
        for(int j = 0; j <mAllChildrenWithMaterials.Length;j++){
            for(int i = 0; i < mAllChildrenWithMaterials[j].renderer.materials.Length; i++){
                mAllChildrenWithMaterials[j].renderer.materials[i].shader = incomingShader;
                mAllChildrenWithMaterials[j].renderer.materials[i].color = mTransparentColor;
                ToggleColliders(state);
            }
        }
        
    }

    public void ChangeColor(){
        for(int j = 0; j <mAllChildrenWithMaterials.Length;j++){
            for(int i = 0; i < mAllChildrenWithMaterials[j].renderer.materials.Length; i++){
                mAllChildrenWithMaterials[j].renderer.materials[i].color = mInvalidSpotColor;
            }
        }
    }

Once I had the Placeable turning transparent when it moved,  I needed the next class, ValidPlaceablePosition(), to check if the object is in a valid spot.  I determined the easiest way to do this was to check colliders. The first problem I ran into with this approach was that the colliders would trigger on EVERYTHING.  So to fix this, I made two new layers and made them only collide with each other.  One layer for everything the Placeable can check and one to indicate which placeables were invalid spots.  Once I made this change, the OnTriggerEnter and OnTriggerExit functions would not be called nearly as often.  

My next revelation came when I realized I could add a collider to all of the panels in the Foreground.  This allowed the Placeable to trigger when it was outside the level bounds (aka covered by the foreground)  and as a result, can be marked as being invalid.  Now that I had everything triggering correctly,  I needed a way to make sure I was checking the correct collider.  Another problem arose when the Placeable would be placed on a floor with a furnace, three colliders would be entered at once.  So I needed to make sure I was storing only the most relevant collider.  Once it was stored, I needed to make sure this collider was not removed until that collider was actually exited. With a series of if statements, I could easily make sure only to apply a new collider if the new collider is an invalid position.  This code shown below.

    void OnTriggerExit(Collider incomingCollider){
        if(mMainBodyCollidingObject == incomingCollider){
            mMainBodyCollidingObject = null;
        }

        IsValidPosition();
    }
    
    void OnTriggerEnter(Collider incomingCollider){
        if(mMainBodyCollidingObject != null){
            if(incomingCollider.gameObject.layer == mPlaceableHazardLayerInt){
                mMainBodyCollidingObject = incomingCollider;
            }
        }else{
            mMainBodyCollidingObject = incomingCollider;
        }

        IsValidPosition();
    }
    
    public bool IsValidPosition(){
        
        mIsValidPosition = true;
        IsThisAnIllegalBackgroundSpace();
        
        return mIsValidPosition;
    }
    
    void IsThisAnIllegalBackgroundSpace(){
        if(mMainBodyCollidingObject != null){
            if(HasAnIllegalTag(mMainBodyCollidingObject.tag)){
                mIsValidPosition = false;
            }
        }
    }
    
    bool HasAnIllegalTag(string incomingTag){
        for(int i = 0; i < mIllegalObjectsTagList.Length;i++){
            if(incomingTag == mIllegalObjectsTagList[i]){
                return true;
            }
        }
        
        return false;
    }

The final result works well as you can see in the videos above.  The hardest tasks was just laying out the system and how to check if I was outside the level.  My mind became stuck, as it sometimes does, on the idea that I would just make sure the position of the peaceable was within the level bounds.    This was not an effective approach but luckily I found a much easier solution.  Feel free to ask any questions or if you would like to to elaborate further on any of this task.