Making the camera always look at the character *solved*

Hello… I have a problem. I’m making a platformer, with 3d objects, but the gameplay is 2d and takes place on the 2d x&y plane (at a Z of 0). The objects can only move on the x and y axes.

I have the camera at a Z position of 100, and a P of -90 so it is looking straight down the Z axis onto the x&y plane. For the most part, this works just fine. If the character gets too far towards the edge of the screen, the camera starts to move and follow the character.

But, I want the camera to always be looking directly at the character. If the character is off to the side, the camera should tilt a little to the side. Normally, I’d use base.camera.lookAt(character). But, when I use it in this situation, where the camera is looking pretty much straight down the Z axis, it rolls/spins the entire view when the character goes to the side. I guess this makes sense – if the H angle is changed when the camera is looking almost straight down, it will roll the view. So what I want is to use base.camera.lookAt(), but have the camera only rotate on the X and Y axes. If it rotates on the Z axis, it rolls/spins the entire view. It should only tilt a little to the side, not spin the whole view.

So far, trying a few methods, I haven’t had any luck. I thought about reworking the game to use the X & Z axes instead of X & Y, which would eliminate the lookAt problem, but I’m using an ODEPlane2dJoint to keep the objects from moving on the Z axis. And, as far as I know, there is no way to change what axes the ODEPlane2dJoint limits movement to.

Any ideas? I appreciate any help, thanks in advance :slight_smile:

Use base.camera.headsUp() instead of lookAt() to keep the Z axis in alignment.

David

Thank you drwr, though I am a bit lost as far as how to use headsUp().
The reference says “undocumented function”

void heads_up(LMatrix3f &mat, LVector3f const &fwd, LVector3f const &up = ((up())), CoordinateSystem cs = (CS_default));
Undocumented function.

Please explain what arguments I would use to get the effect I want.

AFAIK you can use it the same way as lookAt, with an optional parameter for the up vector.
The API reference doesn’t list overloaded functions correctly.

That doesn’t appear to be what it’s doing. I have the code base.camera.headsUp(self.model), but instead of looking at the character, the camera looks off into blank space. If it was the same as lookAt, shouldn’t at least look at the character?

The angle of the camera using headsUp for a few frames:

VBase3(180,0,0)                                                                                                                                  
VBase3(180,0,0)                                                                                                                                  
VBase3(180,0,0)                                                                                                                                  
VBase3(180,0,0)                                                                                                                                  
VBase3(180,0,0)                                                                                                                                  
VBase3(-179.763,0,0)                                                                                                                             
VBase3(-179.763,0,0)                                                                                                                             
VBase3(-179.126,0,0)                                                                                                                             
VBase3(-178.74,0,0)                                                                                                                              
VBase3(-178.284,0,0)                                                                                                                             
VBase3(-178.284, 0, 0)     

No matter where I move the character, headsUp only appears to modify the H rotation of the camera. P and R are always set to 0. I tried adding a second argument for the up vector, but it still acts the same way as before.

Any ideas?

lookAt() will look at the origin of whatever nodePath you give it. If the origin of your character is at some point away from the visible geometry of your character, your character may not be visible. Also if you lookAt() the getPos() of a nodepath parented to something other than the camera’s parent, the camera may be pointing at the right coordinates… in the wrong coordinate space. If you really want to be unnecessarily certain that lookAt is pointing the right way,

  • mark the origin of your target with something
  • wrtReparentTo() both your target and camera to the same space
  • lookAt() the target nodePath
  • wrtReparentTo() your target and camera to their original parents

Generally, though, I believe Panda successfully resolves coordinate space differences as long as you lookAt() a nodePath rather than a Point3/Vec3.

You may also be running into conflicting camera inputs if you haven’t disabled Panda’s default mouse control. I believe the command is just base.disableMouse(), but I usually need to look it up myself.

lookAt() is working fine. It looks straight at the object, it just doesn’t rotate on the axes I want it to.
headsUp() is what I’m having trouble with (see my above post).

Have you tried passing in a few different ‘up’ vectors, as pro-rsoft suggests? That would give you at least some notion of how it’s behaving. You might even try passing an up vector into LookAt- I seem to recall doing that once or twice for, e.g, going around loops in a 3D racing game.

I tried lookAt(self.model,(0,1,0)) and headsUp(self.model,(0,1,0)) and the behavior of each function did not change. Am I doing this correctly?

Try putting those up-vector triads in a Vec3. I’m not sure if LookAt accepts raw tuples. When in doubt, it’s also a good idea to specify which input each argument is supposed to be. The API ref is surprisingly useless for LookAt, but I’d guess ‘pos’ (or perhaps ‘target’), ‘other’ and ‘up’ are the inputs you’re looking for, and that’s probably the default order it will interpret inputs in if you don’t explicitly name them (e.g. nodepath.setPos(pos=Vec3(x,y,z), up=Vec3(0,1,0), other=targetroot) )

Passing a tuple instead of Vec3 should not make any difference at all.

why is all so complicated? i mean, if it is a sorta 2d scroller the cam just gotta move x,z and y is fix, why it is not just enough to put this in a task

