天天看点

SDL游戏开发之七-虚拟摇杆1. Game类2. ShotStick 摇杆类3. Player类

当下的智能机除了音量键、Home键外,几乎没有多余的按键,因而有一部分游戏提供了虚拟摇杆。

SDL2.x并没有提供虚拟摇杆相关的代码,不过实现起来并不算困难;虚拟摇杆包括绘图和事件处理:前者提供视觉效果,后者则捕获事件并作出响应。

示例结果如下:

SDL游戏开发之七-虚拟摇杆1. Game类2. ShotStick 摇杆类3. Player类

图1-虚拟摇杆演示程序

 本示例中大约有四个类:

  1. Game类 主要包含了初始化、事件处理、纹理绘制等功能,是本示例的核心类;
  2. Player类 玩家类,摇杆控制玩家进行移动,主要负责显示;
  3. ShotStick类 虚拟摇杆类,接收事件并处理事件;
  4. 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所示的半透明背景内。只有在背景圆内才表示一次有效的按键。

SDL游戏开发之七-虚拟摇杆1. Game类2. ShotStick 摇杆类3. Player类

图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);
}
           
SDL游戏开发之七-虚拟摇杆1. Game类2. ShotStick 摇杆类3. Player类

图3 偏移的计算

如图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()
{
}
           

代码:https://github.com/sky94520/ShotStick

继续阅读