- Home /
(C#) A better way to limit actions to once per button press?
Hello, I'm working on a simple cannabalt-style platformer where the character jumps (as most platforming characters do). Up until now, I've been using a slightly modified version of the basic character controller found on the Unity Scripting Reference:
using UnityEngine;
using System.Collections;
public class movement : MonoBehaviour {
public float speed = 6.0F;
public float jumpHeight = 8.0F;
public float gravity = 20.0F;
private Vector3 moveDirection = Vector3.zero;
void Update() {
CharacterController controller = GetComponent<CharacterController>();
if (controller.isGrounded) {
moveDirection = Vector3.forward;
moveDirection = transform.TransformDirection(moveDirection);
moveDirection *= speed;
if (Input.GetButton("Jump"))
moveDirection.y = jumpHeight;
}
moveDirection.y -= gravity * Time.deltaTime;
controller.Move(moveDirection * Time.deltaTime);
}
}
For the most part, this worked alright, but my problem with it was that as long as you held down the jump button, the character would keep jumping, but I only want him to jump once per button press. I decided to change the code to this:
using UnityEngine;
using System.Collections;
public class movement : MonoBehaviour {
public float speed = 6.0F;
public float jumpHeight = 8.0F;
public float gravity = 20.0F;
private bool doJump = true;
private Vector3 moveDirection = Vector3.zero;
void Update() {
CharacterController controller = GetComponent<CharacterController>();
if (Input.GetButtonUp("Jump"))
{
doJump = true;
}
if (controller.isGrounded) {
moveDirection = Vector3.forward;
moveDirection = transform.TransformDirection(moveDirection);
moveDirection *= speed;
if (Input.GetButtonDown("Jump") && (doJump = true))
{
moveDirection.y = jumpHeight;
doJump = false;
}
}
moveDirection.y -= gravity * Time.deltaTime;
controller.Move(moveDirection * Time.deltaTime);
}
}
For the most part, adding the boolean check seemed to be working fine. The character only jumped once per pressing of the button, and it looked like things were improved. After further testing though, I found a new problem. What's happening is that if I hit the jump button right after the character lands, it sometimes fails to register at all and the jump doesn't activate. I tested several times to make sure that something was wrong with the game and that I wasn't simply hitting the button before he landed completely, and it indeed fails to activate within the first split second or so of landing. Since this is an endless runner, a single missed jump means death, which will be very frustrating if it's not the player's fault. My question is, how could I better go about limiting the game to one jump per button press without causing the controls to become glitchy and sometimes unresponsive?
if (Input.GetButtonDown("Jump") && (doJump = true))
should be
if (Input.GetButtonDown("Jump") && (doJump == true)) //(missing a "=")
or better yet
if (Input.GetButtonDown("Jump") && doJump) //means the same thing
(not sure if that could be the issue tho..)
also, using doJump is kind of pointless here anyway, since your jump code only happens if grounded anyway, so you could just get rid of that altogether..
also, you don't need to call this line every frame:
CharacterController controller = GetComponent<CharacterController>();
you could declare "controller" outside of the function, and use Start() to initialize it:
CharacterController controller;
void Start(){
controller = GetComponent<CharacterController>();
}
(this to save on performance..)
Thanks for the response. I followed your advice to remove doJump altogether and while I'm getting the desired effect of only having one jump per press, the problem of occasionally not registering persists, although it seems to be happening less frequently now. I also moved the Getcomponent to the Start void like you suggested, and that's working well.
Hmm, well, it would seem that controller.isGrounded may not be going to true quite as early as you would like.. $$anonymous$$aybe your character controller capsule is a bit too high? You might try lowering that a bit, maybe your character is just riding a little bit into the ground, making him appear to be grounded a bit earlier than the CAPSULE becomes grounded... If this doesn't do it, things get a bit more complicated..
Also, your character doesn't have a rigidbody, correct?
After going over the program some more, I agree that it's most likely a problem with the capsule as opposed to the code. I did some more testing to try and find what specific situations the jumping doesn't trigger, and I've found that it's right when I'm at the very edge of a platform. It seems like the very bottom of the Capsule has passed over the edge, but the curved part hasn't yet, so sort of like you said it looks like it's grounded when it's really not. I think switching to a box collider will probably solve the problem, since that has a perfectly flat bottom, and since the gameplay is strictly 2D, the capsule might be overkill anyhow. I'm reasonably sure the box collider will probably solve the problem, so I'm going to say yours the right answer. Thanks so much for all of your help!
Answer by Seth-Bergman · Oct 03, 2012 at 03:16 PM
OK, you've got me thinking now..
This is actually kind of tricky to get working as you want. Now, going along with your idea of using a box collider, the way to try this is:
1: create a NEW, empty game object, and give that a box collider.
2: adjust size/position of the box collider to proper position (line it up in the scene with character's feet)
3: child the object to the player
4: add this script to the new object (make sure it is childed directly to the player object which contains the other script):
using UnityEngine;
using System.Collections;
public class GroundedCheck : MonoBehaviour {
public movement otherScript;
void Start(){
GameObject myParent = transform.parent.gameObject;
otherScript = myParent.GetComponent<movement>();
}
void OnCollisionEnter(Collision other){
if(other.gameObject.tag == "ground")
otherScript.canJump = true;
}
void OnCollisionExit(Collision other){
if(other.gameObject.tag == "ground")
otherScript.canJump = false;
}
}
5: NOW, we are planning to use this var "canJump" INSTEAD of controller.isGrounded, to decide whether we can jump.. So we will update the other script:
using UnityEngine;
using System.Collections;
public class movement : MonoBehaviour {
public boolean canJump = true;
public float speed = 6.0F;
public float jumpHeight = 8.0F;
public float gravity = 20.0F;
private Vector3 moveDirection = Vector3.zero;
private CharacterController controller;
void Start(){
controller = GetComponent<CharacterController>();
}
void Update() {
if (controller.isGrounded) {
moveDirection = Vector3.forward;
moveDirection = transform.TransformDirection(moveDirection);
moveDirection *= speed;
}
if (Input.GetButtonDown("Jump"))
{
if(canJump || controller.isGrounded)
{
moveDirection.y = jumpHeight;
canJump = false;
}
}
moveDirection.y -= gravity * Time.deltaTime;
controller.Move(moveDirection * Time.deltaTime);
}
}
6: Add a RigidBody to the ground, check "is kinimatic", uncheck "use gravity".
7: finally, you'll need to tag your ground objects as "ground".
ok...
This should all work assuming you can only be in contact with one "ground" at a time... but if you have separate "pieces" of ground which can be touched simultaneously, I imagine there could be issues.
This may or may not yield the results you are after..
This seems like it's going to work, and I won't have the character in contact with more than one ground at a time, but I'm getting several error messages with the code in regards to the line: "otherScript = transform.parent.gameObject.GetComponent(movement);"
I'm getting: "Expression denotes a `type', where a `variable', `value' or `method group' was expected"
"The best overloaded method match for `UnityEngine.GameObject.GetComponent(System.Type)' has some invalid arguments"
"Argument `#1' cannot convert `object' expression to type `System.Type'"
"error CS0103: The name `otherScript' does not exist in the current context"
oops, sorry! I'm used to javascript..
change it to:
otherScript = transform.parent.gameObject.GetComponent<movement>();
I'll fix it in the answer...
also, I have inconsistent capitalization.. so change the line:
movement otherscript;
to
movement otherScript;
That's solved several of the errors, but now I'm getting "The type arguments for method `UnityEngine.GameObject.GetComponent()' cannot be inferred from the usage. Try specifying the type arguments explicitly" for that line and I'm still getting "The name `otherScript' does not exist in the current context" as well
hmmm, maybe try splitting it up:
void Start(){
GameObject myParent = transform.parent.gameObject;
otherScript = myParent.GetComponent<movement>();
}
In your start add:
if(otherScript)print("Working");
if it does not print it means the GetComponent does not find the object. You could try:
otherScript = transform.parent.gameObject.GetComponent<movement>() as movement;
or
otherScript = GameObject.Find("ParentObjectName").GetComponent<movement>();
Answer by MibZ · Oct 03, 2012 at 06:59 PM
A much simpler and guaranteed effective solution: previous and current button states.
EDIT: Simplified this a little bit, I realized you don't need the Start function for this.
private bool prevSpacebarState = false;
private bool currSpacebarState = false;
void Update()
{
//Neither state has been updated since the last tick, so right now current is the value we want in previous
//Assign current (of last tick) into previous, then update current
prevSpacebarState = currSpacebarState;
currSpacebarState = Input.GetKey(KeyCode.Space);
if (!prevSpacebarState && currSpacebarState)
Jump();
}
rather than all this, using Get$$anonymous$$eyDown:
function Update(){
if(Input.Get$$anonymous$$eyDown($$anonymous$$eyCode.Space))
Jump();
}
would yield exactly the same results.. but unfortunately does not solve the issue at hand...
Wouldn't that code execute Jump each tick space is held down? Previous and current states are the simplest way of limiting actions to once per down - you can hold the key down for an hour and the character will only jump once, how is the issue not solved?
Thanks for responding. The issue of one jump per press has been solved. It turns out that the main issue I was having was actually with colliders, which is what I'm trying to work through with Seth Bergman.
@$$anonymous$$ibZ - Input.Get$$anonymous$$ey returns true every frame it is held down
Input.Get$$anonymous$$eyDown, on the other hand, returns true ONLY the first frame the button is pushed. It is basically a predefined button state condition, identical to what you are suggesting..
@Seth Bergman Ah, that's right. Last time I used the prev/curr was in an XNA game, and that function is the reason why. Slipped my $$anonymous$$d. Disregard my foolishness :p
Your answer
Follow this Question
Related Questions
Help With Simple Jump Script 1 Answer
The Mario Jump? 2 Answers
Can't Set Animator Boolean Parameter C# 1 Answer