2d movement with 3d camera work

first thing, here is my code:
https://pastebin.com/W6cQaZWw
i tried my best to include comments to describe each part of it

what I’m stuck at is making the player move across the floor. The camera needs to be following the player at all times, which means that the actual thing that “moves” is the floor. That’s how I understand it at least. The problem is that as the floor turns, the movement speeds on the X and Y axi should change relative to the angle, I just don’t know which method I need to use to achieve this.

the calculation for the forward/backward movement
should work like this, from what I understand:
0°: Y speed = 10, X speed = 0
45°: Y speed = 5, X speed = 5
90°: Y speed = 0, X speed = 10
135°: Y speed = -5, X speed = 5
180°: Y speed = -10, X speed = 0
225°: Y speed = -5, X speed = -5
270°: Y speed = 0, X speed = -10
315°: Y speed = 5, X speed = -5
360°, 0°: Y speed = 10, X speed = 0
etc, with everything inbetween

is the pattern visible?

I am just lost as to how to actually do this. I can’t think of any other way
to make it appear that when the player turns, he will walk towards that direction.
I’ve explored many articles talking about 3D camera movement and movement in 2D or 3D space, but even if they do what I need to do in my code, I just can’t figure out how it works, or how to use their implementation for my own code.

If there is a better or more efficient way to do this, then I would really love to know about that. I know that there are many ways to implement this, as with anything, and what I’m trying to do is probably stupid and incredibly basic. I know 3D graphics is a university-level subject, with collisions and camera work and lighting and such, but I am trying to learn what I can for now, because this kind of stuff interests me.

Thanks in advance for any answers!

Hi relix,

Maybe pretend that the camera is tethered to the avatar by an invisible leash and then let the camera function (reference, source code), do the heavy lifting?

This way, you track where the camera is located and where it is looking. The third piece of information, the camera’s up direction (0, 1, 0), doesn’t usually need to be updated in draw.

Then, the placement of the floor and the avatar movement can stay as simple or as complicated as you want to make it. (I’ve simplified the example below by not using tank controls or rotating the player.)

128

PVector tether = new PVector(0.0, -1.0, 1.0).normalize();
PVector eyeCurrent = new PVector();
PVector eyeTarget = new PVector();
PVector lookCurrent = new PVector();
PVector lookTarget = new PVector();
PVector up = new PVector(0.0, 1.0, 0.0);
PVector heroLoc = new PVector();

float tetherLength;
float lookSmoothing = 0.05;

// Recommended that moveSmoothing not be greater
// than lookSmoothing.
float moveSmoothing = 0.025;
float heroMoveSpeed = 2.0;

boolean[] pressed = new boolean[256];

void setup() {
  size(512, 256, P3D);

  // Set the length of the tether.
  tetherLength = height * 0.86602;
  tether.setMag(tetherLength);

  // Set camera to beginning position.
  PVector.add(heroLoc, tether, eyeCurrent);
  lookCurrent.set(heroLoc);
  camera(eyeCurrent.x, eyeCurrent.y, eyeCurrent.z, 
    lookCurrent.x, lookCurrent.y, lookCurrent.z, 
    up.x, up.y, up.z);
}

void draw() {

  // Movement.
  if (pressed[UP]) {
    heroLoc.add(0.0, 0.0, -heroMoveSpeed);
  }

  if (pressed[LEFT]) {
    heroLoc.add(-heroMoveSpeed, 0.0, 0.0);
  }

  if (pressed[RIGHT]) {
    heroLoc.add(heroMoveSpeed, 0.0, 0.0);
  }

  if (pressed[DOWN]) {
    heroLoc.add(0.0, 0.0, heroMoveSpeed);
  }

  // Set target locations.
  lookTarget.set(heroLoc);
  tether.setMag(tetherLength);
  PVector tetherPos = PVector.add(heroLoc, tether, 
    new PVector());

  // Smooth from current to target.
  smoothStep(lookCurrent, lookTarget, lookSmoothing, lookCurrent);
  smoothStep(eyeCurrent, tetherPos, moveSmoothing, eyeCurrent);

  camera(eyeCurrent.x, eyeCurrent.y, eyeCurrent.z, 
    lookCurrent.x, lookCurrent.y, lookCurrent.z, 
    up.x, up.y, up.z);
  directionalLight(255.0, 255.0, 255.0, 
    0.0, 8.0, -0.6);
  background(0xff000000);

  // Draw reference points.
  strokeWeight(1.0);
  for (int i = 0; i < 50; ++i) {
    float iPrc = i * 0.020408163;
    float mz = lerp(-width, width, iPrc);
    for (int j = 0; j < 50; ++j) {
      float jPrc = j * 0.020408163;
      float mx = lerp(-width, width, jPrc);
      float kPrc = (iPrc + jPrc) * 0.5;
      stroke(lerpColor(0x7fff0000, 0x7f0000ff, kPrc, HSB));
      line(mx - 5.0, 0.0, mz, mx + 5.0, 0.0, mz);
      line(mx, 0.0, mz - 5.0, mx, 0.0, mz + 5.0);
    }
  }

  // Draw avatar.
  pushMatrix();
  translate(heroLoc.x, heroLoc.y, heroLoc.z);
  stroke(0xffff0000);
  line(0.0, 0.0, 0.0, 50.0, 0.0, 0.0);
  stroke(0xff00ff00);
  line(0.0, 0.0, 0.0, 0.0, -50.0, 0.0);
  stroke(0xff0000ff);
  line(0.0, 0.0, 0.0, 0.0, 0.0, -50.0);
  noStroke();
  sphere(25.0);
  popMatrix();

  // Check framerate.
  surface.setTitle(String.format("%.2f", frameRate));
}

