godot制作的battle city

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

    截图:

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

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

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


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

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

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

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

在godot里面的时间,如果你是在_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文件夹)

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

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容