最开始的时候我就想制作一个90坦克的demo,之前看了其他的游戏引擎感觉不好搞,后来用了godot感觉可以研究一下,最近学着做了一些。虽然看起来可能跟原版有差距,但是大部分功能都有了,增加了一个地图编辑器。

    截图:

godot制作的battle city_godot godot制作的battle city_物理引擎_02

godot制作的battle city_godot_03

大致功能就如同上面截图一样,截下来就介绍一下实现这个游戏中基本的难点和godot引擎使用的注意地方。在玩过原版坦克大战的时候,如果你仔细观察过就会发现敌人出生的地方如果多辆坦克一起出生的话,刚开始是没有碰撞检测,一旦分开了就会有碰撞检测。砖块被击中会有不同的形状,但是它原来的体积还是在,并且无法通过。坦克在冰块上会有滑动。坦克吃了基地变成石头的道具后,在最后变化的时候,不停的开枪,可以嵌入里面,但是可以移动出来。如果很多敌人坦克挤在一起,不会出现互相卡死的情况。敌人的ai优先向基地出发。

那么如果使用godot的碰撞功能,那么以上的一些功能可能无法实现,比如多个坦克重叠的情况,强行进入砖块中问题,所以碰撞的检测需要自己来实现,实现的功能可以参考一下别人写的文章:https://developer.ibm.com/technologies/javascript/tutorials/wa-build2dphysicsengine/ 这个具体介绍了如何实现一个物理引擎。在碰撞之后需要对碰撞体进行位置的重新设置,这个过程就比较重要了。

接下来就介绍一下项目的基本结构,主要的话把各个功能分开来比较好处理,level里面就是每一关的地图文件。其他的到时可以自己打开看看。

godot制作的battle city_sed_04

最开始就是实现基本的界面,界面的实现主要使用godot自带的ui组件,这个ui组件没有android的ui组件好用,一些布局实在是坑爹,不是很还用,很多的地方需要自己处理。主要使用水平布局和垂直布局,关于字体的的话,这个新建一个动态字体的资源把ttf文件导入然后就可以使用,但是这个字体的大小是统一的,如果你想要不同大小的字体,只能在新建一个。

godot制作的battle city_物理引擎_05

对于玩家和敌人的制作的话,我之前是把它们分开来,估计以后要统一成同一个类,然后继承后进行修改。碰撞的代码全部都在脚本里面进行判断,本身就是进行一个基本的动画和演示。

godot制作的battle city_sed_06

var rect=Rect2(Vector2(-14,-14),Vector2(28,28))
var debug=true
var vec=Vector2.ZERO
var keymap={"up":0,"down":0,"left":0,"right":0,'fire':0}
var level=0 #坦克的级别	0最小 1中等 2是大  3是最大
var dir=0 # 0上 1下 2左 3右
var shootTime=0	
var shootDelay=60
var bullets=[]
var bulletMax=1	#发射最大子弹数
var bullet=Game.bullet
var isInit=false
var state=Game.tank_state.IDLE
var initStartTime=0
var initTime=1200  #ms
var isInvincible=false
var invincibleStartTime=0
var invincibleTime=8000
var isStop=false#是否停止
var playId=2  #1=1p 2=2p
var life=1  #生命默认1
var speed = 70 #移动速度
var bulletPower=Game.bulletPower.normal
var hasShip=false	#是否有船

坦克的基本属性暂时只有这些,坦克的发射子弹是有时间和个数的限制,实现的方式也不是很复杂,主要通过时间判断和容器中子弹物体是否无效,当然也可以在坦克内部添加一个节点用来存储子弹节点,这样坦克被摧毁,子弹也会消失。

#开火
func fire():
	if OS.get_system_time_msecs()-shootTime<shootDelay:
		return
	else:
		shootTime=OS.get_system_time_msecs()
#	print("dir",dir)	
	var del=[]
	for i in bullets: #清理无效对象
	#	print(is_instance_valid(i))
		if not is_instance_valid(i):
			del.append(i)
	for i in del:
		bullets.remove(bullets.find(i))
	if bullets.size()<bulletMax:
		playShot()
		var temp=bullet.instance()
		temp.setType("player")
		temp.position=position
		temp.setPower(bulletPower)
		temp.setDir(dir)
		temp.setPlayerId(playId)
		bullets.append(temp)
		Game.mainScene.add_child(temp)

坦克的移动主要根据按键,但是坦克有1p,2p,所有按键要进行分类,至于如何动态的修改可以参考以下项目:https://github.com/nezvers/Godot-GameTemplate ,按键的后只要改变坐标的话就可以移动坦克了。