charpos=character.getPos()
cam.setPos(charpos[0],fixedYdistance, charpos[2])

:question:

Because he may want to not scroll all over the place past the edges of the level, and he may want to play with different angles than just-straight-down to enhance the use of 3D and/or for cinematic reasons. It should be very possible to get the camera to behave as desired, it’s just a matter of finding the correct way to fill in the egregious gaps in the API reference. Otherwise, heck, why not just prerender everything and do it all in render2D, or rearrange the game to operate on Panda’s default x/z view plane with the default up-vector? :stuck_out_tongue:

Also, depending on when the task is scheduled relative to other tasks and screen refresh, there may be some lag or choppyness in extreme scenarios. It might be better to just parent the camera to the character (perhaps indirectly, to account for character rotation) if you wanted to take that approach.

Another thing to check might be that you aren’t gimbal-locking your camera with some previous rotation before the LookAt, although I’ve never run into that sort of problem in Panda, and I’m not even sure it’s possible in this context.

ah now I get what he’s going to make
the cam just have to stay still, the background and the character moves: and the background gotta start to move when the character reach a certain x position from the origin towards the outer screen limits (i.e +12 o -12) and at that point the x motion of the character stops and vice-versa when the character backs off that position toward the center of the screen the background stops and the character moves. No lookAt needs here.

Unless, as I said, you want some creative/cinematic camera work or really leverage the 3D in your level design.

Ever played, say, MegaMan X8? Or NiGHTS Into Dreams? The gameplay is fundamentally 2D, some levels are even almost completely planar, but there are still points where the camera stops simply tracking the character at fixed angle and instead rotates to face the character from a fixed position, putting the level in some perspective or other. There are also places where the level turns a corner “into” or “out of” the screen and the camera rotates to keep up the illusion of a mechanically 2D playfield.

I guess Duke nukem manhatthan project I played was a cool example of what you mean but we’re talking about finished and polished games. d41 is at the beginning and so he gotta make things roughly works first, then he have plenty of time to add fancy things afterwards (unless he’s actually into a challenge with thin amount of time). FWIK, keep things simple at start is a good motto for see the end of a project, don’t you agree?

Eh, particularly if it’s a 1 or 2 line difference, I tend to prefer to build the flexible system in from the outset, just so I’m more confident nothing will break by changing it later. And as I said, Panda should be able to do just what is needed, by design even. It’s fair to put off the problem until other groundwork is laid, but I’m kinda curious at this point as to why it’s not just working.

Wow, I never knew you could name the parameters like that. Anyway, after playing around with it, I finally got it to work with base.camera.lookAt(point=Point3(self.model.getPos()),up=Vec3(0,1,0))
I also found out if you try something like “base.camera.lookAt(abc=0)” it will give you an error message outlining all the different sets of arguments you can feed into the function, like so:

TypeError: Arguments must match one of:
lookAt(non-const NodePath this, const Point3 point)
lookAt(non-const NodePath this, const NodePath other)
lookAt(non-const NodePath this, const Point3 point, const Vec3 up)
lookAt(non-const NodePath this, const NodePath other, const Point3 point)
lookAt(non-const NodePath this, const NodePath other, const Point3 point, const Vec3 up)
lookAt(non-const NodePath this, float x, float y, float z)
lookAt(non-const NodePath this, const NodePath other, float x, float y, float z)

Thanks everyone for your help :slight_smile:

Well, that’s what I had at first. The game has physics based obstacles, which makes the character move in circles and other dizzying motions. When the camera moves with the same motions as the characters, it was dizzying and not pleasant to watch.

Well, that’s what I did next, and it worked nicely. I thought it would be cool if the camera tilted to the side when the camera wasn’t directly behind the character. This would take advantage of the 3d, and I thought it would be easy enough to do.

I’m only trying to make a small minigame. Also, I consider getting the camera to at least follow the character as one of the beginning parts of making the game (or good enough not to give me a headache while testing :wink: ). I though lookAt would make the camera follow the character nicely, and it’s only one line of code. BTW, I have no time limit for this game, it’s just something I’m working on for fun in my free time.

I couldn’t get ODE to work on the x&z plane only. The 2d joint limits it to the x&y plane and there’s no way AFAIK to change which axes it limits it to.

Yeah, that was why.

Having the camera look directly at the character wasn’t a real important addition, I just wanted to know why it wouldn’t work, because I thought it would be extremely simple (and it is, now that I figured it out)

Again, thanks everyone :slight_smile:

The other neat trick that sort of syntax enables (or drives from, depending on how you look at it) is, e.g.

def myMethod(self, arg1=default1, arg2=default2):

which gives arg1 and arg2 fixed values unless the user inputs their own.

You can even mix’n’match, e.g.

def myMethod(self, arg1, arg2=default2):

which will force the user to enter a first argument, but make the second optional (with a default value if omitted). The only caveat is that, for disambiguation, I believe all arguments without default values must come before arguments with defaults.

Using the None type as an argument’s default value will let you do checks as to whether or not a non-default value is being given, and react accordingly.