void keyPressed() {
  pressed[keyCode] = true;
}

void keyReleased() {
  pressed[keyCode] = false;
}

static PVector smoothStep(PVector a, PVector b, float t, PVector out) {
  if (t <= 0.0) return out.set(a);
  if (t >= 1.0) return out.set(b);
  return lerpUnclamped(a, b, t * t * (3.0 - 2.0 * t), out);
}

static PVector lerpUnclamped(PVector a, PVector b, float t, PVector out) {
  float u = 1.0 - t;
  return out.set(
    u * a.x + t * b.x, 
    u * a.y + t * b.y, 
    u * a.z + t * b.z);
}

The smoothing / easing / tweening / interpolation functions at the bottom of the code are just a way to get from a to b over time.

Thanks for the reply!

This suggestion does work, but it isn’t what I’m trying to do. I’m trying to make a simple third-person-view of the character, just like in games such as WoW or Bless online, etc.
In those games, the character is permanently “stuck” to a position on the screen, and it appears that your character is moving because everything else is moving relative to it. The camera only ever moves further away or closer to the character.

As well as that, your code example doesn’t actually rotate the player at all. And that was the actual issue that I had, not with the camera. I want to be able to move in any direction out of the 360° that are available.

This is actually more of a math question than a programming one, because I am merely trying to figure out how to create a function that increases/decreases the X/Y speed relative to the angle the player is trying to travel- creating the illusion that it is travelling in that direction.

Hi relix,

The maths and programming are interconnected. If you set moveSmoothing and lookSmoothing to 1.0 (100%), your character stays locked to the center of the screen because there’s no more delay between the target and current location. The point is that the way you program the world shouldn’t have to change if you change the way you move your character.

Tank controls and rotation shouldn’t be too bad either, with rotateY and a float to track the angle of rotation. It is easier to see with a box rather than a sphere.

PVector tether = new PVector(0.0, -1.0, 1.0).normalize();
PVector eyeCurrent = new PVector();
PVector eyeTarget = new PVector();
PVector lookCurrent = new PVector();
PVector lookTarget = new PVector();
PVector up = new PVector(0.0, 1.0, 0.0);
PVector heroLoc = new PVector();

float tetherLength;
float lookSmoothing = 1;

// Recommended that moveSmoothing not be greater
// than lookSmoothing.
float moveSmoothing = 1;
float heroMoveSpeed = 2.0;
float heroRotSpeed = 0.1;

float heroRot = 0.0;

boolean[] pressed = new boolean[256];

PMatrix3D heroOrientation = new PMatrix3D();
PVector heroForward = new PVector();

void setup() {
  size(512, 256, P3D);
  tetherLength = height * 0.86602;
  tether.setMag(tetherLength);
  PVector.add(heroLoc, tether, eyeCurrent);
  lookCurrent.set(heroLoc);
  camera(eyeCurrent.x, eyeCurrent.y, eyeCurrent.z, 
    lookCurrent.x, lookCurrent.y, lookCurrent.z, 
    up.x, up.y, up.z);
}

void draw() {
  if (pressed[UP]) {
    heroLoc.sub(heroMoveSpeed * sin(heroRot), 0.0, heroMoveSpeed * cos(heroRot));
  }

  if (pressed[LEFT]) {
    heroRot += heroRotSpeed;
  }

  if (pressed[RIGHT]) {
    heroRot -= heroRotSpeed;
  }

  if (pressed[DOWN]) {
    heroLoc.add(heroMoveSpeed * sin(heroRot), 0.0, heroMoveSpeed * cos(heroRot));
  }

  // Set target locations.
  lookTarget.set(heroLoc);
  tether.setMag(tetherLength);
  PVector tetherPos = PVector.add(heroLoc, tether, 
    new PVector());

  // Smooth from current to target.
  smoothStep(lookCurrent, lookTarget, lookSmoothing, lookCurrent);
  smoothStep(eyeCurrent, tetherPos, moveSmoothing, eyeCurrent);

  camera(eyeCurrent.x, eyeCurrent.y, eyeCurrent.z, 
    lookCurrent.x, lookCurrent.y, lookCurrent.z, 
    up.x, up.y, up.z);
  directionalLight(255.0, 255.0, 255.0, 
    0.0, 8.0, -0.6);
  background(0xff000000);

  // Draw reference points.
  strokeWeight(1.0);
  for (int i = 0; i < 50; ++i) {
    float iPrc = i * 0.020408163;
    float mz = lerp(-width, width, iPrc);
    for (int j = 0; j < 50; ++j) {
      float jPrc = j * 0.020408163;
      float mx = lerp(-width, width, jPrc);
      float kPrc = (iPrc + jPrc) * 0.5;
      stroke(lerpColor(0x7fff0000, 0x7f0000ff, kPrc, HSB));
      line(mx - 5.0, 0.0, mz, mx + 5.0, 0.0, mz);
      line(mx, 0.0, mz - 5.0, mx, 0.0, mz + 5.0);
    }
  }

  pushMatrix();
  translate(heroLoc.x, heroLoc.y, heroLoc.z);
  rotateY(heroRot);
  stroke(0xffff0000);
  line(0.0, 0.0, 0.0, 50.0, 0.0, 0.0);
  stroke(0xff00ff00);
  line(0.0, 0.0, 0.0, 0.0, -50.0, 0.0);
  stroke(0xff0000ff);
  line(0.0, 0.0, 0.0, 0.0, 0.0, -50.0);
  noStroke();
  box(50.0);
  popMatrix();

  surface.setTitle(String.format("%.2f", frameRate));
}

