quick-cocos2d-x从零开始游戏开发笔记(四):参照Flappy Bird制作第一个游戏②

主角有了,下面应该参照原作让它上下微微的起伏。

在MenuScene:ctor()方法里面继续添加代码:

local function titleBirdMove(dt)
local sequence = transition.sequence({
	CCMoveTo:create(0.5, CCPoint(display.cx, display.cy + 55)),
	-- CCDelayTime:create(0.5),
	CCMoveTo:create(0.5, CCPoint(display.cx, display.cy + 45)),
})
sprite:runAction(sequence)
end
local scheduler = require(cc.PACKAGE_NAME .. ".scheduler")
local birdUpDown = scheduler.scheduleGlobal(titleBirdMove, 0.5)

其中需要注意的是scheduler这个对象的使用是需要添加引用的,也就是local scheduler = require(cc.PACKAGE_NAME .. ".scheduler")这行代码。

这里很想吐槽一下quick-x的wiki文档是在是太简陋了。好多具体用法都不是太详细或者干脆没有,好多时候都需要看源码或者上百度查。

F5运行一下player,发现bird是上下移动了,但是没有向前的感觉。

为什么呢?地板没有移动!没有参照物!该添加地板,并且让地板不停的向左移动了。

我最开始的做法是设定一个循环方法,添加两个地板的对象,第二个地板对象排列在第一个地板对象的后面,然后两个地板图片同时向左移动,移动一个图片的距离之后,充值两个图片的初始坐标重新开始向左移动,后来运行的时候觉得有点卡顿的感觉。上网查了些资料改成了根据逐帧的方法来改变图片的位置。这样看起来效果比较平滑,图片切的好的话是无缝的,看不出来图片的切换。

local floor = display.newSprite("#move_bottom.png", display.cx, display.bottom + 30)
local floor2 = display.newSprite("#move_bottom.png", display.cx + 336, display.bottom + 30)

local function floorMove(dt)			-- 地板循环移动造成前进的感觉
	if floor:getPositionX() <= -floor:getContentSize().width*0.5 then
			floor:setPosition(floor:getContentSize().width*1.5 - 2, display.bottom + 30)
	else floor:setPosition(floor:getPositionX() - 2, display.bottom + 30) end 

	if floor2:getPositionX() <= -floor2:getContentSize().width*0.5 then 
			floor2:setPosition(floor2:getContentSize().width*1.5 - 2, display.bottom + 30)
			else floor2:setPosition(floor2:getPositionX() - 2, display.bottom + 30)
	end	
end
	
local FLOOR_MOVE = scheduler.scheduleUpdateGlobal(floorMove)
self:addChild(floor)
self:addChild(floor2)

然后加入一个按钮,用来开始游戏,跳转到MainScene界面。

	local function onBtnStartClicked(tag)		
		app:EnterMainScene()
		scheduler.unscheduleGlobal(birdUpDown)
		scheduler.unscheduleGlobal(FLOOR_MOVE)
	end
 
	local btnStart = ui.newImageMenuItem({
		image = "#bird_start_btn.png",
		imageSelected = "#brid_start_btn_pressed.png",
		x = display.cx,
		y = display.cy - 20,
		listener = onBtnStartClicked
	})
	local menu = ui.newMenu({btnStart})
	self:addChild(menu)

app:EnterMainScene()这个方法是写在MyApp.lua里面的,还有一个EnterMenuScene()方法,是用来回到Menu页面的,一起写在这里了:


最终的Menu页面效果如图,点击按钮就可以开始游戏了


终于来到主界面了。是不是有点小激动了呢?

加入背景图片,这次我们选夜晚那张,和白天那张区别开来

function MainScene:ctor()
      self.bg = display.newSprite("#bird_bg_night.png", display.cx, display.cy) 
      self:addChild(self.bg)
end