func _update(delta):
	if state==Game.tank_state.IDLE:
		initStartTime+=delta*1000
		if initStartTime>=initTime:
			initStartTime=0
			isInit=true
			$ani.playing=false
			setState(Game.tank_state.START)
		pass
	elif state==Game.tank_state.START:
		if Input.is_key_pressed(keymap["up"]):
	#		print("up")
			vec.y=-speed
			vec.x=0
			dir=0
			isStop=false
		elif Input.is_key_pressed(keymap["down"]):
			vec.x=0
			vec.y=speed
			dir=1
			isStop=false
		elif Input.is_key_pressed(keymap["left"]):
			vec.x=-speed
			vec.y=0
			isStop=false
			dir=2	
		elif Input.is_key_pressed(keymap["right"]):	
			vec.y=0
			vec.x=speed
			dir=3
			isStop=false
		else:
			vec=Vector2.ZERO	
		
		if vec!=Vector2.ZERO:
			if !$walk.playing:
				$walk.play()
			if $idle.playing:
				$idle.stop()
			pass	
		else:
			if $walk.playing:
				$walk.stop()
			if !$idle.playing:
				$idle.play()
			
		if Input.is_key_pressed(keymap["fire"]):
		#	print("fire")
			fire()	
			
		animation(dir,vec)	
		if !isStop:
			position+=vec*delta
		
		if isInvincible:
			if OS.get_system_time_msecs()-invincibleStartTime>=invincibleTime:
				invincibleStartTime=0
				isInvincible=false
				_invincible.visible=false
				_invincible.playing=false
			
	pass

对于坦克吃到道具变化的话基本都是通过改变纹理的样子来实现。子弹的设计主要是一张图片然后移动,碰到墙壁爆炸然后消失。主要有以下几种属性。

export var dir=2 # 0上 1下 2左 3右
var speed=160  
var type=Game.bulletType.players
var playerID  #玩家id
var power=Game.bulletPower.normal  #1是基本火力 2是最强火力
#var winSize=Vector2(480,416)	#屏幕大小
var size=Vector2(6,8)	#图片大小
var vec= Vector2.ZERO
var isValid=false
var rect=Rect2(Vector2(-3,-4),Vector2(6,8))

对于游戏中的每个物体的碰撞都是在每个对象里面添加一个var rect=Rect2(Vector2(-3,-4),Vector2(6,8))。这个rect就是用来进行判断是否重叠,如果重叠就是发生了碰撞,那这个重叠有几种情况就是有的是边的重叠,有的是两个矩形重叠面积大。具体可以参考上面的物理引擎的实现。主要是碰撞后要对位置做调整。

for i in _tank.get_children():	#检查坦克与砖块的碰撞
			var rect=i.getRect()
			for y in _brick.get_children():
				if y.get_class()=="brick":
					var type=y.getType() #装快的类型
					if type==Game.brickType.bush or type==Game.brickType.ice:	#草丛
						continue
					
					var rect1=y.getRect()	
					if rect.intersects(rect1,false):  #碰撞  判断是否被包围住
						if rect1.encloses(rect):#完全叠一起
							continue
							
						var dx=(y.getPos().x-i.position.x)/(y.getXSize()/2)
						var dy=(y.getPos().y-i.position.y)/(y.getYSize()/2)
						var absDX = abs(dx)
						var absDY = abs(dy)

						if abs(absDX - absDY) < .1:
							if dx<0:
								i.position.x=y.getPos().x+y.getXSize()/2+i.getSize()/2			
							else:
								i.position.x=y.getPos().x-y.getXSize()/2-i.getSize()/2	

							if dy<0:
								i.position.y=y.getPos().y+y.getYSize()/2+i.getSize()/2			
							else:
								i.position.y=y.getPos().y-y.getYSize()/2-i.getSize()/2						
						elif absDX > absDY:
							if dx<0:
								i.position.x=y.getPos().x+y.getXSize()/2+i.getSize()/2					
							else:
								i.position.x=y.getPos().x-y.getXSize()/2-i.getSize()/2		
						else:
							if dy<0:
								i.position.y=y.getPos().y+y.getYSize()/2+i.getSize()/2	
							else:
								i.position.y=y.getPos().y-y.getYSize()/2-i.getSize()/2	

对于其他物体的碰撞其实都是这样,对于动态的物体的碰撞调整可能需要进行一些处理。

对于坦克间的碰撞,这个需要特殊的处理,如果你有玩过之前的版本,你就会发现多辆坦克有重叠在一起的情况,这种情况需要进行特殊的处理,我这边只判断坦克的前进方向是否有物体,如果有就无法前进,没有就可以前进。但是对于位置不能进行修改,不然下一辆坦克的判断就会出现可以前进的问题。这个问题现在看还是有些地方处理的不好,只能后续处理。

        var tanks=_tank.get_children()
		for i in tanks:	#坦克与坦克的碰撞
			var isStop=false
			for y in tanks:
				if i!=y:
					if i.isInit && y.isInit:
						var rect=i.getRect()
						var rect1=y.getRect()
						var iTankDir=i.dir
						var yTankDir=y.dir
						var xVal =i.position.x-y.position.x
						var yVal =i.position.y-y.position.y
						var absXVal=abs(xVal)
						var absYVal=abs(yVal)
						
						if rect.intersects(rect1,false):
							if iTankDir in [0,1]:	#上下
								if absYVal<i.getSize() and absYVal>i.getSize()/2:
									if yVal<0 and iTankDir==1:
										isStop=true
									elif yVal>0	and iTankDir==0:
										isStop=true
										
