Unity-QFramework框架學習-MVC、Command、Event、Utility、System、BindableProperty

QFramework

QFramework簡介

QFramework是一套漸進式、快速開發框架,適用于任何類型的游戲及應用項目,它包含一套開發架構和大量的工具集

QFramework的特性

  1. 簡潔性:QFramework 強調代碼的簡潔性和易用性,讓開發者能夠快速上手,減少學習成本。

  2. 模塊化設計:該框架采用了模塊化的架構設計,方便開發者根據項目需求自由組合和擴展功能模塊。

  3. 事件驅動:支持事件驅動編程模型,便于實現不同游戲對象之間的通信和交互。

  4. 數據綁定:提供了數據綁定的支持,可以輕松實現UI與邏輯代碼的數據同步。

  5. 資源管理:內置了資源加載和釋放機制,幫助開發者更高效地管理游戲中的各種資源。

  6. MVC/MVVM架構支持:支持傳統的MVC(Model-View-Controller)架構模式,有助于更好地組織和分離代碼。

  7. 熱更新支持:對于需要進行熱更新的游戲,QFramework 提供了相應的支持,使得代碼或資源的在線更新變得更加容易。

  8. 豐富的工具集:包含了一系列實用工具,如調試工具、配置管理等,進一步提升了開發效率。

  9. 社區支持:擁有活躍的社區支持,開發者可以在遇到問題時尋求幫助或者分享自己的經驗。

QFramework架構

QFramework架構是一套簡單、強大、易上手的系統設計架構

這套架構基于MVC架構模式,可 分層,CQRS支持,事件驅動,IOS模塊化,領域驅動設計(DDD)支持,符合SOLID原則,并且源碼不到1000行

架構圖

QFramework的MVC

QFramework基于MVC的開發模式

我們可以通過一個案例來學習MVC模式:計數器應用

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;namespace QFramework.Example
{//Contarollerpublic class CounterAppController : MonoBehaviour{//Viewpublic Button BtnAdd;public Button BtnSub;public Text CounterText;//Modelpublic int Count = 0;private void Start(){BtnAdd.onClick.AddListener(() =>{//交互邏輯Count++;//表現邏輯updateview();});BtnSub.onClick.AddListener(() => {//交互邏輯Count--;//表現邏輯updateview();});//表現邏輯updateview();}void updateview(){CounterText.text = Count.ToString();}}
}

但是這還沒有導入QFramework

代碼很簡單 這是一個非常簡易的MVC的實現,但是我們要用發展的眼光看待問題,如果在未來需要做一個需要大量邏輯代碼,那么count可能會在多個Controller中使用,甚至需要針對count這個數據寫一些其他邏輯,比如增加多個分數,或者需要存儲count等,那目前cout只屬于CounterAppController,顯然在未來是不夠用的。那么就需要count成員變量變成一個共享的數據,最快的做法是把count變量變成靜態變量或者單例,這樣寫起來雖然很快,但在后期維護的時候會產生一些問題

然而,QFramework架構提供了Model的概念

首先導入QFramework框架https://gitee.com/liangxiegame/QFramework/blob/master/QFramework.cs

導入QFramework的方式:復制QFramework.cs的代碼到Unity工程中即可

導入后,我們將CounterAppController的代碼改成:

//CounterModel

namespace QFramework.Example
{public class CounterModel : AbstractModel{public int Count = 0;protected override void OnInit(){Count = 0;}}
}

//CounterApp

namespace QFramework.Example
{//Architecture:用于管理模塊,或者說是Architecture提供了一整套架構的解決方案,而模塊管理和提供Mvc只是功能的一部分public class CounterApp : Architecture<CounterApp>{protected override void Init() {this.RegisterModel(new CounterModel());}}
}

