0
\$\begingroup\$

I have a tile board set up with 500 x 500 pixels = 1 unit. I have code that will move the object over time.

    void Update()
{
    Vector2 pos = transform.position;

    if (Input.GetKey("w"))
    {
        pos.y += speed * Time.deltaTime;
    }
    if (Input.GetKey("s"))
    {
        pos.y -= speed * Time.deltaTime;
    }
    if (Input.GetKey("d"))
    {
        pos.x += speed * Time.deltaTime;
    }
    if (Input.GetKey("a"))
    {
        pos.x -= speed * Time.deltaTime;
    }
    transform.position = pos;
}

I want to move 1 unit after each button press and only by integer values. Speed is a public variable that sets how fast the object moves.

Thanks for the help.

\$\endgroup\$
2
  • \$\begingroup\$ The approach i describe here should work for you (minus the whole tail thing): Smooth out movement of the snake in a 2d Snake game. \$\endgroup\$ – Theraot Oct 25 '20 at 4:27
  • \$\begingroup\$ I am reletively new to Unity and C#. Could you explain it or adjust the code, so I can understand? \$\endgroup\$ – Snazzy Hat Oct 25 '20 at 4:36
0
\$\begingroup\$

This solution is for grid based games. The idea is to have "turns". A turn will happen regardless of whatever or not there is input or not. At the end of the turn the avatar will be aligned to the grid, and a turn always takes the same time.

Pseudo-code:

var current_time = get_current_time();
var elapsed_time = current_time - start_time;
var got_input = get_input();
input = got_input ?? input;
if (elapsed_time > turn_duration)
{
    elapsed_time -= turn_duration;
    start_time += turn_duration;
    start_position = end_position;
    end_position = compute_next_position(start_position, input);
}

current_position = interpolate(start_position, end_position, elapsed_time);

The duration of the turn can be computed from the velocity and the size of the grid. We know that speed = distance / time, solve for time:

speed = distance / time

=>

time * speed = distance

=>

time = distance / speed

Thus:

turn_duration = grid_side_length / speed;

We are storing where it was at the start of the "turn", and where it will be at the end of the turn. Those positions should be aligned to the grid. We use the elapsed time since the start of the turn to compute the current position via interpolation.

When, the turn ends, we use the input and the last position to compute the next position. That would be the position plus the size of the grid in the direction dictated by input, if any.

You can buffer input if you like. You can treat no input as going in the same direction if you like.


We can make the end_position nullable (declare it Vector2? instead of Vector2), and take that as signal that no movement is happening at the moment. We can also work with end_time instead of elapsed_time. Similarly make end_time nullable.

var current_time = get_current_time();
var got_input = get_input();
input = got_input ?? input;

if (input != null && end_position == null)
{
    start_time = current_time;
    end_time = start_time + turn_duration;
    start_position = current_position;
    end_position = compute_next_position(start_position, input);
}

if (end_time == null || end_position == null)
{
    return;
}

if (current_time > end_time.Value)
{
    current_position = end_position.Value;
    end_position = null;
    end_time = null;
}
else
{
    current_position = interpolate(start_position, end_position.Value, elapsed_time);
}

The above code differs in that it will move when it gets some input, and stays there until new input arrive. Note, however, that it will not interrupt the current motion. I do not know what you may want to happen when a key is pressed while the object is moving, it varies from game to game. However, the key to interrupt the motion is to drop the check for end_position == null, and be careful to set the new end_position still on the grid.


In my implementation for Unity and Godot, I made a Motion struct to encapsulate some of this. It holds start and end position, and also duration, and will do the interpolation given the elapsed time since the start of the motion. I also ended up accumulating delta time (Time.deltaTime) instead of using current time.

My code looks something like this (this is Unity code):

if (!_motion.HasValue)
{
    var nextMotion = NextMotion(transform.position, GetNewTarget(), speed);
    if (nextMotion != null)
    {
        _motion = nextMotion;
        _time = 0.0f;
    }
}

if (!_motion.HasValue)
{
    return;
}

_time += Time.deltaTime;
var motion = _motion.Value;
if (_time > motion.Duration)
{
    _motion = null;
    transform.position = motion.End;
}
else
{
    transform.position = motion.GetCurrent(_time);
}

GetNewTarget() computes a target position based on input. In your case, it should check the input, and if there is any, add to the current position the length of the side of the grid in the appropriate direction.

NextMotion creates a new motion, given the current position, the target position, and the speed.

As you can imagine GetCurrent is a linear interpolation. Its code is simply:

return Start + ((End - Start) * time / Duration);

Where Duration is pre-computed as:

var diff = end - start;
var length = diff.magnitude;
Duration = length / speed;

If you want to allow interrupting motion, the code should look something like this:

var target = GetNewTarget();

if (!_motion.HasValue || _motion.Value.End != target)
{
    var nextMotion = NextMotion(transform.position, target, speed);
// ...

And GetNewTarget would work from the start position of the current motion if any, if there is no current motion the use the current position.

\$\endgroup\$
0
\$\begingroup\$

When you want to do a thing each time the player presses a key and not all the time while they are holding a key, use Input.GetKeyDown. This method only returns true on the first Update when the player starts pressing a key.

if (Input.GetKeyDown(KeyCode.W))
{
    pos.y += 1.0f;
}

But perhaps you don't want that movement to be instantaneous and instead animate a smooth movement to the new destination. You can to that by having keypresses not set the new position directly but instead change the position where the object should be. And then you add some code to Update which slowly moves the object form its current position to the desired position. You could implement that by adding a new variable Vector3 desiredPosition to the object. Then you add additional code in Update which moves the object from its current transform.position towards desiredPosition. The simplest form would be to just have linear movement with constant velocity, which can be done with Vector3.MoveTowards:

transform.position = Vector3.MoveTowards(transform.position, desiredPosition, speed * Time.deltaTime);

But you might also want to experiment with some fancier ways of animating that move, like using some interpolation or tweening algorithm, perhaps by using a 3rd party library like DOTween.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.