Announcement

Collapse
No announcement yet.

[Video] Mario Galaxy Gravity & Relative Point Gravity, Cling to Shapes With No Center

Collapse
X
 
  • Filter
  • Time
  • Show
Clear All
new posts

    [Video] Mario Galaxy Gravity & Relative Point Gravity, Cling to Shapes With No Center

    Dear Community,

    Here's another Tutorial with Code and Video for you!

    What is Point Gravity?

    Point gravity is like what you see in a large part of Mario Galaxy where the player unit moves relative to the surface of the object it is standing on as if that object is the only major source of gravity in the universe.

    In this Tutorial I give you the code for:

    1. Regular Point Gravity, where object moves along a completely convex surface which by definition always has a solid center. (0 to 3 minute of video)

    2. Relative Point Gravity (my term), where object can move along any kind of surface, and will always cling to the nearest surface.


    (Relative Point Gravity starts at 3:03 in video below)

    See video to really observe the awesome difference between these two types of Gravity!





    Algorithm Efficiency Seen Via Video
    If you look carefully around 5:50 you can see how the 361 traces dont always all run, sometimes only a few lines shoot out, because the loop tries to exit as early as possible, but when the ball gets really far then you see more traces going out from the ball.



    Convex Objects work with Regular Point Gravity

    As you can see in my video my Relative Point Gravity system works with ANY surface, including a spiral stair case with no center at all!

    ~~~

    Simulation of a True Magnetic Field

    My Relative Point Gravity System is simulating your player object having a true magnetic field and seeking out the nearest surface to cling to if it is range of the magnetic field.

    I say “true” magnetic field because my algorithm produces results as if calculating a full sphere around the object in all directions (it's much more efficient than that tho).

    ~~~

    Entire Code for Point Gravity

    Here's the entire code for regular Mario Galaxy Style Point Gravity using a Kactor.

    If you are using a pawn you will need to set the pawn physics to PHYS_RigidBody while using the point gravity, and manually play movement animations.

    Code:
    Pawn.SetPhysics(PHYS_RigidBody);
    //play move animations directly in code
    I kept this code to only the code that you would want to add to your own player pawn/Kactor class and did not include jumping because getting basics to work first is far more important. Once you understand this code adding jumping is easy.

    I comment out any code that I included more for your own information and use, code that is useful but not used in this sample below.

    Code:
    //could put this code in a pawn class or Kactor class
    class KactorPawnOBJ extends KactorSpawnable placeable;
    
    //var float playerOBJRadius;
    //var float playerOBJHeight;
    
    var Actor mostRecentTouchedActor;
    
    //~~~ Magnet Mode ~~~
    var bool magnetModeOn;
    var Actor gravitySource;
    var float gravityPower;
    var float magnetModeDisengageDistance;
    
    //Delta Time Adjustment
    var float deltaTimePhyiscsBoostMultiplier;
    
    Simulated Event PostBeginPlay() {
    	
    	super.postbeginplay();
    	
    	//set gameinfo, this is my game info
    	//enables me to access the instance of this class
    	//after game starts 
    	CustomGameInfo(WorldInfo.Game).playerOBJ = Self;
    	
    	//call the below again any time you change/update size of this Object	
    	//retrieves bounding info and puts in these two vars 
    	//Self.GetBoundingCylinder(playerOBJHeight, playerOBJRadius);
    	
    	//Ensure RigidBodyCollision() gets called
    	//assumes relevant collision impact velocities will be
    	//way bigger than 1 (relative velocity)
    	Self.StaticMeshComponent.ScriptRigidBodyCollisionThreshold = 1;
    }
    
    
    function clearTimers() {
    	cleartimer('magnetAcquireTimer');
    	mostRecentTouchedActor = none;
    }
    
    function magnetAcquireTimer() {
    	
    	if (gravitySource != mostRecentTouchedActor) {
    		mostRecentTouchedActor = none;
    	}
    }
    
    event RigidBodyCollision (	PrimitiveComponent HitComponent, 
    	PrimitiveComponent OtherComponent, 
    	const out CollisionImpactData RigidCollisionData, 
    	int ContactIndex) 
    {
    	//local RigidBodyContactInfo r;
    	
    	//set most recently touched actor;
    	mostRecentTouchedActor = OtherComponent.owner;
    	
    	//~~~
    	
    	//player only has a 0.3 second window to 
    	//activate magnet mode with a button press
    	//after physically connecting with object surface
    	
    	//I use this for interesting gameplay purposes
    	//SetTimer(0.3, false, 'magnetAcquireTimer');
    	
    	
    	//for your own use and fyi
    	
    	//~~~~~~ Get RB Collision Data ~~~~~~
    	//r = RigidCollisionData.ContactInfos[0];
    	//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    	
    	
    	//r.contactPosition = point of contact
    	//r.contactNormal   = the normal to surface at point of contact
    }
    
    //=================
    //MAGNET MODE
    //=====================
    function setMagnetMode(bool b) {
    	
    	if (mostRecentTouchedActor == none) {
    		magnetModeOn = false;
    		return;
    	}
    	
    	magnetModeOn = b;
    	
    	if (b) {
    		
    	  //disable regular gravity
    	  ( Self.StaticMeshComponent.GetRootBodyInstance() ).CustomGravityFactor = 0;
    		
    	  gravitySource = mostRecentTouchedActor;
    	}
    	
    	//turn magnet mode off
    	else {
    		
    	  //enable regular gravity
    	  ( Self.StaticMeshComponent.GetRootBodyInstance() ).CustomGravityFactor = 1;
    		
    	  mostRecentTouchedActor = none;
    	  gravitySource = none;
    	}
    }
    function applyMagnetModeGravity() {
    	
    	if (!magnetModeOn) return;
    	
    	/*
    	//if you want object to disengage
    	//after certain distance
    	//using point gravity
    	
    	//if ball too far now, turn off magnet mode
    	if (vsize(gravitySource.Location - Location) > magnetModeDisengageDistance) {
    		setMagnetMode(false);
    		return;
    	}*/
    	
    	//apply custom gravity
    	StaticMeshComponent.AddImpulse(
    	  gravityPower * deltaTimePhyiscsBoostMultiplier * 
    	  Normal(gravitySource.Location - Location )
    	);
    	
    }
    
    //runs every moment of game time
    //be careful how much stuff you put here
    Simulated Function tick( Float DeltaTime ) {
    	
    	super.tick(deltatime);
    	
    	//======================
    	//MAGNET MODE
    	//======================
    	applyMagnetModeGravity();
    	
    	//my computer averages 0.016 without fraps running
    	//use this to account for time dilation.
    	deltaTimePhyiscsBoostMultiplier = deltatime * 40;
    }
    
    defaultproperties
    {
    	//I set the mesh in the UDK
    	//you could put a static mesh here
    	//if you liked
    	
    	//to use as is:
    	//go into UDK editor and drag in
    	//this class, and then press f4
    	//do a filter for static mesh
    	//and enter a static mesh of your
    	//choice into the appropriate fields
    	
    	//this class contains all the right code
    	//to add to your pawn/kactor class as is.
    	//except the settng of PHYS_RigidBody
    	
    	bCollideActors = true
    	gravityPower = 570
    	magnetModeDisengageDistance = 700;
    }
    ~~~

    What is a Surface Normal?

    The normals you see everywhere in this code are the surface normals, lines that are perpendicular to the surface at the point where the trace hits the surface.

    The trace is like a projectile, and where the trace hits a surface, a line perpendicular to the surface at that point is the surface normal.

    ~~~

    Point Gravity and the Vector Normal() Function

    By contrast, the vector function Normal() reduces any vector to a unit vector (vect(1,1,1)).

    This is the core of how Point Gravity is able to work!
    Code:
    StaticMeshComponent.AddImpulse(
    	  gravityPower * deltaTimePhyiscsBoostMultiplier * 
    	  Normal(gravitySource.Location - Location )
    );
    I use Normal() in this code so I have the direction of the vector (preserved with Normal()) but I can exactly determine the length of the line without the length of the pre-normalized vector getting into the way of my exact calculations.

    So the above code says
    “Add a physics impulse to the player Object of size GravityPower*deltaTimePhyiscsBoostMultiplier IN THE DIRECTION OF the line between the player Object and the gravity Source.”

    In other words:
    “move the player object toward the gravitySource by this exact amount of GravityPower*deltaTimePhyiscsBoostMultiplier.”

    If I did not normalize the vector, the actual distance between the player obj and the gravitySource would be included in the total impulse force, and this distance is variable and could be quite large.

    But without that vector I do not have the exact line/angle between the player obj and the gravitySource.

    So normal() is your friend!

    Normal() enables point gravity to be accurate and consisent no matter where exactly the player OBJ is relative to the center of the chosen gravitySource.

    ~~~

    Entire Code For My True Magnetic Field System
    Relative Point Gravity Code

    And here it is!



    Code:
    var Actor mostRecentTouchedActor;
    
    //~~~ Magnet Mode ~~~
    var bool magnetModeOn;
    var Actor gravitySource;
    var float gravityPower;
    var float magneticFieldSize;
    var float magnetCurDistToSurface;
    var float magnetCloseEnoughRange;
    var vector magnetSurfaceNormal;
    
    //Delta Time Adjustment
    var float deltaTimePhyiscsBoostMultiplier;
    
    Simulated Event PostBeginPlay() {
    	
    	super.postbeginplay();
    	
    	//set gameinfo, this is my game info
    	//enables me to access the instance of this class
    	//after game starts 
    	CustomGameInfo(WorldInfo.Game).playerOBJ = Self;
    	
    	//see comment on this line in code above
    	//it's important
    	Self.StaticMeshComponent.ScriptRigidBodyCollisionThreshold = 1;
    }
    
    //=======
    //EVENTS
    //=========
    
    function clearTimers() {
    	cleartimer('magnetAcquireTimer');
    	mostRecentTouchedActor = none;
    }
    
    function magnetAcquireTimer() {
    	
    	if (gravitySource != mostRecentTouchedActor) {
    	  mostRecentTouchedActor = none;
    	}
    	
    }
    
    event RigidBodyCollision (	PrimitiveComponent HitComponent, 
    	PrimitiveComponent OtherComponent, 
    	const out CollisionImpactData RigidCollisionData, 
    	int ContactIndex) 
    {
    	
    	//set most recently touched actor;
    	mostRecentTouchedActor = OtherComponent.owner;
    	
    	//see comment in code sample above
    	//SetTimer(0.3, false, 'magnetAcquireTimer');
    }
    
    //=================
    //MAGNET MODE
    //=====================
    function setMagnetMode(bool b) {
    	
    	if (mostRecentTouchedActor == none) {
    	  magnetModeOn = false;
    	  return;
    	}
    	
    	magnetModeOn = b;
    	
    	if (b) {
    		
    	  //disable regular gravity
    	  ( Self.StaticMeshComponent.GetRootBodyInstance() ).CustomGravityFactor = 0;
    		
    	  gravitySource = mostRecentTouchedActor;
    		
    	}
    	
    	//turn magnet mode off
    	else {
    		
    	  //enable regular gravity
    	  ( Self.StaticMeshComponent.GetRootBodyInstance() ).CustomGravityFactor = 1;
    		
    	  mostRecentTouchedActor = none;
    	  gravitySource = none;
    			
    	}
    }
    
    function acquireClosestSurface() {
    	local vector hitLoc;
    	local actor a;
    	local float minDistToSurface;
    	local vector bestNormal;
    	local int v;
    		
    	//initialize minimum
    	minDistToSurface = 100000000;
    	
    	//do a single trace from ball to center
    	//of gravitySource for point gravity case
    	a = trace( hitLoc, magnetSurfaceNormal,
    	  Location + Normal(gravitySource.Location - Location ) * magneticFieldSize, 
    	  Location);
    		
    	//traced gravity source?
    	if (a == gravitySource){
    				
    	  //dist to traced surface
    	  magnetCurDistToSurface = vsize(hitLoc - Location);
    		
    	  //Close Enough Range
    	  if (magnetCurDistToSurface <= magnetCloseEnoughRange) {
    	    //done happily, 
    	    //magnetSurfaceNormal & magnetCurDistToSurface are global
    	    return;
    	 }
    		
    	//not yet sure of closest/close enough trace
    	else {
    	   minDistToSurface= magnetCurDistToSurface;
    	    bestNormal       = magnetSurfaceNormal;
    	  }
    	}
    	
    	//Do a sphere of traces from ball to environment
    	//*** simulating the ball having a gravitational field ***
    	for (v = 0; v < 361; v++ ) {
    		
    	  a = trace(  hitLoc, magnetSurfaceNormal,
    	  Location + VRand() * magneticFieldSize, 
    	  Location);
    		
    	//skip if traced nothing or other object besides
    	//original gravitySource
    	if (a != gravitySource) continue;
    		
    	//dist to traced surface
    	magnetCurDistToSurface = vsize(hitLoc - Location);
    		
    	//testing
    	//DrawDebugLine(Location, Location + VRand() * magneticFieldSize, 0, 0, 0);
    	//`log("dist to surface:"@magnetCurDistToSurface);
    		
    	//Close Enough Range
    	if (magnetCurDistToSurface <= magnetCloseEnoughRange) {
    	  //done happily, 
    	   //magnetSurfaceNormal & magnetCurDistToSurface are global
    	   //and were filled during cur loop iteration
    	   return;
    	}
    		
    	//not Close Enough
    	//check what cur min is
    	   else if(magnetCurDistToSurface < minDistToSurface ){
    			
                 minDistToSurface= magnetCurDistToSurface;
    	     bestNormal 	     = magnetSurfaceNormal;
    	  }
    	} //end for loop
    	
    	//output, global variables
    	magnetSurfaceNormal     = bestNormal;
    	magnetCurDistToSurface = minDistToSurface;
    }
    
    function applyMagnetModeGravity() {
    	
    	if (!magnetModeOn) return;
    	
    	if (gravitySource == none) return;
    	
    	//re-initialize to 0 in case no traces hit
    	//result is that no gravity is applied
    	//if no trace hit gravitySource.
    	magnetSurfaceNormal = vect(0, 0, 0);
    	acquireClosestSurface();
    	
    	//no longer in magnetic field range
    	//turn off magnet mode
    	if( magnetCurDistToSurface > magneticFieldSize  ){
    	  setMagnetMode(false);
    	  return;
    	}
    	
    	//apply relative point gravity
    	StaticMeshComponent.AddImpulse(
    	  gravityPower * deltaTimePhyiscsBoostMultiplier * 
    	  -1 * magnetSurfaceNormal
    	);
    }
    
    
    
    Simulated Function tick( Float DeltaTime ) {
    	
    	super.tick(deltatime);
    	
    	//======================
    	//MAGNET MODE
    	//======================
    	applyMagnetModeGravity();
    	
    	//averages 0.016 without fraps
    	deltaTimePhyiscsBoostMultiplier = deltatime * 40;
    }
    
    defaultproperties
    {
    	
    	bCollideActors = true
    
    	gravityPower                       = 570
    	magneticFieldSize                = 700
    	magnetCloseEnoughRange    = 160
    	
    	deltaTimePhyiscsBoostMultiplier = 2
    }
    Here's the core function of this entire class code

    This is the code that generates the True Magnetic Field around the player Object.

    Code:
    function acquireClosestSurface() {
    	local vector hitLoc;
    	local actor a;
    	local float minDistToSurface;
    	local vector bestNormal;
    	local int v;
    		
    	//initialize minimum
    	minDistToSurface = 100000000;
    	
    	//do a single trace from ball to center
    	//of gravitySource for point gravity case
    	a = trace( hitLoc, magnetSurfaceNormal,
    	  Location + Normal(gravitySource.Location - Location ) * magneticFieldSize, 
    	  Location);
    		
    	//traced gravity source?
    	if (a == gravitySource){
    				
    	  //dist to traced surface
    	  magnetCurDistToSurface = vsize(hitLoc - Location);
    		
    	  //Close Enough Range
    	  if (magnetCurDistToSurface <= magnetCloseEnoughRange) {
    	    //done happily, 
    	    //magnetSurfaceNormal & magnetCurDistToSurface are global
    	    return;
    	 }
    		
    	//not yet sure of closest/close enough trace
    	else {
    	   minDistToSurface= magnetCurDistToSurface;
    	    bestNormal       = magnetSurfaceNormal;
    	  }
    	}
    	
    	//Do a sphere of traces from ball to environment
    	//*** simulating the ball having a gravitational field ***
    	for (v = 0; v < 361; v++ ) {
    		
    	  a = trace(  hitLoc, magnetSurfaceNormal,
    	  Location + VRand() * magneticFieldSize, 
    	  Location);
    		
    	//skip if traced nothing or other object besides
    	//original gravitySource
    	if (a != gravitySource) continue;
    		
    	//dist to traced surface
    	magnetCurDistToSurface = vsize(hitLoc - Location);
    		
    	//testing
    	//DrawDebugLine(Location, Location + VRand() * magneticFieldSize, 0, 0, 0);
    	//`log("dist to surface:"@magnetCurDistToSurface);
    		
    	//Close Enough Range
    	if (magnetCurDistToSurface <= magnetCloseEnoughRange) {
    	  //done happily, 
    	   //magnetSurfaceNormal & magnetCurDistToSurface are global
    	   //and were filled during cur loop iteration
    	   return;
    	}
    		
    	//not Close Enough
    	//check what cur min is
    	   else if(magnetCurDistToSurface < minDistToSurface ){
    			
                 minDistToSurface= magnetCurDistToSurface;
    	     bestNormal 	     = magnetSurfaceNormal;
    	  }
    	} //end for loop
    	
    	//output, global variables
    	magnetSurfaceNormal     = bestNormal;
    	magnetCurDistToSurface = minDistToSurface;
    }
    Understanding the Code

    Basically this code runs every moment of game time via the tick, and:

    1. A single trace is sent to center of current gravitySource object for Point Gravity case

    2. 361 traces are sent from the Player Object center in a sphere with radius of magneticFieldSize

    3. The loop tries as hard as it can to stop before all 361 traces are run, by finding a single match for a distance to a surface of size MagnetCloseEnoughRange. Loop also will not even run AT ALL if the point gravity case passes within MagnetCloseEnoughRange.

    You can see the efficiency case very visually in the video when the traces are being drawn as black lines

    If the loop of 361 is forced to complete all the way, the normal for the minimum distance traced / the closest surface is used, to ensure the proper behavior of a magnetically attractive object finding the nearest surface.

    4. Once the ideal normal from the surface traces is found, it is applied as the gravitational force, with -1 because we are flipping the normal around so ball moves toward the surface rather than away from it.

    Code:
    StaticMeshComponent.AddImpulse(
    	  gravityPower * deltaTimePhyiscsBoostMultiplier * 
    	  -1 * magnetSurfaceNormal
    );
    End Result: The Player Object is always magnetically attracted to the nearest surface within its magnetic field range!

    ~~~

    Summary

    With this code I am providing to you you can either implement basic Point Gravity or my True Magnetic Field gravity system!

    Have fun!

    Rama

    PS: if you are worried about performance, you could put the loop of 361 traces into a timer that runs every 0.3 seconds or so.

    I recommend to first get the current code working before using the timer to make things efficient.

    #2
    Another excellent thread. You sir, are amazing.

    Comment


      #3
      That is awesome.

      Comment


        #4
        Now get a skeletal mesh working as expected; rigid body behaviour is easy, it's getting animation to behave correctly that's the real issue with this kind of implementation

        Also, why 361 traces? This sounds rather excessive?

        Comment


          #5
          Originally posted by skwisdemon666 View Post
          Another excellent thread. You sir, are amazing.
          aww thanks

          Rama

          Comment


            #6
            Originally posted by ambershee View Post
            Now get a skeletal mesh working as expected; rigid body behaviour is easy, it's getting animation to behave correctly that's the real issue with this kind of implementation

            Also, why 361 traces? This sounds rather excessive?
            why dont you do that?

            My game is designed around using a KActor deliberately, it's fun to roll around

            Additionally the main focus of this tutorial is my true magnetic field / relative point gravity system (riding up the underside of a spiral stair case with no center for ex: )

            adapting this to pawn + skeletal mesh will be much easier for anyone who wants to do it now that I've provided the simple core code.


            And if you want to use less traces feel free my dear Ambershee

            Users of this code can freely optimize it, I am presenting the raw core material to be fiddled with as you wish

            Please do post any optimization findings you discover!



            Rama

            These optimizations already in effect

            3. The loop tries as hard as it can to stop before all 361 traces are run, by finding a single match for a distance to a surface of size MagnetCloseEnoughRange. Loop also will not even run AT ALL if the point gravity case passes within MagnetCloseEnoughRange.

            You can see the efficiency cases very visually in the video when the traces are being drawn as black lines (5:50 in video)

            Also note I'm using VRand(), it's fast

            Comment


              #7
              Originally posted by evernewjoy View Post
              why dont you do that?
              I actually have done it - albeit quite some time ago. It's quite a challenge and I don't think anyone has it working nicely unfortunately.

              Originally posted by evernewjoy View Post
              And if you want to use less traces feel free my dear Ambershee
              In Super Mario Galaxy, they'd have done it via iterating through points at the center of each planet (hence 'point' gravity). If you're attracted to any surface, I would probably have used a collision mesh to return potential nearby surfaces, eliminating the need for a large number of traces trying to find one

              Comment


                #8
                Originally posted by ambershee View Post
                If you're attracted to any surface, I would probably have used a collision mesh to return potential nearby surfaces, eliminating the need for a large number of traces trying to find one
                That's a great idea!

                Thanks for sharing!



                Rama

                Comment


                  #9
                  This is some amazing stuff! Well done!

                  Comment


                    #10
                    Aww thanks!

                    by the way jlhill17, every time I see your signature I think to myself

                    "don't forget to set awake to true in your default properties so your loop runs at least once"



                    Rama

                    Comment


                      #11
                      How hard would it be to modify this so that it does this to a player vehicle instead of a kactor and also it never pulls the vehicle in all the way it just has them orbit. I would love to use this so that when my character is flying in space, when he gets close to a planet the planet pulls him into the planets orbit you know?

                      Comment


                        #12
                        Keeping them in orbit is very easy

                        I don't know much about vehicles, but it sounds like your vehicle, if it is in space, wont need to do complicated surface alignments like a vehicle with wheels would

                        so you might be just fun with the whole vehicle.setphysics(PHYS_RigidBody);

                        Please try that and if that goes well, keeping the vehicle in orbit will be easy

                        Rama

                        Comment


                          #13
                          If I'm not mistaken, vehicles already use rigid body physics.

                          Comment


                            #14
                            I'm causually browing this forum and I just wanted to say that you are awesome man. But please buy a new mic. All I canhear is S .. S ss SSS SSSSSSSS ss S Ss

                            Comment


                              #15
                              I agree with you Sailboat, the Sssss are rough!

                              It only does that with FRAPS though, its not my mic but somehow the way FRAPS is recording it so im not sure what to do yet.

                              thanks for the compliments!

                              Rama

                              Comment

                              Working...
                              X