ROS2与Rviz2的贪吃蛇代码学习

举报
zhangrelay 发表于 2022/06/03 23:01:22 2022/06/03
【摘要】 参考网址: github.com/Desperationis/rviz-snake/tree/v1.1.0 端午不休,学习代码。  官方效果如下(引用):   ROS2 humble  编译全过程: zhangrelay@LAPTOP-5REQ7K1L:~/ros_ws$ cd rviz...

参考网址:

github.com/Desperationis/rviz-snake/tree/v1.1.0
 

端午不休,学习代码。 

官方效果如下(引用):

 


ROS2 humble 

编译全过程:


  
  1. zhangrelay@LAPTOP-5REQ7K1L:~/ros_ws$ cd rviz_snake/
  2. zhangrelay@LAPTOP-5REQ7K1L:~/ros_ws/rviz_snake$ colcon build
  3. Starting >>> snake_publisher
  4. -- The C compiler identification is GNU 11.2.0
  5. -- The CXX compiler identification is GNU 11.2.0
  6. -- Detecting C compiler ABI info
  7. -- Detecting C compiler ABI info - done
  8. -- Check for working C compiler: /usr/bin/cc - skipped
  9. -- Detecting C compile features
  10. -- Detecting C compile features - done
  11. -- Detecting CXX compiler ABI info
  12. -- Detecting CXX compiler ABI info - done
  13. -- Check for working CXX compiler: /usr/bin/c++ - skipped
  14. -- Detecting CXX compile features
  15. -- Detecting CXX compile features - done
  16. -- Found ament_cmake: 1.3.2 (/opt/ros/humble/share/ament_cmake/cmake)
  17. -- Found Python3: /usr/bin/python3.10 (found version "3.10.4") found components: Interpreter
  18. -- Found rclcpp: 16.0.1 (/opt/ros/humble/share/rclcpp/cmake)
  19. -- Found rosidl_generator_c: 3.1.3 (/opt/ros/humble/share/rosidl_generator_c/cmake)
  20. -- Found rosidl_adapter: 3.1.3 (/opt/ros/humble/share/rosidl_adapter/cmake)
  21. -- Found rosidl_generator_cpp: 3.1.3 (/opt/ros/humble/share/rosidl_generator_cpp/cmake)
  22. -- Using all available rosidl_typesupport_c: rosidl_typesupport_fastrtps_c;rosidl_typesupport_introspection_c
  23. -- Using all available rosidl_typesupport_cpp: rosidl_typesupport_fastrtps_cpp;rosidl_typesupport_introspection_cpp
  24. -- Found rmw_implementation_cmake: 6.1.0 (/opt/ros/humble/share/rmw_implementation_cmake/cmake)
  25. -- Found rmw_fastrtps_cpp: 6.2.1 (/opt/ros/humble/share/rmw_fastrtps_cpp/cmake)
  26. -- Found OpenSSL: /usr/lib/x86_64-linux-gnu/libcrypto.so (found version "3.0.2")
  27. -- Found FastRTPS: /opt/ros/humble/include
  28. -- Using RMW implementation 'rmw_fastrtps_cpp' as default
  29. -- Found visualization_msgs: 4.2.2 (/opt/ros/humble/share/visualization_msgs/cmake)
  30. -- Found Curses: /usr/lib/x86_64-linux-gnu/libcurses.so
  31. -- Found ament_lint_auto: 0.12.4 (/opt/ros/humble/share/ament_lint_auto/cmake)
  32. -- Added test 'copyright' to check source files copyright and LICENSE
  33. -- Added test 'cppcheck' to perform static code analysis on C / C++ code
  34. -- Configured cppcheck include dirs: /home/zhangrelay/ros_ws/rviz_snake/src/snake_publisher/include/
  35. -- Configured cppcheck exclude dirs and/or files:
  36. -- Added test 'cpplint' to check C / C++ code against the Google style
  37. -- Configured cpplint exclude dirs and/or files:
  38. -- Added test 'lint_cmake' to check CMake code style
  39. -- Added test 'uncrustify' to check C / C++ code style
  40. -- Configured uncrustify additional arguments:
  41. -- Added test 'xmllint' to check XML markup files
  42. -- Configuring done
  43. -- Generating done
  44. -- Build files have been written to: /home/zhangrelay/ros_ws/rviz_snake/build/snake_publisher
  45. [ 50%] Building CXX object CMakeFiles/publisher.dir/src/main.cpp.o
  46. [100%] Linking CXX executable publisher
  47. [100%] Built target publisher
  48. -- Install configuration: ""
  49. -- Installing: /home/zhangrelay/ros_ws/rviz_snake/install/snake_publisher/lib/snake_publisher/publisher
  50. -- Set runtime path of "/home/zhangrelay/ros_ws/rviz_snake/install/snake_publisher/lib/snake_publisher/publisher" to ""
  51. -- Installing: /home/zhangrelay/ros_ws/rviz_snake/install/snake_publisher/share/ament_index/resource_index/package_run_dependencies/snake_publisher
  52. -- Installing: /home/zhangrelay/ros_ws/rviz_snake/install/snake_publisher/share/ament_index/resource_index/parent_prefix_path/snake_publisher
  53. -- Installing: /home/zhangrelay/ros_ws/rviz_snake/install/snake_publisher/share/snake_publisher/environment/ament_prefix_path.sh
  54. -- Installing: /home/zhangrelay/ros_ws/rviz_snake/install/snake_publisher/share/snake_publisher/environment/ament_prefix_path.dsv
  55. -- Installing: /home/zhangrelay/ros_ws/rviz_snake/install/snake_publisher/share/snake_publisher/environment/path.sh
  56. -- Installing: /home/zhangrelay/ros_ws/rviz_snake/install/snake_publisher/share/snake_publisher/environment/path.dsv
  57. -- Installing: /home/zhangrelay/ros_ws/rviz_snake/install/snake_publisher/share/snake_publisher/local_setup.bash
  58. -- Installing: /home/zhangrelay/ros_ws/rviz_snake/install/snake_publisher/share/snake_publisher/local_setup.sh
  59. -- Installing: /home/zhangrelay/ros_ws/rviz_snake/install/snake_publisher/share/snake_publisher/local_setup.zsh
  60. -- Installing: /home/zhangrelay/ros_ws/rviz_snake/install/snake_publisher/share/snake_publisher/local_setup.dsv
  61. -- Installing: /home/zhangrelay/ros_ws/rviz_snake/install/snake_publisher/share/snake_publisher/package.dsv
  62. -- Installing: /home/zhangrelay/ros_ws/rviz_snake/install/snake_publisher/share/ament_index/resource_index/packages/snake_publisher
  63. -- Installing: /home/zhangrelay/ros_ws/rviz_snake/install/snake_publisher/share/snake_publisher/cmake/snake_publisherConfig.cmake
  64. -- Installing: /home/zhangrelay/ros_ws/rviz_snake/install/snake_publisher/share/snake_publisher/cmake/snake_publisherConfig-version.cmake
  65. -- Installing: /home/zhangrelay/ros_ws/rviz_snake/install/snake_publisher/share/snake_publisher/package.xml
  66. Finished <<< snake_publisher [30.4s]

