Last week I mentioned context and how I work better with it. Normally for me its looking over code or a system that's already implemented in some way learning how it works and then creating my own interpretation of it - one such system is the ‘new’ Input System.
My project grew from the Third Person starter assets (TPSA) - a ready made solution to get me started. then, using Code Monkey's Awesome Third Person Shooter I expanded on the implementation. Not only with aiming but also with the ladder climb. I'd also added in my own crouching system and pause/inventory functions, but for some reason it just didn't feel right still and the default Send Messages version didn't work well with the Pixel Crushers dialogue asset (PC:DS) that another Humble Bundle deal got me. Something had to be done - and I had to feel like I coded something myself!
Long story short I have an implementation for the character controller based on the TPSA but my way and using Unity Events. Sorted! Funny thing is as my experience has grown with other elements of the engine. I've found the original issue with PC:DS has gone away with my new understanding of how interfaces work! mainly thanks to Code Monkey again but also to Llamacademy for all the interface stuff. All comes back to context! and now I have interfaces that not only take care of damage but also item interaction and eventually NPC interaction.
So, I have movement I'm happy with. I have interaction I'm happy with. I have a shooting system (that's a work in progress) but I'm happy with that too. What else do you need in a game……oh yea, a challenge, enemies maybe? Yea OK.
One of the first systems I've attempted is a keypad. here's a snippet of the code - the confirm method is tied to the confirmation button on the UI. It compares the result from the Number method to a the answer variable and if correct it'll print to the UI console (the game object) that it is correct. it then sets a game object to true - I've put this in as I found that while the lid was closed/locked the player would still be able to pickup the item due to the collider being bigger than the crate.
public void Number(int number)
{
numText.text += number.ToString();
}
public void Confirm()
{
if (numText.text == _answer)
{
numText.text = "Correct";
_isCorrect = true;
crateContents.SetActive(true);
}
else
{
numText.text = "WRONG!";
_isCorrect = false;
}
}
here's a quick video of it in action. The interaction of this used the Pixel Crushers Dialogue system elements. however since this was created its been superseded by my new found understanding of interfaces.
Another system I've created is the CCTV and Panel interaction, a kind of QTE in its raw form (there's only one button to press right now!) anyway. This is in a way an evolution of the above keypad system.
private void Update()
{
//if (qteUI.submit.action.IsPressed())
if (PlayerController.Instance.playerInput.actions["EastButton"].IsPressed())
{
StopAllCoroutines();
radialScale = 0f;
radialImage.fillAmount = 0f;
PlayerController.Instance.playerInput.SwitchCurrentActionMap("PlayerChar");
QTEPassed?.Invoke();
}
}
public IEnumerator startRadialCount()
{
while (radialScale < 6)
{
radialScale += Time.deltaTime;
float timeremain = radialScale;
radialImage.fillAmount = timeremain/6;
yield return null;
}
}
So, what's happening this time?
I'll start with the startRadialCount() coroutine. At the minute this coroutine starts in the start method of the monobehaviour. However this means it doesn't refresh if interacted with a second or third time so I will be looking into that in the near future. But what does the coroutine do? Its a timer which as time passes it fills an image.
The update method is listening for the button press, it stops the coroutine, resets the fill image and timer as well as switching control back to the player - Oh yea, so when the player interacts initially it switches the action map to the UI (below)
public void Interact()
{
interactCam.Priority += 10;
playerPosition.localPosition = playerShotPosition.position;
playerPosition.localRotation = playerShotPosition.rotation;
PlayerController.Instance.playerInput.SwitchCurrentActionMap("QTEControls");
Debug.Log(PlayerController.Instance.playerInput.currentActionMap);
qteUIPanel.SetActive(true);
}
There is a virtual camera placed just above the interactable. when interact is invoked the camera hard cuts to the VCam as well as moving the player into the correct location. The hard cut of the camera means the player doesn't notice the player character being moved. The action map is switched to the QTE controls map I created and then finally sets the UI panel to active and starting the QTE timer.
As well as the QTE system example, here you can see the detection system on the CCTV camera. While the camera is active the spotlight is amber, the player is detected and the colour changes to Red when the camera is deactivated by the console QTE the light changes to green, which after a set time changes back to amber to show its active again. I had originally planned to have the camera animate to show it was deactivated but felt the colour change was easier for the player to tell what state it was in. What do you guys think?
One last system I want to share with you this week is my game over transitions. this is potentially one that will see me investigate multi scene workflows further down the line - like, after the demo/slice release, there were a number of problems with the implementation that have been hacked together to work. One, for example is the use of the dreaded 'FindObjectWithTag' but its assigned onEnable so I don't think it will be too much of a problem - especially as the target is entry level Desktop and Console. (I'm rocking a Medion Akoya P6645 with a 2Gb GPU!) anyway there was issues with the targeting of the NPCs (loss of player transform information on scene reload) the trail pool is still an issue but I think I'm going to rework that one anyway!
Here's a clip of the Game over action anyway.
Forgive the janky shooting and aiming - that's something on the backlog. So what's going on? Well the hero of our story is in a pretty dire situation getting shot at from two sides.
public void TakeDamage(int damage)
{
int damageTaken = Mathf.Clamp(damage, 0, currentHealth);
currentHealth -= damageTaken;
if (healthIndicator != null)
{
healthIndicator.SetHealth(currentHealth);
}
if (damageTaken != 0)
{
OnTakeDamage?.Invoke(damageTaken);
}
if (currentHealth == 0f && damageTaken != 0)
{
isAlive = false;
OnDeath?.Invoke(transform.position);
healthIndicator.enabled = false;
}
}
Once they die the OnDeath event is called which sets the gameOverScreen GO to active - this is where the Timeline playable is stored.
public class CinematicManager : MonoBehaviour
{
public GameObject gameOverScreen;
[SerializeField] InputSystemUIInputModule UI;
// Start is called before the first frame update
void Start()
{
HealthManager.instance.OnDeath += GameOverCine;
}
public void GameOverCine(Vector3 pos)
{
gameOverScreen.SetActive(true);
}
public void ReloadLevel()
{
StartCoroutine(AsyncReLoading(1));
}
IEnumerator AsyncReLoading(int sceneToLoad)
{
AsyncOperation loadOperation = SceneManager.LoadSceneAsync(sceneToLoad);
while (!loadOperation.isDone)
{
yield return null;
}
SceneManager.LoadScene(sceneToLoad);
gameOverScreen.SetActive(false);
}
IEnumerator LevelRetryLoading(int sceneToLoad)
{
AsyncOperation loadOperation = SceneManager.LoadSceneAsync(sceneToLoad);
loadOperation.allowSceneActivation = false;
while (!loadOperation.isDone)
{
if (loadOperation.progress >= 0.9f)
{
//gameOverScreen.SetActive(false)
if (UI.submit.action.IsPressed())
{
loadOperation.allowSceneActivation = true;
gameOverScreen.SetActive(false);
}
}
yield return null;
}
}
}
The timeline is fairly simple -
- it blends from the player camera to the GameOver camera. The game over camera is a child of the player so it remains with them at all times.
- The game over canvas fades to black.
- Game Over volume creates the black and white grain effect - yes, I know its very GTA-esque
- Finally there are two signals emitted - the first at around 4.75 seconds triggers the ReloadLevel coroutine (above). The second shortly after fires the player respawn - resetting the players position and its health.
That's the main mechanics - the ReloadLevel opens a 'loading scene' which reloads the last played level and then holds - waiting for the player input to finish the last bit and open the level.
Anyway - this one might read a bit disjointed - I'm still getting used to this whole blogging thing! Thanks for reading so far, Until next week.