Change a moving object's position to direct it somewhere inside a radius

Hi guys, I’m making my very own game and I’m permanently stuck. This whole game depends on this very system to work properly so I can’t have the game without it.

I’m making a 2D space game where the camera is facing along the Z-axis (the default set-up).
This game has randomly generated rocks that vary in shape, size, rotation, velocity and direction (the rocks and the spaceship uses Rigidbody2D).

My headache comes from trying to spawn the rocks:
1.) outside of the camera’s viewing radius,
AND
2.) at a position that guaranties that the rock, with its direction and speed + the spaceship’s [u]direction and speed, will enter a radius that is in the middle of the screen[/u].

It doesn’t matter if the rock will just barely be seen at the bottom of the screen for just a second, it just matters that it looks varied and natural that you don’t always hit directly at a rock every time a rock spawns.

I know there’s a possibility that the player changes direction and/or speed after a rock has been instantiated but it shouldn’t matter too much as long as the rock is instantiated as close to the camera’s viewing radius as possible.

It would be best to have the rocks spawn outside of the camera’s viewing radius but have their destinationRadius (most preferably a squared radius) to be a different radius that I can alter so I’ll have more control over the rock’s destination.

I just realized that I haven’t touched on from where the rocks will spawn. I’d like it to be random so that the rocks could enter from any direction. I can’t imagine it being an easy nor obvious task but as long as the result is varied then it’s enough.

I know this whole task is a huge headache but I hope this will be a fun challenge for you guys that want to test your whit at programming and especially vector mathematics.
This will both teach me and most importantly everyone that will read this thread ALLOT of thing.

I don’t expect a complete script at all, just enough to give me an idea of how this could be achieved.

I want this thread to be both easy for you to understand and digest, so I’m gonna write down all of the variables that I can think of for this script:

??? destinationRadius; //Spawned rock's desired destination radius
    float cameraSize; //Camera's orthographic size

    float rockSize; //Rock's randomly generated size
    Vector2 rockV; //Rock's random velocity
    Vector2 rockSpawnPos; //The position we spawn the rock to so that it ends up in the destinationRadius

    Vector2 spaceshipV; //spaceship's velocity

The script is attached to the Camera so the transform.position is at the middle of the screen.

The rocks have no drag, however, the spaceship DOES. I don’t think it affects the precision of the rigidbody2D.velocity but perhaps it does affect something.

I do know how to access the corners of my screen so don’t bother too much with it

(Vector2)transform.position + new Vector2(maxCameraSize * aspectRatio, maxCameraSize);

I do this instead of using ViewportToWorldPoint because the player can zoom in and out at will, so I always spawn outside of the maximum zoomed out size.

Here are some snapshots of the game to give you and idea of how this is set up:

I’ve written about a dozen asteroids games in my time. When I did my first one for Unity I basically identified the edges of the screen and identified an “extra unseen zone” beyond that.

Let’s say the screen is 640x480, therefore left and right is 0 and 640, and top and bottom is 0 and 480

If we choose 20% extra then the left becomes 0 - (640 * 0.2) = -128 and the right becomes 640 + 640 * 0.2 = 768, and same with top/bottom.