rviz2中的蛇游戏;这是为了好玩,作为 ROS2 的介绍。

要求

目前,这仅rclcpp针对 ROS2 Galactic/Humble 进行了测试,尽管它很可能在任何稍旧的设备上都可以正常工作。此外,您需要安装 rviz2 和 ncurses(用于用户输入),通过sudo apt-get install libncurses-dev.

运行

首先,配置 ros2 环境。

  1. 通过进入根目录并运行colcon build.
  2. 源项目通过source install/setup.bash.
  3. 通过运行游戏ros2 run snake_publisher publisher。
  4. 在单独的终端中,运行rviz2 src/snake_publisher/rvizSetup.rviz.

这样,游戏就应该运行了。输入是在ros2 run运行的终端上进行的。

配置

目前实现了以下节点参数:

game_fps- 游戏更新的速率。
input_fps- 从队列中处理输入的速率。
snake_color_*- 蛇在其 RGB 组件中的颜色。
fruit_color_*- RGB 成分中水果的颜色。
grid_color_*- 网格在其 RGB 组件中的颜色。

限制/错误

没有提供启动文件(将来可以制作一个)
如果足够好来填满整个棋盘,游戏就会崩溃。
一段时间后,消息会慢慢延迟明显的数量;只需重新启动 rviz2 或关闭并打开标记显示(不要问为什么会这样)


