原文链接
前言
本来我的想法是使用Kotlin Multiplatform
来做为安卓端和桌面端应用软件跨平台的解决方案,参考本站前文构建跨平台的客户端界面。但是在撰写和尝试更完整的跨平台应用的时候发现,目前使用Kotlin Multiplatform
在社区
和组件选择
以及部分组件的兼容性
还有编辑器的显示支持
上都不够优秀,简单来说遇到了不少的坑,虽然基本都能解决,但是实在不能作为一个具有长期借鉴意义的文章发出(比如可能会介绍蛮多坑,但是这些坑会在之后版本中被修复)。另外由于目前他们还在积极的更新和迭代中,我仍然觉得这是一个对于跨平台应用开发非常值得的选择,在之后他们各个功能都比较完善了,本站会出一个包含源码完整的跨平台应用的实现
想要写跨平台应用的原因也是想给astercasc.com
出一个移动&桌面版本的应用程序,一方面看视频听音乐也比较方便(😄),另一方面能相对比较简单地观察到后端服务的状态。个人开发,不考虑其他人的维护性的话,肯定是跨平台的项目性价比更高的,而且也更具有学习价值和有趣。所以既然目前Kotlin Multiplatform
不是特别好用的话,由于这个想法也没有特别着急,所以这个东西可以暂时搁置。肯定是不会考虑写两份代码的,比如写一套Kotlin Android
写一套Qt
,这种就没意义,纯纯在上班,也出不了文章
这个时候就可以考虑一些可能可以实现跨平台,但是即使没实现也有意思的东西了。于是我查了一下清单,发现之前有写过不少做游戏的点子,按照目前的我的时间,做游戏肯定是没空,但是游戏引擎对于跨平台的支持导出也是一种可以尝试的方案,即使最后效果不理想,也可以了解一下游戏开发
游戏开发引擎的选择,我们这里选择Godot
,没有选择虚幻的原因是虚幻太大了,这个《大》更多的是对于游戏各种功能的支持,这里我们更希望有个轻量级的工具可以处理,不选择Unity
的原因,大伙都知道,就属于懂的都懂了
Which platforms are supported by Godot?
For the editor:
- Windows
- macOS
- Linux, *BSD
- Android (experimental)
- Web (experimental)
For exporting your games:
- Windows
- macOS
- Linux, *BSD
- Android
- iOS
- Web
CSharp环境构建
对于我们jetbrains
全家桶来说,肯定是使用Rider
进行开发的,下载Rider
然后根据提示安装环境即可,目前的默认支持为:
.NET Version | Rider Version | Support |
---|---|---|
.NET SDK 8 | Rider 2023.3 | Full support |
.NET SDK 7 | Rider 2022.3 | Full support |
.NET SDK 6 | Rider 2021.3 | Full support |
.NET SDK 5 | Rider 2020.3 | Full support |
Godot配置
从Godot官网下载所需版本,这里我们选择Godot Engine-.Net
版本,然后解压,运行即可
这里有个小坑,Godot
获取dotnet
不是通过环境变量获取的,也就是说,我们在使用Rider
安装运行环境之后,即使加入环境变量中,Godot
也无法检测到,即使你的命令行可以调通dotnet --version
。Godot
获取dotnet
在Windows
下是直接在Program Files
里面拿的,所以我们需要将Rider
下载的.dotnet
复制成Program Files
下的dotnet
才能让Godot
正常工作。当然你如果使用的是Visual Studio
就不用这么麻烦了,直接在Visual Studio
里面安装就可以直接使用
项目构建
对于Godot
而言,虽然官网的介绍比较详细,但是我这里以前端对应来简单再介绍下。整个Godot
项目为树状结构,整个树状结构为场景树The scene tree
,对应了前端的html
标签。在树上场景Scene
就像是可复用的前端组件,级联选择器,分页表格等,游戏场景不执着于场景,可以是人物,装饰,武器等。而节点Node
就像是我们的基础标签,button
,input
等。最后信号Signal
就对应了前端信号,将事件传递给各个组件
这里就是Godot
几个核心概念:场景树,场景,节点以及信号。官网有简单游戏的创建流程Your first 2D game,非常详细,我们这里就不再重复了,我们这里以网站首页举例,看看构建一个网站页面需要的工作量,下面的内容以您已完成官网的最佳实践为前提或者对Godot
有初步了解
在项目设置中调整基础页面参数,比如初始窗口的宽高,主场景,自定义字体,如果需要使用快捷键也在这里配置,基本配置完成后,我们从基本组件开始,首先是卡片
由于我们这里并不是真的要用这个实现,做个简单的即可,调整后基本代码如下
[gd_scene load_steps=9 format=3 uid="uid://0sffbl8ylhnw"]
[ext_resource type="StyleBox" uid="uid://bawggbd2nu5ye" path="res://style/articlesimplebk.tres" id="1_s1tlr"]
[ext_resource type="StyleBox" uid="uid://bqeox8tc7ff3u" path="res://style/articleinbtn.tres" id="2_qhtdo"]
[ext_resource type="StyleBox" uid="uid://cao5ggt1fjedp" path="res://style/articleinbtn-hover.tres" id="3_4eymw"]
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_fb4l4"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_j0kbe"]
bg_color = Color(0.223529, 0.223529, 0.247059, 1)
corner_radius_top_left = 25
corner_radius_top_right = 25
corner_radius_bottom_right = 25
corner_radius_bottom_left = 25
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_a8ibj"]
content_margin_left = 20.0
content_margin_right = 20.0
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_rjk3e"]
content_margin_left = 20.0
content_margin_top = 60.0
content_margin_right = 20.0
content_margin_bottom = 20.0
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_1c2hf"]
draw_center = false
[node name="CardOut" type="PanelContainer"]
custom_minimum_size = Vector2(450, 300)
offset_right = 450.0
offset_bottom = 300.0
theme_override_styles/panel = SubResource("StyleBoxEmpty_fb4l4")
[node name="Card" type="PanelContainer" parent="."]
layout_mode = 2
size_flags_vertical = 0
theme_override_styles/panel = ExtResource("1_s1tlr")
[node name="CardContainer" type="MarginContainer" parent="Card"]
custom_minimum_size = Vector2(0, 40)
layout_mode = 2
size_flags_horizontal = 0
size_flags_vertical = 0
theme_override_constants/margin_left = -25
theme_override_constants/margin_top = -25
[node name="CardBgContainer" type="PanelContainer" parent="Card/CardContainer"]
layout_mode = 2
theme_override_styles/panel = SubResource("StyleBoxFlat_j0kbe")
[node name="Title" type="Label" parent="Card/CardContainer/CardBgContainer"]
layout_mode = 2
theme_override_font_sizes/font_size = 25
theme_override_styles/normal = SubResource("StyleBoxEmpty_a8ibj")
text = "this is a article title"
[node name="CardContentContainer" type="VBoxContainer" parent="Card"]
layout_mode = 2
[node name="CardContent" type="Label" parent="Card/CardContentContainer"]
custom_minimum_size = Vector2(100, 100)
layout_mode = 2
size_flags_vertical = 0
theme_override_colors/font_color = Color(0, 0, 0, 1)
theme_override_font_sizes/font_size = 20
theme_override_styles/normal = SubResource("StyleBoxEmpty_rjk3e")
text = "this is content this is content this is content this is content this is content
this is contentthis is contentthis is content"
autowrap_mode = 3
[node name="Go" type="Button" parent="Card/CardContentContainer"]
layout_mode = 2
size_flags_horizontal = 8
size_flags_vertical = 8
theme_override_colors/font_color = Color(0.972549, 0.972549, 0.972549, 1)
theme_override_font_sizes/font_size = 20
theme_override_styles/normal = ExtResource("2_qhtdo")
theme_override_styles/hover = ExtResource("3_4eymw")
theme_override_styles/pressed = ExtResource("3_4eymw")
theme_override_styles/focus = SubResource("StyleBoxFlat_1c2hf")
text = "更多内容"
这里我们在创建的时候为了复用使用自定义的tres
,可以认为是css
文件,可以直接导入需要的标签复用,以articlesimplebk.tres
为例:
[gd_resource type="StyleBoxFlat" format=3 uid="uid://bawggbd2nu5ye"]
[resource]
content_margin_left = 0.0
content_margin_top = 0.0
content_margin_right = 40.0
content_margin_bottom = 35.0
bg_color = Color(1, 1, 1, 0.784314)
corner_radius_top_left = 25
corner_radius_top_right = 25
corner_radius_bottom_right = 25
corner_radius_bottom_left = 25
shadow_color = Color(1, 1, 1, 0.0784314)
shadow_size = 20
这样一个我们一个基础组件(场景)就制作好了
现在我们写个主场景树:
[gd_scene load_steps=7 format=3 uid="uid://cr14hf5hn4tp5"]
[ext_resource type="Texture2D" uid="uid://bqe782vjeix25" path="res://img/backgroud.png" id="1_51gst"]
[ext_resource type="Script" path="res://YunoDesktop.cs" id="1_ogbat"]
[ext_resource type="StyleBox" uid="uid://cfh1uqw6lpksw" path="res://style/transbk.tres" id="2_gsi10"]
[ext_resource type="Theme" uid="uid://c8f4q4asmagvt" path="res://style/titlelabel.tres" id="3_6cb30"]
[ext_resource type="Script" path="res://HomeProject.cs" id="3_gbua5"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_gnpks"]
bg_color = Color(1, 1, 1, 1)
[node name="YunoDesktop" type="Node"]
script = ExtResource("1_ogbat")
[node name="HomeScroll" type="ScrollContainer" parent="."]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="HomeContainer" type="PanelContainer" parent="HomeScroll"]
layout_mode = 2
size_flags_horizontal = 3
theme_override_styles/panel = SubResource("StyleBoxFlat_gnpks")
[node name="Home" type="VBoxContainer" parent="HomeScroll/HomeContainer"]
layout_mode = 2
size_flags_horizontal = 3
theme_override_constants/separation = 0
[node name="HomeBkColor" type="ColorRect" parent="HomeScroll/HomeContainer/Home"]
layout_mode = 2
[node name="HomeBkImg" type="TextureRect" parent="HomeScroll/HomeContainer/Home"]
layout_mode = 2
texture = ExtResource("1_51gst")
expand_mode = 5
[node name="HomeMain" type="MarginContainer" parent="HomeScroll/HomeContainer/Home"]
layout_mode = 2
theme_override_constants/margin_left = 50
theme_override_constants/margin_top = -200
theme_override_constants/margin_right = 50
theme_override_constants/margin_bottom = 100
[node name="HomeStyle" type="PanelContainer" parent="HomeScroll/HomeContainer/Home/HomeMain"]
layout_mode = 2
theme_override_styles/panel = ExtResource("2_gsi10")
[node name="HomeProject" type="VBoxContainer" parent="HomeScroll/HomeContainer/Home/HomeMain/HomeStyle"]
layout_mode = 2
script = ExtResource("3_gbua5")
[node name="HomeArticle" type="Label" parent="HomeScroll/HomeContainer/Home/HomeMain/HomeStyle/HomeProject"]
layout_mode = 2
theme = ExtResource("3_6cb30")
text = "技术备录"
[node name="HomeArticleContainer" type="HFlowContainer" parent="HomeScroll/HomeContainer/Home/HomeMain/HomeStyle/HomeProject"]
layout_mode = 2
theme_override_constants/h_separation = 100
theme_override_constants/v_separation = 100
[node name="HomeLife" type="Label" parent="HomeScroll/HomeContainer/Home/HomeMain/HomeStyle/HomeProject"]
layout_mode = 2
theme = ExtResource("3_6cb30")
text = "生活题记"
[node name="HomeLifeContainer" type="HFlowContainer" parent="HomeScroll/HomeContainer/Home/HomeMain/HomeStyle/HomeProject"]
layout_mode = 2
theme_override_constants/h_separation = 100
theme_override_constants/v_separation = 100
[node name="ArticleRequest" type="HTTPRequest" parent="HomeScroll/HomeContainer/Home/HomeMain/HomeStyle/HomeProject"]
[node name="LifeRequest" type="HTTPRequest" parent="HomeScroll/HomeContainer/Home/HomeMain/HomeStyle/HomeProject"]
我们需要根据后端请求更新在该场景树上的卡片场景:
using Godot;
using System;
using System.Text;
using System.Text.Encodings.Web;
public partial class HomeProject : VBoxContainer
{
public override void _Ready()
{
HttpRequest articleRequest = GetNode<HttpRequest>("ArticleRequest");
articleRequest.RequestCompleted += OnArticleReqCompleted;
articleRequest.Request(ServerInfo.ServerBase.KotomiAddress + "/article/list?articleType=1&offset=0&limit=10");
HttpRequest lifeRequest = GetNode<HttpRequest>("LifeRequest");
lifeRequest.RequestCompleted += OnLiftReqCompleted;
lifeRequest.Request(ServerInfo.ServerBase.KotomiAddress + "/article/list?articleType=2&offset=0&limit=10");
}
private void OnArticleReqCompleted(long result, long responseCode, string[] headers, byte[] body)
{
rentCard(body, "HomeArticleContainer");
}
private void OnLiftReqCompleted(long result, long responseCode, string[] headers, byte[] body)
{
rentCard(body, "HomeLifeContainer");
}
private void rentCard(byte[] body, string containerName)
{
Godot.Collections.Dictionary json = Json.ParseString(Encoding.UTF8.GetString(body)).AsGodotDictionary();
var articleList = json["data"].AsGodotArray();
if (articleList.Count > 0)
{
var articleContainer = GetNode<HFlowContainer>(containerName);
var scene = GD.Load<PackedScene>("res://ArticleSimple.tscn");
for (int count = 0; count < articleList.Count; count++)
{
var objDic = articleList[count].AsGodotDictionary();
var instance = scene.Instantiate();
var thisLabel = instance.GetNode<Label>("Card/CardContainer/CardBgContainer/Title");
thisLabel.Text = objDic["articleTitle"].AsString();
var thisContent = instance.GetNode<Label>("Card/CardContentContainer/CardContent");
thisContent.Text = objDic["articleBrief"].AsString().Replace("\n", "") + "...\n";
var thisBtn = instance.GetNode<Button>("Card/CardContentContainer/Go");
thisBtn.Pressed += () => onPressToReadMore(objDic["id"].AsString());
articleContainer.AddChild(instance);
}
}
}
private void onPressToReadMore(string articleId)
{
GD.Print(articleId);
OS.Alert("不支持当前功能,请移步astercasc.com体验完整功能");
}
}
最后我们运行主场景可以得到:
项目发布
这里我们可以选择发安卓端以及桌面端,桌面端基本直接导出就可以了,安卓端需要填写密钥库,创建后即可发布。经过测试,桌面端基本没有问题,安卓端部分功能在转换之后还是有点问题,但是如果是做游戏应该不影响,因为做游戏一般也用不到侧边栏滑动
最后
全部代码可以在yuno-door-godot找到。可能是我不是很熟练的原因,用godot
来模拟web
端逻辑上没有什么问题,但有两个痛点,第一个是很多web
以及android
比较方便就可以实现的功能,由于游戏里面不存在,在转换脚本里面没有处理(比如侧边栏滑动)需要单独处理。第二个是写样式太痛苦了,同样的样式可能web
或者android
只需要一行,在godot
需要迭代两到三层