Suggestions for Better Camera Movements with Mouse

Hey everyone, so I am currently attempting a third person camera movement with mouse scrolling inputs and I am hoping some people could give some suggestions here.

To preface this post, I want to say that I have looked into multiple older forums posts here for suggestions but I cannot find a solution to the current problem I seem to be having, so perhaps an alternative way to code this would be beneficial. However, if you do know of a post/code snippet that would help, do not hesitate to post it! Additionally, I will first explain what my code is doing for a TLDR format and then also post my code below for others that want to take a deep dive into this. I am grateful for any and all suggestions and critiques!

Code Intention:
Basically, I am viewing my person from a third-person view with a dummy node positioned at the base of the character. When I move my mouse to the left or to the right, I want the to update the dummy node and so, the camera (the child node) to turn accordingly and obviously stop when the mouse stops, with the mouse always being at the center of the screen. That way it never leaves the screen and my character can turn indefinitely.

Code Application:
What the code currently does is it finds the center of the screen and saves that as a variable. Then, whenever it detects movement in the positive or negative direction from the center, it will rotate the dummy node, thus rotating my camera, and then at the end of the frame, it will restore the mouse pointer to the center of the screen.

Code Issue:
While it will turn, the turning is extremely choppy. So lets say I update the rotation by 1 degree, when it recognizes the mouse movement, this makes the turn only update my 1 degree each frame, then resets the mouse pointer, making the movement extremely slow.
Now, if I try and change it by more than 1 degree, if will be extremely choppy/jump and give no room for accuracy in a third person shooter e.g. I move left and my cursor/crosshairs jump way past the target.
So then I thought, ā€œwell, what if I use a counter, and once it recognizes, say left movement, it continues to turn left through 15 frames, i.e. my counter set to 15.ā€ Well, as you would expect, although this makes the camera fluid, it also means I have to ā€˜waitā€™ until 15 frames until the camera comes to a stop. No good.
So my last thought was to make a loop that goes from X to a certain amount of degree turn to make it more fluid. But then again, if I execute the loop, it still goes through a single frame and is the same as just adding, say 15 degrees like setH(getH + 15)

Code:
#Initialized at the start of the program
self.MouseCounter = 3
self.LookLeft = False
self.LookRight = False
self.MouseSensitivity = 10
X = int(base.win.getXSize()/2)
Y = int(base.win.getYSize()/2)
base.win.movePointer(0, X, Y)
self.mouseCenter = [X,Y]

#Code Being updated:
if base.mouseWatcherNode.hasMouse():
XDiff = base.mouseWatcherNode.getMouseX()

#This is just for updating movement for more than one frame but is currently not in use
if XDiff == 0:
self.MouseCounter += 1
if self.MouseCounter > 1:
self.MouseCounter = 0
self.LookRight = False
self.LookLeft = False

        if XDiff < 0:
            self.LookLeft = True
            self.LookRight = False
            base.win.movePointer(0, self.mouseCenter[0], self.mouseCenter[1])
            
        if XDiff > 0:
            self.LookRight = True
            self.LookLeft = False
            base.win.movePointer(0, self.mouseCenter[0], self.mouseCenter[1])
        
        if self.LookLeft:
            self.dummyNode.setH(self.dummyNode.getH()+1)
            self.Orientation = self.dummyNode.getH()
            self.RealAxisX = sin(self.Orientation*pi/180+pi)
            self.RealAxisY = cos(self.Orientation*pi/180)
            self.RealAxisX2 = sin((self.Orientation+90)*pi/180+pi)
            self.RealAxisY2 = cos((self.Orientation+90)*pi/180)

        if self.LookRight:
            self.dummyNode.setH(self.dummyNode.getH()-1) 
            self.Orientation = self.dummyNode.getH()
            self.RealAxisX = sin(self.Orientation*pi/180+pi)
            self.RealAxisY = cos(self.Orientation*pi/180)
            self.RealAxisX2 = sin((self.Orientation+90)*pi/180+pi)
            self.RealAxisY2 = cos((self.Orientation+90)*pi/180)
            
        return Task.cont

Hereā€™s a quick video of whatā€™s happening:

There are two issues that I see here, I think:

First, movement by a fixed value will tend to be choppy:

In general, the time between one frame and the previous (the delta-time, or dt) will vary. Since speed = distance / time, if the distance remains the same, the speed will vary with the time, and thus be uneven.

This can be rectified by mutliplying the movement-distance by the dt, thus cancelling out the contribution of time and producing an even speed.

(Another solution is to set a fixed frame-rateā€“as long as the machine successfully meets that value.)

Second and similarly, when moving a mouse, the distance that it moves will vary from one input to the next.