下面参照MenuScene,把界面画好

	self.getReady = display.newSprite("#get_ready.png", display.cx, display.top - 125)
	self:addChild(self.getReady)

	local frames = display.newFrames("bird%01d.png", 1, 3)
	self.sprite = display.newSprite(frames[1])
	self.sprite:zorder(3)
	local animation = display.newAnimation(frames, 1 / 7)   --1秒播放3帧 
	self.HERO_ACTION = self.sprite:playAnimationForever(animation)    -- 循环播放动画
	self.sprite:setPosition(display.cx - 55, display.cy)
	-- sprite:setScale(1)   -- 设定精灵大小

	local function titleBirdMove(dt)
	  local sequence = transition.sequence({
	      CCMoveTo:create(0.5, CCPoint(display.cx - 55, display.cy + 5)),
	      -- CCDelayTime:create(0.5),
	      CCMoveTo:create(0.5, CCPoint(display.cx - 55, display.cy - 5)),
	  })
	  self.sprite:runAction(sequence)
	end
	self.birdUpDown = scheduler.scheduleGlobal(titleBirdMove, 1)    -- 精灵上下微动

	self:addChild(self.sprite)

	self.tip = display.newSprite("#bird_tip.png", display.cx, display.cy - 10)
	self.tip:addTo(self)

	self.floor = display.newSprite("#move_bottom.png")
	self.floor:setPosition(self.floor:getContentSize().width* 0.5, display.bottom + 30)
	self.floor2 = display.newSprite("#move_bottom.png")
	self.floor2:setPosition(self.floor2:getContentSize().width* 1.5, display.bottom + 30)

	local function floorMove(dt)      -- 地板循环移动造成前进的感觉
	  local sp1 = self.floor    
	  if sp1:getPositionX() <= -sp1:getContentSize().width*0.5 then
	    sp1:setPosition(sp1:getContentSize().width*1.5 - 2, display.bottom + 30)
	  else sp1:setPosition(sp1:getPositionX() - 2, display.bottom + 30) end 

	  local sp2 = self.floor2
	  if sp2:getPositionX() <= -sp2:getContentSize().width*0.5 then 
	    sp2:setPosition(sp2:getContentSize().width*1.5 - 2, display.bottom + 30)
	    else sp2:setPosition(sp2:getPositionX() - 2, display.bottom + 30)
	  end
	end

	self.FLOOR_MOVE = scheduler.scheduleUpdateGlobal(floorMove)
	self:addChild(self.floor, 2)
	self:addChild(self.floor2, 2)

F5看一下效果:


根据原作的逻辑,在屏幕上任意地方点击一下,游戏就开始了。那么我们首先要添加对屏幕的触摸监听事件:

在MainScene:ctor()里面添加touchLayer

	-- touchLayer 用于接收触摸事件
	self.touchLayer = display.newLayer()
	self:addChild(self.touchLayer)

并且添加MainScene的onEnter事件和onTouch事件

function MainScene:onEnter()
	-- 注册touch事件处理函数
	self.touchLayer:addTouchEventListener(function(event, x, y)
		return self:onTouch(event, x, y)
	end)
	self.touchLayer:setTouchEnabled(true)
end


function MainScene:onTouch(event, x, y)
	if self.FIRST_TAP then
		self:InitGame()
		self.CollisionDetection = scheduler.scheduleUpdateGlobal(function() self:UpdateGame() end)
	end
	
	if STATE_GAME_OVER == true then
		return false
	end


	GRAVITY = -2 								-- 重力恢复
	-- audio.playEffect(MUSIC.wing, false)			-- 播放挥动翅膀音效


	scheduler.unscheduleGlobal(self.birdUpDown)


	transition.stopTarget(self.sprite)
	transition.execute(self.sprite, CCRotateTo:create(0.2, -30), {
		-- delay = 0.1,
		easing = "backout",
		onComplete = function()
			local sequence = transition.sequence({
				CCDelayTime:create(0.4),
				CCRotateTo:create(0.6, 90)
			})
			self.sprite:runAction(sequence)
		end,
	})
	self.HERO_ACTION = self.sprite:playAnimationForever(display.newAnimation(display.newFrames("bird%01d.png", 1, 3), 1 / 7))


	if self.sprite:getPositionY() > display.height then
		self.sprite:setPositionY(display.height + 20)
	end
end
在onTouch方法里面需要判断是否是第一次点击屏幕,如果是的话,需要初始化一些游戏的内容,开始添加障碍物,并且开始碰撞检测。字体文件是我在sample里面随便找的,需要拷到res文件夹里面,一共两个文件,futura-48.fnt、futura-48.png。本来想用原版的字体,没去找字体制作的软件。亏我还把资源里面的原版字体一个个抠出来了……

