无论是3D开放世界、横版卷轴、足球、你问我答,都有一个游戏空间。
我们在这里选用的是六角格棋盘的形式。
这个阶段最后场景结构在godot编辑器里如图所示。下面一步步展开来讲
在引擎里编辑场景和创建TileSet、TileMap可以参看官方文档,这里直接讲需要变更的参数设置,
因为Godot的TileMap没有直接的六角网格样式,需要设置HalfOffset参数使四方格错开半格模拟六角格(并在数学计算上做些调整,本文稍后会有一例)
CellSize的设置,如前图所示的排列方式的话,y设为六角格的高度,x设为六角格宽度的4分之3(关于六角格方面各种计算有详细介绍建议参看这里(Hexagonal Grids from Red Blob Games),这里有翻译(六角网格大观))
(场景里单分出个网格层,存下来以后还能在选项里切换显示不显示或者格线粗细什么的)
TileMap有了那该开始贴tile了,该贴多少个呢?我也不知道……因为要用同一个场景载入各种大小不同的地图,那就用代码来做。
给出init()里新加的代码:
void game_scene::init(const string& file_name) { _map = static_cast<map_scene*>(Ref<PackedScene>(ResourceLoader::load(String("res://level/") + file_name.c_str() + ".tscn"))->instance()); add_child(_map); _grids = static_cast<TileMap*>(get_node(NodePath("Overlays/Grids"))); auto tile = _grids->get_tileset()->find_tile_by_name("grid"); for( int i = 0; i < _map->get_map_width(); i++ ) for( int j = 0; j < _map->get_map_height(); j++ ) _grids->set_cell(i, j, tile); _selected = static_cast<Sprite*>(get_node(NodePath("Overlays/Selected"))); _camera = static_cast<Camera2D*>(get_node(NodePath("Camera2D"))); _map_width = (_grids->get_used_rect().get_size().width + (1.f / 3)) * _grids->get_cell_size().width; _map_height = (_grids->get_used_rect().get_size().height + (1.f / 2)) * _grids->get_cell_size().height; }
前面那个res://level/file_name.tscn就是要加载的关卡地图,创建关卡地图场景时以map_scene为根节点。
地图数据里要加不少东西,所以跟以前一样继承出了个新Node,比如这个样子:
class map_scene : public Node { GDCLASS(map_scene, Node); static void _bind_methods(); public: void set_map_width(int width) { _map_width = width; } int get_map_width() { return _map_width; } void set_map_height(int height) { _map_height = height; } int get_map_height() { return _map_height; } private: int _map_width; //格子个数 int _map_height; };
除了向编辑器输出自制节点,我们也想向编辑器输出节点属性以方便编辑使用,像这样:
这里有坑,文档说的不太清楚,说让你看源代码当例子,源代码那么多,哪些有用哪些没用都混在一起,可是费了不少时间。
总之要向Godot编辑器输出一个属性,可以像下面这样建个函数:
void map_scene::_bind_methods() { ClassDB::bind_method(D_METHOD("set_map_width", "map_width"), &map_scene::set_map_width); ClassDB::bind_method(D_METHOD("get_map_width"), &map_scene::get_map_width); ClassDB::bind_method(D_METHOD("set_map_height", "map_height"), &map_scene::set_map_height); ClassDB::bind_method(D_METHOD("get_map_height"), &map_scene::get_map_height); ADD_PROPERTY(PropertyInfo(Variant::INT, "map_width"), "set_map_width", "get_map_width"); ADD_PROPERTY(PropertyInfo(Variant::INT, "map_height"), "set_map_height", "get_map_height"); }
到这里就算完成了。不过,如果地图大过屏幕大小的话要想看到各个位置就需要一些额外的操作。(像是拖动地图、全屏时鼠标放到屏幕边缘来滚动地图,或者是键盘手柄的上下左右。这里做了拖动,其他的以后可以适时添加)
看起来Camera2D适合完成这个工作。
不过这个官方文档里也没有细说,还是费时间研究了一番:
首先_camera->make_current();(或者在编辑器里设置该属性),这样镜头才会被启用。
然后_camera->set_h_drag_enabled(false);_camera->set_v_drag_enabled(false);(也可以在编辑器里设置),这俩属性似乎是给镜头跟随玩家角色时用的,这里禁用,使得地图拖动一点就动一点。
最后界面根节点(比如HUD)要换成CanvasLayer,不让它跟着镜头一起乱动。
我还试了_camera->set_limit(MARGIN_LEFT, 0);
_camera->set_limit(MARGIN_TOP, 0);
_camera->set_limit(MARGIN_RIGHT, (_grids->get_used_rect().get_size().width + (1 / 3)) * _grids->get_cell_size().width);
_camera->set_limit(MARGIN_BOTTOM, (_grids->get_used_rect().get_size().height + (1 / 2)) * _grids->get_cell_size().height);
看起来像是能限制镜头在地图范围内,但实际不好用,还不如自己写一个,拖动镜头位置时用:
Vector2 game_scene::clamp_camera(const Vector2& position) { auto pos = position; auto v_size = get_viewport()->get_size(); if( _map_width < v_size.width ) pos.x = _map_width / 2; else pos.x = CLAMP(pos.x, v_size.width / 2, _map_width - v_size.width / 2); if( _map_height < v_size.height ) pos.y = _map_height / 2; else pos.y = CLAMP(pos.y, v_size.height / 2, _map_height - v_size.height / 2); return pos; }
设置镜头位置要获取鼠标的输入,要用到一个虚函数_unhandled_input()
不过我在源代码里找了半天也没找到,这不是个真的虚函数,是假的,是加了特技的!
只好在编辑器里给game_scene附上两句GDScript:(前两篇日志里的SCons参数还要去掉module_gdscript_enabled=no)
func _unhandled_input(event): handle_input(event)
输入处理函数要注册给Script:
void game_scene::_bind_methods() { ClassDB::bind_method(D_METHOD("handle_input", "input"), &game_scene::handle_input); }
输入处理函数:
void game_scene::handle_input(Ref<InputEvent> input) { if( cast_to<InputEventScreenDrag>(input.ptr()) ) { auto pos = _camera->get_position() - cast_to<InputEventScreenDrag>(input.ptr())->get_relative(); _camera->set_position(clamp_camera(pos)); } }
因为判读鼠标动作太麻烦,我看Godot里有个InputEventScreenDrag,就在项目设置里开了鼠标模拟触摸,判断触摸拖动,这算是个歪门技巧吧……
正好有了能接收鼠标输入的函数,那再加上一个肯定会用到基础操作,响应点击:
void game_scene::handle_input(Ref<InputEvent> input) { if( cast_to<InputEventMouseButton>(input.ptr()) ) { if( cast_to<InputEventMouseButton>(input.ptr())->get_button_index() == BUTTON_LEFT && cast_to<InputEventMouseButton>(input.ptr())->is_pressed() ) { Vector2 coord = pixel_to_hex(cast_to<InputEventMouseButton>(input.ptr())->get_position() + _camera->get_position() - get_viewport()->get_size() / 2); //或者Vector2 coord = pixel_to_hex(_grids->get_local_mouse_position()); if( coord.x >= 0 && coord.x < _map->get_map_width() && coord.y >= 0 && coord.y < _map->get_map_height() ) __controller->click_map(coord.x, coord.y); } } }
前面提到,TileMap里的网格并不是六边格,所以需要pixel_to_hex计算一下。
关于点到格的坐标转换计算方式真是非常的多样。
我这里用到的,仔细观察一下文章最前面那张截图,可以发现一个四方格被分割成了两个三角形和一个五边形,那么就可以对这三个位置进行判断:
Vector2 game_scene::pixel_to_hex(const Vector2& pos) { Vector2 coord = _grids->world_to_map(pos); auto cell_size = _grids->get_cell_size(); auto x = pos.x - (cell_size.width * coord.x); if( x < cell_size.width / 3 ) { bool is_odd = (int)coord.x & 1; auto gradient = (cell_size.height / 2) / (cell_size.width / 3); auto y = pos.y - (cell_size.height * coord.y) - (is_odd ? cell_size.height / 2 : 0); if( y < (cell_size.height / 2) - (x * gradient) ) { coord.x -= 1; if( !is_odd ) coord.y -= 1; } else if( y > (cell_size.height / 2) + (x * gradient) ) { coord.x -= 1; if( is_odd ) coord.y += 1; } } return coord; }
最后,一个单独分开的选择框图标位置移动函数:(因为不只是鼠标点击可以设置选择框,用键盘手柄或者单位选择按钮也可以。并且点击单位后还要有更多的处理,像弹出指令菜单之类的,不过那将是以后要讲的了)
void game_scene::mark_selected(uint32_t x, uint32_t y) { _selected->set_position(_grids->map_to_world(Vector2(x, y))); }
最后的最后,_grids返回的cell坐标是左上角,要把Selected这个Sprite的OffsetCentered参数设置为false。
当前的结果如图:
本来觉得挺简单的实际做起来又有不少东西,下一篇是创建游戏中的对象-战斗单位,应该就真没多少东西了……
我结束这一回合!
总算看到有人用godot做游戏了