//CounterAppController

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;namespace QFramework.Example
{public class CounterAppController : MonoBehaviour,IController{public Button BtnAdd;public Button BtnSub;public Text CounterText;private CounterModel mModel;private void Start(){mModel = this.GetModel<CounterModel>();BtnAdd.onClick.AddListener(() =>{//交互邏輯mModel.Count++;//表現邏輯updateview();});BtnSub.onClick.AddListener(() => {//交互邏輯mModel.Count--;//表現邏輯updateview();});//表現邏輯updateview();}void updateview(){CounterText.text = mModel.Count.ToString();}public IArchitecture GetArchitecture(){return CounterApp.Interface;}}
}

注意:需要共享的數據放在Model里,不需要共享的能不放就不放?Model的引入是為了解決數據共享的問題,而不是說但只是為了讓數據和表現分離

數據共享分為兩種:空間上的共享和時間上的共享

空間上的共享非常簡單,就是多個點的代碼需要訪問Model里的數據

時間上的共享就是存儲功能,將上一次關閉App之前的數據存儲到一個文件里,這次打開時獲得上次關閉App之前的數據

雖然以上代碼引入了Model,但是這套代碼隨著項目規模的發展還是有很多的問題,其中的Controller會越來越臃腫

什么是交互邏輯和表現邏輯

交互邏輯:就是從用戶輸入開始到數據變更的邏輯 順序是View->Controller->Model

表現邏輯:就是數據變更到在界面顯示的邏輯 順序是Model->Controller->View

View、Controller和Model的交互邏輯和表現邏輯形成了一個閉環,構成了完整的MVC閉環

引入Command

Controller本身之所以臃腫,是因為,她負責了兩種職責,即改變Model數據的交互邏輯,以及Model數據變更之后更新到界面的表現邏輯

而在一個有一定規模的項目中,表現邏輯和交互邏輯非常多,而一個controller很容易就做到上千行代碼,而大部分的MVC方案,解決Controller臃腫用的是引入Command的方式,即引入命令模式,通過命令來分擔Controller的交互邏輯的職責

將Command引入代碼中:

創建IncreaseCountCommand.cs文件:

namespace QFramework.Example
{public class IncreaseCountCommand : AbstractCommand{protected override void OnExecute(){this.GetModel<CounterModel>().Count++;}}
}

創建DecreaseCountCommand.cs文件:

namespace QFramework.Example
{public class DecreaseCountCommand : AbstractCommand{protected override void OnExecute(){this.GetModel<CounterModel>().Count--;}}
}

修改CounterAppController.cs文件:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;namespace QFramework.Example
{public class CounterAppController : MonoBehaviour,IController{public Button BtnAdd;public Button BtnSub;public Text CounterText;private CounterModel mModel;private void Start(){mModel = this.GetModel<CounterModel>();BtnAdd.onClick.AddListener(() =>{//交互邏輯this.SendCommand<IncreaseCountCommand>();//表現邏輯updateview();});BtnSub.onClick.AddListener(() => {//交互邏輯this.SendCommand<DecreaseCountCommand>();//表現邏輯updateview();});//表現邏輯updateview();}void updateview(){CounterText.text = mModel.Count.ToString();}public IArchitecture GetArchitecture(){return CounterApp.Interface;}}
}

通過引入Command,幫助分擔了Controller的交互邏輯。使得Controller成為一個薄薄一層,在需要修改Model的時候?,Controller只要調用一句簡單的Command即可

Command的作用

  • Command可以復用,Command也可以調用Command
  • Commad可以比較方便實現撤銷功能
  • 如果遵循一定規范,可以實現使用Command跑自動化測試
  • Command可以指定Command可以制定Command隊列,也可以讓Command按照特定的方式執行
  • 一個Command也可以封裝成一個Http或者Tcp里的一次數據請求
  • Command可以實現Command中間件模式等等

Command的優點

Command最明顯的好處就是就算代碼再亂,也只是一個Command對象里亂,而不會影響其他對象,將方法封裝成命令對象,可以實現對命令對象的組織、排序、延時等操作

引入Event

以上引入Command后,幫助Controller分擔了一部分的交互邏輯,但是表現邏輯的代碼目前看起來不是很智能。不如說在每次調用邏輯之后,表現邏輯都需要手動調用一次UpdateView方法

在一個項目中表現邏輯的調用次數很多。因為只要修改了數據,對應的就要把數據的變化在界面上表現出來。所以可以引入一個事件機制來解決這個問題

這個事件機制的使用其實是和Command一起使用的,通過Command修改數據,當數據發生修改后發送對應的數據變更事件,這個是簡化版本的CQRS原則,即讀寫分離原則。引入這項原則會很容易實現事件驅動、數據驅動架構

首先定義數據變更事件CountChangedEvent.cs

namespace QFramework.Example
{public struct CountChangedEvent{}
}

然后在IncreaseCountCommand、DecreaseCountCommand引入Event事件

//DecreaseCountCommand 
namespace QFramework.Example
{public class DecreaseCountCommand : AbstractCommand{protected override void OnExecute(){this.GetModel<CounterModel>().Count--;this.SendEvent<CountChangedEvent>();}}
}//IncreaseCountCommand 
namespace QFramework.Example
{public class IncreaseCountCommand : AbstractCommand{protected override void OnExecute(){this.GetModel<CounterModel>().Count++;this.SendEvent<CountChangedEvent>();}}
}

最后在CounterAppController中編寫表現邏輯部分代碼

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;namespace QFramework.Example
{public class CounterAppController : MonoBehaviour,IController{public Button BtnAdd;public Button BtnSub;public Text CounterText;private CounterModel mModel;private void Start(){mModel = this.GetModel<CounterModel>();BtnAdd.onClick.AddListener(() =>{//交互邏輯this.SendCommand<IncreaseCountCommand>();});BtnSub.onClick.AddListener(() => {//交互邏輯this.SendCommand<DecreaseCountCommand>();});//表現邏輯this.RegisterEvent<CountChangedEvent>(e =>{updateview();}).UnRegisterWhenGameObjectDestroyed(gameObject);updateview();}void updateview(){CounterText.text = mModel.Count.ToString();}public IArchitecture GetArchitecture(){return CounterApp.Interface;}}
}

通過事件方式,將表現邏輯更新進行解耦,就是說我們并不要主動調用表現邏輯,而是定義好表現邏輯后,然后在數據變更的同時發送對應的事件,表現邏輯只需要訂閱這個事件并定義好對應執行的邏輯即可。這樣不論任何角色發生了數據變更,同時需要負責發送事件

引入Utility

在學Utility之前,先來用之前學習的內容來支持CounterApp的數據存儲功能

使用PlayerPrefs

using UnityEngine;namespace QFramework.Example
{public class CounterModel : AbstractModel{public int mCount = 0;public int Count{get{ return mCount; }set{if (mCount!=value){mCount = value;PlayerPrefs.SetInt(nameof(Count),value);}}}protected override void OnInit(){Count = PlayerPrefs.GetInt(nameof(Count),0);}}
}

當然我們現在存儲少量的數據是非常可行的,但如果需要存儲大量數據的時候,Model層就會有大量的存儲、加載相關的代碼,還有如果以后不想使用PlayerPrefs時,需要修改的時候,就會造成大量修改工作量

在QFramework中提供了一個Utility層,專門用來解決上述問題,使用方法非常簡單

首先創建Storage類,定義Utility層

using UnityEngine;namespace QFramework.Example
{public class Storage:IUtility{public void SaveInt(string key,int value){PlayerPrefs.SetInt(key, value);}public int LoadInt(string key, int value) { return PlayerPrefs.GetInt(key, value);}}
}

在CounterApp中注冊Model

namespace QFramework.Example
{public class CounterApp : Architecture<CounterApp>{protected override void Init() {this.RegisterUtility(new Storage());this.RegisterModel(new CounterModel());}}
}

在CounterModel中編寫要存儲數據的代碼

using UnityEngine;namespace QFramework.Example
{public class CounterModel : AbstractModel{public int mCount = 0;private Storage storage;public int Count{get{ return mCount; }set{if (mCount!=value){mCount = value;storage.SaveInt(nameof(Count), value);}}}protected override void OnInit(){storage = this.GetUtility<Storage>();Count = storage.LoadInt(nameof(Count),0);}}
}

這樣的話,如果我們想要修改PlayerPrefs為其他存儲函數時只需要對Storage.cs進行相應的修改即可

引入System

我們設置一個功能,及策劃提出了一個成就達成的功能,當Count點擊到10的時候,觸發一個點擊達人成就,點擊到20的時候,觸發一個點擊專家的成就

讓我們編寫相關的代碼

using UnityEngine;
namespace QFramework.Example
{public class IncreaseCountCommand : AbstractCommand{protected override void OnExecute(){var couterModel = this.GetModel<CounterModel>();couterModel.Count++;this.SendEvent<CountChangedEvent>();if (couterModel.Count == 10){Debug.Log(couterModel.Count + "點擊達人成就完成");}else if (couterModel.Count==20){Debug.Log(couterModel.Count + "點擊專家成就完成");}}}
}

ok,這個功能完成了,但策劃又說,希望再增加一個點擊到-10時,觸發一個點擊菜鳥成就,并且點擊達人成就和點擊專家成就太容易達成了,需要改成1000和2000次時,就需要我們去修改兩處的代碼,結果在造成了多處修改,這說明代碼有問題

那么針對以上的問題QFramework提供了一個System對象

首先創建AchievementSystem.cs類

using UnityEngine;
namespace QFramework.Example
{public class AchievementSystem:AbstractSystem{protected override void OnInit(){var model = this.GetModel<CounterModel>();this.RegisterEvent<CountChangedEvent>(e =>{if (model.Count == 10){Debug.Log("點擊達人成就達成");}else if (model.Count == 20){Debug.Log("點擊專家成就達成");}else if (model.Count == -10){Debug.Log("點擊菜鳥成就達成");}});}}
}

然后在CounterApp里注冊AchievementSystem

namespace QFramework.Example
{public class CounterApp : Architecture<CounterApp>{protected override void Init() {this.RegisterSystem(new AchievementSystem());this.RegisterUtility(new Storage());this.RegisterModel(new CounterModel());}}
}

QFramework的四個層級

  • 表現層:IController
  • 系統層:ISystem
  • 數據層:IModel
  • 工具層:IUtility

除了四個層級,還接觸了為Controller的交互邏輯減負的Command和為表現邏輯減負的Event

BindableProperty 優化事件

BindableProperty是包含數據+數據變更事件的一個對象

BindableProperty的基本使用

namespace QFramework.Example
{public class CounterModel : AbstractModel{public BindableProperty<int> Count = new BindableProperty<int>(0);protected override void OnInit(){var storage = this.GetUtility<Storage>();Count.Value = storage.LoadInt(nameof(Count),0);Count.Register(count =>{storage.SaveInt(nameof(Count), count);});}}
}using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;namespace QFramework.Example
{public class CounterAppController : MonoBehaviour,IController{public Button BtnAdd;public Button BtnSub;public Text CounterText;private CounterModel mModel;private void Start(){mModel = this.GetModel<CounterModel>();BtnAdd.onClick.AddListener(() =>{//交互邏輯this.SendCommand<IncreaseCountCommand>();});BtnSub.onClick.AddListener(() => {//交互邏輯this.SendCommand<DecreaseCountCommand>();});//表現邏輯mModel.Count.RegisterWithInitValue(count =>{updateview();}).UnRegisterWhenGameObjectDestroyed(gameObject);}void updateview(){CounterText.text = mModel.Count.ToString();}public IArchitecture GetArchitecture(){return CounterApp.Interface;}}
}using UnityEngine;
namespace QFramework.Example
{public class AchievementSystem:AbstractSystem{protected override void OnInit(){var model = this.GetModel<CounterModel>();model.Count.Register(count =>{if (count == 10){Debug.Log("點擊達人成就達成");}else if (count == 20){Debug.Log("點擊專家成就達成");}else if (count == -10){Debug.Log("點擊菜鳥成就達成");}});}}
}

BindableProperty 除了提供 Register 這個 API 之外,還提供了 RegisterWithInitValue API,意思是 注冊時 先把當前值返回過來

BindableProperty是一個獨立的工具,可以脫離QFramework框架使用,也就是說不用非要用QFramework的MVC才能用BindableProperty,而是可以在自己的項目中隨意使用

一般情況下,像主角的金幣、分數等數據非常適合用 BindableProperty 的方式實現。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/82938.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/82938.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/82938.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

R3GAN訓練自己的數據集

簡介 簡介&#xff1a;這篇論文挑戰了"GANs難以訓練"的廣泛觀點&#xff0c;通過提出一個更穩定的損失函數和現代化的網絡架構&#xff0c;構建了一個簡潔而高效的GAN基線模型R3GAN。作者證明了通過合適的理論基礎和架構設計&#xff0c;GANs可以穩定訓練并達到優異…

【PhysUnits】15.1 引入P1后的加一特質(add1.rs)

一、源碼 代碼實現了類型系統中的"加一"操作&#xff08;Add1 trait&#xff09;&#xff0c;用于在編譯期進行數字的增量計算。 //! 加一操作特質實現 / Increment operation trait implementation //! //! 說明&#xff1a; //! 1. Z0、P1,、N1 1&#xff0…

記錄算法筆記(2025.5.29)最小棧

設計一個支持 push &#xff0c;pop &#xff0c;top 操作&#xff0c;并能在常數時間內檢索到最小元素的棧。 實現 MinStack 類: MinStack() 初始化堆棧對象。void push(int val) 將元素val推入堆棧。void pop() 刪除堆棧頂部的元素。int top() 獲取堆棧頂部的元素。int get…

Android高級開發第一篇 - JNI(初級入門篇)

文章目錄 Android高級開發JNI開發第一篇&#xff08;初級入門篇&#xff09;&#x1f9e0; 一、什么是 JNI&#xff1f;? 為什么要用 JNI&#xff1f; ?? 二、開發環境準備開發工具 &#x1f680; 三、創建一個支持 JNI 的 Android 項目第一步&#xff1a;創建新項目項目結構…

PyTorch Image Models (timm) 技術指南

timm PyTorch Image Models (timm) 技術指南功能概述 一、引言二、timm 庫概述三、安裝 timm 庫四、模型加載與推理示例4.1 通用推理流程4.2 具體模型示例4.2.1 ResNeXt50-32x4d4.2.2 EfficientNet-V2 Small 模型4.2.3 DeiT-3 large 模型4.2.4 RepViT-M2 模型4.2.5 ResNet-RS-1…

openEuler安裝MySql8(tar包模式)

操作系統版本&#xff1a; openEuler release 22.03 (LTS-SP4) MySql版本&#xff1a; 下載地址&#xff1a; https://dev.mysql.com/downloads/mysql/ 準備安裝&#xff1a; 關閉防火墻&#xff1a; 停止防火墻 #systemctl stop firewalld.service 關閉防火墻 #systemc…

從零開始的數據結構教程(六) 貪心算法

&#x1f36c; 標題一&#xff1a;貪心核心思想——發糖果時的最優分配策略 貪心算法 (Greedy Algorithm) 是一種簡單直觀的算法策略。它在每一步選擇中都采取在當前狀態下最好或最優&#xff08;即最有利&#xff09;的選擇&#xff0c;從而希望得到一個全局最優解。這就像你…

CPP中CAS std::chrono 信號量與Any類的手動實現

前言 CAS&#xff08;Compare and Swap&#xff09; 是一種用于多線程同步的原子指令。它通過比較和交換操作來確保數據的一致性和線程安全性。CAS操作涉及三個操作數&#xff1a;內存位置V、預期值E和新值U。當且僅當內存位置V的值與預期值E相等時&#xff0c;CAS才會將內存位…

Axure設計案例——科技感對比柱狀圖

想讓數據對比展示擺脫平淡無奇&#xff0c;瞬間抓住觀眾的眼球嗎&#xff1f;那就來看看這個Axure設計的科技感對比柱狀圖案例&#xff01;科技感設計風格運用獨特元素打破傳統對比柱狀圖的常規&#xff0c;營造出一種極具沖擊力的視覺氛圍。每一組柱狀體都仿佛是科技戰場上的士…

怒更一波免費聲音克隆和AI配音功能

寶子們&#xff01; 最近咱軟件TransDuck的免費聲音克隆和AI配音功能被大家用爆啦&#xff01;感謝各位自來水瘋狂安利&#xff01;&#xff01; DD這里也是收到好多用戶提的寶貴建議&#xff01;所以&#xff0c;連夜肝了波更新&#xff01; 這次重點更新使用克隆音色進行A…

UDP協議原理與Java編程實戰:無連接通信的奧秘

1.UDP協議核心原理 1. 無連接特性&#xff1a;快速通信的基石 UDP&#xff08;User Datagram Protocol&#xff0c;用戶數據報協議&#xff09;是TCP/IP協議族中無連接的輕量級傳輸層協議。與TCP的“三次握手”建立連接不同&#xff0c;UDP通信無需提前建立鏈路&#xff0c;發送…

vue-seamless-scroll 結束從頭開始,加延時后滾動

今天遇到一個大屏需求&#xff1a; 1??初始進入頁面停留5秒&#xff0c;然后開始滾動 2??最后一條數據出現在最后一行時候暫停5秒&#xff0c;然后返回1?? 依次循環&#xff0c;發現vue-seamless-scroll的方法 ScrollEnd是監測最后一條數據消失在第一行才回調&#xff…

[Protobuf] 快速上手:安全高效的序列化指南

標題&#xff1a;[Protobuf] (1)快速上手 水墨不寫bug 文章目錄 一、什么是protobuf&#xff1f;二、protobuf的特點三、使用protobuf的過程&#xff1f;1、定義消息格式&#xff08;.proto文件&#xff09;(1)指定語法版本(2)package 聲明符 2、使用protoc編譯器生成代碼&…

uniapp調用java接口 跨域問題

前言 之前在Windows10本地 調試一個舊項目&#xff0c;手機移動端用的是Uni-app&#xff0c;vue的版本是v2。后端是java spring-boot。運行手機移動端的首頁請求后臺接口老是提示錯誤信息。 錯誤信息如下&#xff1a; Access to XMLHttpRequest at http://localhost:8080/api/…

[ Qt ] | Qlabel使用

目錄 屬性 setTextFormat 插入圖片 設置圖片根據窗口大小實時變化 邊框和對其方式 ?編輯 設置縮進 設置伙伴 Qlabel可以用來顯式圖片和文字 屬性 text textFormat Qlabel獨有的機制&#xff1a;buddy setTextFormat 插入圖片 設置圖片根據窗口大小實時變化 Qt中表…

Springboot 項目一啟動就獲取HttpSession

在 Spring Boot 項目中&#xff0c;HttpSession 是有狀態的&#xff0c;通常只有在用戶發起 HTTP 請求并建立會話后才會創建。因此&#xff0c;在項目啟動時&#xff08;即應用剛啟動還未處理任何請求&#xff09;是無法獲取到 HttpSession 的。 方法一&#xff1a;使用 HttpS…

Step9—Ambari Web UI 初始化安裝 (Ambari3.0.0)

Ambari Web UI 安裝 如果還不會系統性的部署&#xff0c;或者前置內容不熟悉&#xff0c;建議從Step1 開始閱讀。不通版本針對于不同操作系統可能存在差異&#xff01;這里我也整理好了 https://doc.janettr.com/install/manual/ 1. 進入 Ambari Web UI 并登錄 在瀏覽器中訪…

熱門大型語言模型(LLM)應用開發框架

我們來深入探索這些強大的大型語言模型&#xff08;LLM&#xff09;應用開發框架&#xff0c;并且我會嘗試用文本形式描述一些核心的流程圖&#xff0c;幫助您更好地理解它們的工作機制。由于我無法直接生成圖片&#xff0c;我會用文字清晰地描述流程圖的各個步驟和連接。 Lang…

機器學習數據降維方法

1.數據類型 2.如何選擇降維方法進行數據降維 3.線性降維&#xff1a;主成分分析&#xff08;PCA&#xff09;、線性判別分析&#xff08;LDA&#xff09; 4.非線性降維 5.基于特征選擇的降維 6.基于神經網絡的降維 數據降維是將高維數據轉換為低維表示的過程&#xff0c;旨在保…

太陽系運行模擬程序-html動畫

太陽系運行模擬程序-html動畫 by AI: <!DOCTYPE html> <html lang"zh"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>交互式太陽系…