當下的智能機除了音量鍵、Home鍵外,幾乎沒有多餘的按鍵,因而有一部分遊戲提供了虛擬搖桿。
SDL2.x並沒有提供虛擬搖桿相關的代碼,不過實現起來並不算困難;虛擬搖桿包括繪圖和事件處理:前者提供視覺效果,後者則捕獲事件並作出響應。
示例結果如下:
本示例中大約有四個類:
- Game類 主要包含了初始化、事件處理、紋理繪製等功能,是本示例的核心類;
- Player類 玩家類,搖桿控制玩家進行移動,主要負責顯示;
- ShotStick類 虛擬搖桿類,接收事件並處理事件;
- TextureManager類 紋理管理單例類,負責加載紋理,並把圖片按照鍵值對保存。
在SDL遊戲開發之六-簡單的SDL程序一節中,有對Game類以及SDL的遊戲流程做了一個簡單的介紹,對於Game類的結構在這裏不再贅述。
1. Game類
首先簡單地說明一下Game的函數。
1.1 Game::init
bool Game::init(const char *title, int xpos, int ypos, int width, int height, int flags)
{
m_bRunning = false;
if (SDL_Init(SDL_INIT_EVERYTHING) == 0)
{
/// if succeeded create our window
m_pWindow = SDL_CreateWindow(title, xpos, ypos, width, height, flags);
if (m_pWindow != NULL)
m_pRenderer = SDL_CreateRenderer(m_pWindow, -1,SDL_RENDERER_ACCELERATED|SDL_RENDERER_PRESENTVSYNC);
if (m_pRenderer != NULL)
SDL_SetRenderDrawColor(m_pRenderer,210,250,255,255);
else
return false;
}
else
return false;
先對SDL庫進行初始化,然後創建了窗口和渲染器。
m_bRunning = true;
std::string platform = SDL_GetPlatform();
// init
if (platform == "Android") {
SDL_GetWindowSize(m_pWindow,&m_gameWidth,&m_gameHeight);
}
else {
m_gameWidth = width;
m_gameHeight = height;
}
SDL_Log("width=%d, height=%d\n", m_gameWidth, m_gameHeight);
在桌面操作系統下如windows,SDL會根據傳遞的窗口大小進行創建窗口;而對於搭載了android系統等的手機來說,其窗口大小就是手機的分辨率。(當然也是可以通過SDL_SetScale設置縮放比,不過會造成圖片不同程度的拉伸)
//結合SDL_Renderer
TheTextureManager::Instance()->bind(m_pRenderer);
/*加載圖片資源*/
try
{
TheTextureManager::Instance()->load("Resources/icon.png","player");
TheTextureManager::Instance()->load("Resources/shotStick1.png","shotStick1");
TheTextureManager::Instance()->load("Resources/shotStick.png","shotStick2");
}
catch (std::runtime_error& e)
{
std::cout << e.what() << std::endl;
return false;
}
m_pPlayer = new Player();
m_pPlayer->init("player");
m_pShotStick = new ShotStick();
m_pShotStick->init("shotStick1", "shotStick2");
return true;
在Game::init函數的後半段,先是加載了所需要的圖片資源,然後創建了一個玩家對象和虛擬搖桿對象。
1.2 Game::render
void Game::render()
{
SDL_SetRenderDrawColor(m_pRenderer,210,250,255,255);
///clear the renderer to the draw color
SDL_RenderClear(m_pRenderer);
///draw
SDL_SetRenderDrawColor(m_pRenderer, 0, 0, 0, 255);
SDL_Rect rect = { 0, 0, 200, 200 };
SDL_RenderDrawRect(m_pRenderer, &rect);
m_pPlayer->draw(m_pRenderer);
m_pShotStick->draw(m_pRenderer);
///draw to the screen
SDL_RenderPresent(m_pRenderer);
}
Game::render()負責渲染。後繪製的圖片可能會遮擋之前繪製的圖片,所以需要確認好繪製次序(其中的幾句代碼還繪製了一個黑色矩形,與本例無關,只是提醒在SDL中繪製圖形,需要先把渲染器的繪製顏色和清屏顏色不同才行)
1.3 Game::handleEvents
void Game::handleEvents()
{
SDL_Event event;
while (SDL_PollEvent(&event))
{
switch (event.type)
{
case SDL_QUIT:
m_bRunning = false;
break;
}
m_pShotStick->handleEvents(&event);
}
auto velocity = m_pShotStick->getVelocity();
m_pPlayer->setVelocity(velocity);
}
Game::handlerEvents()負責處理。有事件發生時則會交給搖桿對象進行處理,之後再根據搖桿對象的偏移程度來更改操作玩家對象。
1.4 Game::update
void Game::update()
{
m_pPlayer->update();
}
Game::handleEvents()只是負責設置玩家的速度,在update函數中還需要把速度乘以時間轉爲距離。
2. ShotStick 搖桿類
2.1 初始化函數
bool ShotStick::init(const std::string& rockerID, const std::string& backgroundID)
{
m_rockerID = rockerID;
m_backgroundID = backgroundID;
//獲取搖桿rect
SDL_Rect rect1 = TheTextureManager::Instance()->getTextureRectFromId(rockerID);
SDL_Rect rect2 = TheTextureManager::Instance()->getTextureRectFromId(backgroundID);
//圓半徑
m_outCircle.radius = rect1.w/2;
m_inCircle.radius = rect2.w/2;
return true;
}
init函數會保存搖桿和搖桿背景圖片的鍵名,以便於確定位置和進行繪製。
2.2 繪製函數
void ShotStick::draw(SDL_Renderer * ren)
{
// 畫出虛擬搖桿畫出外圓
int screenW = TheGame::getInstance()->getGameWidth();
int screenH = TheGame::getInstance()->getGameHeight();
// 圓心
m_inCircle.x = m_outCircle.x = m_outCircle.radius;
m_inCircle.y = m_outCircle.y = screenH - m_outCircle.radius;
TheTextureManager::Instance()->draw(m_rockerID
,m_outCircle.x - m_outCircle
.radius,m_outCircle.y - m_outCircle.radius);
// 畫出內圓
TheTextureManager::Instance()->draw(m_backgroundID
,m_inCircle.x + m_relativePoint.x - m_inCircle.radius
,m_inCircle.y + m_relativePoint.y - m_inCircle.radius);
}
虛擬搖桿默認顯示在左下角。
2.3 ShotStick::handleEvents
void ShotStick::handleEvents(SDL_Event* event)
{
switch (event->type)
{
case SDL_MOUSEBUTTONDOWN:
{
if (event->button.button != SDL_BUTTON_LEFT)
break;
Sint32 x = event->motion.x, y = event->motion.y;
auto distance = std::sqrt(std::pow(x - m_outCircle.x, 2) + std::pow(y - m_outCircle.y, 2));
//超出距離
if (distance > m_outCircle.radius) {
m_fingerId = -1;
break;
}
else
m_fingerId = 0;
}
當鼠標左鍵按下時會觸發SDL_MOUSEBUTTONDOWN並且event->button.button的值是SDL_BUTTON_LEFT。
爲便於在電腦上調試,虛擬搖桿會接收鼠標左鍵事件,判斷按下的點是否在圖2所示的半透明背景內。只有在背景圓內才表示一次有效的按鍵。
case SDL_MOUSEMOTION:
{
if (m_fingerId == -1)
break;
Sint32 x = event->motion.x, y = event->motion.y;
//保證不會超過搖桿背景圓
double x_average = x - m_outCircle.x;
double y_average = y - m_outCircle.y;
double d = std::sqrt(std::pow(x_average, 2) + std::pow(y_average, 2));
double m = d > m_outCircle.radius ? m_outCircle.radius : d;
m_relativePoint.x = m * (x_average / d);
m_relativePoint.y = m * (y_average / d);
}break;
當處理SDL_MOUSEBUTTONDOWN事件中產生了一個有效的點擊後,再發生移動則會計算搖桿的圓心到搖桿背景圓心的距離,得到的值保存到m_relativePoint中。
case SDL_MOUSEBUTTONUP:
{
if (event->button.button != SDL_BUTTON_LEFT)
break;
m_fingerId = -1;
m_relativePoint.x = 0.0;
m_relativePoint.y = 0.0;
}break;
當產生鼠標左按鍵鬆開後,搖桿歸零。
2.4 ShotStick::getVelocity
Vector2D ShotStick::getVelocity()
{
double x = m_relativePoint.x;
double y = m_relativePoint.y;
double r = std::sqrt(x * x + y * y);
if (r == 0)
return Vector2D(0, 0);
/* sin = y/r; cos = x/r; */
r = m_outCircle.radius;
return Vector2D(x / r, y / r);
}
如圖3所示,O2減去O1則是m_relativePoint的值,再根據勾股定理則可以知道r的值。當r==0時,m_relativePoint=(0, 0);在r != 0後,又把r賦值爲搖桿背景圓的半徑大小,這樣能保證其值域在[0, 1]之間。這樣做的好處就是返回的速度不僅和方向有關,還和速度有關。當不爲0時,則返回歸一化後的值(值域[0, 1])。
後續還有則是在移動平臺下的處理,效果同上類似,應該沒什麼問題(很久之前測試過)
case SDL_FINGERDOWN:
{
SDL_Finger finger;
finger.x = event->tfinger.x * TheGame::getInstance()->getGameWidth();
finger.y = event->tfinger.y * TheGame::getInstance()->getGameHeight();
// 綁定有效id
if (m_fingerId == -1
&& std::sqrt(std::pow(finger.x - m_outCircle.x, 2) +
std::pow(finger.y - m_outCircle.y,
2)) <= m_outCircle.radius)
m_fingerId = event->tfinger.fingerId;
}
case SDL_FINGERMOTION:
{
SDL_FingerID id = event->tfinger.fingerId;
// 如果不相等,退出
if (m_fingerId != id)
break;
SDL_Finger finger;
finger.id = id;
finger.x = event->tfinger.x * TheGame::getInstance()->getGameWidth();
finger.y = event->tfinger.y * TheGame::getInstance()->getGameHeight();
double x_average = finger.x - m_outCircle.x;
double y_average = finger.y - m_outCircle.y;
double d = std::sqrt(std::pow(x_average, 2) + std::pow(y_average, 2));
double m = d > m_outCircle.radius ? m_outCircle.radius : d;
m_relativePoint.x = m * (x_average / d);
m_relativePoint.y = m * (y_average / d);
}break;
case SDL_FINGERUP:
{
SDL_FingerID id = event->tfinger.fingerId;
// 如果爲有效id
if (id == m_fingerId)
{
m_relativePoint.x = 0;
m_relativePoint.y = 0;
m_fingerId = -1;
}
}break;
移動平臺下和桌面操作系統類似,只不過移動平臺下一般爲多點觸碰,所以也是需要綁定SDL_Finger的id的(類似於只有鼠標左鍵才能操作)。
3. Player類
玩家類則相對比較簡單,只是負責顯示精靈和確認位置而已。
bool Player::init(const string& spriteID)
{
m_spriteID = spriteID;
return true;
}
void Player::setVelocity(const Vector2D& velocity)
{
m_velocity = velocity;
}
void Player::draw(SDL_Renderer*ren)
{
TheTextureManager::Instance()->draw(m_spriteID,(int)m_position.getX(),(int)m_position.getY());
}
void Player::update()
{
m_velocity *= 2;
m_position += m_velocity;
//m_velocity += m_acceleration;
}
void Player::clean()
{
}