Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. We have updated the language to the Editor Terms based on feedback from our employees and community. Learn more.
    Dismiss Notice
  3. Join us on November 16th, 2023, between 1 pm and 9 pm CET for Ask the Experts Online on Discord and on Unity Discussions.
    Dismiss Notice

Rigidbody 2D

Discussion in 'Scripting' started by TheAPGames, Nov 30, 2021.

  1. TheAPGames

    TheAPGames

    Joined:
    Jun 1, 2020
    Posts:
    44
    Hello,

    I have the below scripts, where upon the start of the game, the player's 2D rigidbody is set to kinematic, where it stays level until we press a button to activate it. The intention is for the endless runner game character to be still for like 3 seconds, then upon pressing a button, it activates.

    However, the player has to die in order for us to be able to control it.

    I have added : CurrentPlayableCharacters[0].GetComponent().isKinematic=true; in the protected virtual void Start(), and set it to false in the public virtual void LevelStart(). Now the public virtual void LevelStart() overrides the protected virtual void Start() function and the player immediately falls on the ground if no input is received.

    Thanks a lot



    1. Code (CSharp):
      1. using System.Collections;
      2. using System.Collections.Generic;
      3. using System.Linq;
      4. using System;
      5. using UnityEngine;
      6. using UnityEngine.SceneManagement;
      7. using MoreMountains.Tools;
      8. namespace MoreMountains.InfiniteRunnerEngine
      9. {  
      10.      /// <summary>
      11.      /// Spawns the player, and
      12.      /// </summary>
      13.      public class LevelManager : MMSingleton<LevelManager>
      14.      {      
      15.          public enum Controls { SingleButton, LeftRight, Swipe }
      16.          /// The current speed the level is traveling at
      17.          public float Speed { get; protected set; }  
      18.          /// The distance traveled since the start of the level
      19.          public float DistanceTraveled { get; protected set; }
      20.          /// the prefab you want for your player
      21.          [Header("Prefabs")]
      22.          public GameObject StartingPosition;
      23.          /// the list of playable characters - use this to tell what characters you want in your level, don't access that at runtime
      24.          public List<PlayableCharacter> PlayableCharacters;
      25.          /// the list of playable characters currently instantiated in the game - use this to know what characters ARE currently in your level at runtime
      26.          public List<PlayableCharacter> CurrentPlayableCharacters { get; set; }
      27.          /// the x distance between each character
      28.          public float DistanceBetweenCharacters = 1f;
      29.          /// the elapsed time since the start of the level
      30.          public float RunningTime { get; protected set; }
      31.          /// the amount of points a player gets per second
      32.          public float PointsPerSecond = 20;
      33.          /// the text that will be shown (if not empty) at the start of the level
      34.          [Multiline]
      35.          public String InstructionsText;
      36.          [Space(10)]
      37.          [Header("Level Bounds")]
      38.          /// the line after which objects can be recycled
      39.          public Bounds RecycleBounds;
      40.          [Space(10)]
      41.          /// the line after which playable characters will die - leave it to zero if you don't want to use it
      42.          public Bounds DeathBounds;
      43.            
      44.          [Space(10)]
      45.          [Header("Speed")]
      46.          /// the initial speed of the level
      47.          public float InitialSpeed = 10f;
      48.          /// the maximum speed the level will run at
      49.          public float MaximumSpeed = 50f;
      50.          /// the acceleration (per second) at which the level will go from InitialSpeed to MaximumSpeed
      51.          public float SpeedAcceleration=1f;
      52.        
      53.          [Space(10)]
      54.          [Header("Intro and Outro durations")]
      55.          /// duration of the initial fade in
      56.          public float IntroFadeDuration=1f;
      57.          /// duration of the fade to black at the end of the level
      58.          public float OutroFadeDuration=1f;
      59.        
      60.        
      61.          [Space(10)]
      62.          [Header("Start")]
      63.          /// the duration (in seconds) of the initial countdown
      64.          public int StartCountdown;
      65.          /// the text displayed at the end of the countdown
      66.          public string StartText;
      67.          [Space(10)]
      68.          [Header("Mobile Controls")]
      69.          /// the mobile control scheme applied to this level
      70.          public Controls ControlScheme;
      71.          [Space(10)]
      72.          [Header("Life Lost")]
      73.          /// the effect we instantiate when a life is lost
      74.          public GameObject LifeLostExplosion;
      75.          // protected stuff
      76.          protected DateTime _started;
      77.          protected float _savedPoints;  
      78.          protected float _recycleX;
      79.          protected Bounds _tmpRecycleBounds;
      80.          protected bool _temporarySpeedFactorActive;
      81.          protected float _temporarySpeedFactor;
      82.          protected float _temporarySpeedFactorRemainingTime;
      83.          protected float _temporarySavedSpeed;
      84.            
      85.          /// <summary>
      86.          /// Initialization
      87.          /// </summary>
      88.          protected virtual void Start()
      89.          {
      90.              Speed = InitialSpeed;
      91.              DistanceTraveled = 0;
      92.              InstantiateCharacters();
      93.              ManageControlScheme();
      94.              CurrentPlayableCharacters[0].GetComponent<Rigidbody2D>().isKinematic=true;
      95.              // storage
      96.              _savedPoints =GameManager.Instance.Points;
      97.              _started = DateTime.UtcNow;
      98.              GameManager.Instance.SetStatus(GameManager.GameStatus.BeforeGameStart);
      99.              GameManager.Instance.SetPointsPerSecond(PointsPerSecond);
      100.              if (GUIManager.Instance != null)
      101.              {
      102.                  // set the level name in the GUI
      103.                  GUIManager.Instance.SetLevelName(SceneManager.GetActiveScene().name);      
      104.                  // fade in
      105.                  GUIManager.Instance.FaderOn(false,IntroFadeDuration);
      106.                  CurrentPlayableCharacters[0].GetComponent<Rigidbody2D>().isKinematic = true;
      107.              }
      108.              PrepareStart();
      109.          }
      110.        
      111.          /// <summary>
      112.          /// Handles everything before the actual start of the game.
      113.          /// </summary>
      114.          protected virtual void PrepareStart()
      115.          {      
      116.              //if we're supposed to show a countdown we schedule it, otherwise we just start the level
      117.              if (StartCountdown>0)
      118.              {
      119.                  GameManager.Instance.SetStatus(GameManager.GameStatus.BeforeGameStart);
      120.                  StartCoroutine(PrepareStartCountdown());  
      121.              }  
      122.              else
      123.              {
      124.                  LevelStart();
      125.              }  
      126.          }
      127.        
      128.          /// <summary>
      129.          /// Handles the initial start countdown display
      130.          /// </summary>
      131.          /// <returns>The start countdown.</returns>
      132.          protected virtual IEnumerator PrepareStartCountdown()
      133.          {
      134.              int countdown = StartCountdown;      
      135.              GUIManager.Instance.SetCountdownActive(true);
      136.            
      137.              // while the countdown is active, we display the current value, and wait for a second and show the next
      138.              while (countdown > 0)
      139.              {
      140.                  if (GUIManager.Instance.CountdownText!=null)
      141.                  {
      142.                      GUIManager.Instance.SetCountdownText(countdown.ToString());
      143.                  }
      144.                  countdown--;
      145.                  yield return new WaitForSeconds(1f);
      146.              }
      147.            
      148.              // when the countdown reaches 0, and if we have a start message, we display it
      149.              if ((countdown==0) && (StartText!=""))
      150.              {
      151.                  GUIManager.Instance.SetCountdownText(StartText);
      152.                  yield return new WaitForSeconds(1f);
      153.              }
      154.            
      155.              // we turn the countdown inactive, and start the level
      156.              GUIManager.Instance.SetCountdownActive(false);
      157.              LevelStart();
      158.          }
      159.          /// <summary>
      160.          /// Handles the start of the level : starts the autoincrementation of the score, sets the proper status and triggers the corresponding event.
      161.          /// </summary>
      162.          public virtual void LevelStart()
      163.          {
      164.              GameManager.Instance.SetStatus(GameManager.GameStatus.GameInProgress);
      165.              GameManager.Instance.AutoIncrementScore(true);
      166.              MMEventManager.TriggerEvent(new MMGameEvent("GameStart"));
      167.              CurrentPlayableCharacters[0].GetComponent<Rigidbody2D>().isKinematic = false;
      168.          }
      169.          /// <summary>
      170.          /// Instantiates all the playable characters and feeds them to the gameManager
      171.          /// </summary>
      172.          protected virtual void InstantiateCharacters()
      173.          {
      174.              CurrentPlayableCharacters = new List<PlayableCharacter>();
      175.              /// we go through the list of playable characters and instantiate them while adding them to the list we'll use from any class to access the
      176.              /// currently playable characters
      177.              // we check if there's a stored character in the game manager we should instantiate
      178.              if (CharacterSelectorManager.Instance.StoredCharacter != null)
      179.              {
      180.                  PlayableCharacter newPlayer = (PlayableCharacter)Instantiate(CharacterSelectorManager.Instance.StoredCharacter, StartingPosition.transform.position, StartingPosition.transform.rotation);
      181.                  newPlayer.name = CharacterSelectorManager.Instance.StoredCharacter.name;
      182.                  newPlayer.SetInitialPosition(newPlayer.transform.position);
      183.                  CurrentPlayableCharacters.Add(newPlayer);
      184.                  MMEventManager.TriggerEvent(new MMGameEvent("PlayableCharactersInstantiated"));
      185.                  return;
      186.              }
      187.              if (PlayableCharacters == null)
      188.              {
      189.                  return;
      190.              }
      191.              if (PlayableCharacters.Count==0)
      192.              {
      193.                  return;
      194.              }
      195.              // for each character in the PlayableCharacters list
      196.              for (int i = 0; i < PlayableCharacters.Count; i++)
      197.              {
      198.                  // we instantiate the corresponding prefab
      199.                  PlayableCharacter instance = (PlayableCharacter)Instantiate(PlayableCharacters[i]);          
      200.                  // we position it based on the StartingPosition point
      201.                  instance.transform.position = new Vector3(StartingPosition.transform.position.x + i * DistanceBetweenCharacters, StartingPosition.transform.position.y, StartingPosition.transform.position.z);
      202.                  // we set manually its initial position
      203.                  instance.SetInitialPosition(instance.transform.position);
      204.                  // we feed it to the game manager
      205.                  CurrentPlayableCharacters.Add(instance);
      206.              }
      207.              MMEventManager.TriggerEvent(new MMGameEvent("PlayableCharactersInstantiated"));
      208.          }
      209.          /// <summary>
      210.          /// Resets the level : repops dead characters, sets everything up for a new game
      211.          /// </summary>
      212.          public virtual void ResetLevel()
      213.          {
      214.              InstantiateCharacters();
      215.              PrepareStart();
      216.          }
      217.          /// <summary>
      218.          /// Turns buttons on or off depending on the chosen mobile control scheme
      219.          /// </summary>
      220.          protected virtual void ManageControlScheme()
      221.          {
      222.              String buttonPath = "";
      223.              switch (ControlScheme)
      224.              {
      225.                  case Controls.SingleButton:
      226.                      buttonPath = "Canvas/MainActionButton";
      227.                      if (GUIManager.Instance == null) { return; }
      228.                      if (GUIManager.Instance.transform.Find(buttonPath) == null) { return; }
      229.                      GUIManager.Instance.transform.Find(buttonPath).gameObject.SetActive(true);
      230.                      break;
      231.                  case Controls.LeftRight:
      232.                      buttonPath = "Canvas/LeftRight";
      233.                      if (GUIManager.Instance == null) { return; }
      234.                      if (GUIManager.Instance.transform.Find(buttonPath) == null) { return; }
      235.                      GUIManager.Instance.transform.Find(buttonPath).gameObject.SetActive(true);
      236.                      break;
      237.                  case Controls.Swipe:
      238.                      buttonPath = "Canvas/SwipeZone";
      239.                      if (GUIManager.Instance == null) { return; }
      240.                      if (GUIManager.Instance.transform.Find(buttonPath) == null) { return; }
      241.                      GUIManager.Instance.transform.Find(buttonPath).gameObject.SetActive(true);
      242.                      break;
      243.              }
      244.          }
      245.          /// <summary>
      246.          /// Every frame
      247.          /// </summary>
      248.          public virtual void Update()
      249.          {
      250.              _savedPoints = GameManager.Instance.Points;
      251.              _started = DateTime.UtcNow;
      252.              // we increment the total distance traveled so far
      253.              DistanceTraveled = DistanceTraveled + Speed * Time.fixedDeltaTime;
      254.            
      255.              // if we can still accelerate, we apply the level's speed acceleration
      256.              if (Speed<MaximumSpeed)
      257.              {
      258.                  Speed += SpeedAcceleration * Time.deltaTime;
      259.              }
      260.              HandleSpeedFactor ();
      261.              RunningTime+=Time.deltaTime;
      262.          }
      263.        
      264.          /// <summary>
      265.          /// Sets the speed.
      266.          /// </summary>
      267.          /// <param name="newSpeed">New speed.</param>
      268.          public virtual void SetSpeed(float newSpeed)
      269.          {
      270.              Speed = newSpeed;
      271.          }
      272.        
      273.          /// <summary>
      274.          /// Adds speed to the current level speed
      275.          /// </summary>
      276.          /// <param name="speedAdded">Speed added.</param>
      277.          public virtual void AddSpeed(float speedAdded)
      278.          {
      279.              Speed += speedAdded;
      280.          }
      281.          /// <summary>
      282.          /// Temporarily multiplies the level speed by the provided factor
      283.          /// </summary>
      284.          /// <param name="factor">The number of times you want to increase/decrease the speed by.</param>
      285.          /// <param name="duration">The duration of the speed change, in seconds.</param>
      286.          public virtual void TemporarilyMultiplySpeed(float factor, float duration)
      287.          {
      288.              _temporarySpeedFactor = factor;
      289.              _temporarySpeedFactorRemainingTime = duration;
      290.              if (!_temporarySpeedFactorActive)
      291.              {
      292.                  _temporarySavedSpeed = Speed;
      293.              }
      294.              Speed = _temporarySavedSpeed * _temporarySpeedFactor;
      295.              _temporarySpeedFactorActive = true;
      296.          }
      297.          /// <summary>
      298.          /// Called every frame, this modified the current level speed if we're under the effect of a speed factor
      299.          /// </summary>
      300.          protected virtual void HandleSpeedFactor()
      301.          {
      302.              if (_temporarySpeedFactorActive)
      303.              {
      304.                  if (_temporarySpeedFactorRemainingTime <= 0)
      305.                  {
      306.                      _temporarySpeedFactorActive = false;
      307.                      Speed = _temporarySavedSpeed;
      308.                  }
      309.                  else
      310.                  {
      311.                      _temporarySpeedFactorRemainingTime -= Time.deltaTime;
      312.                  }
      313.              }
      314.          }
      315.          /// <summary>
      316.          /// Determines if the object whose bounds are passed as a parameter has to be recycled or not.
      317.          /// </summary>
      318.          /// <returns><c>true</c>, if the object has to be recycled, <c>false</c> otherwise.</returns>
      319.          /// <param name="objectBounds">Object bounds.</param>
      320.          /// <param name="destroyDistance">The x distance after which the object will get destroyed.</param>
      321.          public virtual bool CheckRecycleCondition(Bounds objectBounds,float destroyDistance)
      322.          {
      323.              _tmpRecycleBounds = RecycleBounds;
      324.              _tmpRecycleBounds.extents+=Vector3.one * destroyDistance;
      325.              if (objectBounds.Intersects(_tmpRecycleBounds))
      326.              {
      327.                  return false;
      328.              }
      329.              else
      330.              {
      331.                  return true;
      332.              }
      333.          }
      334.          public virtual bool CheckDeathCondition(Bounds objectBounds)
      335.          {
      336.              if (objectBounds.Intersects(DeathBounds))
      337.              {
      338.                  return false;
      339.              }
      340.              else
      341.              {
      342.                  return true;
      343.              }
      344.          }
      345.        
      346.          /// <summary>
      347.          /// Gets the player to the specified level
      348.          /// </summary>
      349.          /// <param name="levelName">Level name.</param>
      350.          public virtual void GotoLevel(string levelName)
      351.          {      
      352.              GUIManager.Instance.FaderOn(true,OutroFadeDuration);
      353.              StartCoroutine(GotoLevelCo(levelName));
      354.          }
      355.        
      356.          /// <summary>
      357.          /// Waits for a short time and then loads the specified level
      358.          /// </summary>
      359.          /// <returns>The level co.</returns>
      360.          /// <param name="levelName">Level name.</param>
      361.          protected virtual IEnumerator GotoLevelCo(string levelName)
      362.          {
      363.              if (Time.timeScale > 0.0f)
      364.              {
      365.                  yield return new WaitForSeconds(OutroFadeDuration);
      366.              }
      367.              GameManager.Instance.UnPause();
      368.              if (string.IsNullOrEmpty(levelName))
      369.              {
      370.                  MMSceneLoadingManager.LoadScene("StartScreen");
      371.              }
      372.              else
      373.              {
      374.                  MMSceneLoadingManager.LoadScene(levelName);
      375.              }
      376.            
      377.          }
      378.        
      379.          /// <summary>
      380.          /// Triggered when all lives are lost and you press the main action button
      381.          /// </summary>
      382.          public virtual void GameOverAction()
      383.          {
      384.              GameManager.Instance.UnPause();
      385.              GotoLevel(SceneManager.GetActiveScene().name);
      386.          }
      387.          /// <summary>
      388.          /// Triggered when a life is lost and you press the main action button
      389.          /// </summary>
      390.          public virtual void LifeLostAction()
      391.          {  
      392.              ResetLevel();
      393.          }
      394.        
      395.          /// <summary>
      396.          /// Kills the player.
      397.          /// </summary>
      398.          public virtual void KillCharacter(PlayableCharacter player)
      399.          {
      400.              StartCoroutine(KillCharacterCo(player));
      401.          }
      402.        
      403.          /// <summary>
      404.          /// Coroutine that kills the player, stops the camera, resets the points.
      405.          /// </summary>
      406.          /// <returns>The player co.</returns>
      407.          protected virtual IEnumerator KillCharacterCo(PlayableCharacter player)
      408.          {
      409.              LevelManager.Instance.CurrentPlayableCharacters.Remove(player);
      410.              player.Die();
      411.              //yield return new WaitForSeconds(0.5f);
      412.              yield return new WaitForSeconds(0f);
      413.                        
      414.              // if this was the last character, we trigger the all characters are dead coroutine
      415.              if (LevelManager.Instance.CurrentPlayableCharacters.Count==0)
      416.              {
      417.                  AllCharactersAreDead();
      418.              }
      419.                    
      420.          }
      421.        
      422.          /// <summary>
      423.          /// What happens when all characters are dead (or when the character is dead if you only have one)
      424.          /// </summary>
      425.          protected virtual void AllCharactersAreDead()
      426.          {
      427.              // if we've specified an effect for when a life is lost, we instantiate it at the camera's position
      428.              if (LifeLostExplosion != null)
      429.              {
      430.                  GameObject explosion = Instantiate(LifeLostExplosion);
      431.                  explosion.transform.position = new Vector3(Camera.main.transform.position.x, Camera.main.transform.position.y,0) ;
      432.              }
      433.              // we've just lost a life
      434.              GameManager.Instance.SetStatus(GameManager.GameStatus.LifeLost);
      435.              MMEventManager.TriggerEvent(new MMGameEvent("LifeLost"));
      436.              _started = DateTime.UtcNow;
      437.              GameManager.Instance.SetPoints(_savedPoints);
      438.              GameManager.Instance.LoseLives(1);
      439.              if (GameManager.Instance.CurrentLives<=0)
      440.              {
      441.                  GUIManager.Instance.SetGameOverScreen(true);
      442.                  GameManager.Instance.SetStatus(GameManager.GameStatus.GameOver);
      443.                  MMEventManager.TriggerEvent(new MMGameEvent("GameOver"));
      444.              }
      445.          }
      446.          /// <summary>
      447.          /// Override this if needed
      448.          /// </summary>
      449.          protected virtual void OnEnable()
      450.          {
      451.          }
      452.          /// <summary>
      453.          /// Override this if needed
      454.          /// </summary>
      455.          protected virtual void OnDisable()
      456.          {
      457.            
      458.          }
      459.      }
      460. }
     
  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,946
    Line 100 tests the GUIManager instance... do you expect that to ever be null? If not, why are you testing it?

    And if it is null, that would cause line 106 to not fire, causing your problem.

    Generally you should not make systems needlessly dependent on each other like that. Making game logic dependent on a GUI would be a disaster if you ever changed out the GUI, such as making a TV controller or console GUI that was different.

    As for all that protected virtual inherited nonsense, I would highly suggest staying away from that construct with Unity messages (such as Start, Update, etc.) since a) no Unity developer I know expects those to be overloaded, and b) there is no automatic call mechanism in Unity that will call up to base. This also could cause base instance MonoBehaviours to fail in very mysterious ways, in fact EXACTLY like yours is failing.

    Regardless, you must find a way to get the information you need in order to reason about what the problem is.

    What is often happening in these cases is one of the following:

    - the code you think is executing is not actually executing at all
    - the code is executing far EARLIER or LATER than you think
    - the code is executing far LESS OFTEN than you think
    - the code is executing far MORE OFTEN than you think
    - the code is executing on another GameObject than you think it is

    To help gain more insight into your problem, I recommend liberally sprinkling Debug.Log() statements through your code to display information in realtime.

    Doing this should help you answer these types of questions:

    - is this code even running? which parts are running? how often does it run? what order does it run in?
    - what are the values of the variables involved? Are they initialized? Are the values reasonable?
    - are you meeting ALL the requirements to receive callbacks such as triggers / colliders (review the documentation)

    Knowing this information will help you reason about the behavior you are seeing.

    You can also put in Debug.Break() to pause the Editor when certain interesting pieces of code run, and then study the scene

    You could also just display various important quantities in UI Text elements to watch them change as you play the game.

    If you are running a mobile device you can also view the console output. Google for how on your particular mobile target.

    Another useful approach is to temporarily strip out everything besides what is necessary to prove your issue. This can simplify and isolate compounding effects of other items in your scene or prefab.

    Here's an example of putting in a laser-focused Debug.Log() and how that can save you a TON of time wallowing around speculating what might be going wrong:

    https://forum.unity.com/threads/coroutine-missing-hint-and-error.1103197/#post-7100494
     
    TheAPGames likes this.
  3. TheAPGames

    TheAPGames

    Joined:
    Jun 1, 2020
    Posts:
    44
    I appreciate your detailed response Kurt. I will have a closer look and update here. I'm very new with the whole C# and coding as I'm an artist, so its all very overwhelming.
     
  4. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,946
    Understood... I'm sure it can seem a bit mysterious and arbitrary at times!

    As you seem interested in the learning process, the main thing I suggest is just to try lots of little tutorials for all kinds of games, treating each one as a separate project, and come post questions here if you get too confused by it.

    When you are doing tutorials, keep this in mind to maximize your success, especially Step #2:

    How to do tutorials properly, two (2) simple steps to success:

    Tutorials are a GREAT idea. Tutorials should be used this way:

    Step 1. Follow the tutorial and do every single step of the tutorial 100% precisely the way it is shown. Even the slightest deviation (even a single character!) generally ends in disaster. That's how software engineering works. Every step must be taken, every single letter must be spelled, capitalized, punctuated and spaced (or not spaced) properly, literally NOTHING can be omitted or skipped.

    Fortunately this is the easiest part to get right: Be a robot. Don't make any mistakes.
    BE PERFECT IN EVERYTHING YOU DO HERE!!

    If you get any errors, learn how to read the error code and fix your error. Google is your friend here. Do NOT continue until you fix your error. Your error will probably be somewhere near the parenthesis numbers (line and character position) in the file. It is almost CERTAINLY your typo causing the error, so look again and fix it.

    Step 2. Go back and work through every part of the tutorial again, and this time explain it to your doggie. See how I am doing that in my avatar picture? If you have no dog, explain it to your house plant. If you are unable to explain any part of it, STOP. DO NOT PROCEED. Now go learn how that part works. Read the documentation on the functions involved. Go back to the tutorial and try to figure out WHY they did that. This is the part that takes a LOT of time when you are new. It might take days or weeks to work through a single 5-minute tutorial. Stick with it. You will learn.

    Step 2 is the part everybody seems to miss. Without Step 2 you are simply a code-typing monkey and outside of the specific tutorial you did, you will be completely lost. If you want to learn, you MUST do Step 2.

    Of course, all this presupposes no errors in the tutorial. For certain tutorial makers (like Unity, Brackeys, Imphenzia, Sebastian Lague) this is usually the case. For some other less-well-known content creators, this is less true. Read the comments on the video: did anyone have issues like you did? If there's an error, you will NEVER be the first guy to find it.

    Beyond that, Step 3, 4, 5 and 6 become easy because you already understand!
     
    TheAPGames likes this.