#									if yVal<0:
#										i.position.y=y.getPos().y-y.getSize()/2-i.getSize()/2			
#									else:
#										i.position.y=y.getPos().y+y.getSize()/2+i.getSize()/2	
								else:
									isStop=false
								pass
							elif iTankDir in [2,3]:	#左右
								if absXVal<i.getSize() and absXVal>i.getSize()/2:			
									if xVal<0 and iTankDir==3:
										isStop=true
									elif xVal>0 and iTankDir==2:
										isStop=true
#									if xVal<0:
#										i.position.x=y.getPos().x-y.getSize()/2-i.getSize()/2	
#									else:
#										i.position.x=y.getPos().x+y.getSize()/2+i.getSize()/2
								else:
									isStop=false
								pass				
					pass
				pass
			i.setStop(isStop)

游戏里面有声音的播放,这个由于有限制,mp3的无法播放所以一些用的是ogg,但是ogg一旦播放就会无法停下来,所以要特殊处理。

var point = $point.stream as AudioStreamOGGVorbis
point.set_loop(false)
var power1= $power1.stream as AudioStreamOGGVorbis
power1.set_loop(false)

在游戏开始界面的时候,有一个动画慢慢升起来的标题,这个制作需要准备两个动画,然后按下的时候,直接播放结束的那个,具体可以看下代码。

func _input(event):
	if event is InputEventKey:
		if event.is_pressed():
			if (event as InputEventKey).scancode==KEY_DOWN:		
				if index<2:
					index+=1
					setMode(index)
			elif (event as InputEventKey).scancode==KEY_UP:
				if index>0:
					index-=1
					setMode(index)
			elif (event as InputEventKey).scancode==KEY_ENTER:	
				if _ani.get_current_animation()=="start" and \
					_ani.is_playing():
					_ani.play("end")
					return
				if mode in [1,2]:
					Game.mode=mode
					Game.changeSceneAni(Game._mainScene)
				else:
					var scene = preload("res://scenes/map.tscn"	)
					var temp=scene.instance()
					temp.mode=1
					queue_free()
					set_process_input(false)
					get_tree().get_root().add_child(temp)
					set_process_input(true)
					#Game.changeSceneAni(Game._welcomeScene)

游戏中地图的生成,游戏里面自带了地图的编辑器,对于游戏中的地图的制作,首先地图是一个26x26的小方块组成的。几个特殊的地方无法编辑的,基地的位置,玩家,敌人出生地都是无法编辑的,编辑之后数据的并保存主要是以json的格式保存,格式为{"name":'',"data":[],"base":[],"author":"absolve", "description":""},每个方块为{'x':indexX,'y':indexY,"type":0},方块的类型是0,1,2,3,4,方块,石头,水,草丛,冰块。读取的时候根据位置显示在界面上,这样基本就成了。界面上的点击事件主要是靠_input函数,获取鼠标的事件来判断是否按下

func _input(event):
	if _fileDiaglog.visible or _loadDiaglog.visible or lock or mode!=1:
		return
	if event is InputEventMouseButton:
		if event.button_index == BUTTON_LEFT and  event.pressed:
			isPress=true
			if currentItem!=-1:	
				if !mapRect.has_point(get_global_mouse_position()):
					return
				if! checkItem(get_global_mouse_position()):
					addItem(get_global_mouse_position())	
			elif currentItem==-1:
				clearItem(get_global_mouse_position())		
		elif !event.pressed:
			isPress=false
	elif event is InputEventMouseMotion:	#移动
		if isPress:
			if currentItem!=-1:	
				if !mapRect.has_point(get_global_mouse_position()):
					return
				if! checkItem(get_global_mouse_position()):
					addItem(get_global_mouse_position())	
			elif currentItem==-1:
				clearItem(get_global_mouse_position())
		pass

godot制作的battle city_ios_07

计分的画面的制作,首先需要制作出基本的界面,每个坦克类型需要判断是否大于0,然后统计完后进入下一个,直到完成为止,这个过程只需要更改每个状态,直到最后的计数完成为止,然后进入下关或者游戏结束。具体可以看下代码。

godot制作的battle city_物理引擎_08

在godor里面的时间,如果你是在_process(delta)里面每一帧不是固定的,有时快有时慢,用自带的定时器就可以。