function MainScene:InitGame()
	if self.FIRST_TAP ~= true then return false end

	self.tip:zorder(-1)
	self.getReady:zorder(-1)

	self.passNum = ui.newBMFontLabel({
		text = "0",
		font = "futura-48.fnt",
		x = display.cx,
		y = display.top - 60,
	})
	self:addChild(self.passNum, 4)

	function Gravity()	-- HERO持续受到的重力
		GRAVITY = GRAVITY - 0.15
		self.sprite:setPositionY(self.sprite:getPositionY() + GRAVITY)
		if self.sprite:getPositionY() <= 105 then 		-- 如果飞的过低,游戏结束			
			STATE_GAME_OVER = true
		end
	end

	self.heroGravity = scheduler.scheduleUpdateGlobal(Gravity)

	local function addPipes()	-- 每1.5秒在一定范围内随机加入一对水管,但水管之间的距离不变
		self.pipes_up = display.newSprite("#obstacle_up.png", display.right + 50, display.cy + 140 +  math.random(0, 200))
		self.pipes_down = display.newSprite("#obstacle_down.png", display.right + 50, self.pipes_up:getPositionY() - 400)
		self.pipes_up:addTo(self)
		self.top_pipes[#self.top_pipes + 1] = self.pipes_up
		self.pipes_down:addTo(self, 1)
		self.bottom_pipes[#self.bottom_pipes + 1] = self.pipes_down
	end

	self.ADD_PIPES = scheduler.scheduleGlobal(addPipes, 1.5)
	self.FIRST_TAP = false
end
UpdateGame()方法里面东西比较多,在里面有很重要的方法getBoundingBox(),这个方法返回的对象通过intersectsRect(target)方法就能实现碰撞检测了。

实现小鸟的点击跳跃没有使用MoveTo,而是在UpdateGame()里面给小鸟一个持续向下的重力

GRAVITY = GRAVITY - 0.15

同时让小鸟有一个 持续向上的移动速度,这个速度保持不变

self.sprite:setPositionY(self.sprite:getPositionY() + 5.1)

由于这个方法是逐帧实行的,所以很快向下的速度就会超过向上的速度。每次点击屏幕的时候,就初始化小鸟的重力,始向下的速度小于向上的速度,小鸟就会猛的向上飞一段距离,然后快速向下坠。

之所以没有使用MoveTo动画是因为这样看起来动画效果更平滑,而且现实中重力是递增的,符合物理规律。

function MainScene:UpdateGame()
	GRAVITY = GRAVITY - 0.15 					-- HERO持续受到的重力
	self.sprite:setPositionY(self.sprite:getPositionY() + GRAVITY)
	if self.sprite:getPositionY() <= 105 then 		-- 如果飞的过低,游戏结束			
		STATE_GAME_OVER = true
	end
	self.sprite:setPositionY(self.sprite:getPositionY() + 5.1)

	local rHero = self.sprite:getBoundingBox()

	for k,v in pairs(self.top_pipes) do
		v:setPositionX(v:getPositionX() - 2)
		local rtop_Pipes = v:getBoundingBox()
		local bump = rHero:intersectsRect(rtop_Pipes)
		if bump then
			STATE_GAME_OVER = true
		end
		if v:getPositionX() < -30 then
			v:removeSelf()
			table.remove(self.top_pipes, k)
		end			
	end	

	for k,v in pairs(self.bottom_pipes) do
		v:setPositionX(v:getPositionX() - 2)
		local rbottom_Pipes = v:getBoundingBox()
		local bump = rHero:intersectsRect(rbottom_Pipes)
		if v:getPositionX() + 20 < self.sprite:getPositionX() then
			if PASS_NUMBER < 10 then
				self.passNum:setString(string.format("%01d", PASS_NUMBER))
			elseif PASS_NUMBER < 100 then
				self.passNum:setString(string.format("%02d", PASS_NUMBER))
			else
				self.passNum:setString(string.format("%03d", PASS_NUMBER))
			end
		end
		if bump then
			STATE_GAME_OVER = true
		end
		if v:getPositionX() < -30 then
			v:removeSelf()
			table.remove(self.bottom_pipes, k)
			PASS_NUMBER = PASS_NUMBER + 1
			-- audio.playEffect(MUSIC.point, false)			-- 播放得分音效
		end	
	end	

	if STATE_GAME_OVER then
		echoInfo("-----GAME OVER-----")
		-- audio.playEffect(MUSIC.hit, false)					-- 播放撞击音效
		transition.removeAction(self.HERO_ACTION)		-- 停止小鸟挥动翅膀
		transition.pauseTarget(self.floor)					-- 停止地表移动
		transition.pauseTarget(self.floor2)
		transition.removeAction(self.FLAPPY)				-- 停止小鸟向上飞的动画
		scheduler.unscheduleGlobal(self.ADD_PIPES)		-- 停止添加水管
		transition.pauseTarget(self.pipes_up)				-- 停止上水管移动的动画
		transition.pauseTarget(self.pipes_down)			-- 停止下水管移动的动画
		transition.pauseTarget(self.top_pipes[#self.top_pipes -1])
		transition.pauseTarget(self.bottom_pipes[#self.bottom_pipes -1])
		transition.moveTo(self.sprite,{y = 100, time = self.sprite:getPositionY() / 270})
		scheduler.unscheduleGlobal(self.CollisionDetection)
		scheduler.unscheduleGlobal(self.FLOOR_MOVE)
		PASS_NUMBER = 1
		function restart( )
			STATE_GAME_OVER = false
			app:EnterMainScene()
		end
		scheduler.performWithDelayGlobal(restart, 2)
	end
end

其中有些动画细节,比如每次点击屏幕,小鸟的向上旋转,等一小段时间如果不继续点击屏幕,小鸟向下旋转90°什么的也在代码中做了处理。看代码应该能够明白,就不一一解释了。

另外一些效果音播放,还有如何打包成手机上可用的apk程序下次再说吧。



游戏难度还是挺高的……你们能得多少分呢?我最多10几分:)
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值