核心代码:


  
  1. #ifndef SNAKEGAME_HPP
  2. #define SNAKEGAME_HPP
  3. #include <memory>
  4. #include <thread>
  5. #include <functional>
  6. #include <chrono>
  7. #include <vector>
  8. #include <ncurses.h>
  9. #include <csignal>
  10. #include <list>
  11. #include "rclcpp/rclcpp.hpp"
  12. #include "rclcpp/executor.hpp"
  13. #include "visualization_msgs/msg/marker.hpp"
  14. #include "geometry_msgs/msg/point.hpp"
  15. #include "std_msgs/msg/color_rgba.hpp"
  16. #include "Markers.hpp"
  17. using namespace std::chrono_literals;
  18. /**
  19. * Snake.hpp
  20. *
  21. * This files holds classes that directly relate to running "Snake". Everything
  22. * is managed interally as simply points, and only get turned into cubes the
  23. * moment they are rendered; This allows for multiple possible ways to render
  24. * the game.
  25. */
  26. /**
  27. * Details the color of each game piece.
  28. */
  29. struct GamePieceColors {
  30. std_msgs::msg::ColorRGBA snakeColor;
  31. std_msgs::msg::ColorRGBA fruitColor;
  32. std_msgs::msg::ColorRGBA gridColor;
  33. };
  34. /**
  35. * Wrapper for GridMarker so that origin is at the topleft, x-axis is positive
  36. * to the right, y-axis is positive downwards, and the grid is drawn. This is
  37. * where points are turned into cubes.
  38. */
  39. class SnakeGrid {
  40. public:
  41. enum GRID_PIECES {EMPTY, SNAKE, FRUIT};
  42. private:
  43. int sideLength;
  44. int worldXOffset, worldYOffset;
  45. std::vector<std::vector<GRID_PIECES>> gridElements;
  46. public:
  47. /**
  48. * Creates a grid that is at maximum size `sideLength` on each side.
  49. */
  50. SnakeGrid(int sideLength) {
  51. this->sideLength = sideLength;
  52. worldXOffset = -sideLength / 2;
  53. worldYOffset = -sideLength / 2;
  54. for(int i = 0; i < sideLength; i++) {
  55. gridElements.push_back(std::vector<GRID_PIECES>());
  56. for(int j = 0; j < sideLength; j++) {
  57. gridElements[i].push_back(EMPTY);
  58. }
  59. }
  60. }
  61. /**
  62. * Determine whether a point is within the bounds of [0, side_length).
  63. */
  64. bool InBounds(int x, int y) {
  65. return x < sideLength && x >= 0 &&
  66. y < sideLength && y >= 0;
  67. }
  68. /**
  69. * Reserve a specific spot to be drawn as a snake in the next frame, given
  70. * that it is in bounds. If it is not in bounds, nothing will happen.
  71. */
  72. void ReserveSnake(int x, int y) {
  73. if (this->InBounds(x, y)) {
  74. gridElements[y][x] = SNAKE;
  75. }
  76. }
  77. /**
  78. * Reserve a specific spot to be drawn as a fruit in the next frame, given
  79. * that it is in bounds. If it is not in bounds, nothing will happen.
  80. */
  81. void ReserveFruit(int x, int y) {
  82. if (this->InBounds(x, y)) {
  83. gridElements[y][x] = FRUIT;
  84. }
  85. }
  86. /**
  87. * Get the type of piece that is currently reserved. By default, every
  88. * square on the grid is reserved EMPTY on every frame.
  89. */
  90. GRID_PIECES GetReserved(int x, int y) {
  91. return gridElements[y][x];
  92. }
  93. /**
  94. * Returns the side length of the grid.
  95. */
  96. int GetSideLength() {
  97. return sideLength;
  98. }
  99. /**
  100. * Draws the grid by iterating through all reserved portions and drawing a
  101. * specific cube based on its type. For SNAKE and FRUIT, the square is
  102. * elevated by 1 unit in order to give the apperance of 3D.
  103. */
  104. void Draw(std::shared_ptr<MarkerPublisher> publisher, GamePieceColors colors) {
  105. GridMarker grid;
  106. for(size_t i = 0; i < gridElements.size(); i++) {
  107. for(size_t j = 0; j < gridElements[i].size(); j++) {
  108. GRID_PIECES type = gridElements[i][j];
  109. Cube cube;
  110. cube.SetPos(i + worldXOffset, j + worldYOffset, 1);
  111. switch(type) {
  112. case EMPTY:
  113. cube.SetPos(i + worldXOffset, j + worldYOffset, 0);
  114. cube.color = colors.gridColor;
  115. break;
  116. case SNAKE:
  117. cube.color = colors.snakeColor;
  118. break;
  119. case FRUIT:
  120. cube.color = colors.fruitColor;
  121. break;
  122. };
  123. grid.AddCube(cube);
  124. }
  125. }
  126. publisher->PublishMarker(grid);
  127. }
  128. /**
  129. * Completely clears the grid with EMPTY tiles.
  130. */
  131. void Clear() {
  132. for(int i = 0; i < sideLength; i++) {
  133. for(int j = 0; j < sideLength; j++) {
  134. gridElements[i][j] = EMPTY;
  135. }
  136. }
  137. }
  138. };
  139. struct Fruit {
  140. Fruit(int x, int y) {
  141. p.x = x;
  142. p.y = y;
  143. }
  144. geometry_msgs::msg::Point p;
  145. };
  146. /**
  147. * Manager for controlling the spawning and erasing of fruits on the board.
  148. */
  149. class FruitManager {
  150. public:
  151. FruitManager() {
  152. requestedFruit = 0;
  153. }
  154. /**
  155. * Randomly spawn a single fruit that is not occupied by a SNAKE tile.
  156. * TODO: Prevent this from being an infinite loop should a player good
  157. * enough completely fill up the board.
  158. */
  159. void SpawnFruit(SnakeGrid snakeGrid) {
  160. while(requestedFruit > 0) {
  161. int x = rand() % snakeGrid.GetSideLength();
  162. int y = rand() % snakeGrid.GetSideLength();
  163. if(snakeGrid.GetReserved(x, y) == SnakeGrid::EMPTY) {
  164. fruits.push_back(Fruit(x, y));
  165. snakeGrid.ReserveFruit(x, y);
  166. requestedFruit--;
  167. }
  168. }
  169. }
  170. /**
  171. * Request a single fruit to be drawn the next frame. Can be called
  172. * multiple times for multiple fruits.
  173. */
  174. void RequestFruit() {
  175. requestedFruit++;
  176. }
  177. /**
  178. * Try to eat a fruit at a specific point. If there is not fruit at that
  179. * point, return false. If there is, return true and erase the fruit from
  180. * existence.
  181. */
  182. bool Eat(int x, int y) {
  183. for(size_t i = 0; i < fruits.size(); i++) {
  184. auto fruit = fruits[i];
  185. if(fruit.p.x == x && fruit.p.y == y) {
  186. fruits.erase(fruits.begin() + i);
  187. return true;
  188. }
  189. }
  190. return false;
  191. }
  192. /**
  193. * Get the list of points that occupy fruits.
  194. */
  195. std::vector<Fruit> GetFruits() {
  196. return fruits;
  197. }
  198. private:
  199. std::vector<Fruit> fruits;
  200. // This variable holds the amount of requested fruit that should be spawned
  201. // on the next frame.
  202. int requestedFruit;
  203. };
  204. /**
  205. * The Snake. Body is completely represented by points.
  206. */
  207. class Snake {
  208. public:
  209. enum DIRECTION {LEFT, RIGHT, UP, DOWN};
  210. Snake(int x, int y) {
  211. Respawn(x, y);
  212. }
  213. std::list<geometry_msgs::msg::Point> GetBody() {
  214. return body;
  215. }
  216. /**
  217. * Updates the snake by a single frame. Essentially, it determines when it
  218. * dies, how to move, and how to eat fruits.
  219. */
  220. void Update(SnakeGrid& snakeGrid, FruitManager& fruitManager) {
  221. if(!dead) {
  222. geometry_msgs::msg::Point nextPoint;
  223. nextPoint = body.front();
  224. switch(currentDirection) {
  225. case LEFT:
  226. nextPoint.x -= 1;
  227. break;
  228. case RIGHT:
  229. nextPoint.x += 1;
  230. break;
  231. case UP:
  232. nextPoint.y -= 1;
  233. break;
  234. case DOWN:
  235. nextPoint.y += 1;
  236. break;
  237. };
  238. actualDirection = currentDirection;
  239. dead = WillDie(nextPoint.x, nextPoint.y, snakeGrid);
  240. if(!dead) {
  241. body.push_front(nextPoint);
  242. bool fruitEaten = fruitManager.Eat(nextPoint.x, nextPoint.y);
  243. if(!fruitEaten) {
  244. body.pop_back();
  245. }
  246. else {
  247. fruitManager.RequestFruit();
  248. }
  249. }
  250. }
  251. }
  252. void SetDirection(DIRECTION dir) {
  253. currentDirection = dir;
  254. }
  255. DIRECTION GetDirection() {
  256. return currentDirection;
  257. }
  258. DIRECTION GetActualDirection() {
  259. return actualDirection;
  260. }
  261. bool IsDead() {
  262. return dead;
  263. }
  264. /**
  265. * Checks whether or not the head is out of bounds our intersected its own
  266. * body to determine if it died.
  267. */
  268. bool WillDie(int x, int y, SnakeGrid& snakeGrid) {
  269. if(!snakeGrid.InBounds(x, y))
  270. return true;
  271. for(auto point : body) {
  272. if (point.x == x && point.y == y) {
  273. return true;
  274. }
  275. }
  276. return false;
  277. }
  278. /**
  279. * Respawn the snake at a specific point.
  280. */
  281. void Respawn(int x, int y) {
  282. body.clear();
  283. geometry_msgs::msg::Point p;
  284. p.x = x;
  285. p.y = y;
  286. body.push_front(p);
  287. dead = false;
  288. currentDirection = RIGHT;
  289. }
  290. private:
  291. // First element is head, final element is tail.
  292. std::list<geometry_msgs::msg::Point> body;
  293. bool dead;
  294. DIRECTION currentDirection;
  295. // User input and the game are asynchronous as run at different hertz, so
  296. // this gives the direction the snake is headed based on the last frame so
  297. // that the user doesn't circumnavigate the input-checking code to prevent
  298. // crashing directly backwards.
  299. DIRECTION actualDirection;
  300. };
  301. /**
  302. * ROS Node that actually runs the game. Due to not being able to create a
  303. * standard game loop with a delay, this node runs the loop via wall timers and
  304. * asynchronously receives input from the terminal via ncurses.
  305. */
  306. class GameNode : public rclcpp::Node {
  307. private:
  308. void OnGameFPSChange(const rclcpp::Parameter& p) {
  309. SetGameFPS(p.as_int());
  310. }
  311. void OnInputFPSChange(const rclcpp::Parameter& p) {
  312. SetInputFPS(p.as_int());
  313. }
  314. void SetGameFPS(int FPS) {
  315. int millisecondsToDelay = static_cast<int>(1000.0 / FPS);
  316. auto duration = std::chrono::duration<int, std::milli>(millisecondsToDelay);
  317. this->renderTimer = this->create_wall_timer(duration, std::bind(&GameNode::Loop, this));
  318. }
  319. void SetInputFPS(int FPS) {
  320. int millisecondsToDelay = static_cast<int>(1000.0 / FPS);
  321. auto duration = std::chrono::duration<int, std::milli>(millisecondsToDelay);
  322. this->inputTimer = this->create_wall_timer(duration, std::bind(&GameNode::UserInput, this));
  323. }
  324. public:
  325. GameNode(std::shared_ptr<MarkerPublisher> publisher) : Node("game_node"), snake(5,5), snakeGrid(15) {
  326. snake.SetDirection(Snake::RIGHT);
  327. this->declare_parameter("game_fps", 12);
  328. this->declare_parameter("input_fps", 100);
  329. this->declare_parameter("snake_color_r", 0);
  330. this->declare_parameter("snake_color_g", 255);
  331. this->declare_parameter("snake_color_b", 0);
  332. this->declare_parameter("fruit_color_r", 255);
  333. this->declare_parameter("fruit_color_g", 0);
  334. this->declare_parameter("fruit_color_b", 0);
  335. this->declare_parameter("grid_color_r", 255);
  336. this->declare_parameter("grid_color_g", 255);
  337. this->declare_parameter("grid_color_b", 255);
  338. this->SetGameFPS(get_parameter("game_fps").as_int());
  339. this->SetInputFPS(get_parameter("input_fps").as_int());
  340. // Handle parameter changes
  341. paramSubscriber = std::make_shared<rclcpp::ParameterEventHandler>(this);
  342. gameFPSHandle = paramSubscriber->add_parameter_callback("game_fps", std::bind(&GameNode::OnGameFPSChange, this, std::placeholders::_1));
  343. inputFPSHandle = paramSubscriber->add_parameter_callback("input_fps", std::bind(&GameNode::OnInputFPSChange, this, std::placeholders::_1));
  344. this->publisher = publisher;
  345. fruitManager.RequestFruit();
  346. fruitManager.SpawnFruit(snakeGrid);
  347. }
  348. /**
  349. * Game loop.
  350. */
  351. void Loop() {
  352. snake.Update(snakeGrid, fruitManager);
  353. for(auto body : snake.GetBody()) {
  354. snakeGrid.ReserveSnake(body.x, body.y);
  355. }
  356. fruitManager.SpawnFruit(snakeGrid);
  357. for(auto fruit : fruitManager.GetFruits()) {
  358. snakeGrid.ReserveFruit(fruit.p.x, fruit.p.y);
  359. }
  360. snakeGrid.Draw(publisher, GetColors());
  361. snakeGrid.Clear();
  362. }
  363. /**
  364. * Sole place for handling user input.
  365. */
  366. void UserInput() {
  367. int c = getch();
  368. auto currentDirection = snake.GetActualDirection();
  369. if (c != -1) {
  370. if(c == 'w' && currentDirection != Snake::DOWN)
  371. snake.SetDirection(Snake::UP);
  372. if(c == 'a' && currentDirection != Snake::RIGHT)
  373. snake.SetDirection(Snake::LEFT);
  374. if(c == 's' && currentDirection != Snake::UP)
  375. snake.SetDirection(Snake::DOWN);
  376. if(c == 'd' && currentDirection != Snake::LEFT)
  377. snake.SetDirection(Snake::RIGHT);
  378. if(c == '\n')
  379. snake.Respawn(5,5);
  380. }
  381. }
  382. GamePieceColors GetColors() {
  383. GamePieceColors colors;
  384. colors.snakeColor.r = this->get_parameter("snake_color_r").as_int() / 255;
  385. colors.snakeColor.g = this->get_parameter("snake_color_g").as_int() / 255;
  386. colors.snakeColor.b = this->get_parameter("snake_color_b").as_int() / 255;
  387. colors.snakeColor.a = 1;
  388. colors.gridColor.r = this->get_parameter("grid_color_r").as_int() / 255;
  389. colors.gridColor.g = this->get_parameter("grid_color_g").as_int() / 255;
  390. colors.gridColor.b = this->get_parameter("grid_color_b").as_int() / 255;
  391. colors.gridColor.a = 1;
  392. colors.fruitColor.r = this->get_parameter("fruit_color_r").as_int() / 255;
  393. colors.fruitColor.g = this->get_parameter("fruit_color_g").as_int() / 255;
  394. colors.fruitColor.b = this->get_parameter("fruit_color_b").as_int() / 255;
  395. colors.fruitColor.a = 1;
  396. return colors;
  397. }
  398. private:
  399. Snake snake;
  400. SnakeGrid snakeGrid;
  401. std::shared_ptr<MarkerPublisher> publisher;
  402. FruitManager fruitManager;
  403. rclcpp::TimerBase::SharedPtr renderTimer, inputTimer;
  404. std::shared_ptr<rclcpp::ParameterEventHandler> paramSubscriber;
  405. std::shared_ptr<rclcpp::ParameterCallbackHandle> gameFPSHandle;
  406. std::shared_ptr<rclcpp::ParameterCallbackHandle> inputFPSHandle;
  407. };
  408. #endif

文章来源: zhangrelay.blog.csdn.net,作者:zhangrelay,版权归原作者所有,如需转载,请联系作者。

原文链接:zhangrelay.blog.csdn.net/article/details/125108123

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。