Hello all! I just wanted to make a quick post to give my site an update on what I’ve been up to! This past week, I finished my degree & graduated from USC with a BA in Interactive Entertainment. After being an undergrad student for 6 years, I am more than ready to enter the industry and begin to work & learn in a professional environment. I am very grateful for the experiences I gained from working on games at USC, and today I’m proud to show off the game that I worked on over the past year as a part of USC Games.
The Trials of Snowshoe Thompson is a game I worked on as the Lead Engineer, alongside a team of 5 other engineers. You can see posts here about the development process of Snowshoe, but in short: our goal as engineers on this project was to recreate the feeling & physics of realistic cross country skiing. I am very proud of how this project turned out, and greatly value the lessons I learned in the process. I most enjoyed being able to work on large scale Unity architecture, and working alongside very brilliant engineers.
I may eventually come back and write a more reflective post on this project, but for now I am going to leave just the link to the download, and hope you are able to enjoy it! You will need a playstation or xbox controller to play the game!
Hello all, this is a walkthrough of my solution for detecting the device that the player is using in a single player “gamepad” based game. In my specific case, I’m using this information to have my UI constantly represent the correct “controls” UI for the type of device my player is using. At the time of writing this, my system is specifically built to sort between PlayStation & Xbox controllers, with Xbox being the default. With that said, this system is easily extendable, and I hope that will be clear through my code & explanations. This guide is meant to help those who have routed all of their input beneath the “game pad” umbrella in the Unity Input System, but still want the specific type of controller used by the player to be accessible.
This post is using the “New” Unity Input System, NOT the Unity Legacy Input
I ran into a lot of very interesting road blocks in my quest to create an abstracted UI Image system which would live react to the type of controller being used by the player…. It sounds funny, because you would think it would be simple to detect the type of controller being used by the player, but the way that the Unity Input System deals with game pad style controllers seems to do a pretty good job of burying this data.
There were 2 ways which I thought I could approach this. The first would be to go into my actual Input Map and specify control schemes by controller type, and then have a manager which detects input from any/all control schemes. and switches state based upon the most recent input. I chose not to do this because, in my opinion, one of the biggest appeals of the Unity Input System is that it supports the most common controller types all beneath the gamepad umbrella.
It’s important to note that in my project, I have my PlayerInput Behavior set to Invoke C# Events.
My first step in this system was listening in to the InputSystem.onDeviceChange, which must be assigned to a function which takes an InputDevice and InputDeviceChange. This event will fire off every time a change to device is detected. These include a controller being: added, disconnected, removed, & reconnected. InputDeviceChange represents which one of these events was detected. Here’s what those look like:
Note: _currentController is just a basic enum meant to internally store the controller state
private void GameManagerRegisterInput()
{
//Binds onDeviceChange event to InputDeviceChanged
InputSystem.onDeviceChange += InputDeviceChanged;
}
//Method called when a device change event is fired
private void InputDeviceChanged(InputDevice device, InputDeviceChange change)
{
switch (change)
{
//New device added
case InputDeviceChange.Added:
Debug.Log("New device added");
break;
//Device disconnected
case InputDeviceChange.Disconnected:
controllerDisconnected.Invoke();
Debug.Log("Device disconnected");
break;
//Familiar device connected
case InputDeviceChange.Reconnected:
controllerReconnected.Invoke();
Debug.Log("Device reconnected");
break;
//Else
default:
break;
}
}//Method called when a device change event is fired
public void InputDeviceChanged(InputDevice device, InputDeviceChange change)
{
switch (change)
{
//New device added
case InputDeviceChange.Added:
Debug.Log("New device added");
//Checks if is Playstation Controller
if (device.description.manufacturer == "Sony Interactive Entertainment" && _currentController != CurrentControllerType.PlayStation)
{
//Sets UI scheme
Debug.Log("Playstation Controller Detected");
currentImageScheme.SetImagesToPlaystation();
_currentController = CurrentControllerType.PlayStation;
controllerTypeChange.Invoke();
}
//Else, assumes Xbox controller
//device.description.manufacturer for Xbox returns empty string
else if(device.description.manufacturer != "Sony Interactive Entertainment" && _currentController != CurrentControllerType.Xbox)
{
Debug.Log("Xbox Controller Detected");
currentImageScheme.SetImagesToXbox();
_currentController = CurrentControllerType.Xbox;
controllerTypeChange.Invoke();
}
break;
//Device disconnected
case InputDeviceChange.Disconnected:
controllerDisconnected.Invoke();
_currentController = CurrentControllerType.Other;
Debug.Log("Device disconnected");
break;
//Familiar device connected
case InputDeviceChange.Reconnected:
controllerReconnected.Invoke();
Debug.Log("Device reconnected");
//Checks if is Playstation Controller
if (device.description.manufacturer == "Sony Interactive Entertainment" && _currentController != CurrentControllerType.PlayStation)
{
//Sets UI scheme
Debug.Log("Playstation Controller Detected");
currentImageScheme.SetImagesToPlaystation();
_currentController = CurrentControllerType.PlayStation;
controllerTypeChange.Invoke();
}
//Else, assumes Xbox controller
//device.description.manufacturer for Xbox returns empty string
else if(device.description.manufacturer != "Sony Interactive Entertainment" && _currentController != CurrentControllerType.Xbox)
{
Debug.Log("Xbox Controller Detected");
currentImageScheme.SetImagesToXbox();
_currentController = CurrentControllerType.Xbox;
controllerTypeChange.Invoke();
}
break;
//Else
default:
break;
}
} private void GameManagerRegisterInput()
{
//Binds onDeviceChange event to InputDeviceChanged
InputSystem.onDeviceChange += InputDeviceChanged;
}
//Method called when a device change event is fired
private void InputDeviceChanged(InputDevice device, InputDeviceChange change)
{
switch (change)
{
//New device added
case InputDeviceChange.Added:
Debug.Log("New device added");
break;
//Device disconnected
case InputDeviceChange.Disconnected:
controllerDisconnected.Invoke();
Debug.Log("Device disconnected");
break;
//Familiar device connected
case InputDeviceChange.Reconnected:
controllerReconnected.Invoke();
Debug.Log("Device reconnected");
break;
//Else
default:
break;
}
}
While this was the first step I took, I was working backwards a bit. With this event being listened to, we can react to new controllers being connected, to controllers losing connection, and a bunch of other events. These events usually represent a reason to re-evaluate the current images being displayed for controls.
I wrote a fairly simple Scriptable Object which holds all the textures we will be using in our “controls” UI. So this scriptable objects holds all the images used to represent xbox and playstation controls. Additionally, it has a set of private textures which hold the “current” image for that button. Through this abstraction, I can have the Scriptable Object flip it’s set of “current” images, and have all UI read from this ScriptableObject for the image it needs to display.
Now that I had the foundation, and a way to store/represent the “controller type” state, now I just needed to determine what type of controller was being connected/added, and pass that state on to my Scriptable Object. Using the API for the type Device, which is passed on this event as a direct reference to the Device that instigated the event, I was able to determine that the field description has sub fields which are used to define the specific type of controller, the manufacturer, and more. The field product is a string which would come out something like “Dual Shock 4” or “Xbox One Elite”. While this is certainly useful, I wanted things to remain as generic as they could be. The manufacturer proved to be the most abstracted I could get, while still distinguishing between the specific types of gamepads. However, herein lies the first issue I encountered. While the device.description.manufacturer field on a Playstation controller returns a nice & neat “Sony Interactive Entertainment”, that same field for the Xbox controller is entirely empty. Since I only have to support the 2 types, I wrote these conditions into an if/else, and called it a day. But as I extend the system to include more controller types, I would make use of the device.description.product and distinguish between the remaining types of controllers. Here’s what that same chunk from above looks like with these cases written in.
A note: currentImageScheme is my Scriptable Object described above. This scriptable object provides the texture to raw image prefabs in my UI. controllerTypeChange is an event which is listened to by the UI. The UI will react to this event by setting their texture to the “current” texture stored in the ScriptableObject
//Method called when a device change event is fired
public void InputDeviceChanged(InputDevice device, InputDeviceChange change)
{
switch (change)
{
//New device added
case InputDeviceChange.Added:
Debug.Log("New device added");
//Checks if is Playstation Controller
if (device.description.manufacturer == "Sony Interactive Entertainment")
{
//Sets UI scheme
Debug.Log("Playstation Controller Detected");
currentImageScheme.SetImagesToPlaystation();
controllerTypeChange.Invoke();
}
//Else, assumes Xbox controller
//device.description.manufacturer for Xbox returns empty string
else
{
Debug.Log("Xbox Controller Detected");
currentImageScheme.SetImagesToXbox();
controllerTypeChange.Invoke();
}
break;
//Device disconnected
case InputDeviceChange.Disconnected:
controllerDisconnected.Invoke();
Debug.Log("Device disconnected");
break;
//Familiar device connected
case InputDeviceChange.Reconnected:
controllerReconnected.Invoke();
Debug.Log("Device reconnected");
//Checks if is Playstation Controller
if (device.description.manufacturer == "Sony Interactive Entertainment")
{
//Sets UI scheme
Debug.Log("Playstation Controller Detected");
currentImageScheme.SetImagesToPlaystation();
controllerTypeChange.Invoke();
}
//Else, assumes Xbox controller
//device.description.manufacturer for Xbox returns empty string
else
{
Debug.Log("Xbox Controller Detected");
currentImageScheme.SetImagesToXbox();
controllerTypeChange.Invoke();
}
break;
//Else
default:
break;
}
}
So remember when I said I was working backwards? At this point, my system detects and reacts to big controller events, but at the start of the program has no idea what controller is being used. This is where I encountered the majority of my hardship, but I’m very proud of the end result!
Something I learned quickly is that the Unity Input System stores all known input devices, regardless of them being currently connected or not. So if my player is playing on their xbox controller, it’s batteries die, and they switch to their PS controller, InputSystem.devices still stores both devices. Additionally, while the type Device has a property enabled, this returns true for all devices registered to the Unity Input System. So all of this results in perhaps the most notable roadblock I encountered with this System: I could not find a way in which the Unity Input System distinguishes between the currently connected/in use controllers and those which are simply known by the Unity Input System, ie not in use at all. Devices can easily be individually assigned, and switched between, and detected, but there is no property of the Type Device (that I could find) which represents whether an individual device is actively connected. I pray I’m wrong about that, but the closest I could get was InputDevice.lastUpdateTime, which InputSystem inherits from. Even that field returns extremely unreliable values, as on PS it counts gyroscopic data as input, every frame, and in my tests, was returning the same value for all my devices, connected or not.
TlDr; I could not find a way to distinguish between connected and non connected input devices.
My response to this, however, is where I’m most proud of this code! I found that if I remove all the stored devices on game start, the controller being used by the player instantly re-constructs itself, and is quickly the only device stored in InputSystem.devices[]. From here, it’s easy to just check InputSystem.devices[0].manufacturer, and we have the same check on game start as we do when a controller event happens. Here’s that code!
Once again, currentImageScheme is my SO
//Called in GameManagerGameStart() to set the UI initially
private void UIImageSchemeInitialSet()
{
//Disables all devices currently read by InputSystem
for (int rep = 0; rep < InputSystem.devices.Count - 1; rep++)
{
InputSystem.RemoveDevice(InputSystem.devices[rep]);
}
if (InputSystem.devices[0] == null) return;
//Checks the first slot of the InputSystem devices list for controller type
if (InputSystem.devices[0].description.manufacturer == "Sony Interactive Entertainment")
{
//Sets UI scheme to PS
Debug.Log("Playstation Controller Detected");
currentImageScheme.SetImagesToPlaystation();
_currentController = CurrentControllerType.PlayStation;
controllerTypeChange.Invoke();
}
else
{
//Sets UI scheme to XB
Debug.Log("Xbox Controller Detected");
currentImageScheme.SetImagesToXbox();
_currentController = CurrentControllerType.Xbox;
controllerTypeChange.Invoke();
}
}
From there, to complete my system, all I had to write was a MonoBehaviour which stores a reference to my SO, and listens to my controllerTypeChange event, reacting by just setting its image to the corresponding image stored in the Scriptable Object. Here’s that code, for those who are curious!
public class ControllerImageScript : MonoBehaviour
{
public UIImageSchemeSO uiImageScheme;
public enum buttonImage
{
southButton,
eastButton,
northButton,
westButton,
rightTrigger,
leftTrigger
}
public buttonImage myButtonImage;
private RawImage myImage;
// Start is called before the first frame update
void Start()
{
myImage = gameObject.GetComponent<RawImage>();
SetButtonImage();
}
public void SetButtonImage()
{
if (myButtonImage == buttonImage.southButton)
{
myImage.texture = uiImageScheme.GetSouthButton();
}
else if (myButtonImage == buttonImage.eastButton)
{
myImage.texture = uiImageScheme.GetEastButton();
}
else if (myButtonImage == buttonImage.northButton)
{
myImage.texture = uiImageScheme.GetNorthButton();
}
else if (myButtonImage == buttonImage.westButton)
{
myImage.texture = uiImageScheme.GetWestButton();
}
else if (myButtonImage == buttonImage.leftTrigger)
{
myImage.texture = uiImageScheme.GetLeftTrigger();
}
else if (myButtonImage == buttonImage.rightTrigger)
{
myImage.texture = uiImageScheme.GetRightTrigger();
}
}
}
I spent all day on this code, and I found it a surprisingly underdocumented topic. I hope that this proves helpful to those who are in my situation, just as all the wonderful discussions/threads online helped me!
Hello all! I’m posting today with a brief follow up on my post about terrain texture detection in Unity. If you haven’t checked that out, it will definitely be informing this post! I have been working further on getting this system to work with my current PlayerMovement system, and have come to a neat solution of abstraction which can be branched to communicate anywhere in your game what the current terrain texture is beneath the player.
Here’s what this solution solves:
Our player’s movement is effected constantly by what type of ground the player is on. So for example, braking power & amount of friction (2 separate variables in the PlayerMovement). Since we need these values to be dynamic, a layer of abstraction is helpful in allowing these values to be continuously modified by factors such as the terrain.
Our designers have not finalized the variables effecting movement at large, this solution allows for testing of multiple combinations of these movement variables as extreme ease to the design team. This will be used for AB testing in our playtests down the road.
The solution:
It’s very simple, but has helped a lot with working this data into my PlayerMovement in a clean and clutter free way. All I have done is created a new Scriptable Object type, MovementVariables, and moved all of the “designer variables” into this scriptable object. Additionally, I have created a simple class, TerrainType, which stores all of the variables that are dynamic and dependent upon the terrain the player is on. I’ve made this class serializable, and within my MovementVariables I have a public array of TerrainType that allows the designers to set each terrain type uniquely for each variant of the MovementVariable type.
MovementVariables has a public function, SetToTerrain(), that takes in an int representing the terrain texture the player is currently on (remember this is stored in an int by the Alpha map). Upon taking that int, MovementVariables will set the necessary variables to match those of the corresponding TerrainType in the local array. So, for example, terrainTypes[0] is created by the designer to have float frictionLevel = 1, and float brakingPower = 3. Once SetToTerrain() takes in an int 0, then MovementVariables will set the frictionLevel and brakingPower according to whatever is in terrainTypes[0].
From here, all that is necessary for setup (besides setting the designer variables) is to create a reference to a MovementVariables in both the PlayerMovement and the TerrainTextureGetter. The former will simply read the scripted values from this Scriptable Object, and the latter will pass the int representing the texture into SetToTerrain().
… and that’s it! It’s a super simple solution but really has helped me in passing these terrain settings into my movement, in making my PlayerMovement less beefy, and in aiding the design team with finalizing their movement variables… Here’s some code
[System.Serializable]
public class TerrainType
{
public string terrainTypeName;
[Range(0f, 100f)]public float stridePushPower;
[Range(0f, 15f)] public float brakingPower;
[Range(0f, 5f)] public float slidingFrictionPower;
[Range(0f, 10f)] public float turningSpeed;
}
This is the TerrainType class which is key in allowing this abstraction to work. These are the variables in PlayerMovement which we want to be altered by the terrain type beneath the player.
This chunk of code above is located within my MovementVariables Scriptable Object. The top variables are what are being referenced within PlayerMovement, but they are being set below. SetToTerrain() is called every time a terrain texture change is detected. An important note is the [ShowOnly] editor attribute was written and made public by Stack Overflow user Lev-Lukomskyi. Huge thanks for that, as it keeps the designers from touching things they shouldnt ;). I’m just kidding… I hope that this post was helpful for anyone who needed a follow up from my last post about terrain texture detection! Until next time!
Hello all, I hope that as you read this, all is well in your world. The last week has been a truly fitting capstone on the crazy year that has been 2020, especially for U.S. citizens. I want to take your mind off the madness for a moment and talk about something I worked on over the last few days.
As you can read about in my October Update, I’ve been making a skiing game! It’s been awesome so far, and really enjoyable. Something that is crucial to the design of our game is different textures on the terrain beneath the player having differing effects on gameplay. For example, we have a texture that represents “heavy snow”, which will bring down the speed at which the player treks through the snow, as well as an “ice” texture, which would instead speed the player up at a much higher rate, and slow down at a much slower rate. The process itself is not complicated, it just involves getting the terrainData of terrain below the player, and using the player’s relative position to the terrain to calculate what the texture is that is present at that point on of the terrain, and in what strength. While these calculations are somewhat daunting (involving the 3D float array used to represent an alpha map), they’re actually not too complex when broken down.
Where my specific need for this process differs from a lot of what I’ve seen online is that our Unity scene involves upwards of 20 separate terrain objects (and therefore over 20 individual TerrainData data type). The solution here for me was to setup a function to work in tandem with the terrain splat mapping calculations above. This function takes the player’s position, and compares it to the center point of the terrains stored in the array Terrain.activeTerrains. The terrain returned is then interpolated on to determine what textures is beneath the player. Here’s the code return the closest terrain to the player! Just remember I use Scriptable Objects to store live values (such as player posisiton), hence the need for me to specify “.value” to get the Vector3.
Terrain GetCurrentTerrain()
{
//Array of all terrains
Terrain[] totalTerrains = Terrain.activeTerrains;
//Checks on length
if (totalTerrains.Length == 0) return null;
else if (totalTerrains.Length == 1) return totalTerrains[0];
//closest terrain, Initialized with totalTerrains[0]
Terrain closestTerrain = totalTerrains[0];
//Center of terrain at totalTerrains[0]
Vector3 terrainCenter = new Vector3(closestTerrain.transform.position.x + closestTerrain.terrainData.size.x / 2, playerPos.value.y, closestTerrain.transform.position.z + closestTerrain.terrainData.size.z / 2);
//will be closest distance between player a terrain. Initialized with totalTerrains[0]
float closestDistance = Vector3.Distance(terrainCenter, playerPos.value);
//Iterate through list of all terrains
for (int rep = 1; rep < totalTerrains.Length; rep++)
{
//currently selected terrain
Terrain terrain = totalTerrains[rep];
terrainCenter = new Vector3(terrain.transform.position.x + terrain.terrainData.size.x / 2, playerPos.value.y, terrain.transform.position.z + terrain.terrainData.size.z / 2);
//Check on distance compared to closest terrain
float d = Vector3.Distance(terrainCenter, playerPos.value);
if (d < closestDistance)
{
closestDistance = d;
closestTerrain = totalTerrains[rep];
}
}
//Returns the closest terrain
return closestTerrain;
}
So now that we have the closest terrain to our player, we need to convert the player’s position in the game to their position on the specific alpha map. This process looks something like this, noting that currentTerrain has just been set to whatever is returned by GetCurrentTerrain():
void GetPlayerTerrainPosition() { //Player position relative to terrain Vector3 playerTerrainPosition = playerPos.value - currentTerrain.transform.position;
//Player position on alphamap of terrain using offset Vector3 alphamapPosition = new Vector3 (playerTerrainPosition.x / currentTerrain.terrainData.size.x, 0, playerTerrainPosition.z / currentTerrain.terrainData.size.z);
//Properly scales players x and z float xCoord = alphamapPosition.x * currentTerrain.terrainData.alphamapWidth; float zCoord = alphamapPosition.z * currentTerrain.terrainData.alphamapHeight;
//Casts as int and sets xPos = (int)xCoord; zPos = (int)zCoord; }
We get out of this call with now our 2 xPos and zPos fields set to the player’s coordinates on the terrain. All that’s left is to take these coordinates and get the alpha map at the player’s position, and determine which terrain texture is applied at that location. One important note is how alpha maps store references to textures. the alpha map is a 3d array where the third value refers to the texture being checked for. For example alphaMap[0,0,0] will return the strength of the texture in slot 0 of the terrain texture layer. alphaMap[0,0,1] will return the strength of texture in slot 1. Hence, splatmapping! We are able to interpolate on various combinations of strength of textures, not just simply player is or isnt on ice. Instead, we can say 30% ice, 70% regular snow, and have our movement variables adjust to that specific combination…. I’m getting off track, but just know this:
textureValues[] is an array of floats representing the strength of each texture at the specified x & z pos. The length of this array is simply set to the number of textures in our terrain layers.
here, rep is used to tie the corresponding spot in textureValues to the value of the texture in that slot of the 3d array
SetPlayerMovementVariables() is currently where we are interpolating on the data gathered here, but essentially the value is clamped from 0 to 1, representing how much of the splatmap at that point is of the texture in the corresponding spot in aMap, and from there we are setting values in our PlayerMovement script. Take a look!
//gets the float (clamped between 0 and 1) of how much of each texture is present at the x & z coord void CheckTextureBelowPlayer() { //Will store the alpha map of the current terrain float[,,] aMap;
//Uses x position and z position to set aMap to correct alpha map aMap = currentTerrain.terrainData.GetAlphamaps(xPos, zPos, 1, 1);
//textureValues stores the current stength of the texture stored in the corresponding slot in the alpha Map for (int rep = 0; rep < textureValues.Length; rep++ ) { //stores stength of values at that point textureValues[rep] = aMap[0, 0, rep]; }
//Iterates through to check if any values are greater than 0 for(int rep = 0; rep < textureValues.Length; rep++ ) { //If terrain is present, sets player movement values if(textureValues[rep] > 0) { SetPlayerMovementVariables(rep, textureValues[rep]); }
} }
This flow of operations effectively allows my team’s PlayerMovement script to iterate as usual, but be fed different live values decided by the terrain type. So far this works really well for me, but if I find more to change and tweak, I absolutely will update it here! I hope this helps anyone who is setting out to do this themselves, much like I was a few days ago! Hope you enjoy the code!
Hello All, I hope everyone is at least as well as the last time I posted on here, hopefully even better. I wanted to write here today to update on my current projects. I am happy to announce that Liquidators, a yearlong project I have been lucky enough to be the lead producer of, has released a playable and open demo, for free. I would love to have support from my site and receive feedback on this game! It is a survival horror game based on the real life events which followed the reactor meltdown at Chernobyl. The demo can be downloaded HERE.
I will of course keep my site in the loop about this game, as we plan for a steam release this summer, hopefully. However, this is a site dedicated to my personal code and projects so I will carry on to stuff that you will definitely find less interesting than a nuclear reactor survival horror game which is literally free right up there ^. You could play it for FREE but you’re still reading this? Fine, if you’ve made it this far lets talk about scriptable objects and how I’ve been using them in my Finite State Machines in a separate, yet still cool, side project.
A little about this “side” project:
Been in production for almost 3 weeks
I’m working as the only engineer, Game being built in Unity, Being built for gamepad controllers (using primitive input system (I know))
it’s a 2 player endless runner
Player 1(Dog) can choose when to throw player 2(Boomerang)
Boomerang player aims themselves before throw
Dog player only has control of left and right movement, constantly moving forward
Boomerang player can control left, right, and forward back, still constantly moving forward, just at adjusted rate
Players try to survive as long as possible
Each have abilities they can use to help each other
“Tokens” used when using ability
Cylinder is enemy only boomeang can kill, kills dog. Wall blockades kill both
Here’s a clip:
Please excuse GIF quality
So here, you could imagine there are a few player states. Specifically, a state for each player when the boomerang is with the dog, for when the dog and boomerang are separate, and for when the boomerang dies and the dog persists. A Finite State Machine helps us out here because none of these states will be coexisting. So, each player will have their own instance of a StateMachine, which takes an iState, and they must communicate with one another, to ensure they are in proper states at all times, since one of their states being out of sync would inherently break the other’s state (since their controls are dependent upon one another).
If you have read my other posts here , or here, or here, or here or- okay you get it.. If you’ve seen those posts or the title of this one, you know that I love SODA. So given our state machine above, SODA fits into this very well. The first issue presented above is that our state machines must know the state of one another. SODA Events really help us with this here. When the dog throws the boomerang, it’s an event. The dog script doesn’t even communicate with the boomerang script directly. It simply invokes that event, and the GameEventListener on the Boomerang is quick to respond, switching to its “free” state, after a throwing thread, of course. This exists for all my state transitions here, except for the boomerang being caught by the Dog for that requires a synchronous tap of a button by both players. But you can see how that would ensure our State machine stay in our intended state.
The next BIG help from SODA in these FSMs is that my IntVariable type, the Scriptable Object int I’ve made, can be passed into my states, where they have free access to the value they need, and receive all live updates to that number via that reference. Confused? Imagine this: My dog player location is stored in my Vector3Variable Scriptable Object. In my state constructor, I take a type Vector3Variable _playerLocation. Now _playerLocation.value will be a reference to the exact spot in memory where my Player Location value is stored. Whether I just want to access it, or even adjust it, that value is live, and feeding into any other script that may need that live number. This is huge because normally I would have to make a reference to my player in every script that needs that number, and store it again in that script. Furthermore, by taking it in the state constructor, I don’t have to pester my player script to retrieve it every frame, because in the case of a value that is changed elsewhere, my state script will receive that update instantly through the magic of SODA!
I hope this makes sense, but in case it doesn’t, here’s some code:
//This is a state for player 1's movement, while the boomerang is in its backpack
//Here you have my declaration of the Variables I will need in this State
//Protecting these variables calmed down an empty value warning I was gettig
protected Vector3Variable _dogLocation;
//Player speed can be changed at any time
protected FloatVariable _playerSpeed;
//Player can move, or not
protected BoolVariable _playerCanMove;
//Reference to Dog script
protected DogPlayerMovement _player;
//Constructor
public ISDogRunning (Vector3Variable dogLocation, FloatVariable playerSpeed, BoolVariable playerCanMove, DogPlayerMovement player)
{
//Simply taking the references I have, and setting equal to the Variable I need
_dogLocation = dogLocation;
_playerSpeed = playerSpeed;
_playerCanMove = playerCanMove;
_player = player;
}
//.............................
//This is my state Tick, run every frame
public void OnStateTick ()
{
if (_playerCanMove.value)
{
//Moves player forwards
_player.transform.Translate (Vector3.forward * Time.deltaTime * _playerSpeed);
//Allows player to move LR
_player.transform.Translate (Vector3.right * Time.deltaTime * Input.GetAxis ("P1Left Stick Horizontal") * horizontalMovementMod);
//Throw boomerang when A pressed
//Keyboard controls for debug
if (Input.GetButtonDown ("P1A Button") || Input.GetKeyDown (KeyCode.E))
{
//Tells player to throw
_player.BoomerangThrown (aimLocation);
}
//Reads for player using ability
if (Input.GetButtonDown ("P1B Button") || Input.GetKeyDown (KeyCode.R))
{
_player.UseSelectedDogAbility ();
}
//Allows player to switch through abilities
if (Input.GetButtonDown ("P1X Button") || Input.GetKeyDown (KeyCode.F))
{
_player.SwitchDogAbility ();
}
}
//Gets player's aim
//This state actually reads input from player 2 as well, as aiming component was initially part of dog
//Axis is between -1 and 1, adding 1 and then dividing by 2 to get complete input
aimLocation = Vector3.Lerp (_player.GetLeftAimLimit (), _player.GetRightAimLimit (), ((Input.GetAxis ("P2Left Stick Horizontal")) + 1) / 2);
//Will rotate arrow assigned to plauer
arrowGO.transform.LookAt (aimLocation);
//Puts aim point at aim locations
aimPointGO.transform.position = aimLocation;
//Updates player's location
_dogLocation.value = _player.transform.position;
}
Hello all, this is an update to this project. That post contains a link to both the up to date engineering manifest of this project, as well as context to this post. But if you’re not here for the code talk then… it doesn’t matter I guess! I hope who ever may be reading this is having a great day and all is right in their world. Today I bring with me what will likely be the final build of Imponderabilia. The game has been extremely fun to work on, and really has taught me so much about architecture that I really look forward to applying to future projects.
Final build? But this is Version 0.7? Great observation! I wouldn’t feel comfortable dubbing this build as a completed version as we have not worked much (or at all) on the aesthetic of the game. So, as I’ve said in the previous build postings, I’m not an artist, this build is simply a proof of mechanics and architecture.
What has changed from previous build?Version 0.3 was largely an implementation of independent features which utilized the “InputGrouping” class I wrote and posted here.
Feedback
Emphasis on letting player know when an action has been completed
Indicators of when an interaction is available and how engage
Features
Implemented Rice Prep interaction
Implemented dependency between interactions
Player must cook rice first, then roll rice with fish, then cut roll.
Player can hold as many rice or uncut rolls as desired
Score system
Simple score system which considers the amount of sushi and quality/completion of required interactions
Added a timer which will end game after count down.
Bug Fixes
Key Sprite Manager (check engineering manifest in link at top of page)
Added FlipSprite method which fixed the irregularity in key sprite “pressing” animation
Sushi Roller
Countdown animation no longer misfiring when checking for input
Player Animator
Small bug fixes, added front and back walk animations
Here you go! Please enjoy, and direct any feed back you have to aidantakami@gmail.com
For those of you seeking a little more meat, below is my RiceMeter Game Object script. The product of this script can be seen when you approach the diamond colored grill in my build above. It consists of 2 sliders, and is meant to simulate the act of making rice.
By pressing space bar before the game start, the player adds rice to the pot which they will begin to cool once they “Interact”. The interaction begins once the player presses the indicated keys “AWDS” in that order. The player will now have begun the interaction. Players repeat that same motion of AWDS repeatedly to add water and clean the rice. however, once the water is full, the player can no longer add water, nor clean the rice. But players can drain the water by holding down space, which will allow room for more water to be added. But watch out, if you drain too much water, you will lose some rice. The 2 sliders represent the amount of rice, and the amount of water currently in the pot.
By adding rice, the player ends the interaction with more rice and can make more sushi from this rice, however the more rice added, the more tedious it is for the player to balance between adding water and draining it once the pot is full.
This script is only the half of the overall game, the other half is where all specific key strokes are registered, and where the outcome of the game is handled. However, this game object stores and deals with the values pertaining to how much rice and water are present, added, lost/drained, and all the above, making it (hopefully) a more enjoyable script to read. Here’s some code:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro;
using UnityEngine.UI;
public class RiceMeterGO : MonoBehaviour
{
//Sliders for the rice and water level
public Slider riceLevel;
public Slider waterLevel;
//TMP used to indicate how much rice is in the pot
public TextMeshProUGUI riceText;
//float used to determine how much the rice must be washed
// before the slider image, below, is lerped to green
public float requiredWashes;
//Slider image
public Image waterFill;
//How fast water is added and drained
public float waterAddIncrement;
public float waterDrainIncrement;
//Used to progressively lerp slider image
private float lerpInt = 0;
private float timesWashed = 0;
//Color32 used for lerping slider image
private Color32 unwashedColor;
private Color32 washedColor;
//Enter key used to tell player when finished
public SpriteRenderer enterKey;
public SpriteRenderer enterKeyHighlighted;
//float used to increment the key sprite switcher
private float keySwitchIncrementer;
public void Start()
{
//Set slider max and min
riceLevel.minValue = 0;
riceLevel.maxValue = 10;
waterLevel.minValue = 0;
waterLevel.maxValue = 10;
//Instantiate colors
unwashedColor = new Color32(255, 0, 38, 255);
washedColor = new Color32(0, 255, 47, 150);
//reset
ResetMeter();
}
//Will add rice to the rice level slider.
//Max rice that can be added is 5
//Tis will be start of the game for UI
public void AddRice()
{
//Adds rice if under or equal to 5 cups
if(riceLevel.value <= 5)
{
//Increments
riceLevel.value += 1;
//If this is the first increment
if(waterLevel.value == 0)
{
//set to 0.5 higher
waterLevel.value = 1.5f;
}
else
{
//else just increase
waterLevel.value += 1;
}
}
//Else increment
else
{
riceLevel.value = 1;
waterLevel.value = 1.5f;
Debug.Log("Rice reset");
}
//Sets Rice Text UI to rice level
riceText.text = riceLevel.value.ToString();
}
//Will return the number of rice in meter
public float GetNumberOfRice()
{
return riceLevel.value;
}
//Will add water to the Water slider
public void AddWater()
{
//If water level is okay add water
if (!isWaterFull())
{
waterLevel.value += waterAddIncrement;
lerpInt += waterAddIncrement;
//Change color & value of water
waterFill.color = Color32.Lerp(unwashedColor, washedColor, lerpInt * 0.04f);
//increments times washeds
timesWashed++;
}
else Debug.Log("Full");
}
public void DrainWater()
{
//If water level okey drain water
if (isRiceLevelOkay())
{
waterLevel.value -= waterDrainIncrement;
}
else LoseRice();
}
//Used to check if water level is acceptable
private bool isWaterFull()
{
//If water is higher than rice
if (waterLevel.value == waterLevel.maxValue) return true;
else return false;
}
private bool isRiceLevelOkay()
{
if (waterLevel.value > riceLevel.value) return true;
else return false;
}
//Will return int representing the quality of the rice
/*
* Retrun 0: bad rice, no cookie
* Retrun 1: okay rice... still no cookie for now
* Retrun 2: good rice, cookie
* Return 3: Master rice, cookies
*
*/
public int RiceQualityCheck()
{
if(timesWashed <= 49)
{
return 0;
}
else if(timesWashed <= 64)
{
return 1;
}
else if(timesWashed <= 79)
{
return 2;
}
else if(timesWashed > 80)
{
return 3;
}
else
{
return 0;
}
}
private void Update()
{
//Will indicate to player when they have washed rice enough, triggers right before lerp is finished
if(timesWashed > 80)
{
//Triggers enter key flashing
if (keySwitchIncrementer < 1f)
{
enterKeyHighlighted.gameObject.SetActive(false);
enterKey.gameObject.SetActive(true);
enterKey.transform.position = new Vector2(gameObject.transform.position.x + 0.2f, gameObject.transform.position.y - 0.2f);
}
else if(keySwitchIncrementer < 2f)
{
enterKeyHighlighted.gameObject.SetActive(true);
enterKey.gameObject.SetActive(false);
enterKeyHighlighted.transform.position = new Vector2(gameObject.transform.position.x + 0.2f, gameObject.transform.position.y - 0.2f);
}
else
{
keySwitchIncrementer = 0;
}
keySwitchIncrementer += Time.deltaTime;
}
}
public void LoseRice()
{
if(riceLevel.value >= 2)
{
riceLevel.value--;
//Sets Rice Text UI to rice level
riceText.text = riceLevel.value.ToString();
}
else
{
//Game End
}
}
public void ResetRiceValue()
{
riceLevel.value = 1;
}
public void ResetWaterValue()
{
waterLevel.value = 1.5f;
}
public void ResetMeter()
{
ResetRiceValue();
ResetWaterValue();
lerpInt = 0;
timesWashed = 0;
waterFill.color = unwashedColor;
enterKey.gameObject.SetActive(false);
enterKeyHighlighted.gameObject.SetActive(false);
riceText.text = "1";
}
//Used to set slider active or not
public void SetRMActive(bool setActive)
{
waterLevel.gameObject.SetActive(setActive);
riceLevel.gameObject.SetActive(setActive);
riceText.gameObject.SetActive(setActive);
}
}
Hello all, I just wanted to post an update to my post here about my current game project, as I do now have a build ready to share! Obviously, this is a super early build, but it will hopefully provide some meaning to the code I posted last week. But once again please keep in mind this project has only been in motion for a week, we have only used free or self made art assets, and exemplified here is only a proof of mechanics and basic architecture for those mechanics.
A good designer really shouldn’t provide tips to players before a play test, but since I’m not there to observe people play, I want to make a few notes about how this game is meant to be played, as it’s likely different from most games you have been exposed to!
You will play as a sushi chef, and be able to engage in the interactions that will eventually make up the foundation of the mechanics of this game! So be prepared to treat your keyboard as your work station as you go into this experience.
With your right hand, navigate usingarrow keys.
With your left hand, drag fingers along letter keys to engage “active interactions”.
Please don’t rush, treat your sushi gently.
Please report any bugs you find to me! aidantakami@gmail.com.
This is a VERY early build, there are bound to be lots of bugs and problems, but by play testing early, these bugs can be weeded out early, and save me the trouble later.
For my tech crowd, please take note that Input Manager at work there can be found in my last post, linked at the top of post. Additionally that post contains a link to my Engineering Manifest for the project, which contains summaries of all classes written for the project, as well ad general architecture overviews where necessary.
… and of course I would never leave you all without a script update. Today I’ll share with you guys a script which saved my life. After playing the build above, I’m sure you noticed the keys which animate alongside the player while they are close to an “active interaction”. Generally, one would either instantiate the specific key sprites when needed(both pressed and unpressed) or have each sprite referenced in code and pool them somewhere in the scene to use when needed, and be out of the view of player when not.
My KeySpriteManager sort of does both… but is useful for me as it can can take KeyCodes as args and return the corresponding “key sprite”. That’s right, public SpriteRenderer GetLetterSprite(KeyCode keyCode, bool keyPressed). Basically, my KeySpriteManager contains a List of SpriteRenderers of all the “key sprites” (which if you don’t get by now are the keys of a keyboard, individually cut out and used to emphasize the pressing of keys). Each letter contains both a pressed and unpressed sprite, so I allow access to the pressed sprites as well with the bool keyPressed.
Oh wow you made a list that takes KeyCodes and returns pictures who cares. I know. But this is incredibly useful for my game as InputGroupings (the class I shared in my last post (linked at top of post)) contain only keyCodes, and I can simply access these KeyCodes from any script and get the appropriate, matching key sprite from my KeySpriteManager. In addition to storing sprites, public SpriteRenderer FlipSprite(SpriteRenderer sr) will take a KeySprite, find it in the List, and based upon the spot it finds it in the List, return either the pressed or unpressed version of that key sprite, opposite of whatever was given to the function.
The code is quite repetitive, but I thought it would be good to share because of how greatly it works in conjunction with my InputGroupings class…. which I shared…. last week… link up top….. sorry. I’ll spare you guys the repetition of this code and trip it down, but you will certainly get the idea… Thanks for reading, here’s some code and a picture from the build I posted above!
using System.Collections.Generic;
using UnityEngine;
public class KeySpriteManager : MonoBehaviour
{
//keySprites must have letter sprites entered in alphabetical order to function
[SerializeField]
public List<SpriteRenderer> keySprites = new List<SpriteRenderer>();
//Will return corresponding SR for the KeyCode given
//List is in aplhabetical order, A to Z, then 1 - 10
//Array must have list[x] = un pressed key, list[x+1] = pressed key
public SpriteRenderer GetLetterSprite(KeyCode keyCode, bool keyPressed)
{
//This will be repetitive
//Return Corresponding KeyCode
if (keyCode == KeyCode.A)
{
//Returns unpressed key Sprite
if (!keyPressed)
{
return keySprites[0];
}
//Returns pressed Key Sprite, sets ket to pressed
return keySprites[1];
}
//Return Corresponding KeyCode
else if (keyCode == KeyCode.B)
{
//Returns unpressed key Sprite
if (!keyPressed)
{
return keySprites[2];
}
//Returns pressed Key Sprite, sets ket to pressed
return keySprites[3];
}
/*
*
*
* I told you I would spare you the repetition...
*
*
*/
//Catch
else return keySprites[0];
}
//Will returned the flipped sprite from the one provided
public SpriteRenderer FlipSprite(SpriteRenderer sr)
{
//Finds sprite in List
for(int rep = 0; rep < keySprites.Count; rep++)
{
//finds equivalent sprite
if (keySprites[rep].sprite.Equals(sr.sprite))
{
if(rep % 2 == 0)
{
return keySprites[rep + 1];
}
else
{
return keySprites[rep - 1];
}
}
}
Debug.Log("Couldn't find sprite to flip.. probably the .Equals()");
return keySprites[0];
}
}
Hello all, I hope as you read this blog post all is well in your world, and that hopefully reading about some video game architecture might alleviate some of the stress in your life. I’ve talked about a few projects on this site before, but usually just the specific code or concepts that I contributed to the project. However, this is different. Throughout all of last week, a friend and I were ping ponging ideas off of one another until we came up with an expandable mechanic system, a design to foster that system, and an aesthetic that we hope to use to tell a compelling narrative.
Why is this project special? It’s not. It’s just a small game project with a buddy. This happens all the time. I know that. But it’s special in that I’ve written every script that goes into a simple game, which is significant because I’m starting from scratch and able to take who ever might be reading this, along with me.
The Game:
Mechanics built around affordances of the Keyboard.
Players will control 2D top down sushi chef with right hand
Players will engage in unique “active interactions” with left hand by dragging fingers along keyboard in specific patterns, as specified by each individual “active interaction”.
Example: Dragging your pointer, middle, and ring finger along the “QWE” keys and downwards (to “ASD” the to “ZXC”) to simulate the rolling of sushi.
Perfection of mechanics yields higher rewards to the player
Player will engage in various acts involved in being a sushi chef, from cutting rolls to purchasing fish, and live out a life controlled by the daily requirements and upkeep.
The Aesthetics Pitch: So far most energy has been aimed in the direction of building a system of solid mechanics and enjoyable gameplay, however this is what we DO have for aesthetic: You are a sushi chef, and run your own small shop where people often come and go. You live here, because you always work. However, after a disease sends your nation into quarantine, your daily life becomes controlled by a checklist of repetitious tasks.
NOTE: I’m not an artist, most of the assets being used in my game are from this pack. Very great assets. The Player animations and art are done by Shad.din. I’m actually working with Shad.din to get more animations for the player, and more that pertain specifically to this game.
Engineers Corner
This game is my child. I’ve written every piece of code in it, and I’m very very excited to share it with you. In fact, I’m so excited I’m actually going to make available my Engineering Manifest for this project, which will be live updated with all new content. HERE is the link! In this manifest you will find a comprehensive breakdown of all the architecture at work in the project. On my site, however, you will continue to receive deep dives into specific scripts and concepts, they will just likely all pertain to this game project for the time being.
Where and When Can I Play? I will post the first “early early” build on this site, soon… at the time of this post (check that link above, it’s definitely different now) I have constructed and implemented all the architecture necessary for the unique style of input which this game is built on. It has been implemented in 2 unique “active interactions”, with just very early UI and animations. The player controller and animator are finished. And then of course, SODA is completely implemented, making the entire project modular, adjustable, and “dependency free”…. okay maybe a few dependencies.
Where is the Beef? I get it, why come to this post if there’s no code talk. I respect that. Well, let me tell you about InputGrouping. InputGrouping() is a class I wrote for this project which allows us to detect input from specific, pre determined, groupings of keys. So think, how would you go about detecting when the player presses “Q”, then “A”, then “Z”? Did someone say new List<KeyCode>();? That person is wrong and stupid, you would use a new InputGrouping(); Because it does all the work of a List and more. So with that in mind, try to understand what’s going on here, and how I must be using it.
/*
* An InputGrouping holds the keycodes necesary to execute a specific Advanced Interaction
*
* This will be used by our InputManager Script
*/
using System;
using UnityEngine;
using System.Collections.Generic;
public class InputGrouping
{
//List of they keys sought by advanced mechanic, in order.
public List<KeyCode> desiredInput = new List<KeyCode>();
//InputGrouping will track it's own "progress" if keys are to be pressed in order
public int currentStreakInt;
public KeyCode GetNextKey(int placeInList)
{
//Should always be true
if (desiredInput[placeInList] != null)
{
return desiredInput[placeInList];
}
//Debug section
Debug.Log("Input Grouping value could not be found.");
InputGroupingDebugger();
return KeyCode.Backspace;
}
//Allows us to add keys to the list
public void AddKey(KeyCode keytoBeAdded)
{
//Adds keycode to List
desiredInput.Add(keytoBeAdded);
}
//Returns the size of the array, starting from 1
public int GetSize()
{
return desiredInput.Count;
}
//returns progress through List
public int CurrentStreak()
{
return currentStreakInt;
}
//Increases progress through List
public void IncreaseCurrentStreak()
{
currentStreakInt++;
}
//Resets progress through List
public void ResetCurrentStreak()
{
currentStreakInt = 0;
}
//Returns true if all keys in desiredInput are being pressed
public bool AllKeysPressed()
{
//Int of how many keys are down
int keysCorrect = 0;
//Iteractes through keycodes
for(int rep = 0; rep < desiredInput.Count; rep++)
{
//increments int if key is registered
if (Input.GetKey(desiredInput[rep]))
{
keysCorrect++;
}
}
//Returns true if int matches desiredInput count
if(keysCorrect == desiredInput.Count)
{
return true;
}
else
{
return false;
}
}
//call for multiple frame window and save highest return
//Returns how many keys are pressed in List
public int TotalNumberPressed()
{
//Int of how many keys are down
int keysCorrect = 0;
//Iteractes through keycodes
for (int rep = 0; rep < desiredInput.Count; rep++)
{
//increments int if key is registered
if (Input.GetKey(desiredInput[rep]))
{
keysCorrect++;
}
}
return keysCorrect;
}
public void InputGroupingDebugger()
{
Debug.Log("InputGrouping Debug report:");
for(int rep = 0; rep < desiredInput.Count; rep++)
{
Debug.Log("desiredInput[" + rep + "] = " + desiredInput[rep] + ". ");
}
Debug.Log("Report complete. Have a nice day!");
}
}
It’s not too complicated, but given that my game is heavily dependent on specific keystroke patterns, InputGroupings have proven extremely successful in managing the mess that can come of dealing with complex input detection. And so for an example of how this script appears in my game, I’ll give some snippets below. to note in this code: desidredInput is the name of the List<KeyCode> contained in InputGrouping, as well as currentStreakInt is the internal int which iterates through the List.
//InputGrouping qazKeys
if (Input.GetKeyDown(qazKeys.desiredInput[qazKeys.currentStreakInt]))
{
//Sets current streak plus 1
qazKeys.IncreaseCurrentStreak();
//Resets internal clock, giving player more time to link interaction
internalClock = Time.time;
if (qazKeys.currentStreakInt == qazKeys.GetSize())
{
//Begin Interaction
Debug.Log("QAZ Input Detected. MakiMaker in range? " + makiMakerInRange.ToString());
ResetAllStreaks();
if (makiMakerInRange)
{
//Raise Event
makiMaker.Invoke();
}
}
}
Of course you would have to see some SODA in my scripts. In case you’re unfamiliar with that makiMaker.Invoke() I highly urge you to check out some of my other posts about Scriptable Object Dependent Architecture. Well… that’s it for now. I hope that you enjoyed this post as much as I enjoyed sharing with you. And please check in for updates about the game, including playable builds! Wash your hands!
Image from Asset Pack we will be using, by GuttyKreum, linked above.
Hello all, I hope that this blog post finds you all well. Given the COVID-19 circumstances, I (and everyone I know) have been pushed into online classrooms. It’s been fine, but luckily it has also given me some personal time to try and post on this site, and work on more personal projects… That being said this post is a class project by Nicole Havin, and myself. Nicole worked on the mostly on the aesthetic side of things, implementing sounds, Timer UI and Main menu. I’m writing this post to talk more about Scriptable Object Dependent Architecture. I’m posting this game here to provide my readers with an opportunity to experience the architecture of which I will be writing about.
Disclaimers: I do not own anything in this game
This is a remake, for a class project, of Konami’s 1981 classic “Frogger”
This game is buggy and was made under a time crunch for class. I am only posting this game here to give examples of the architecture I am speaking of, and showing how it is used in this specific build. I am also experimenting with UnityWebGL and trying to get those games hosted here on my site. For those who don’t know, exporting to UnityWebGL carries with it a plethora of issues in how the game in converted to be playable via a browser. There are certainly lots of bugs which I have not run into on the WebGL version. So please consider this as you play this game! Please feel free to contact me if you find any bugs! If this goes well I will continue to try and import playable demos of the architecture of which I’m writing about. Give it a few plays through and I’ll catch you on the other side.
This short experience is build to be modular, easily debuggable, and dependency free through the use of SODA Game Events & Listeners, and SODA Variables.
I’d like to first draw your attention to how when the player is in the first half of the map they must avoid collisions. Collisions here are from incoming cars, which are deadly to frogger. However, the second half of the map, this is flipped. The second half of the map the player must seek collisions. Collisions in the latter half of the map are from the stable log platforms or the finish line colliders.
So how might we “normally” do this? I would say it would involve an edge trigger collider that would tell the player script that it needs to collide or else die. But alas, the eternal game dev issue of detecting if something isn’t there, in this case that something would be the log. I don’t want to get into how we might complete “this” way of solving the issue, but it’s just messy and involves heavy dependencies.
What is the SODA way of doing this? I appreciate you asking. Imagine if we could just have a Scriptable Object which holds a bool value, and we call it BoolVariable “isOverWater”. We set up a collider at the edge of land and water and have that trigger toggle between the 2 states of isOverWater, true and false. Now how do we get logs in there? Simple, the same thing, just have a single BoolVariable, playerIsOnMe, which all log prefabs reference. Does that make sense? This single variable is being operated on by all instances of the log prefab. These logs simply set the variable to true OnTriggerEnter() and to false OnTriggerExit(). Here’s that code:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class LogScript : MonoBehaviour
{
//BoolVariable to represent if player is on this log
public BoolVariable playerIsOnMe;
//Used for spawning purposes
private bool startsOnRight;
public SpriteRenderer sr;
//Used to move the player when on log
[SerializeField]public IntVariable logSpeedToPlayer;
[SerializeField] public int logSpeed;
//Flips sprite depending on start side
private void Start()
{
if(transform.position.x > 0)
{
startsOnRight = true;
sr = gameObject.GetComponent<SpriteRenderer>();
sr.flipX = true;
}
}
// Update is called once per frame
void Update()
{
//Moves the log in accordance to where it was spawned
if (!startsOnRight)
{
transform.position += transform.right * logSpeed * Time.deltaTime;
}
else
{
transform.position -= transform.right * logSpeed * Time.deltaTime;
}
}
public void OnTriggerEnter2D(Collider2D col)
{
//Player enters log
if (col.gameObject.CompareTag("Player"))
{
//Set BoolVariable value
Debug.Log("Player is on");
playerIsOnMe.SetValue(true);
//Gives this logs speed to the IntVariable which gives it to the Player
if (!startsOnRight)
{
logSpeedToPlayer.SetValue(logSpeed);
}
else
{
logSpeedToPlayer.SetValue(-logSpeed);
}
}
}
public void OnTriggerExit2D(Collider2D col)
{
//Player Leaves Log
if (col.gameObject.CompareTag("Player"))
{
Debug.Log("Player is off");
playerIsOnMe.SetValue(false);
logSpeedToPlayer.SetValue(0);
}
}
}
If this makes any sense to you, then odds are you can understand what is going on in the rest of this script (beyond the OnTriggers), but more on that later. The main note here is that when the player enters the log, beneath the Debug statement, the BoolVariable playerIsOnMe is set to true. This lets the player movement script know to continue displaying the idle sprite, this bool lets the GameManager know the player is still alive. These BoolVariables (playerIsOnMe and isOverWater) are both accessible by all other scripts, and never providing conflicting values because all scripts are refrencing the same ScriptableObject.
How are these two BoolVariables being compared? Once again, well thought out question. So since both these Bools are being determined elsewhere in the script, our PlayerMovement script, which also serves as the animator controller, simply has to reference these 2 variables values to know the state of the player. I did this in Frogger by having an event at the end of every “Jump” animation which would check the state of the 2 BoolVariables, as at the end of the Jump animation the player would either be securely on a log or dead.
About the IntVariables: If you understand the above architecture then you’ll enjoy knowing what’s going on with the IntVariable logSpeedToPlayer in the above code. Basically, that IntVariable, when set to 0, will not affect the player’s movement at all. When set to x it will apply a constant drag to the player to the right to simulate the effect of the player being on the log. So… Do you see why this is so rad? I can just SET THIS INT in my LOG SCRIPT and then my player will INSTANTLY BE EFFECTED by the change to this variable! COME ON!!! How Cool?! So notice that logspeed is just a regular int, but it is used to set the value of logSpeedToPlayer when needed. For now, that’s it! I will try to be more present on here, but I’m busy! But I hope you enjoy! and stay safe!
Hello all, I hope everyone has had a nice holiday season, and beginning to their year! I’ve been pretty busy trying to keep up with my game projects just as much as trying to keep up with my friends who’ve also been on break! But amongst all this I’ve actually been working on a ton of code for “Project RAT”, as my friends and I have been calling it. And specifically, over the last 2 days I’ve been working on a behavior for one of our ranged enemy types, who is meant to avoid the player, and avoid collisions with walls and pitfalls.
How I did this wasn’t terribly difficult, but it definitely proved very fun to write as it did take a fair amount of effort! It operates on a List which stores all Walls and Players OnTriggerEnter() and removes them OnTriggerExit(). From there I’m just taking the Vector of the object in my list, normalizing it, and multiplying it by the reverse distance from the transform of the enemy. Oh! and then we made that shit negative. Easy enough! We go through all elements in the List with that same operation, adding the resulting Vector2 to a field which we then use Vector2.MoveTo()to send our enemy in that direction.
This Behavior is complete, but still requires some polishing! I would like to make the movement of the AI smoother and less exact, while also creating a better catch for when the AI is backed into a corner (as of right now it just stops moving). The Bot has 2 states. The idle state is obviously just a place holder which has the bot pacing back and forth. The Avoid behavior is the real highlight here! But without further ado, here’s some code:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//The abstract keyword enables you to create classes and class members that are incomplete and must be implemented in a derived class.
public class Avoidance: MonoBehaviour
{
//Variables for the walk speed, and the duration of the pace back and forth
public FloatVariable walkSpeed;
public FloatVariable paceDuration;
public FloatVariable visionDistance;
public StringVariable wallsTag;
public SphereCollider aiVision;
//Arr to store all obstacles, Vector for avoid, bool for is avoiding, and then moveRight for idle
public List<GameObject> Radius = new List<GameObject>();
public Vector2 avoidVector = new Vector2();
public bool isAvoiding;
public bool moveRight;
private bool playerIn;
//Timer used for pacing back and forth
private float timer = 0.0f;
private void Start()
{
aiVision.radius = visionDistance;
}
// Update is called once per frame
void Update()
{
//As long as not avoiding
if (!isAvoiding)
{
//Will proceed with idle
idleMovements();
}
else
{
AvoidMovement();
}
}
//The default for the enemy with no walls or players around
private void idleMovements()
{
//Will AI move to the right
if (moveRight)
{
//moves AI right
transform.Translate(2 * Time.deltaTime * walkSpeed, 0, 0);
//flips AI
transform.localScale = new Vector2(transform.localScale.x, transform.localScale.y);
}
else
{
//moves AI left
transform.Translate(-2 * Time.deltaTime * walkSpeed, 0, 0);
//overrides the central motherboard of the player, installing 15 terabytes of My Little Pony episodes
transform.localScale = new Vector2(-transform.localScale.x, transform.localScale.y);
}
//adds to timer
timer += Time.deltaTime;
//once timer is over the duration
if(timer > paceDuration)
{
//move left
if (moveRight) moveRight = false;
else moveRight = true;
//reset
timer = timer - paceDuration;
}
}
//Controls avoidance behaviors
private void AvoidMovement()
{
transform.Translate(Vector2.MoveTowards(transform.position, avoidVector, 1) * Time.deltaTime);
}
//Recalculates the avoid vecotr for avoidance behavior
private void RecalculateAvoid()
{
//clears vector2 and instantiates a temp for reverse distance
float tempDistance = 0f;
Vector2 tempVec;
//If the player isnt present or if the player is present and there is only 1 wall
if (!playerIn || playerIn && Radius.Count < 3) {
//Iterates through array of Positions
for (int rep = 0; rep < Radius.Count; rep++)
{
tempVec = new Vector2(Radius[rep].transform.position.x, Radius[rep].transform.position.y);
ClearAvoid();
//Gets the reverse distance from the AI transform.
tempDistance = (visionDistance - (Vector2.Distance(tempVec, transform.position)));
//Normalized and multiplies by reverse distance
tempVec.Normalize();
tempVec *= tempDistance;
//Adds to the "avoid" vector
avoidVector += tempVec;
}
//Flips the sign of the AvoidVector
avoidVector = avoidVector * -1;
}
else if (playerIn)
{
ClearAvoid();
}
}
//Resets vector field
private void ClearAvoid()
{
avoidVector = Vector2.zero;
}
//On player or wall enter into collider
private void OnTriggerEnter(Collider other)
{
Debug.Log("Triggered");
//If Game Object is wall
if(other.gameObject.tag == wallsTag)
{
//Set temp
GameObject temp = other.gameObject;
//add and recalculate
Radius.Add(temp);
RecalculateAvoid();
isAvoiding = true;
}
//If Game Object is player
else if(other.gameObject.tag == "Player")
{
//Set temp
GameObject temp = other.gameObject;
//Add and recalculate
Radius.Add(temp);
RecalculateAvoid();
isAvoiding = true;
playerIn = true;
Debug.Log("Gonna recalculate!");
}
}
//When a tracked object leaves the zone
private void OnTriggerExit(Collider other)
{
//If player exits
if (other.gameObject.tag == "Player")
{
//Iterate through List
for(int rep = 0; rep < Radius.Count; rep++)
{
//If the selected tag is player
if(Radius[rep].gameObject.tag == "Player")
{
//Remove player, recalculates avoid vector, and break
Radius.RemoveAt(rep);
RecalculateAvoid();
playerIn = false;
break;
}
}
}
//If it's a wall
else if(other.gameObject.tag == wallsTag)
{
//Iterate through list
for(int rep = 0; rep < Radius.Count; rep++)
{
//If the position of the selected matches that which exited
if(Radius[rep].gameObject.transform.position == other.gameObject.transform.position)
{
//Remove, recalculate avoid vector, and break
Radius.RemoveAt(rep);
RecalculateAvoid();
break;
}
}
}
//If the List is empty
if (Radius.Count == 0)
{
//Reset all
isAvoiding = false;
ClearAvoid();
}
}
}