How to move object with 100% precision in 2d space?

Im trying to make simple 2D game and faced situation where moving objects with transform.position + speedFloat * Time.deltaTime produce visible imperfections.

This is how my scene looks like:

Squares scale values are 1, SquaresHolder position is (-0.5, 0, 0) and SquaresHolder2 position is (0.5, 0, 0)

Here is code attached to both SquareHolders, but isShift=true for left column only

using UnityEngine;

public class TEST_OBJ_MOVEMENT : MonoBehaviour
{
    public bool isShift;
    float speed = .5f;
    // Start is called once before the first execution of Update after the MonoBehaviour is created
    void Start()
    {
        if (isShift) {
            Invoke("ShiftToTop", 2);
        }
    }

    // Update is called once per frame
    void Update()
    {
        if (!isShift) {
            transform.position = new Vector3(transform.position.x, transform.position.y - speed * Time.deltaTime, transform.position.z);
        }
    }

    void LateUpdate() {
        if (isShift) {
            transform.position = new Vector3(transform.position.x, transform.position.y - speed * Time.deltaTime, transform.position.z);            
        }
    }

    void ShiftToTop() {
        transform.position = new Vector3(transform.position.x, transform.position.y + 1, transform.position.z);
        Debug.Break();
    }
}

After shift happened columns looks good, without any gaps, but after I unpause game and columns start moving again I see smth like that(1st column is slightly above from the expected position):

I tried to increase orthographic camera size from 5 to 1000 or 10_000, but gap is still present after shift and Im not sure if it break my game later, when columns become longer and have 1000+ squares, perhaps I will go out of possible transform.position values range.
I found some suggestions to use only int values in position, but it doesnt look like a good solution which produce smooth movement and will not create situation where 1st column y value will be 10.99 and rounded to int 10, but other column value will be 11.01 and rounded to int 11.

So is there a good solution how to achieve precisious movement to make movement on the same speed dont cause noticiable gaps between squares in different columns?

Calculate the destination position, and move said object towards that position.

Methods like Vector3.MoveTowards won’t overshoot.

Note that you are doubling the speed of the movement by calling the code in both Update and LateUpdate.

And keep in mind: you want to avoid code duplication above all things. Call a method instead if you need to use the same functionality in multiple places. And variables also help reduce verbosity and complexity:

    void Update()
    {
        if (!isShift) {
            MoveIt();
        }
    }

    void MoveIt() {
        var pos = transform.position;
        transform.position = new Vector3(pos.x, pos.y - speed * Time.deltaTime, pos.z);            
    }

IDEs have this wonderful “Refactor - Extract Method” functionality. Select one or more lines of code and it’ll try to figure out how to best move that to a separate method.

If you ever rename that method this code breaks without letting you know. With nameof() you can ensure the compiler will point out if there’s a name mismatch:

Invoke(nameof(ShiftToTop), 2);

This is doesnt work or I misunderstood your advice.
I replaced moving “transform.position = new Vector3(…)” with
transform.position = Vector3.MoveTowards(transform.position, targetPos, speed * Time.deltaTime);
where targetPos = new Vector3(transform.position.x, int.MinValue, transform.position.z);
to make squres down and such moving still showing gaps and also start to show gaps even after shift sometimes

Thank you for advices, I will follow them in the future. In piece of code above I just tried to show what movement is not work for me in a the simplest way. Also I believe

Note that you are doubling the speed of the movement by calling the code in both Update and LateUpdate.

is not a case, because update and lateUpdate have different condition in 'if’s, so right column will be moved by Update, and left column will be moved by LateUpdate

So you know, I removed the 2D-Physics tag and added the Scripting tag as your question is unrelated to 2D-Physics.

I see, yes that ! is easy to overlook. But it’s still strange to have those in two different update methods.

This is what your code is doing, and there’s no difference between the two movement methods - so you move the same thing the same way regardless of isShift:

    void Update()
    {
        if (!isShift) {
            transform.position = new Vector3(transform.position.x, transform.position.y - speed * Time.deltaTime, transform.position.z);
        }
        else {
            transform.position = new Vector3(transform.position.x, transform.position.y - speed * Time.deltaTime, transform.position.z);            
        }
    }

Two things to try:

  • Put both shifts in the same type of update. Suspicion: Your pausing lets either update() or lateUpdate() execute one more time than the other.
    What was your motivation to split it up btw?
  • Just to see if that’s the problem, use Application.targetFrameRate to pin the framerate and hardcode a value for Time.deltaTime.

Is everything in a grid? Don’t move it based on the transform.position, use the grid position to drive your transform.position and then change the grid.

Tile-based / grid-based 2D games: match3, tetris, chips challenge, rogue, pac man, etc:

For any tile-based game such as Match3 or Tetris or a grid-based Roguelike, do all the logical comparisons in your own data storage mechanism for the tiles, such as a 2D array of tiles.

Otherwise you needlessly bind your game logic into Unity objects and the Unity API, making it about 10x more complicated than it needs to be.

