In this part of the Cocos2d-x tutorial series we are going to take a look at what’s involved in handling keyboard events. If you went through the mouse/touch tutorial, a lot of this is going to seem very familiar, as the process is quite similar. That said, keyboard handling does have it’s own special set of problems to deal with.
Let’s jump straight in to an example. Once again I assume you already know how create your own AppDelegate, if you can’t, I suggest you jump back to this part first.
Handling Keyboard Events
Our first example is simply going to respond to WASD and Arrow keys to move the Cocos2d-x logo around the screen. In this example I made no special modifications to a standard scene, so the header is unchanged from previous tutorials.
KeyboardScene.cpp
#include "KeyboardScene.h" USING_NS_CC; Scene* KeyboardScene::createScene() { auto scene = Scene::create(); auto layer = KeyboardScene::create(); scene->addChild(layer); return scene; } bool KeyboardScene::init() { if ( !Layer::init() ) { return false; } auto sprite = Sprite::create("HelloWorld.png"); sprite->setPosition(this->getContentSize().width/2, this->getContentSize().height/2); this->addChild(sprite, 0); auto eventListener = EventListenerKeyboard::create(); eventListener->onKeyPressed = [](EventKeyboard::KeyCode keyCode, Event* event){ Vec2 loc = event->getCurrentTarget()->getPosition(); switch(keyCode){ case EventKeyboard::KeyCode::KEY_LEFT_ARROW: case EventKeyboard::KeyCode::KEY_A: event->getCurrentTarget()->setPosition(--loc.x,loc.y); break; case EventKeyboard::KeyCode::KEY_RIGHT_ARROW: case EventKeyboard::KeyCode::KEY_D: event->getCurrentTarget()->setPosition(++loc.x,loc.y); break; case EventKeyboard::KeyCode::KEY_UP_ARROW: case EventKeyboard::KeyCode::KEY_W: event->getCurrentTarget()->setPosition(loc.x,++loc.y); break; case EventKeyboard::KeyCode::KEY_DOWN_ARROW: case EventKeyboard::KeyCode::KEY_S: event->getCurrentTarget()->setPosition(loc.x,--loc.y); break; } }; this->_eventDispatcher->addEventListenerWithSceneGraphPriority(eventListener,sprite); return true; }
When run, you see the logo centered and can move it around using either WASD or arrow keys.
The code works almost identically to our earlier Touch examples. You create an EventListener, in this case a EventListenerKeyboard, implement the onKeyPressed event handler. The first paramater passed in is the EventKeyboard::KeyCode enum, which is a value representing the key that was pressed. The second value was the Event target, in this case our sprite. We use the Event pointer to get the target Node and update it’s position in a direction depending on which key is pressed. Finally we wire up our scene’s _eventDispatcher to receive events. Nothing really unexpected here.
Polling the Keyboard
You may however ask yourself… what If I want to poll for keyboard events? For example, what if you wanted to check to see if the spacebar was pressed at any given time?
Short answer is, you can’t. Cocos2d-x is entirely event driven.
Long answer however is, it’s relatively easy to roll your own solution, so let’s do that now. I’ll jump right in with the code and discuss it after.
KeyboardScene.h
#pragma once #include "cocos2d.h" #include <map> class KeyboardScene : public cocos2d::Layer { public: static cocos2d::Scene* createScene(); virtual bool init(); bool isKeyPressed(cocos2d::EventKeyboard::KeyCode); double keyPressedDuration(cocos2d::EventKeyboard::KeyCode); CREATE_FUNC(KeyboardScene); private: static std::map<cocos2d::EventKeyboard::KeyCode, std::chrono::high_resolution_clock::time_point> keys; cocos2d::Label * label; public: virtual void update(float delta) override; };
KeyboardScene.cpp
#include "KeyboardScene.h" USING_NS_CC; Scene* KeyboardScene::createScene() { auto scene = Scene::create(); KeyboardScene* layer = KeyboardScene::create(); scene->addChild(layer); return scene; } bool KeyboardScene::init() { if ( !Layer::init() ) { return false; } label = cocos2d::Label::createWithSystemFont("Press the CTRL Key","Arial",32); label->setPosition(this->getBoundingBox().getMidX(),this->getBoundingBox().getMidY()); addChild(label); auto eventListener = EventListenerKeyboard::create(); Director::getInstance()->getOpenGLView()->setIMEKeyboardState(true); eventListener->onKeyPressed = [=](EventKeyboard::KeyCode keyCode, Event* event){ // If a key already exists, do nothing as it will already have a time stamp // Otherwise, set's the timestamp to now if(keys.find(keyCode) == keys.end()){ keys[keyCode] = std::chrono::high_resolution_clock::now(); } }; eventListener->onKeyReleased = [=](EventKeyboard::KeyCode keyCode, Event* event){ // remove the key. std::map.erase() doesn't care if the key doesnt exist keys.erase(keyCode); }; this->_eventDispatcher->addEventListenerWithSceneGraphPriority(eventListener,this); // Let cocos know we have an update function to be called. // No worries, ill cover this in more detail later on this->scheduleUpdate(); return true; } bool KeyboardScene::isKeyPressed(EventKeyboard::KeyCode code) { // Check if the key is currently pressed by seeing it it's in the std::map keys // In retrospect, keys is a terrible name for a key/value paried datatype isnt it? if(keys.find(code) != keys.end()) return true; return false; } double KeyboardScene::keyPressedDuration(EventKeyboard::KeyCode code) { if(!isKeyPressed(EventKeyboard::KeyCode::KEY_CTRL)) return 0; // Not pressed, so no duration obviously // Return the amount of time that has elapsed between now and when the user // first started holding down the key in milliseconds // Obviously the start time is the value we hold in our std::map keys return std::chrono::duration_cast<std::chrono::milliseconds> (std::chrono::high_resolution_clock::now() - keys[code]).count(); } void KeyboardScene::update(float delta) { // Register an update function that checks to see if the CTRL key is pressed // and if it is displays how long, otherwise tell the user to press it Node::update(delta); if(isKeyPressed(EventKeyboard::KeyCode::KEY_CTRL)) { std::stringstream ss; ss << "Control key has been pressed for " << keyPressedDuration(EventKeyboard::KeyCode::KEY_CTRL) << " ms"; label->setString(ss.str().c_str()); } else label->setString("Press the CTRL Key"); } // Because cocos2d-x requres createScene to be static, we need to make other non-pointer members static std::map<cocos2d::EventKeyboard::KeyCode, std::chrono::high_resolution_clock::time_point> KeyboardScene::keys;
And when you run it:
So, what are we doing here? Well essentially we record key events as they come in. We have two events to work with, onKeyPressed and onKeyReleased. When a key is pressed, we store it in a std::map, using the KeyCode as the key and the current time as the value. When the key is released, we remove the released key from the map. Therefore at any given time, we know which keys are pressed and for how long. In this particular example, in the update() function ( ignore that for now, I’ll get into it later! ) we poll to see if the Control key is pressed. If it is, we find out for how long and display a string.
So, even though polling isn’t built in to Cocos2d-x, it is relatively easy to add.
Dealing with Keyboards on Mobile Devices
So, what about keyboards on mobile devices? All Android phones and iOS devices are able to display a Soft Keyboard ( the onscreen keyboard ), can we use it? The answer is… sort of.
What’s about physical keyboards on mobile devices?
You may be wondering, how does a physical keyboard on a mobile device work with Cocos2d-x? In the case of an iPad, the answer is, it doesn’t. When I hooked up a Bluetooth Keyboard, absolutely nothing happened. The same occurred when I paired the keyboard to my Android phone. However, I do not have an Android device with a physical keyboard, such as the Asus Transformer, but my gut says it wouldn’t work either. At least, not with you doing a lot of legwork that is
Sort of isn’t really a great answer so I will go into a bit more detail. Yes you can use the soft keyboard, but in a very limited manner. Basically you can use it for text entry only. Truth is though, this should be enough, as controlling a game using a soft keyboard would be a horrid experience.
Let’s take a look at an example using TextFieldTTF and implementing an TextFieldDelegate:
KeyTabletScene.h
#pragma once #include "cocos2d.h" class KeyTabletScene : public cocos2d::Layer, public cocos2d::TextFieldDelegate { public: virtual ~KeyTabletScene(); virtual bool onTextFieldAttachWithIME(cocos2d::TextFieldTTF *sender) override; virtual bool onTextFieldDetachWithIME(cocos2d::TextFieldTTF *sender) override; virtual bool onTextFieldInsertText(cocos2d::TextFieldTTF *sender, const char *text, size_t nLen) override; virtual bool onTextFieldDeleteBackward(cocos2d::TextFieldTTF *sender, const char *delText, size_t nLen) override; virtual bool onVisit(cocos2d::TextFieldTTF *sender, cocos2d::Renderer *renderer, cocos2d::Mat4 const &transform, uint32_t flags) override; static cocos2d::Scene* createScene(); virtual bool init(); CREATE_FUNC(KeyTabletScene); };
KeyTabletScene.cpp
#include "KeyTabletScene.h" USING_NS_CC; Scene* KeyTabletScene::createScene() { auto scene = Scene::create(); auto layer = KeyTabletScene::create(); scene->addChild(layer); return scene; } bool KeyTabletScene::init() { if ( !Layer::init() ) { return false; } // Create a text field TextFieldTTF* textField = cocos2d::TextFieldTTF::textFieldWithPlaceHolder("Click here to type", cocos2d::Size(400,200),TextHAlignment::LEFT , "Arial", 42.0); textField->setPosition(this->getBoundingBox().getMidX(), this->getBoundingBox().getMaxY() - 20); textField->setColorSpaceHolder(Color3B::GREEN); textField->setDelegate(this); this->addChild(textField); // Add a touch handler to our textfield that will show a keyboard when touched auto touchListener = EventListenerTouchOneByOne::create(); touchListener->onTouchBegan = [](cocos2d::Touch* touch, cocos2d::Event * event) -> bool { try { // Show the on screen keyboard auto textField = dynamic_cast<TextFieldTTF *>(event->getCurrentTarget()); textField->attachWithIME(); return true; } catch(std::bad_cast & err){ return true; } }; this->_eventDispatcher->addEventListenerWithSceneGraphPriority(touchListener, textField); return true; } KeyTabletScene::~KeyTabletScene() { } bool KeyTabletScene::onTextFieldAttachWithIME(TextFieldTTF *sender) { return TextFieldDelegate::onTextFieldAttachWithIME(sender); } bool KeyTabletScene::onTextFieldDetachWithIME(TextFieldTTF *sender) { return TextFieldDelegate::onTextFieldDetachWithIME(sender); } bool KeyTabletScene::onTextFieldInsertText(TextFieldTTF *sender, const char *text, size_t nLen) { return TextFieldDelegate::onTextFieldInsertText(sender, text, nLen); } bool KeyTabletScene::onTextFieldDeleteBackward(TextFieldTTF *sender, const char *delText, size_t nLen) { return TextFieldDelegate::onTextFieldDeleteBackward(sender, delText, nLen); } bool KeyTabletScene::onVisit(TextFieldTTF *sender, Renderer *renderer, const Mat4 &transform, uint32_t flags) { return TextFieldDelegate::onVisit(sender, renderer, transform, flags); }
And when you run it:
Essentially when the user touches the screen, we display the onscreen keyboard with a call to attachWithIME(), the rest is handled by the textfield.
I have a sneaking feeling this method is going to be depreciated at some point in the future, being replaced by cocos::ui classes, but for now it works just fine. For the record, it is actually possible to force up the onScreen keyboard by calling Director::getInstance()->getOpenGLView()->setIMEKeyboardState(true), but it seemingly pushes your scene to the background, so isn’t a viable option for controlling a game. I was going to look into a work around but then thought, really… this is a downright stupid thing to do. Doing anything other than text entry with a soft keyboard is just a bad idea.