(This is in part because of the variability of the dt: even if the mouse is being moved at an exactly even speed, the variation of the dt will result in variation of movement-distance.) Thus a fixed movement-distance doesnā€™t represent the movement of the mouse all that well.

The solution, then:

To start with, Iā€™m presuming that you want the camera to move with a speed that matches that of the mouseā€“that is, that fast mouse-movements should produce fast camera-movements, and slow mouse-movements should produce slow camera-movements. If Iā€™m mistaken in that, just fix the first issue above, and you should be fine!

Now, as noted, the movement of the mouse already includes an element of delta-time. As a result, the first issue noted above is, in a sense, already accounted forā€“we donā€™t need to fix that. (I mentioned it primarily because itā€™s something that can be easily missed in other types of movement.)

Fixing the second issue then is simple enough: we want the movement of the camera to be representative of the movement of the mouse (presumably), so we simply multiply the movement-distance by the mouse-offset. Something like this:

turnSpeed = 10.0
myNodePath.setH(myNodePath, turnSpeed*mousePos)
1 Like

Hey Thaumaturge, thank you for such a detailed and fast response as always!

So, I believe I am having trouble effectively implementing exactly what you described. Let me explain what I believe your solution is and then you can tell me where I am going wrong. So it seems that you are saying to fix the choppiness and also to represent an accurate mouse movement to camera movement, I should implement two variables, time/frame and DX (distance of mouse movement), which in the case of my code is named XDiff.

So then, I would use these two variables for updating my turn speed. However, I cant necessarily see how this will fix it because the way I still update the turn is like this with a setH

self.dummyNode.setH(self.dummyNode.getH()+50000dtabs(XDiff))
#The 50000 had to be included because dt and XDiff are generally small decimals

So no matter what I do, it still goes from lets say 35 degrees, to 42 degrees for a given frame. The variables that I am multiplying are additive (even if now they do represent actual mouse movements), so it still ā€œjumpsā€ from one degree to another per frame. I tried something like

myNodePath.setH(myNodePath, turnSpeed*mousePos)

But it actually tilted and turned my camera for some reason, rather than just turning it.

Itā€™s my pleasure. :slight_smile:

Not quite: as I said above, the mouse-position already includes an element of timeā€“after all, the more time that passes between one frame and the next, the further the mouse will be moved during that frame. Thus you donā€™t need to include the dt when implementing mouse-movement like thisā€“itā€™s already there, in a way.

Hmmā€¦ Iā€™m not quite sure of what problem youā€™re describing here.

The angle in question should change its value in a given frame (although Iā€™d expect non-integer values, given the use of a dt-value).

Also, which variables are additive in your code?

It may be that your NodePath has a bit of a tilt or roll, causing its local frameā€“its ā€œperspectiveā€ to have rotated axes, and thus a relative rotation to be not quite what youā€™re expecting.

In that case, something like " myNodePath.setH(myNodePath.getH() + turnSpeed*mousePos) "ā€“or even, if called for, " myNodePath.setH(render, myNodePath.getH(render) + turnSpeed*mousePos) "ā€“might be more reliable.

~

All that said, perhaps a fuller example might be helpful. It might look something like this:

if base.mouseWatcherNode.hasMouse():
    XDiff = base.mouseWatcherNode.getMouseX()
    base.win.movePointer(0, self.mouseCenter[0], self.mouseCenter[1])
else:
    XDiff = 0
    # Just a precaution, in case we >don't< have the mouse

turnSpeed = 5.0
# This may not be a good value; experiment with it

self.dummyNode.setH(self.dummyNode.getH() + turnSpeed*XDiff)

Note that since the origin for the mouse-position lies at the centre of the screen, it will be negative when the mouse is to the left and positive when the mouse is to the right. (I think that itā€™s that way around, at least.) As a result, the sign of XDiff should indicate the direction in which we want to turn.

This means that we can control the direction in which we turn simply by applying XDiff (multiplied by a scalar as appropriate): when XDiff is negative the angle will be reduced, thus turning us one way, and when XDiff is positive the angle will be increased, thus turning us the other.

There is one caveat: if you find your character rotating in the opposite direction to the intended, reverse the sign of XDiffā€“that is, subtract it instead of adding it.

Ah apologies, yes that does make sense.

So, what I am trying to say is this is what I believe is making it look so ā€œchoppy.ā€ True, it will be non-integer values, but I think going from a large change in degree of rotation in a single frame is what is causing the choppiness.

So, when I said ā€œadditiveā€ I meant the

Where weā€™re adding turnspeed*XDiff to the current angle. So whether the value is something like 5.3234 degrees or 8.3434 etc, it still adds the value to the angle in one frame, making it ā€œjump.ā€

Okay, so I super simplified my code to what you have written here. I obviously had overly complex at the beginning haha. Here is what it looks like with the updated code

I doubt itā€“both slow changes and quick ones can be smooth.