If you have no idea how to work with 2D arrays, hurry to some basic C# tutorials for the language portions of it, then look at any good tutorial that uses a 2D array

Here is my Match3 demo using this technique of storing data in a grid. Full source linked in game comments.

It stores all of its data in a 2D array:

PieceController[,] Board;

This allows for easy simple checking in code, not relying on anything like physics.

You should strive to use that pattern for all logic, then only use Unity to present what is happening in the game logic.

I mean your code just calculates target position as its own position? Then it wouldn’t move anywhere.

But like Kurt says, if you’ve got something grid based, then calculate the position from the grid coordinates it is intended to move to.

Im not sure its can be called ‘grid based’, I thought about smth like piano tiles game

Like multiple columns with items going down, but I faced an issue with placing one column to align with another in a way to make tiles from 1st column to have maxY and minY to match with corresponding tiles from another column.
Like after “ShiftToTop” method called I see in inspector that tiles of different columns are on the same level and form straight lines from top/bot sides, but after scroll of columns happen small gap appear and tiles from one column is slightly above than tiles from other column.

I guess I made similar to

For any tile-based game such as Match3 or Tetris or a grid-based Roguelike, do all the logical comparisons in your own data storage mechanism for the tiles, such as a 2D array of tiles.

By ensuring transform Y value is always int by creating method like
UpdateYPosition(float yDelta) {
transform.position = new Vector3(transform.position.x, Convert.ToInt32(transform.position.y + yDelta), transform.position.z); and changing camera size to match screen height(1920 for example). By this I tried to achieve smooth movement because I move by ‘pixels’ of the screen and it is not possible to face situation with gaps which appear with code from the post, when tiles looks good if 1st column yPos=10.5, 2nd column yPos=5.3, I change both values in theirs update by applying speed * Time.deltaTime(lets say 0.33) and 1st yPos become 10.83 but yPos of second column become 5.629801, creating gap which you can see in the second image in my post.

Is it bad approach and I should create some grid system you mentioned even if columns should have different behaviour, including movement behaviour?

I thought using int.MinValue as an Y value, while keeping y and z values the same will results in “down” movement :slight_smile:

That piano game looks really more like a subway surfer kinda multi-row endless runner, but with some extra rules when a large number comes through, where it hangs long enough to let you bang away the numbers with taps.

That game probably makes all of the objects in advance (spaced out vertically), puts them all under a single parent object and then slides that one parent object down steadily… when a “hang and tap” thing is being processed, the parent object moves slowly, which keeps all the other stuff from getting out of sync.

Yeah, but for my project I cant place all tiles in single parent, because I want to make different columns move with different speed, or in different directions, so I created object for each column and placed tiles of each column inside corresponding parent. And movement with simple transform.position += new Vector3(0, speed * Time.deltaTime, 0); looks fine, but when I try to implement ‘snap’ functionality to make tiles from different columns(this columns have the same speed) be on the same level, so go from pic1 to pic2 I face an issue I tried to explain with my poor english here:

tiles looks good if 1st column yPos=10.5, 2nd column yPos=5.3, I change both values in theirs update by applying speed * Time.deltaTime(lets say 0.33) and 1st yPos become 10.83 but yPos of second column become 5.629801, creating gap which you can see in the second image in my post.

pic1


pic2

For some reason moving columns with the same speed saves tiles on the same level until you update column position. But after I update column position to smth like


and continue move columns with the same speed, adding same yDelta to different position.y produce funny results and tiles are not displayed on the same level any more. Approach with making position.y always int works good, but Im afraid of some bugs in the future because of possible limitation on max/min possible value for transform.position, like column with 1000 tiles will became 1000 * screenHeight / 4 units high and its 480 000 unity units in column height. Like having possibility to use floats as Y position allows to have big columns in smaller size

^ ^ ^ This is just a bug, or perhaps more than one bug… we know numbers compute correctly so find out why yours are not. Do you have inadvertent non-identity scaling somewhere in your scene?

By debugging you can find out exactly what your program is doing so you can fix it.

Use the above techniques to get the information you need in order to reason about what the problem is.

You can also use Debug.Log(...); statements to find out if any of your code is even running. Don’t assume it is.

Once you understand what the problem is, you may begin to reason about a solution to the problem.

Remember with Unity the code is only a tiny fraction of the problem space. Everything asset- and scene- wise must also be set up correctly to match the associated code and its assumptions.

Don’t worry about the future. You have plenty of bugs to solve here first. When all your current bugs are solved, then you can move forward and maybe there won’t be any more bugs.

^ ^ This means you may need to consider floating point imprecision to avoid jitter. Are you prepared to consider the math and logic behind such a system? Or are you prepared to engineer another system based around a sliding window of positions as they arrive and depart the important area(s) of the screen?

If you’re looking to spawn a series of things at precise intervals, keep a separate position variable telling where to spawn the next one, then when you move all items down, move that position variable by the same amount. When that position variable gets too close to your sliding window, use it and advance it.

Here’s as super-quick even-spacing spawner I just made to demonstrate:

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

// @kurtdekker - spawn stuff relentlessly at the same gap
//
// uses a moving cursor
//
// to test: make a blank scene with camera and light, put this somewhere and press play
//
public class SpawnAtConstantGaps : MonoBehaviour
{
	Vector3 nextSpawnPosition;

	// no matter how fast you go, it will make things at an equal spacing
	[Header("Change this in the inspector at runtime:")]
	[Range(1, 1000)]
	public float xSpeed = 10;

	const float xSpacing = 5;

	// make these far enough out so you can't see them spawn or remove
	const float LeftEdge = -10;
	const float RightEdge = +10;

	List<GameObject> items = new List<GameObject>();

	private void Update()
	{
		// remove dead items
		items.RemoveAll(x => !x);

		// how far to move?
		float step = xSpeed * Time.deltaTime;
		Vector3 move = Vector3.left * step;

		// move all items by step
		foreach( var item in items)
		{
			item.transform.position += move;

                        // delete stuff far enough to the left
			if (item.transform.position.x < LeftEdge) Destroy(item);
		}
		// and move our spawn
		nextSpawnPosition += move;

		// is it time to spawn?
		while( nextSpawnPosition.x < RightEdge)
		{
			// make it at our cursor
			var item = GameObject.CreatePrimitive(PrimitiveType.Sphere);
			item.transform.position = nextSpawnPosition;

			// record it
			items.Add(item);

			// advance spawn cursor
			nextSpawnPosition.x += xSpacing;
		}
	}
}

I have tried to debug with debug.log() and it just shows numbers in position dont work as Im expecting from them.

And Im worry about future bugs because it may make my solution with int only values for position.y useless, if I cant make long columns with 1000+ tiles in a column. Im ready to make some code for columns which make them store not all tiles in the same time, but this sound as complex task and I hoped for a simplier solution.

Thank you for your code, my code looks kinda similar to yours, and yours have the same problem with position as I tried to describe above. Its just less noticeble when you do it with 3d objects which are far away from each other.


you can see at screenshots that distance between them approximately 5, but not 5, and such difference is very noticeble when its applied to tiles from columns which share side

Have you tried my suggestions?

With a hardcoded value instead of time.deltaTime it’s mathematically impossible to get to such crooked numbers (unless you end up in float point inaccuracy territory).

Also Im sure this problem isnt really related to the code, because I can reproduce it in editor without any code.
Precondition:

  1. Camera in orthographic mode with size=128
  2. Create empty object as parent for a tiles from 1st column, position=(15, -43.4, 0), scale=(1,1,1)
  3. Create 4 2d objects from “2d Objects → Sprites → square” in the object from previous step with black/white colors, scale = (30, 30, 1) and next positions: (0, 90, 0), (0, 60, 0), (0, 30, 0), (0, 0, 0)
  4. Copy object from 2 and 3 steps and change position of copy to (-15, -43.4, 0)

So result will look like this:

When I use maximum zoom to the bottom tiles I see they are on the same level, without any gaps, like this:

Then I move in editor left column up at one tile, by changing pos (-15, -43.4, 0) to (-15, -13.4, 0), because height of every tile is 30 units, zoom in again and see visible gaps like this:

Same happens when Im trying to make tiles be on the same level via code.

Having both shifts in the same method (Update) was my first attempt and I got gaps here at the first place, I thought that perhaps sometimes execution order of updates are different during runtime, so I tried to use LateUpdate to be sure that column with tiles I try to align with will not change its position value during frame with shift

  • Just to see if that’s the problem, use Application.targetFrameRate to pin the framerate and hardcode a value for Time.deltaTime.

I havent tried this because I really dont want to stop using Time.deltaTime and lost smoothness in movement, but in my original, not simplified code from the post I use Application.targetFrameRate = 60; and also have different monobehaviours scripts on each column. I printed speed * Time.deltaTime in each of this scripts via Debug.Log() and it always were the same values

Im starting to think that Im thinking too much and I just should accept that perfection in editor is not possible and such imperfections didnt affect how the game looks on real devices.
Here is how it looks in editor right now:

In scene zoom gap is visible, but in game window and on real devices black tiles from different columns appear perfectly aligned.

The only change I did is starting update parent objects of tiles by next extension methods:

    public static void UpdatePositionYRound(this Transform transform, float newY, int roundDigits = 2) {
        Vector3 newPosition = transform.position;
        newPosition.y = MathF.Round(newY, roundDigits);
        transform.position = newPosition;
    }

    public static void UpdateLocalPositionYRound(this Transform transform, float newY, int roundDigits = 2) {
        Vector3 newPosition = transform.localPosition;
        newPosition.y = MathF.Round(newY, roundDigits);
        transform.localPosition = newPosition;
    }

Also I want to mention that camera size=128 and tile object scale is (48, 64, 1)

Before rounding Y value by MathF the gap was noticeble not only in scene zoom, but on real devices as well