Introduction programming
Introduction
My goal for programming is becoming a better programmer. I have four sub goals:
- Make a dress-up system in the game and have it work efficiently (for example the throwable/environment versions of clothing).
- Learn about hand tracking and have it recognize different gestures.
- Hold myself to higher standards in terms of naming files/variables.
- Hold my code to higher standards (see throwable/environment link above).
You can read about this in this chapter.
Changes since last feedback round
I changed a lot since the last feedback round, I made the carousel and improved on a lot of naming/programming things.
Conclusion and reflection
I think I definitely reached this goal, because I have been very adamant about it during the last weeks and since the last feedback round. I also included the group members in some things and as you can read in the posts linked above I improved a lot myself. The only thing I can still improve is better comments in my code and I will do this as a learning goal in a next project.
Navmesh, making characters walk
This is a new thing I learned from my teammate Scott and something he specifically learned for this project. He teached me about navmesh baking and I made a proximity code.
Scott made a system with waypoints and I used this to make the characters walk on the runway. We couldn’t get the baking to only bake the runway, because it was too narrow, so I used a lot of waypoints to make sure the characters stayed on the runway when walking. We also noticed that once a character arrived at a waypoint they would stand still for a short period of time and this made it so they didn’t walk smooth. I fixed this with code that changes the waypoint to a new waypoint in the list when the character is in close proximity. I got this idea from here, but I made the code myself.
Reflection
What I learned from this is already described in the text above and I know now how I can make my characters walk in future projects and I learned a bit about baking.
Here you can see the code Scott made with my adjustments for the proximity in it:
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
using System.Linq;
public class NavMeshTest : MonoBehaviour {
[SerializeField]
private List<Transform> waypoints;
private NavMeshAgent agent;
private int nextPosition;
[SerializeField]
private float proximity = 0.2f;
void Start() {
agent = GetComponent<NavMeshAgent>();
if(agent == null) {
Debug.LogError("no agent found");
}
if(!waypoints.Any() || waypoints[nextPosition] == null) {
Debug.LogError("no waypoints to go");
} else {
Debug.Log("we have " + waypoints.Count + " waypoints");
}
}
void Update() {
agent.destination = waypoints[nextPosition].position;
if(/*transform.position != waypoints[nextPosition].position*/CalculateDistance() > proximity) {
return;
}
nextPosition++;
if(nextPosition != waypoints.Count) {
return;
}
nextPosition = 0;
}
private float CalculateDistance() {
float distanceToNextWaypoint = Vector3.Distance(agent.destination, transform.position);
return distanceToNextWaypoint;
}
}
Carousels
For the clothing in the scene we wanted to have carousels to display them. Each clothing category (shirts, pants, dresses, etc) would be one carousel and to have the carousels accommodate all of the clothing I designed it so that the carousel displays five objects at one time but to exchange the last one in the back for a new clothing piece each time it is rotated. You can read about my thought process here. I also thought of a way to show load only the carousel that’s in focus, but due to time constraints I haven’t implemented that.
Because I could only start programming on this in the last sprint I looked up code online, so that would save me some time. I immediately found what I was looking for and I only had to update it to make the exchanging I described above possible. I found this code via this site.
This is my version of the code after adding the exchange functionality. I put it at the end of this text.
The first thing I did to make the exchange possible was working with two LinkedLists, where the first one was the objects currently displayed in the carousel and the second one was the excess. Each time the carousel was rotated I would move the first one of the excess LinkedList to the end of the displayed LinkedList and move the first one of the displayed LinkedList to the end of the excess LinkedList. Eventually because of other needs (like reading the index in the list and trying to also have the right angles within the carousel rotation) I moved back to arrays. I chose for arrays because they are actually a bit better than lists since an array is a primitive (and there were no differences for me in this use case, because the size was fixed). I learned this from here.
The bug
After I finished the excess exchange part of the carousel I started testing it and while doing that I saw some things going wrong where clothing would spawn at the same spot as other clothing. I did not really see what went wrong exactly so I started testing by displaying UI-text with numbers on every clothing piece so that I could link them to their places in the array and in the carousel itself.
This really helped me and made me identify the first problem: every piece spawned at the same angle. This was before the screenshot above and all of the clothing pieces would load at the same spot. This meant that I changed the code so that every piece that is displayed the first time will have an angle 360/amountOfDisplayedObjects and then I made an array with the values of those angles and every time a new object would be displayed in the array it grabbed the next angle in the array and kept cycling through that every time.
After solving this issue and other issues something strange happened. Everything worked perfectly until you had seen all available objects and the cycle began again. From the second cycle on objects would load one spot too far. So if you have the angles: [72, 144, 216, 288, 360]: what should happen is that object 0: 72deg, object 1: 144 …… object 8: 288, object 10: 72, object 0: 144. This went well for the first cycle from 0-10 (or 1-11), but not afterwards.
I have debugged this and the angles seem to be correct and I have found nothing on Google. I tried resetting the count or dividing it by 2, but nothing helped. The closest I came to a solution was where it worked except for the first two objects. I even made an excel sheet displaying every carousel turn and the visible objects and their angles to make sure I wasn’t calculating anything wrong. But I still haven’t solved it.
The only thing I haven’t been able to try was changing things to how the rotation works (not rotating the carousel gameobject or only doing that). This was because there were other things with a higher priority and I had a temporary solution for this object: making one big carousel that does not exchange clothing but instead shows all clothing at the same time and has the user in the center of it.
Reflection
What I learned from this is how you make a carousel, but also how to work with angles and this already helped me when I made a quick thing where the bee and its text follow the user around the scene. But I also learned that I should have asked for help earlier, as you can see with the problem above. I was stubborn and didn’t ask for help because I understood from teammates that when you ask for help the teachers told them ways to figure it out yourself but I didn’t want that because I did not have enough time for that. But when having individual coaching with Lisette she told me that if I had asked in a specific way I would have received the help I needed.
The code for the bee and text following the users rotation:
using UnityEngine;
public class FollowPlayerEyes : MonoBehaviour {
[SerializeField]
private GameObject playerVRCamera;
private float previousRotation;
private float currentRotation;
private float turningRotation;
// Start is called before the first frame update
void Start() {
currentRotation = playerVRCamera.transform.rotation.eulerAngles.y;
previousRotation = currentRotation;
}
// Update is called once per frame
void Update() {
Debug.Log("Current: " + currentRotation + " Previous: " + previousRotation);
currentRotation = playerVRCamera.transform.rotation.eulerAngles.y;
turningRotation = currentRotation - previousRotation;
this.transform.RotateAround(playerVRCamera.transform.position, new Vector3(0, 1, 0), turningRotation);
previousRotation = currentRotation;
}
}
Carousel code
using UnityEngine;
/*
* to use this script just place it on any gameobject in the scene, this element shouldn't have a collider so an empty game object is ideal
* add elements to the carouselObjects array, make sure these items have a collider
* make sure there are no elements with colliders in between the center and the carousel elements
*/
public class Carousel : MonoBehaviour {
private GameObject[] carouselObjects;//the elements of the carousel
public float distanceFromCenter = 0.4f;//the distance from the center of the carousel
public int chosenObject = 0; //index of the object that is centered in the carousel
public float speedOfRotation = 0.1f; //the speed in which the carousel rotates: values should be between 0.01f -> 1.0f, zero will stop the rotation
private static float diameter = 360.0f; //the diameter is always 360 degrees
private float angle = 0.0f; //the angle for each object in the carousel
private float newAngle = 0.0f; //the calculated angle
private bool firstTime = true; //used to calculate the offset for the first time
//Iris Oostra
[SerializeField]
private int maximumObjects = 5;
public OVRInput.Button button;
public OVRInput.Controller controller;
private float[] objectAngles;
private int[] currentVisibleIndexes;
private Vector3 positionCarousel;
private Vector3 axisCarousel;
int currentSpawnAngle;
int amountOfCarouselObjects;
bool isBiggerThanMax;
void Start() {
carouselObjects = new GameObject[transform.childCount];
for(int i = 0; i < transform.childCount; i++) {
carouselObjects[i] = transform.GetChild(i).gameObject;
}
currentSpawnAngle = 0;
objectAngles = new float[maximumObjects];
currentVisibleIndexes = new int[maximumObjects];
positionCarousel = this.transform.position;
axisCarousel = new Vector3(0, 1, 0);
amountOfCarouselObjects = carouselObjects.Length;
isBiggerThanMax = true;
if(amountOfCarouselObjects >= maximumObjects) {
isBiggerThanMax = true;
} else {
isBiggerThanMax = false;
}
Debug.Log("FILE NAME: Carousel.cs " + "MESSAGE: --- " + "Name of the first displayed object: " + carouselObjects[0].name);//just display the name of the first chosen element in the console
if(isBiggerThanMax) {
angle = diameter / (float) maximumObjects;
} else {
angle = diameter / (float) amountOfCarouselObjects;
}
for(int c = chosenObject; c < currentVisibleIndexes.Length; c++) {
currentVisibleIndexes[c] = c;
}//TODO: fix this loop
float ObjectAngle = angle;//create a temp value that keeps track of the angle of each element
//if(isBiggerThanMax) {
for(int m = 0; m < amountOfCarouselObjects; m++) {
objectAngles[m] = ObjectAngle;
ObjectAngle += angle;
}
/*} else {
for(int m = 0; m < amountOfCarouselObjects; m++) {
objectAngles[m] = ObjectAngle;
ObjectAngle += angle;
}
}*/
for(int i = 0; i < amountOfCarouselObjects; i++) { //loop through the objects
carouselObjects[i].transform.position = this.transform.position;//Reset objects to the postion of the carousel center
carouselObjects[i].transform.rotation = Quaternion.identity; //make sure their rotation is zero
carouselObjects[i].transform.parent = this.transform; // make the element child to the carousel center
carouselObjects[i].transform.position = new Vector3(this.transform.position.x, this.transform.position.y, this.transform.position.z + distanceFromCenter);//move each carousel item from the center an amount of "DistanceFromCenter"
if(i < maximumObjects) {
carouselObjects[i].transform.RotateAround(positionCarousel, axisCarousel, objectAngles[i]);//position the element in their respective locations according to the center through rotation
} else if(i >= maximumObjects && i < maximumObjects * 2) {
carouselObjects[i].GetComponent<ClothingPieceHandler>().SetActiveness(false);
carouselObjects[i].transform.RotateAround(positionCarousel, axisCarousel, objectAngles[i - maximumObjects]);
} else {
carouselObjects[i].GetComponent<ClothingPieceHandler>().SetActiveness(false);
carouselObjects[i].transform.RotateAround(positionCarousel, axisCarousel, objectAngles[i -10]);
}
}
//Make sure an element is perfectly centered.
if(isBiggerThanMax) {
if(maximumObjects % 2 != 0) {
float rotateAngle = angle + angle / 2;
transform.eulerAngles = new Vector3(transform.eulerAngles.x, rotateAngle, transform.eulerAngles.z);
newAngle = rotateAngle;
} else {
transform.eulerAngles = new Vector3(transform.eulerAngles.x, angle, transform.eulerAngles.z);
newAngle = angle;
}
} else {
if(amountOfCarouselObjects % 2 != 0) {
float rotateAngle = angle + angle / 2;
transform.eulerAngles = new Vector3(transform.eulerAngles.x, rotateAngle, transform.eulerAngles.z);
newAngle = rotateAngle;
} else {
transform.eulerAngles = new Vector3(transform.eulerAngles.x, angle, transform.eulerAngles.z);
newAngle = angle;
}
}
}
// Update is called once per frame
void Update() {
Quaternion newRotation = Quaternion.AngleAxis(newAngle, Vector3.up); // pick the rotation axis and angle
transform.rotation = Quaternion.Slerp(transform.rotation, newRotation, speedOfRotation); //animate the carousel
if(OVRInput.GetDown(button, controller)) {
Debug.Log("hey");
}
}
public void rotateTheCarousel() {// call this function to rotate the carousel
if(firstTime) {// if run the first time calcule the offset
newAngle = transform.eulerAngles.y;
newAngle -= angle;
firstTime = false; // stop this piece of code from running in the future
} else {
newAngle -= angle; //calculate the new angle
}
//The code below was actually temporarily commented out by me because I was still working on fixing a bug in it before I came up with an alternate solution. I uncommented it for readability.
if(isBiggerThanMax) {
carouselObjects[currentVisibleIndexes[0]].GetComponent<ClothingPieceHandler>().SetActiveness(false);
for(int i = 0; i < currentVisibleIndexes.Length; i++) {
if(currentVisibleIndexes[i] >= amountOfCarouselObjects - 1) {
currentVisibleIndexes[i] = 0;
} else {
currentVisibleIndexes[i]++;
}
}
carouselObjects[currentVisibleIndexes[maximumObjects - 1]].GetComponent<ClothingPieceHandler>().SetActiveness(true);
Debug.Log("FILE NAME: Carousel.cs " + "MESSAGE: --- " + "Carousel rotated to the right, current selected piece: " + carouselObjects[currentVisibleIndexes[0]].name); //show in the console the name of the selected element
carouselObjects[currentVisibleIndexes[maximumObjects - 1]].transform.RotateAround(positionCarousel, axisCarousel, objectAngles[currentSpawnAngle]);
Debug.Log("Current angle: " + objectAngles[currentSpawnAngle]);
if(currentSpawnAngle >= maximumObjects - 1) {
currentSpawnAngle = 0;
} else {
currentSpawnAngle++;
} //TODO: make this neater
if(currentVisibleIndexes[maximumObjects - 1] == amountOfCarouselObjects - 1) {
//currentSpawnAngle = maximumObjects-1;
//resetTime = true;
}
}
}
}
Better code for dress-up and clothing rack
After feedback from Remco I decided that I wanted to redo some of my code to make it more reusable and logical. I wanted to do this because he said that game development students most of the time always do things that they already can do and thus do not get a high mark on improving their development skills. One of things I think I can improve on is writing better code and not just doing it the easy way, because the easy way always turns into a hard time later on. Things I did were making better debug-logs, removing redundant scripts because they were only one method (and belonged in another file).
Duplicate by grab
To make the clothing dress-up work better I made it so that you have two types of gameObjects. One is for in the environment, for example on Bob or in the carousel and the other is the one you can use. They both have the same script named ClothingPieceHandler and this script controls objects from the script ClothingPiece.
Once you try to grab a clothing piece that is of the environment type there is a duplicate made and this is the one you can throw on the avatar to dress it. Once a throwable type object hits the avatar (or the clothing rack) the environment type object that is already on the (but inactive) will be set active. At the same time the throwable piece will disappear and the object on the avatar will have its collider turned on. Otherwise if all colliders are always on you get problems with the throwable piece bouncing off of the avatar.
I also made this because it made it easier to respawn things on the carousel at the correct place and it made it easier to make the clothing on the avatar grabable. This was easier because the objects will always stay at the same place.
String manipulation
I have only done string manipulation in C++ and web-based languages and have never actually needed it in C# before, so when I needed it this time I was struck with: “I know how but I don’t”, so this was definitely something new for me and something that is useful in my opinion, because I used it for searching for the carousels and for searching for clothing pieces. I learned how to do this from here and here.
private string RemoveEndOfString(string stringToTrim, string removeThis) {
string outputString = stringToTrim;
int positionWordToRemove = stringToTrim.IndexOf(removeThis);
if(positionWordToRemove >= 0) {
outputString = outputString.Remove(positionWordToRemove);
outputString.TrimEnd();
}
return outputString;
}
Moving code to methods and removing redundant scripts
Because of working without thinking I made some tiny script that were only one method and I moved those to the ClothingPiece class, this class has everything that has something to do with the clothing pieces like respawning on the carousels and despawning after a certain amount of time.
Clothing rack
I made the code for the clothing rack in the same way as the code for the dressing of the avatar, but this time I made it independent of the type of clothing/if something was already there, because all clothing pieces should be able to hand on the rack at the same time.
Better debugs
I had a lot of debug.logs running when making the dress-up system, but eventually I did not know what log was coming from where so I decided to give them structure by having the file-name in the message and using the same format every time. I also changed some messages to be more clear and not show a single variable but also type what is being tested.
For example, from the ClothingPiece class:
Debug.Log("FILE NAME: ClothingPiece.cs " + "MESSAGE: --- " + "Duplicate of " + this.FindRealName() + " is made");
Conventions and consistency
One of the things Remco said that really stood out to me was having better names etcetera in our project, these are things like variable names but also names in the scene. The issues were that some names meant nothing (for example a bush prefab was named “we”), capitalization and consistency in language (English/Dutch). This was actually feedback for the whole group, because I just didn’t pay attention enough to how the others were doing it, but I took it upon myself to fix it. I also send the group a message to ask them to please pay attention to it, to again tell them how important this is (I also did this at the start of the project but understandably people forgot it because working in Unity/Git/Etc was very new for them). I also put some other things in that message, because these were some reasons that our git repository broke. (I explained more in detail in a group call.)
Reflection
I learned a lot of techniques that I can start using from the start of every project. For instance the debug messages and better naming. I still haven’t used enough comments in my code in my opinion, so this is something I will make a learning goal in a next project. One thing I still have trouble with is I don’t want to have to continuously nag everyone in the group about things like naming/code conventions (which is why I tried to do that not as much during this project) but it always turns out that it would have been better if I did.
The code
using UnityEngine;
public class ClothingPieceHandler : MonoBehaviour {
private ClothingPiece clothingPiece/* = new ClothingPiece()*/;
[SerializeField]
private bool setIsThrowable;
[SerializeField]
private GameObject setThrowableVersion;
// Start is called before the first frame update
void OnEnable() {
clothingPiece = new ClothingPiece(gameObject, setIsThrowable, setThrowableVersion);
}
private void Update() {
if(clothingPiece.IsCounting) {
clothingPiece.TimerCheck();
}
}
public string GetRealName() {
return clothingPiece.PieceName;
}
public void SetActiveness(bool value) {
clothingPiece.SetOnOff(value);
}
public void RespawnOnCarousel(string searchName) {
clothingPiece.CarouselRespawn(searchName);
}
public void ToDoWhenEnterGrab() {
clothingPiece.EnterGrab();
}
public void ToDoWhenExitGrab() {
clothingPiece.ExitGrab();
}
}
using System;
using UnityEngine;
[Serializable]
public class ClothingPiece {
private string _environmentPieceIdentifierString = "Environment";
private string _throwableIdentifierString = "Throwable";
private string _cloneIdentifierString = "(Clone)";
private string _space = " ";
private string _pieceName;
private GameObject _thisGameObject;
private bool _isThrowable;
private GameObject _throwableVersion;
private string _carouselName = "Carousel";
private GameObject _correspondingCarousel;
/// <summary>
/// Variables for despawning the object when it has not been in hand for X seconds
/// </summary>
//TODO: maybe make this editable
private int _amountOfSecondsTillDespawn = 10;
//Timer
private float _timer = 0.0f;
private int _zero = 0;
//TODO: make these two in own function
private int _timerInSeconds = 0;
private int _amountOfMSInASecond = 60;
private bool _isCounting = false;
public string PieceName {
get {
//return _pieceName;
return FindRealName();
}
}
public bool IsCounting {
get {
return _isCounting;
}
}
public ClothingPiece(GameObject givenGameObject, bool isThrowable, GameObject throwableVersion) {
_thisGameObject = givenGameObject;
//ond werkt niet
_pieceName = FindRealName();
_correspondingCarousel = FindCarousel();
_isThrowable = isThrowable;
_throwableVersion = throwableVersion;
}
public void EnterGrab() {
if(_isThrowable) {
ResetTimer();
TurnKinematicOffOrOn(true);
} else {
if(!CheckIfDuplicateExists() && CheckIfChildActive()) {
Debug.Log("FILE NAME: ClothingPiece.cs " + "MESSAGE: --- " + "Duplicate of " + this.FindRealName() + " is made");
CreateDuplicate();
}
}
}
public void ExitGrab() {
if(_isThrowable) {
TurnKinematicOffOrOn(false);
StartTimer();
}
}
private void StartTimer() {
_isCounting = true;
}
private void ResetTimer() {
_isCounting = false;
_timer = _zero;
_timerInSeconds = _zero;
}
public void TimerCheck() {
_timer += Time.deltaTime;
_timerInSeconds = (int) _timer % _amountOfMSInASecond;
if(_timerInSeconds >= _amountOfSecondsTillDespawn) {
Despawn();
}
}
public void SetOnOff(bool value) {
GetChildObject().SetActive(value);
GetBoxCollider().enabled = value;
}
private GameObject GetChildObject() {
return _thisGameObject.transform.GetChild(0).gameObject;
}
private Collider GetBoxCollider() {
return _thisGameObject.GetComponent<BoxCollider>();
}
private void Despawn() {
string destroyedName = FindRealName();
GameObject.Destroy(_thisGameObject);
//TODO: test if next line still works
CarouselRespawn(destroyedName);
}
public void CarouselRespawn(string carouselPieceName) {
//IDK!?
_correspondingCarousel = FindCarousel();
_correspondingCarousel.GetComponent<RespawnClothing>().CheckIfPieceNeedsActivation(carouselPieceName);
}
private void TurnKinematicOffOrOn(bool value) {
_thisGameObject.GetComponent<Rigidbody>().isKinematic = value;
}
private bool CheckIfChildActive() {
bool returnBool = false;
if(GetChildObject().activeInHierarchy) {
returnBool = true;
} else {
returnBool = false;
}
return returnBool;
}
private void CreateDuplicate() {
GameObject duplicateGameObject = GameObject.Instantiate(_throwableVersion);
duplicateGameObject.transform.position = _thisGameObject.transform.position;
Debug.Log("FILE NAME: ClothingPiece.cs " + "MESSAGE: --- " + "Making a duplicate");
SetOnOff(false); //Does this work?
}
private bool CheckIfDuplicateExists() {
bool doesItExist = false;
string nameToSearchFor = _pieceName + _throwableIdentifierString + _cloneIdentifierString;
if(GameObject.Find(nameToSearchFor) == null) {
doesItExist = false;
} else {
doesItExist = true;
}
return doesItExist;
}
public string FindRealName() {
string realName = _thisGameObject.name;
if(_isThrowable) {
string throwableEndName = _throwableIdentifierString + _cloneIdentifierString;
realName = RemoveEndOfString(realName, throwableEndName);
} else {
realName = RemoveEndOfString(realName, _environmentPieceIdentifierString);
}
return realName;
}
public GameObject FindCarousel() {
string nameToSearchFor =/* _thisGameObject.tag + _space +*/ _carouselName;
Debug.Log("FILE NAME: ClothingPiece.cs " + "MESSAGE: --- " + "Name of the carousel we are searching for: " + nameToSearchFor);
GameObject foundCarousel = GameObject.Find(nameToSearchFor);
return foundCarousel;
}
private string RemoveEndOfString(string stringToTrim, string removeThis) {
string outputString = stringToTrim;
int positionWordToRemove = stringToTrim.IndexOf(removeThis);
if(positionWordToRemove >= 0) {
outputString = outputString.Remove(positionWordToRemove);
outputString.TrimEnd();
}
return outputString;
}
}
Making the character wear clothing
To make the character wear clothing I did research whether there would be an easy way which would be good for performance. This was because in a previous project I learned to do it with disabled gameobjects and turning them on when needed. I thought there could maybe be a better way.
I found an asset which promised to be that better way. It was this asset. After testing I discovered that the asset only works when the characters and clothing have bones and ours did not (yet) have that and at this point I did not think they would in the future. Because I could not fix this quickly I decided to start doing research again and I found that almost everyone (that did not use a pre-made asset) did it the way I described in the first paragraph. So I started making it that way.
First I put all clothes on Bob (our avatar) and positioned them correctly. You can see this in this screenshot:
In these prefabs of the clothing I made them non-grabable and I removed all physics, because otherwise they would fall off. This is what the normal prefabs look like:
When making the normal prefabs I discovered that the grabbing did not work correctly and you can read how I solved that here.
Next I made a script I put on Bob which detects clothing thrown at him. In the inspector you can link all the gameobjects from the clothing that you want to use and the script will use these to detect what clothing has been thrown and determine which clothing he has to wear. This is what it looks like in the inspector:
It does this with the ClothingType class which looks like this:
I made this class to make it easier to add new clothing types and add new clothing to a type. One special thing about this class is the boolean fullOutfit. When you check this the code for dressing knows that the piece is a full outfit and removes both tops and bottoms from the character before applying the outfit. This also means that when you throw a top/bottom it removes a full outfit if present. This makes it so that you can combine all tops with all bottoms but you cannot overlay/underlay it with an outfit. You can read the script for dressing the character below:
Reflection
What did I learn from this? That I can use layers/names to find the correct gameobjects and manipulate only the ones that I want. This way you cannot throw different objects than clothing to Bob and have the system be confused. It also helped to make a class and I think I could implement that more often in my coding than I am doing now, it makes work easier. I also learned that sometimes the old way is still the best way. I still want to improve my system, because now you have to manually adjust all clothing that is attached to Bob to not have gravity etc. In the future I want it so that you have prefabs where you can attach a clothing model too and then it just works. This will also make it easier for people not familiar with Unity to work with it.
Adding controllers to the game
I have added controllers to the game because we wanted to focus on developing instead of focussing on errors with hand tracking.
First I added the same things as I did when making my pressure cooker project, you can read this here. You can also read about grabbing in that article.
One bug that I encountered during the grabbing was that the clothing was high in the air. You can read how I solved that here.
Different than with my pressure cooker I added teleportation. This is the tutorial I used for it:
To make teleportation I needed to add a device-based ray interactor. This has all the components needed. I also added a teleportation area component to the ground. I also used layers to make it that the ray is not able to interact with things other than the ground so that the user cannot (for example) grab clothing with the ray. Next I added the following script to the VR Rig which controls the rays.
Reflection
I learned a lot of bug solving from this and I refreshed my memory with what I learned in the pressure cooker.
Hand Tracking Grabbing objects
For dressing the character we want to let the player grab clothing from the world and throw it onto the character. For this I followed another tutorial from the same person as I followed the other hand tracking tutorials from.
To make grabbing possible you need to do a few things. First of all I wrote the following script which overrides the grab script for controllers so I can use it for hand tracking (to do this I added the virtual keyword to the old script and used override in my own script).
The script checks whether the player is pinching and is not already holding something and there is a grabbable object near. If all of these are true then the player grabs the object.
We (I was sick but Scott took over the finishing touches) also tried to make it so that the player can throw the object, but it did not work because the method we overrided did not work well with hand tracking. The method is supposed to take the last position of the object and compute this with the position of your hand and create a velocity from this, but this did not translate from controller to hand, because the Oculus Integration has a mistake where the lastPos is equal to the current transform.
Hand Tracking Gestures
We wanted to utilize gesture recognization for molding the character in the game so I looked up a tutorial at the same YouTube channel where I learned how to implement hand tracking and I followed that.
In this script you can see the data I recorded using it, the position of the hand/finger bones will be recorded when you are in record-mode and press the space button. When you are playing the game the script checks whether it sees one of the gestures that has been saved into the variables. You can then also assign UnityEvents to the gestures which get executed when it is recognized.
In this screenshot you can see what it looks like in Unity when you have a gesture saved, all the coordinates from the bones are displayed here.
Hand Tracking
Because one of my learning goals is using hand tracking in VR and it is one of the core features of our concept we immediately started implementing it at the start of the project. I had already found good tutorials during the pressure cooker so I used these here as well.
Implementing hand tracking with Oculus integration is really easy, because you can use a few prefabs they give you in Unity. The OVRPlayerController contains all the objects that make the VR possible in the scene. The OVRCameraRig contains left and right hand anchors where you also put the OVRHandPrefab for each hand. In this prefab you can tweak a few things like how the hands are rendered and which hand is which.
What I learned from this is that it is really easy to get started with using hand tracking with the Oculus Quest (2).