void keyPressed() {
  pressed[keyCode] = true;
}

void keyReleased() {
  pressed[keyCode] = false;
}

static PVector smoothStep(PVector a, PVector b, float t, PVector out) {
  if (t <= 0.0) return out.set(a);
  if (t >= 1.0) return out.set(b);
  return lerpUnclamped(a, b, t * t * (3.0 - 2.0 * t), out);
}

static PVector lerpUnclamped(PVector a, PVector b, float t, PVector out) {
  float u = 1.0 - t;
  return out.set(
    u * a.x + t * b.x, 
    u * a.y + t * b.y, 
    u * a.z + t * b.z);
}

Now the question is, how can the camera’s looking forward match the blue line in the example, the forward of the avatar.

1 Like

The camera() function is a tool in Processing, if I ever move on to other languages I won’t understand anything from it if I just use a function that does it for me. The “hard work” you talked about- that’s what I’m trying to understand. The basic principles behind making something like a camera, I want to make all that on my own.
If I wanted to make a 3D game and not worry about any graphics, I could just use something like Unity and get it over with, but I’m trying to understand how it works.

As for the “tank controls”- that’s exactly what I was looking for. Thank you.

I’m using a float to track the angle of rotation (separate to the camera angle), but when I try to do what you did, which is just

if(up){
  x+=speed*cos(rot);
  y+=speed*sin(rot);
} else if(down){
  x-=speed*cos(rot);
  y-=speed*sin(rot);
}

It kind of works, but for some reason the character is “vibrating” in a motion that appears to be a sin/cos graph/wave. The interesting part is that it is actually somewhat moving in the correct direction. There’s probably something small I’m missing here, but I’ll look up some guides on coding “tank controls”.

Once again, thank you!

Okay, after looking around for 20 minutes I still can’t find anything even remotely related to a “tank control” coding guide.

I can’t figure it out. I practically copied your code over and it still does the exact same thing that my code did. I don’t know what makes the box in your code turn and move in the direction it is facing, because mine seems to move completely randomly.

I just don’t know what’s going on anymore. I have no clue what it could be, because the code for the actual movement is identical.

I know that sin(rotationAngle) returns a number between -1 and 1. But this number goes from -1 to 1 in a single degree, unlike it is supposed to! I tried mulling it over as hard as I could and I’m honestly getting a headache from it. It’s just a dumb simple problem, but I can’t figure it out. Even literally copying the code and changing the variable names doesn’t solve it. I don’t get it. It makes zero sense.

Could you (or anyone else who is willing), instead of showing code of your own, see if you can fix that small part of my code so it does what it’s supposed to? Just so I can see what I did wrong. The code in question is the move() function at the end.

Here’s the latest code:
https://pastebin.com/pMXpB8c4

Cos and sin need values as radians, not degree

Use cos(radians(angle

I did, it broke everything

latest code:
https://pastebin.com/j74Q9PpM

EDIT: found the issue.
the issue was that when i was doing

loc.sub(spd*sin(radians(frotZ)), 0,0, spd*cos(radians(frotZ)));

it was actually changing the Z value instead of Y, so the fix was

loc.sub(spd*sin(radians(frotZ)), spd*cos(radians(frotZ)));

Once again, thanks to everyone for the replies.

1 Like

Can you please post the last working version?

I’ll post my latest version. This version has some additional features, such as auto-adjusting the cam, jumping, moving in multiple directions while being able to hold right click to turn the player, etc.
All the code is commented so it should be pretty clear as to what it does, I put it on github because I also included a floor texture:

The actual calculation for the movement happens at the very end (which was the issue of this topic), if anyone is only interested in that.

Thank you very much. I appreciate it!

what is the difference between adjust camera true and false please?

Chrisir

@Chrisir if adjust camera is on, the camera will always be aligned with where the player is looking.

It’s a setting for the game World of Warcraft which I was basing the camera off of, so I decided to include it.

It just determines which side of the camera “anchor point” you’re closer to, and either increments or decrements the angle until it is 0