godot制做的battle city

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

    截图:java

 

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

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

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

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

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

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	#是否有船

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

#开火
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

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

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

 

参考资料:https://github.com/shinima/battle-city

https://github.com/newagebegins/BattleCity

https://github.com/krystiankaluzny/Tanks

https://www.sounds-resource.com/nes/battlecity/sound/3710/

项目地址:https://github.com/absolve/godotgame  (tank文件夹)

其它想到在补充,有啥问题,记得反馈。

相关文章
相关标签/搜索