Ah, I see. No, thatā€™s a perfectly standard approach, that can be quite smooth, I do believe.

Consider this: each frame, the program runs an updateā€“and in general, nothing happens in-between. That means that, in general, all changes happen in short ā€œjumpsā€ā€“theyā€™re not continuous.

Thus we want to crate changes that take into account these ā€œjumpsā€: since a certain amount of time has been ā€œaddedā€ to the clock, we want to ā€œaddā€ to our positions, rotations, etc.

Looking at thatā€“and stepping through frame-by-frameā€“it looks like, for some reason, the rotation isnā€™t being performed every frame. It looks like the camera turns in one frame, after which a few frames pass with no change, before the next frame in which the camera turns.

If Iā€™m correct, then thatā€™s not likely to do with the maths being used for your rotations, in and of itselfā€“instead, it seems like for some reason that maths isnā€™t being run every frame.

Perhaps an issue with your task, or some piece of code thatā€™s only conditionally running your rotation code?

1 Like

Ugh yeah of course lol. I guess I was trying to force my explanation because thats all I could think of. Fairly illogical on my part.

So, youā€™re certainly correct. After looking into it, for some reason my ā€œXDiffā€ that updates the position, only changes approximately once every 15 frames. Look at what happens when I print it out

-0.023750007152557373
0.0
0.0
0.0
0.0
0.0
0.
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
-0.010531246662139893
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
-0.008148431777954102

The only time I revalue XDiff is at the beginning of the code:

        if base.mouseWatcherNode.hasMouse():
            XDiff = base.mouseWatcherNode.getMouseX()
            base.win.movePointer(0, self.mouseCenter[0], self.mouseCenter[1])
        else:
            XDiff = 0 #Just to be sure I renamed this else statement to just be "pass" and it the problem was still occurring. 
        self.dummyNode.setH(self.dummyNode.getH() + 900*XDiff*-1) 
        self.Orientation = self.dummyNode.getH()

P.S. I used a regular mouse just to insure I am moving it fluidly from on point to the other

Hmm, very odd. I feel like Iā€™ve seen output like that before, but Iā€™m honestly not sure of where, Iā€™m afraid. :/

Perhaps someone else will have some insight!

1 Like

No worries! Youā€™ve been extremely helpful. Iā€™ll continue to look into it and see if Im using faulty logic elsewhere that could be making this happen!

Fair enough! Iā€™m glad if Iā€™ve been of service, and good luck with your investigations!

1 Like

Itā€™s quite hard to tell right now what exactly the problem could be. Since you already simplified your code, could you post a minimal (but completely runnable) sample that shows the same problem?

If thatā€™s too difficult, Iā€™ve made my own sample that I derived from a project of mine (the orbit task does pretty much the same thing as the code that Thaumaturge posted). It loads the environment from the roaming Ralph sample to provide a frame of reference when rotating the camera:

from panda3d.core import *
from direct.showbase.ShowBase import ShowBase


class MyApp(ShowBase):

    def __init__(self):

        ShowBase.__init__(self)

        self.disable_mouse()

        self.environment = loader.loadModel('../samples/Roaming-Ralph/models/world')
        self.environment.reparentTo(render)

        target_pos = Point3(0., 0., 0.)
        self.cam_target = self.render.attach_new_node("camera_target")
        self.cam_target.set_pos(target_pos)
        self.camera.reparent_to(self.cam_target)
        self.camera.set_y(-50.)
        win_props = self.win.get_properties()
        w, h = win_props.get_x_size(), win_props.get_y_size()
        self.win.move_pointer(0, w // 2, h // 2)
        self.turnSpeed = 10.
        self.accept("escape", self.userExit)
        self.task_mgr.add(self.orbit, "orbit")

    def orbit(self, task):
        """
        Orbit the camera about its target point by offsetting the orientation
        of the target node with the mouse motion.

        """

        if self.mouseWatcherNode.has_mouse():
            d_h, d_p = self.mouseWatcherNode.get_mouse() * self.turnSpeed
            target = self.cam_target
            target.set_hpr(target.get_h() - d_h, target.get_p() + d_p, 0.)

        win_props = self.win.get_properties()
        w, h = win_props.get_x_size(), win_props.get_y_size()
        self.win.move_pointer(0, w // 2, h // 2)

        return task.cont


app = MyApp()
app.run()

If the code doesnā€™t do exactly what you want, you might discover what goes wrong when you attempt to gradually adapt it to your own code, checking the results at each step.

2 Likes

Absolutely! Took me a little bit to trim some of the fat, but here it is and the same issue is still present in this code:

from direct.showbase.ShowBase import ShowBase
from panda3d.core import *

class Game(ShowBase):
def init(self):
ShowBase.init(self)
self.accept(ā€œescapeā€, base.userExit)
self.disableMouse()
self.setFrameRateMeter(True)

    self.environment = loader.loadModel("Environment/PracticeMap2")
    self.environment.reparentTo(render)
    self.environment.setPos(0,0,-2)
    
    self.dummyNode = render.attachNewNode("dummyNode")
    self.dummyNode.setPos(-2,-5,2)
    self.cameraNode = base.camera.reparentTo(self.dummyNode)
    self.camera.setPos(.5, -4, 5)
    self.dummyNode.setP(30)
    self.camera.setP(-35)

    self.MouseSensitivity = 10
    X = int(base.win.getXSize()/2)
    Y = int(base.win.getYSize()/2)
    base.win.movePointer(0, X, Y)
    self.mouseCenter = [X,Y]

    self.task_mgr.add(self.Look, "Look")

def Look(self,task):

    if base.mouseWatcherNode.hasMouse():
        XDiff = base.mouseWatcherNode.getMouseX()
        base.win.movePointer(0, self.mouseCenter[0], self.mouseCenter[1])
    else:
        XDiff = 0
    print(XDiff)
    print(self.dummyNode.getH())
    self.dummyNode.setH(self.dummyNode.getH() + 50*XDiff*-1) 
    

    return task.cont

app = Game()
app.run()

I will certainly look through your code in more depth than I have already to try and compare the two and look for an issue. But I thought I would go ahead and post my code as well for you

EDIT - TO BE NOTED
I ran your code, and just removed the map, and I am getting the same issue. So I am guessing somehow its not the code at all, but something else, maybe in the download of my engine or some such that could be causing the issue?

Something that might be worth noting:

If I run your code (omitting the model-loading), I likewise get zero-values.

However, the scene also runs at over two-thousand FPS, if I recall correctly.

And if I then limit the frame-rate to sixty FPS, I suddenly get few to no zero-values. (Save when the mouse is still, of course.)

Now, this suggests that the problem may in part be that the system is polling the mouse at a rate lower than the gameā€™s frame-rate, resulting in zeroes appearing in the frames that occur between mouse-pollings.

However, there is still one issue if this is the source of your problem: based on your previous output and gameplay video, it looks like youā€™re getting non-zero values roughly every fifteen frames, at a frame-rate of sixty. This seems to imply that your mouse is being polled at a rate of once every quarter second, whichā€¦ is an awfully low polling rate, I would thinkā€¦ o_0

1 Like

I limited my FPS in the config file to both 60 and then 30 frames and I am still getting 0ā€™s
(To be precise, at both frame rates, I get a 0 value at every other frame, consistently when moving the mouse)

It definitely does seem like this is the case and I have no clue why it would be nor do I know how I would change the polling rate lol

Hmmā€¦ If you get zeroes once every other frame at both sixty and thirty FPS, that might put a hole in my ā€œmouse polling rateā€ hypothesisā€¦

Oh, one thing worth mentioning: when testing this sort of thing, it might be a good idea to move your mouse in long, quick motions, rather that slow or short ones: doing so should reduce the probability of the mouse actually being still for just a moment, as I think can happen when moving the mouse slowly.

Ok apologies. I am not sure how I got that result, but after retrying @Epihaius code, my own code, and my test code, I cannot seem to get the 0 every other value when restricting my FPS to 60 or 30. I am quite confused how I might have seen that result because it is back to normal. Additionally, for me to actually get it at about 0 at every other frame, I have to go down to about 4 FPS.

1 Like

Down? Thatā€™s very strangeā€“Iā€™d expect to get more zeroes the higher the frame-rate!

Humā€“perhaps someone better-versed in the input system will chime in with more information!

1 Like

Err, so what I meant was that running my game at only 4 frames per second gave me a smaller amount of 0ā€™s, so yes, the higher frame rate I have, the more 0ā€™s.

So, Ive been doing this all on my Macbook. When I loaded this onto my desktop to test it, I didnt get any 0ā€™s and it worked flawlessly. Maybe I will try to reinstall the game engine to see if that fixes anything. Its not with the code at all. It has to be specific to my laptop in some case.

With that being said, I am extremely grateful to both of you for helping me through this process. I would have never have gotten to this conclusion on my own haha

2 Likes

Ahhh, yes, that makes much more sense! Sorry for misreading! ^^;

That is interesting. I donā€™t use Mac myself, so I donā€™t have much insight there Iā€™m afraid. But Iā€™m glad to know that you do at least have a setup in which the code works as expected!

For my part itā€™s my pleasure! :slight_smile:

1 Like

So, considering that the problem is very different than first realized, I am going to make a new forum post, in hopes that I can gain some assistance. I will edit this comment with a link to the new post when I make it. I think the original title of this post has been solved successfully!

1 Like