At that stage, you just watch the X and Y coordinates for them to go outside those bounds, and then you wrap them by the width of the limits (i.e., 768 + 128 == 896 horizontally and vertically it would be a smaller amount, 480 + 480 * 0.4 for instance. It is precisely how the original Asteroids game works except they have a very small edge fraction.

In fact, I do it here in my Rocket Raiders game:

ios is at ‎Rocket Raiders on the App Store

Android is at https://play.google.com/store/apps/details?id=com.plbm.roxx

Just played the game. How did you generate the stars in the background? The only thing I though of for my game was setting a plane behind the player and offsetting the texture to mach the player’s movement.

Let me correct my first answer: the camera in ROXX is slightly tilted back so the viewplane on y == 0 is a trapezoid, not rectangle. But the same behavior is going on. Here is the PlaneOfActionManager and the functions to wrap.

STATE.WorldSizePercentageAboveViewport is equal to 0.10f, so 10% on either side “overscan.”

The entire game takes place “flat,” in Y == 0, so Z is going “up screen” and X is left and right.

I’ll dig up the starfield code in a second. Next post.

/*
    The following license supersedes all notices in the source code.
*/

/*
    Copyright (c) 2015 Kurt Dekker/PLBM Games All rights reserved.

    http://www.twitter.com/kurtdekker
 
    Redistribution and use in source and binary forms, with or without
    modification, are permitted provided that the following conditions are
    met:
 
    Redistributions of source code must retain the above copyright notice,
    this list of conditions and the following disclaimer.
 
    Redistributions in binary form must reproduce the above copyright
    notice, this list of conditions and the following disclaimer in the
    documentation and/or other materials provided with the distribution.
 
    Neither the name of the Kurt Dekker/PLBM Games nor the names of its
    contributors may be used to endorse or promote products derived from
    this software without specific prior written permission.
 
    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
    IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
    TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
    PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
    HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
    SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
    TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
    PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
    LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
    NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
    SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

using UnityEngine;
using System.Collections;

public class PlaneOfActionManager : MonoBehaviour
{
    // right AT the corners of the screen
    static Vector3 v3UL, v3UR, v3LL, v3LR;
    // trapezoidal limits of the world
    public static Vector3 o3UL, o3UR, o3LL, o3LR;
    GameObject goUL, goUR, goLL, goLR;
    Plane p;
 
    public static Vector3 GenerateRandomEdgePosition()
    {
        Vector3 pos;
        pos = o3UL;
        switch( Random.Range(0,4))
        {
        default :
        case 0 :
            // across top edge of world
            pos = Vector3.Lerp(
                PlaneOfActionManager.o3UL,
                PlaneOfActionManager.o3UR,
                Random.Range( 0.0f, 1.0f));
            break;
        case 1 :
            // across right edge of world
            pos = Vector3.Lerp(
                PlaneOfActionManager.o3UR,
                PlaneOfActionManager.o3LR,
                Random.Range( 0.0f, 1.0f));
            break;
        case 2:
            // across bottom edge of world
            pos = Vector3.Lerp(
                PlaneOfActionManager.o3LL,
                PlaneOfActionManager.o3LR,
                Random.Range( 0.0f, 1.0f));
            break;
        case 3 :
            // across right edge of world
            pos = Vector3.Lerp(
                PlaneOfActionManager.o3UL,
                PlaneOfActionManager.o3LL,
                Random.Range( 0.0f, 1.0f));
            break;
        }
        return pos;
    }
 
    public static Vector3 v3Center()
    {
        return Vector3.Lerp(
                            Vector3.Lerp( v3UL, v3UR, 0.5f),
                              Vector3.Lerp( v3LL, v3LR, 0.5f),
                            0.5f
                        );
    }
 
    void CastToPlane( Vector3 viewport, out Vector3 hitpoint)
    {
        float distance;
        // cast to the actual mathematical y = 0 plane
        p.Raycast( Camera.main.ViewportPointToRay(
            new Vector3( 0.5f, viewport.y, 0)),
            out distance);

        // distance now must be adjusted down by our FOV angle,
        // since the distance to the plane at edges is different (fan of fov)

// this is pure cosine in the Y direction, relying on the p.Raycast
// above being (0,viewport.y,0)... it is not right for X != 0 (corners)
        distance *= Mathf.Cos( (Mathf.PI * 0.5f * Camera.main.fieldOfView) / 180.0f);

        // Now distance is axial to the camera's direction, let's project!
        hitpoint = Camera.main.ViewportToWorldPoint(
            new Vector3( viewport.x, viewport.y, distance));
     
        // fuck it, I'm hammering it in... close enough for gummint work!
        hitpoint.y = 0;
    }
 
    void Start ()
    {
        p = new Plane( Vector3.up, Vector3.zero);
        LateUpdate();
    }
 
    // size of trapezoidal play area
    static float w1;        // world width at top edge of screen
    static float w2;        // world width at bottom edge of screen
    static float h;            // world height screen top to bottom

    void LateUpdate()
    {
        if (goUL == null) { goUL = new GameObject(); goUL.name = "goUL"; }
        if (goUR == null) { goUR = new GameObject(); goUR.name = "goUR"; }
        if (goLL == null) { goLL = new GameObject(); goLL.name = "goLL"; }
        if (goLR == null) { goLR = new GameObject(); goLR.name = "goLR"; }
     
        CastToPlane( new Vector3( 0, 1, 0), out v3UL);
        goUL.transform.position = v3UL;
        CastToPlane( new Vector3( 1, 1, 0), out v3UR);
        goUR.transform.position = v3UR;
        CastToPlane( new Vector3( 0, 0, 0), out v3LL);
        goLL.transform.position = v3LL;
        CastToPlane( new Vector3( 1, 0, 0), out v3LR);
        goLR.transform.position = v3LR;
     
        o3UL = v3UL;
        o3UR = v3UR;
        o3LL = v3LL;
        o3LR = v3LR;
        w1 = Mathf.Abs ( o3UL.x - o3UR.x);
        w2 = Mathf.Abs ( o3LL.x - o3LR.x);
        h = Mathf.Abs ( o3UL.z - o3LL.z);
        o3UL.x -= w1 * STATE.WorldSizePercentageAboveViewport;
        o3UL.z += h * STATE.WorldSizePercentageAboveViewport;
        o3UR.x += w1 * STATE.WorldSizePercentageAboveViewport;
        o3UR.z += h * STATE.WorldSizePercentageAboveViewport;
        o3LL.x -= w2 * STATE.WorldSizePercentageAboveViewport;
        o3LL.z -= h * STATE.WorldSizePercentageAboveViewport;
        o3LR.x += w2 * STATE.WorldSizePercentageAboveViewport;
        o3LR.z -= h * STATE.WorldSizePercentageAboveViewport;

        w1 = Mathf.Abs ( o3UL.x - o3UR.x);
        w2 = Mathf.Abs ( o3LL.x - o3LR.x);
        h = Mathf.Abs ( o3UL.z - o3LL.z);
     
        goUL.transform.position = o3UL;
        goUR.transform.position = o3UR;
        goLL.transform.position = o3LL;
        goLR.transform.position = o3LR;
    }
 
    public static bool TrapezoidalWrapAround( GameObject go)
    {
        bool weWrapped;
        weWrapped = false;
     
        if (go.transform.position.y != 0)
        {
            go.transform.position += Vector3.down * go.transform.position.y;
        }
     
        if (go.transform.position.z > o3UL.z)
        {
            go.transform.position += Vector3.back * h;
            weWrapped = true;
        }
        if (go.transform.position.z < o3LL.z)
        {
            go.transform.position += Vector3.forward * h;
            weWrapped = true;
        }

        // where we are vertically within our available window (normalized)
        float myh = (go.transform.position.z - o3LL.z) / h;
     
        // how wide the window is at this point
        float myw = w1 * myh + w2 * (1.0f - myh);
     
        if (go.transform.position.x < o3UL.x + (o3LL.x - o3UL.x) *
            (go.transform.position.z - o3UL.z) / (o3LL.z - o3UL.z))
        {
            go.transform.position += Vector3.right * myw;
            weWrapped = true;
        }
        if (go.transform.position.x > o3UR.x + (o3LR.x - o3UR.x) *
            (go.transform.position.z - o3UR.z) / (o3LR.z - o3UR.z))
        {
            go.transform.position += Vector3.left * myw;
            weWrapped = true;
        }
     
        return weWrapped;
    }
 
    public static bool CheckIsVisible( GameObject go, float extraDistance)
    {
        if (go.transform.position.z > v3UL.z + extraDistance)
        {
            return false;
        }
        if (go.transform.position.z < v3LL.z - extraDistance)
        {
            return false;
        }

        if (go.transform.position.x < v3UL.x + (v3LL.x - v3UL.x) *
            (go.transform.position.z - v3UL.z) / (v3LL.z - v3UL.z) - extraDistance)
        {
            return false;
        }
        if (go.transform.position.x > v3UR.x + (v3LR.x - v3UR.x) *
            (go.transform.position.z - v3UR.z) / (v3LR.z - v3UR.z) + extraDistance)
        {
            return false;
        }
     
        return true;
    }
}

The reason for the CheckIsVisible() function is that things like spaceships and spaceships shots do NOT wrap around because that would be kinda deadly.

Well, digging that up was a fun trip down memory lane! The stars are separately managed and wrap around in an even simpler hard-coded way. Each star is the star4_2 prefab you see loaded around line 68.

The starfield controller code is here. It also supports “galaxies” but I couldn’t find any good royalty-free galaxy images that were low-key enough to really work well: I kept thinking they were an enemy out to destroy me whenever they scrolled onscreen.

Hope this gives you some interesting ideas to play around with. Thanks for trying out my game. I still love playing it myself all these years ago, which is why I made it!

/*
    The following license supersedes all notices in the source code.
*/

/*
    Copyright (c) 2015 Kurt Dekker/PLBM Games All rights reserved.

    http://www.twitter.com/kurtdekker
   
    Redistribution and use in source and binary forms, with or without
    modification, are permitted provided that the following conditions are
    met:
   
    Redistributions of source code must retain the above copyright notice,
    this list of conditions and the following disclaimer.
   
    Redistributions in binary form must reproduce the above copyright
    notice, this list of conditions and the following disclaimer in the
    documentation and/or other materials provided with the distribution.
   
    Neither the name of the Kurt Dekker/PLBM Games nor the names of its
    contributors may be used to endorse or promote products derived from
    this software without specific prior written permission.
   
    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
    IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
    TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
    PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
    HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
    SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
    TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
    PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
    LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
    NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
    SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class Background : MonoBehaviour
{
    public const float StarFieldSpanX = 70;
    public const float StarFieldPlaneY = -5;
    public const float StarFieldSpanY = 50;
    public const float StarFieldSpanZ = 70;
   
    List<GameObject> theStars;
    int numStars;
   
    public const float GalaxySpanX = 150;
    public const float GalaxyPlaneY = -50;
    public const float GalaxySpanY = 50;
    public const float GalaxySpanZ = 150;
   
    List<GameObject> theGalaxies;
    int numGalaxies;
   
    void Start ()
    {
        theStars = new List<GameObject>();
       
        numStars = 200;
       
        for (int i = 0; i < numStars; i++)
        {
            GameObject go = Instantiate( Resources.Load<GameObject>(
                "Models/star4_2/star4_2")) as GameObject;
            go.transform.position = new Vector3(
                Random.Range( -StarFieldSpanX, StarFieldSpanX),
                StarFieldPlaneY - (StarFieldSpanY * i) / numStars,
                Random.Range( -StarFieldSpanZ, StarFieldSpanZ));
            go.transform.localScale = Vector3.one * 0.3f;
            go.name = "Background Star #" + i;
            go.hideFlags = HideFlags.HideInHierarchy;
            theStars.Add(go);
        }
       
        theGalaxies = new List<GameObject>();
       
        numGalaxies = 4;
       
        numGalaxies = 0;
       
        for (int i = 0; i < numGalaxies; i++)
        {
            string[] possibleGalaxies = {
                // <WIP> need to find some royalty-free galaxy images!
            };
            GameObject go = Instantiate( Resources.Load<GameObject>(
                possibleGalaxies[ Random.Range( 0, possibleGalaxies.Length)]
            )) as GameObject;
            go.transform.position = new Vector3(
                Random.Range( -GalaxySpanX, GalaxySpanX),
                GalaxyPlaneY - (GalaxySpanY * i) / numGalaxies,
                Random.Range( -GalaxySpanZ, GalaxySpanZ));
            go.transform.localScale = Vector3.one * Random.Range( 20.0f, 45.0f);
            // go.transform.localRotation = Quaternion.Euler( 0, 0, Random.Range( 0.0f, 360.0f));
            go.name = "Background Galaxy #" + i;
            go.hideFlags = HideFlags.HideInHierarchy;
            theGalaxies.Add(go);
        }
    }
   
    void UpdateObjSF( GameObject go)
    {
        if (STATE.goPlayer == null) return;
       
        Vector3 pos = go.transform.position - STATE.goPlayer.transform.position;
       
        if (pos.x < -StarFieldSpanX)
        {
            go.transform.position += Vector3.right * StarFieldSpanX * 2;
        }
        if (pos.x >  StarFieldSpanX)
        {
            go.transform.position += Vector3.left * StarFieldSpanX * 2;
        }
        if (pos.z < -StarFieldSpanZ)
        {
            go.transform.position += Vector3.forward * StarFieldSpanZ * 2;
        }
        if (pos.z >  StarFieldSpanZ)
        {
            go.transform.position += Vector3.back * StarFieldSpanZ * 2;
        }
    }
   
    void UpdateObjGA( GameObject go)
    {
        if (STATE.goPlayer == null) return;
       
        Vector3 pos = go.transform.position - STATE.goPlayer.transform.position;
       
        if (pos.x < -GalaxySpanX)
        {
            go.transform.position += Vector3.right * GalaxySpanX * 2;
        }
        if (pos.x >  GalaxySpanX)
        {
            go.transform.position += Vector3.left * GalaxySpanX * 2;
        }
        if (pos.z < -GalaxySpanZ)
        {
            go.transform.position += Vector3.forward * GalaxySpanZ * 2;
        }
        if (pos.z >  GalaxySpanZ)
        {
            go.transform.position += Vector3.back * GalaxySpanZ * 2;
        }
    }
   
    void Update()
    {
        foreach (GameObject go in theStars)
        {
            UpdateObjSF(go);
        }
        foreach (GameObject go in theGalaxies)
        {
            UpdateObjGA(go);
        }
    }
}

A much more performant way to make a starfield is to use the Unity particle systems, which are awesome once you take the time to learn them.

At the time I wrote Roxx, I wasn’t too comfortable with the particles, but attached is a recent starfield prefab I made for a similar unreleased game.

This prefab includes the camera pointing downwards, a black plane, and the particle system itself. Press PLAY and then in the scene grab the root of the rig and move it around and you’ll see how it works.

These stars twinkle too and are different colors but to see the colors you will have to have that black plane beneath them because they are additive. If you move the camera around too much the particles don’t keep up and things get pretty sparse, so you might want to make the particle box larger. For my game that uses it, the player motion is slow.

2799186–203114–StarfieldRig.unitypackage (18.3 KB)

Thank you so much for all of your help so far! I’ve watched an hour long introduction to Unity’s particle system and I think it’s almost perfect for generating the stars in my game. I’ve also played around with your game after reading your code that wraps the meteors and as efficient and simple it is it won’t do for my game because.

  • My game is somewhat of an exploring game so the same meteors can’t keep reappearing, looking all the same.
  • I can also interact with the meteors, move them around, land on them and such so having only 3 meteors wrapping around the map won’t do.
  • Also having them constantly wrapping means the only way you’ll ever encounter a meteor is by moving towards them, instead of having the meteors move around and hitting you or just passing by for effect.
  • Finally, my meteors can range from a grain to 1000x times the player’s size.

Because of these reasons the wrapping won’t do and the only thing I can think of that gives me what I want is what I described at the top of the thread. The problem is I’ve got no idea how I can achieve it. I’m not advance enough at Vector math to add Time to the equation :confused: