Controlling walk, sprint, run with one key using events

I’ve noticed I’m now the last three posts in scripting issues… not sure if that’s a bad thing or not. Anyway.

I’m scripting a character controller and I want, when the player moves forward pressing ‘W’, to be able to toggle the speed by pressing ‘Q’. So if the player is walking, and they press q, they will start running, then sprinting, then back to running, then walking.

In diagram: Walking <–> Running <–> Sprinting

I’m struggling with getting the event handlers and the methods to line up correctly, trying to place event handlers in the methods leads to the following error:

method not callable in accept (ignoring)

class DragonPlayer(gameobject.GameObject):

    def __init__(self):

	    super().__init__(1000)

	    self.actor = Actor('low_poly_dragon.egg',{
		    'Idle':'low_poly_dragon-Idle.egg',
		    'Walk':'low_poly_dragon-Walk.egg',
		    'Run':'low_poly_dragon-Run.egg',
		    'Sprint':'low_poly_dragon-Sprint.egg',
		    'Flap':'low_poly_dragon-Flap.egg',
		    'Glide':'low_poly_dragon-Glide.egg'})
	
	    camera = base.camera
	    base.disableMouse()
	    camera.reparentTo(self.actor)
	    camera.setPos(self.actor, 0, 40, 30)
	    camera.setH(180)
	    camera.setP(-30)

	    texture = loader.loadTexture('home_background.JPG')
	    self.actor.setTexture(texture)

	    self.actor.setPos(100,100,200)
	    self.actor.reparentTo(render)

	    self.actor.loop("Idle")

	    PlayerControls(self.actor)

    def movePlayer(self, task):

	    actor = self.actor
	    startpos = actor.getPos()

class PlayerControls():

    def __init__(self, player):

	    self.player = player
	    self.walkForce = 10
            self.runForce = 20
	    self.sprintForce = 40
	    self.jumpForce = 50
	    self.flapForce = 100

	    running = False
	    sprinting = False

	    bnode = BulletRigidBodyNode("player")
	    self.player.attachNewNode(bnode)

	    player.accept("w", self.move)
	    #player.accept("a", self.move)
	    #player.accept("s", self. move)
	    #player.accept("d", self.move)
	    #player.accept("space-repeat", self.move)
	    #player.accept("space-up", self.move)
	    player.accept("q", self.togglespeed)

    def move(self):

	    player = self.player

	    if player.accept('w', self):

		    #run
		    if running:

			    player.loop("Run")

		    #sprint
		    elif sprinting:

			    player.loop('Sprint')

		    #walk
		    else:

			    player.loop('Walk')

    def togglespeed(self):

	    player = self.player

	    if player.accept('q', self):

		    running = not running

		    if q and running:

			    sprinting = not sprinting

What are you trying to do with this line?

if player.accept('q', self):

The accept() method doesn’t return anything, nor is self in this case a function that can be called as a result of the event being thrown.

Furthermore, the running and sprinting variables in your methods aren’t actually bound; they will just disappear at the end of the method. You should store them on the controller itself, ie. via self.running.

In your case, where your player is transitioning between different states, I would advise you to make the player inherit from FSM (finite state machine). This will let you tell it which state to switch to, and you can define methods that contain what to do when switching to a state (for example to play or stop an animation).

Then, what you just need to do is set up some event handlers that will transition the player to the appropriate state when the keys are pressed. For example, if holding down W should make it move, but Q toggles sprinting on/off, you could do this:

class PlayerControls(DirectObject):
    def __init__(self):
        self.player = player
        # ...
        self.sprinting = False

        self.accept("w", self.walk)
        self.accept("w-up", self.stopWalk)
        self.accept("q", self.toggleSprint)

    def walk(self):
        if self.sprinting:
            self.player.request("Sprint")
        else:
            self.player.request("Walk")

    def stopWalk(self):
        self.player.request("Idle")

    def toggleSprint(self):
        self.sprinting = not self.sprinting

        # If it's currently walking or sprinting, transition it to the right state
        # Calling walk() is an easy way to do this
        if self.player.state != "Idle":
            self.walk()

As displayed, I would also suggest just making PlayerControls inherit from DirectObject, and just using self.accept. After all, DirectObject is about receiving events, and it’s the controller that is interested in receiving the events. But that’s a minor organisatorial point.

Thank you! Only problem is nothing happens when I hit “q.” I’ve tried to only toggle running to see if it’s my code but q doesn’t do anything. If you don’t mind will you also explain where “.request” and “.state” are coming from? They both give me errors.

EDIT: I think the reason q isn’t working is because the script won’t accept multiple events at a time. I discovered this while trying to play the forward animation while turning the character at the same time. I’ve updated my code to match.

class PlayerControls(DirectObject):

def __init__(self, player):

	self.player = player
	self.turnDegree = 1.5

	self.running = False
	self.sprinting = False

	self.accept("w", self.forward)
	self.accept("w-up", self.stopforward)
	self.accept("a-repeat", self.turnleft)
	self.accept("d-repeat", self.turnright)
	self.accept("space", self.flap)
	self.accept('space-up', self.stopforward)
	self.accept("space-repeat", self.glide)
	self.accept("q-up", self.togglespeed)

def forward(self):

	player = self.player
	running = self.running
	sprinting = self.sprinting

	#run
	if running:

		player.loop("Run")

	#sprint
	#elif sprinting:

		#player.loop('Sprint')

	#walk
	else:

		player.loop('Walk')

def stopforward(self):

	self.player.loop("Idle")

def togglespeed(self):

	running = self.running
	#sprinting = self.sprinting

	running = not running

	#if running:

		#sprinting = not sprinting

     #### stops if "w" is pressed
     # also experiences delay before start not found on others
def turnright(self):

	currentHeading = self.player.getH()
	self.player.setH(currentHeading + self.turnDegree)

def turnleft(self):

	currentHeading = self.player.getH()
	self.player.setH(currentHeading - self.turnDegree)

Your code doesn’t really look anything like mine, and also, this does absolutely nothing:

	running = self.running

	running = not running

You’re just copying the value into a local variable called running, and inverting that, but you’re not actually storing the result back in self.running, nor are you changing the animation when q is pressed. In my example, it does store the result back into the self.sprinting variable:

        self.sprinting = not self.sprinting

As for request and state, these are part of FSM, based on my suggestion to make your player class inherit from FSM as well. However, that’s optional for now, but you will need to know whether the player is currently walking so that you can know what animation to switch to when you toggle sprinting.

Regarding the following:

I think that the reason for this is that it’s driven by a “repeat” key-event. If I’m not much mistaken, that means that the event isn’t fired when the key is initially pressed, but only after it’s held for a moment.

I’m not sure of whether this is tied to the OS’s key-repetition logic, but I wouldn’t be surprised if it was. Either way, that same logic might provide an example of what is happening:

In any text-field on your computer (be it in a word-processor, or a search bar, or the entry in which you type replies here, or whatever), press a letter-key and hold it. You should see the letter appear once, then a pause, then a stream of the same letter appearing rapidly. That pause is (at least analogous to) the pause that you’re seeing, I think.

I’m guessing that what you want is for the character to turn as the key is held. What I suggest for that is that you store the state of the control–i.e. whether it’s pressed or not–and then use a task (see this page, and especially the one after) to check it each frame, and respond accordingly.

On another note, it may be easier to take a look at the Roaming Ralph sample program, and study how it handles movement. As an exercise to help you learn how events and tasks work together, you can try to modify Roaming Ralph to handle such a setting to toggle between walking and running.

Alright, I’ll do that. Thanks to both of you! (and happy holidays!)