Robcup2D足球学习记录【2020.01.10】
本次学习过程主要阅读了bhv_chain_action
对于代码的理解与存在的疑问(用注释标出)
注释格式如下:
/*
问题:
xxxx
理解:
xxxx
By ChenYanTing
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include "bhv_chain_action.h"
#include "action_chain_holder.h"
#include "action_chain_graph.h"
#include "action_state_pair.h"
#include "field_analyzer.h"
#include "bhv_pass_kick_find_receiver.h"
#include "bhv_normal_dribble.h"
#include "body_force_shoot.h"
#include "neck_turn_to_receiver.h"
#include <rcsc/action/bhv_scan_field.h>
#include <rcsc/action/body_clear_ball.h>
#include <rcsc/action/body_go_to_point.h>
#include <rcsc/action/body_smart_kick.h>
#include <rcsc/action/body_hold_ball.h>
#include <rcsc/action/body_turn_to_point.h>
#include <rcsc/action/neck_scan_field.h>
#include <rcsc/action/neck_turn_to_goalie_or_scan.h>
#include <rcsc/action/kick_table.h>
#include <rcsc/player/intercept_table.h>
#include <rcsc/player/soccer_intention.h>
#include <rcsc/player/player_agent.h>
#include <rcsc/common/server_param.h>
#include <rcsc/common/logger.h>
using namespace rcsc;
namespace {
class IntentionTurnTo
: public SoccerIntention {
private:
int M_step;
Vector2D M_target_point;
public:
IntentionTurnTo( const Vector2D & target_point )
: M_step( 0 ),
M_target_point( target_point )
{ }
bool finished( const PlayerAgent * agent );
bool execute( PlayerAgent * agent );
private:
};
/*-------------------------------------------------------------------*/
/*!
*/
/*
问题:
wm.interceptTable()中的一些属性分别有什么,可以在哪里查看?
理解:
此函数为每次球员移动结束后的提示函数,其主要功能为:每次球员完成了移动动作后,输出移动完成时的球员状态或者其他状态
此函数的内容如下:
记录步数的变量自加,然后输出移动的步数
如果移动的步数大于二,则输出时间溢出,并且返回true
如果自己是不可踢球状态,则输出(finished) no kickable并且返回true
如果存在可踢球的敌人,则输出(finished) exist kickable opponent
如果拦截表中,敌人的可达半径小于1,则输出(finished) opponent may be kickable
如果自己下一步距离球的距离大于可以踢球的距离,输出 (finished) unkickable at next cycle
By ChenYanTing
*/
bool
IntentionTurnTo::finished( const PlayerAgent * agent )
{
++M_step;
dlog.addText( Logger::TEAM,
__FILE__": (finished) step=%d",
M_step );
if ( M_step >= 2 )
{
dlog.addText( Logger::TEAM,
__FILE__": (finished) time over" );
return true;
}
const WorldModel & wm = agent->world();
//
// check kickable
//
if ( ! wm.self().isKickable() )
{
dlog.addText( Logger::TEAM,
__FILE__": (finished) no kickable" );
return true;
}
//
// check opponent
//
if ( wm.existKickableOpponent() )
{
dlog.addText( Logger::TEAM,
__FILE__": (finished) exist kickable opponent" );
return true;
}
if ( wm.interceptTable()->opponentReachCycle() <= 1 )
{
dlog.addText( Logger::TEAM,
__FILE__": (finished) opponent may be kickable" );
return true;
}
//
// check next kickable
//
double kickable2 = std::pow( wm.self().playerType().kickableArea()
- wm.self().vel().r() * ServerParam::i().playerRand()
- wm.ball().vel().r() * ServerParam::i().ballRand()
- 0.15,
2 );
Vector2D self_next = wm.self().pos() + wm.self().vel();
Vector2D ball_next = wm.ball().pos() + wm.ball().vel();
if ( self_next.dist2( ball_next ) > kickable2 )
{
// unkickable if turn is performed.
dlog.addText( Logger::TEAM,
__FILE__": (finished) unkickable at next cycle" );
return true;
}
return false;
}
/*-------------------------------------------------------------------*/
/*
问题:
agent->setNeckAction( new Neck_ScanField() ),这个设置颈部动作是不是在那个agent代码里面,属于后面看的代码?
理解:
这是球员移动到目标点的执行函数
函数内容如下:
首先输出目标点的x,y坐标
然后执行Body_TurnToPoint( M_target_point ).execute( agent )函数,执行移动到目标点的函数
最后设置颈部动作,环顾四周
By ChenYanTing
*/
bool
IntentionTurnTo::execute( PlayerAgent * agent )
{
dlog.addText( Logger::TEAM,
__FILE__": (intention) facePoint=(%.1f %.1f)",
M_target_point.x, M_target_point.y );
agent->debugClient().addMessage( "IntentionTurnToForward" );
Body_TurnToPoint( M_target_point ).execute( agent );
agent->setNeckAction( new Neck_ScanField() );
return true;
}
}
/*-------------------------------------------------------------------*/
/*!
*/
Bhv_ChainAction::Bhv_ChainAction( const ActionChainGraph & chain_graph )
: M_chain_graph( chain_graph )
{
}
/*-------------------------------------------------------------------*/
/*!
*/
Bhv_ChainAction::Bhv_ChainAction()
: M_chain_graph( ActionChainHolder::i().graph() )
{
}
/*-------------------------------------------------------------------*/
/*
问题:
ServerParam::i()代表什么?ServerParam这个类有必要看源代码吗?
M_chain_graph.getFirstAction(),这里获得的第一个动作指的什么?
agent->setNeckAction( new Neck_TurnToReceiver( M_chain_graph ) )这个设置的颈部动作是什么意思?
wm.gameMode().type(),不理解,vm的源代码需要看吗?
整个球场的中心坐标在哪里,球场的长和宽是多少?如果需要自己了解这些,可以在哪里查看数据?
Body_ClearBall(),这是指的什么意思?
理解:
这个函数为动作链的执行函数,简单通俗的讲,就是判断链式动作里面的第一个动作属于什么动作(射门、运球、持球等等),然后通过switch语句,根据不同的判断结果,给出不同的执行过程。
函数的内容如下:
首先输出Bhv_ChainAction
判断是否正在执行doTurnToForward( agent ),如果是则返回true
获得动作链式图里面的第一个动作,然后进行进入switch语句,判断第一个动作属于什么,根据不同的判断结果执行不同的过程。
如果当前比赛暂停或者处于不是正在比赛的过程中,则返回false。其他球员正在执行动作的情况,返回true。
*/
bool
Bhv_ChainAction::execute( PlayerAgent * agent )
{
dlog.addText( Logger::TEAM,
__FILE__": Bhv_ChainAction" );
if ( doTurnToForward( agent ) )
{
return true;
}
const ServerParam & SP = ServerParam::i();
const WorldModel & wm = agent->world();
const CooperativeAction & first_action = M_chain_graph.getFirstAction();
ActionChainGraph::debug_send_chain( agent, M_chain_graph.getAllChain() );
const Vector2D goal_pos = SP.theirTeamGoalPos();
agent->setNeckAction( new Neck_TurnToReceiver( M_chain_graph ) );
switch ( first_action.category() ) {
case CooperativeAction::Shoot:
{
dlog.addText( Logger::TEAM,
__FILE__" (Bhv_ChainAction) shoot" );
if ( Body_ForceShoot().execute( agent ) )
{
agent->setNeckAction( new Neck_TurnToGoalieOrScan() );
#ifdef DEBUG2014
std::cerr << wm.time().cycle() << __FILE__ << wm.self().unum() << ": (Bhv_ChainAction) shoot...\n";
#endif // DEBUG2014
return true;
}
#ifdef DEBUG2014
std::cerr << wm.time().cycle() << __FILE__ << wm.self().unum() << ": (Bhv_ChainAction) shoot failed...\n";
#endif // DEBUG2014
break;
}
case CooperativeAction::Dribble:
{
if ( wm.gameMode().type() != GameMode::PlayOn
&& ! wm.gameMode().isPenaltyKickMode() )
{
agent->debugClient().addMessage( "CancelChainDribble" );
dlog.addText( Logger::TEAM,
__FILE__" (Bhv_ChainAction) cancel dribble" );
return false;
}
const Vector2D & dribble_target = first_action.targetPoint();
dlog.addText( Logger::TEAM,
__FILE__" (Bhv_ChainAction) dribble target=(%.1f %.1f)",
dribble_target.x, dribble_target.y );
NeckAction::Ptr neck;
double goal_dist = goal_pos.dist( dribble_target );
if ( goal_dist < 18.0 )
{
int count_thr = 0;
if ( goal_dist < 13.0 )
{
count_thr = -1;
}
agent->debugClient().addMessage( "ChainDribble:LookGoalie" );
neck = NeckAction::Ptr( new Neck_TurnToGoalieOrScan( count_thr ) );
}
if ( Bhv_NormalDribble( first_action, neck ).execute( agent ) )
{
#ifdef DEBUG2014
std::cerr << wm.time().cycle() << __FILE__ << wm.self().unum() << ": (Bhv_ChainAction) Dribble...\n";
#endif // DEBUG2014
return true;
}
break;
}
case CooperativeAction::Hold:
{
if ( wm.gameMode().type() != GameMode::PlayOn )
{
agent->debugClient().addMessage( "CancelChainHold" );
dlog.addText( Logger::TEAM,
__FILE__" (Bhv_ChainAction) cancel hold" );
return false;
}
if ( wm.ball().pos().x < -SP.pitchHalfLength() + 8.0
&& wm.ball().pos().absY() < SP.goalHalfWidth() + 1.0 )
{
agent->debugClient().addMessage( "ChainHold:Clear" );
dlog.addText( Logger::TEAM,
__FILE__" (Bhv_ChainAction) cancel hold. clear ball" );
Body_ClearBall().execute( agent );
agent->setNeckAction( new Neck_ScanField() );
return true;
}
agent->debugClient().addMessage( "hold" );
dlog.addText( Logger::TEAM,
__FILE__" (Bhv_ChainAction) hold" );
if ( Body_HoldBall().execute( agent ) )
{
agent->setNeckAction( new Neck_ScanField() );
#ifdef DEBUG2014
std::cerr << wm.time().cycle() << __FILE__ << wm.self().unum() << ": (Bhv_ChainAction) Hold ball...\n";
#endif // DEBUG2014
return true;
}
break;
}
case CooperativeAction::Pass:
{
dlog.addText( Logger::TEAM,
__FILE__" (Bhv_ChainAction) pass" );
if ( first_action.targetPlayerUnum() == 1 )
{
#ifdef DEBUG2014
std::cerr << wm.time().cycle() << __FILE__ << wm.self().unum() << ": DO NOT PASS TO OUR GOALIE!!!\n\n";
#endif // DEBUG2014
break;
}
if ( Bhv_PassKickFindReceiver( M_chain_graph ).execute( agent ) )
{
if ( wm.self().pos().x < -42.0 )
{
#ifdef DEBUG2014
std::cerr << "wm.gameMode().type() = " << wm.gameMode().type() << std::endl;
std::cerr << "targetPoint = " << '(' << first_action.targetPoint().x << first_action.targetPoint().y << ')'
<< std::endl;
std::cerr << "targetPlayerUnum = " << first_action.targetPlayerUnum() << std::endl;
#endif // DEBUG2014
}
#ifdef DEBUG2014
std::cerr << wm.time().cycle() << __FILE__ << wm.self().unum() << ": (Bhv_ChainAction) Pass...\n\n";
#endif // DEBUG2014
return true;
}
break;
}
case CooperativeAction::Move:
{
dlog.addText( Logger::TEAM,
__FILE__" (Bhv_ChainAction) move" );
if ( Body_GoToPoint( first_action.targetPoint(),
1.0,
SP.maxDashPower() ).execute( agent ) )
{
agent->setNeckAction( new Neck_ScanField() );
#ifdef DEBUG2014
std::cerr << wm.time().cycle() << __FILE__ << wm.self().unum()
<< ": (Bhv_ChainAction) Move to "
<< first_action.targetPoint().x << ',' << first_action.targetPoint().y << std::endl;
#endif // DEBUG2014
return true;
}
break;
}
case CooperativeAction::Clear:
{
dlog.addText( Logger::TEAM,
__FILE__" (Bhv_ChainAction) clear" );
if ( Body_SmartKick( first_action.targetPoint(),
first_action.firstBallSpeed(),
first_action.firstBallSpeed() * 0.99,
first_action.kickCount() ).execute( agent ) )
{
agent->setNeckAction( new Neck_ScanField() );
#ifdef DEBUG2014
std::cerr << wm.time().cycle() << __FILE__ << wm.self().unum() << ": (Bhv_ChainAction) Clear...\n";
#endif // DEBUG2014
return true;
}
#ifdef DEBUG2014
std::cerr << wm.time().cycle() << __FILE__ << wm.self().unum() << ": (Bhv_ChainAction) Clear failed...\n";
#endif // DEBUG2014
break;
}
case CooperativeAction::NoAction:
{
dlog.addText( Logger::TEAM,
__FILE__" (Bhv_ChainAction) no action" );
return true;
break;
}
default:
dlog.addText( Logger::TEAM,
__FILE__" (Bhv_ChainAction) invalid category" );
break;
}
return false;
}
/*-------------------------------------------------------------------*/
/*
问题:
( face_point - wm.self().pos() ).th(),face_point是指的面向的坐标吗?.th是指的什么?我又忘记这个了
opponent_dist_thr的意义?
(*o)->posCount(),地方的位置数代表的意义是?
getKeepBallVel( agent->world() ),获得的是球的什么速度?
理解:
这个函数为执行转向的函数。
函数的内容如下:
首先判断游戏是否正在进行,如果游戏不是正在进行,则返回false
获取身体面向与脸面向的角度差
如果角度差在110以内,则认为不需要转向,返回false
否则,尝试转向
判断地方的每个敌人距离自己的距离,如果距离小于某一临界值,则返回false,不执行转向
如果自己接下来距离球接下来的距离小于kikable2的距离,则执行转向函数,并且返回true
*/
bool
Bhv_ChainAction::doTurnToForward( PlayerAgent * agent )
{
const WorldModel & wm = agent->world();
if ( wm.gameMode().type() != GameMode::PlayOn )
{
return false;
}
Vector2D face_point( 42.0, 0.0 );
const double body_angle_diff = ( ( face_point - wm.self().pos() ).th() - wm.self().body() ).abs();
if ( body_angle_diff < 110.0 )
{
dlog.addText( Logger::TEAM,
__FILE__" (doTurnToForward) already facing the forward direction. angle_diff=%.1f",
body_angle_diff );
return false;
}
dlog.addText( Logger::TEAM,
__FILE__" (doTurnToForward) angle_diff=%.1f. try turn",
body_angle_diff );
// const double opponent_dist_thr = ( wm.self().pos().x > ServerParam::i().theirPenaltyAreaLineX() - 2.0
// && wm.self().pos().absY() > ServerParam::i().goalHalfWidth()
// ? 2.7
// : 4.0 );
const double opponent_dist_thr = 4.0;
const PlayerPtrCont::const_iterator o_end = wm.opponentsFromSelf().end();
for ( PlayerPtrCont::const_iterator o = wm.opponentsFromSelf().begin();
o != o_end;
++o )
{
double dist = (*o)->distFromSelf();
dist -= bound( 0, (*o)->posCount(), 3 ) * (*o)->playerTypePtr()->realSpeedMax();
if ( dist < opponent_dist_thr )
{
dlog.addText( Logger::TEAM,
__FILE__" (doTurnToForward) exist opponent" );
return false;
}
if ( dist > 10.0 )
{
break;
}
}
// TODO: find the best scan target angle
face_point.y = wm.self().pos().y * 0.5;
double kickable2 = std::pow( wm.self().playerType().kickableArea()
- wm.self().vel().r() * ServerParam::i().playerRand()
- wm.ball().vel().r() * ServerParam::i().ballRand()
- 0.2,
2 );
Vector2D self_next = wm.self().pos() + wm.self().vel();
Vector2D ball_next = wm.ball().pos() + wm.ball().vel();
if ( self_next.dist2( ball_next ) < kickable2 )
{
Body_TurnToPoint( face_point ).execute( agent );
agent->setNeckAction( new Neck_ScanField() );
return true;
}
Vector2D ball_vel = getKeepBallVel( agent->world() );
if ( ! ball_vel.isValid() )
{
dlog.addText( Logger::TEAM,
__FILE__": (doKeepBall) no candidate." );
return false;
}
//
// perform first kick
//
Vector2D kick_accel = ball_vel - wm.ball().vel();
double kick_power = kick_accel.r() / wm.self().kickRate();
AngleDeg kick_angle = kick_accel.th() - wm.self().body();
dlog.addText( Logger::TEAM,
__FILE__": (doTurnToForward) "
" ballVel=(%.2f %.2f)"
" kickPower=%.1f kickAngle=%.1f",
ball_vel.x, ball_vel.y,
kick_power,
kick_angle.degree() );
if ( kick_power > ServerParam::i().maxPower() )
{
dlog.addText( Logger::TEAM,
__FILE__": (doTurnToForward) over kick power" );
Body_HoldBall( true,
face_point ).execute( agent );
agent->setNeckAction( new Neck_ScanField() );
#ifdef DEBUG2014
std::cerr << wm.time().cycle() << __FILE__ << wm.self().unum() << ": (Bhv_ChainAction) doTurnToForward: Hold???\n";
#endif // DEBUG2014
}
else
{
agent->doKick( kick_power, kick_angle );
agent->setNeckAction( new Neck_ScanField() );
#ifdef DEBUG2014
std::cerr << wm.time().cycle() << __FILE__ << wm.self().unum() << ": (Bhv_ChainAction) doTurnToForward: Kick???n";
#endif // DEBUG2014
}
agent->debugClient().addMessage( "Chain:Turn:Keep" );
agent->debugClient().setTarget( face_point );
//
// set turn intention
//
dlog.addText( Logger::TEAM,
__FILE__": (doTurnToFoward) register intention" );
agent->setIntention( new IntentionTurnTo( face_point ) );
return true;
}
/*-------------------------------------------------------------------*/
/*
问题:
keep_pos代表什么?
std::pow( ptype.playerSize()+ SP.ballSize(),2 ),这个pow代表什么?是代表的指数运算吗?还是和power有关的函数?
playerSize和ballSize,这个size是指的什么size?
ptype.kickableMargin(),球员类型的可踢边境?这个代表着每个球员类型都有自己的可踢边境的划分吗?
keep_dist的含义代表什么?
keep_pos的含义代表什么?
Vector2D::INVALIDATED代表什么数据?这个可以从哪里看到?
Vector2D::from_polar()函数的作用是什么?如果不懂可以从哪里找到?
Vector2D.r()是指的向量的大小吗?那Vector2D.r2()又是什么?
理解:
说实话这个函数,我看不太懂。。。
我的所有理解都写成注释,写在函数里面了,但是因为这个函数里面太多调用我都不明白原理,所以我理解起来很困难。
看了几遍,还是不能把这些东西串起来,先放放吧。。。看看以后再看是不是就能看懂了。。。
*/
Vector2D
Bhv_ChainAction::getKeepBallVel( const WorldModel & wm )
{
//初始化s_update_time时间为0
static GameTime s_update_time( 0, 0 );
//初始化s_best_ball_vel,横向速度和纵向速度都为零
static Vector2D s_best_ball_vel( 0.0, 0.0 );
//如果wm.time()与s_update_time相等,即wm.time()等于零,那么返回s_best_ball_vel
if ( s_update_time == wm.time() )
{
return s_best_ball_vel;
}
//设置s_update_time为wm.time()
s_update_time = wm.time();
//ANGLE_DIVS代表将360度分割成了十二份
const int ANGLE_DIVS = 12;
//获得SP参数对象、ptype球员类型对象
const ServerParam & SP = ServerParam::i();
const PlayerType & ptype = wm.self().playerType();
//定义collide_dist2(碰撞距离),但是pow具体是指什么?
const double collide_dist2 = std::pow( ptype.playerSize()
+ SP.ballSize(),
2 );
//keep_dist的含义代表什么?
const double keep_dist = ptype.playerSize()
+ ptype.kickableMargin() * 0.5
+ ServerParam::i().ballSize();
//获得自己下一个与下下个自己所处的位置
const Vector2D next_self_pos
= wm.self().pos() + wm.self().vel();
const Vector2D next2_self_pos
= next_self_pos
+ wm.self().vel() * ptype.playerDecay();
//
// create keep target point
//
// 初始化几个变量
int best_angle = -1;
Vector2D best_ball_vel = Vector2D::INVALIDATED;
int best_opponent_step = 0;
double best_ball_speed = 1000.0;
/*
将三百六十度分成十二份,每次转三十度。每次线获取keep_pos,如果keep_pos符合某个条件则不进行处理
否则,则进行如下处理(看下面的注释)。
*/
for ( int a = 0; a < ANGLE_DIVS; ++a )
{
Vector2D keep_pos
= next2_self_pos
+ Vector2D::from_polar( keep_dist,
360.0/ANGLE_DIVS * a );
if ( keep_pos.absX() > SP.pitchHalfLength() - 0.2
|| keep_pos.absY() > SP.pitchHalfWidth() - 0.2 )
{
continue;
}
//初始化ball_move、ball_speed,这里的ball_move是一个向量
Vector2D ball_move = keep_pos - wm.ball().pos();
double ball_speed = ball_move.r() / ( 1.0 + SP.ballDecay() );
Vector2D max_vel
= KickTable::calc_max_velocity( ball_move.th(),
wm.self().kickRate(),
wm.ball().vel() );
//这个临界条件的设定不是很明白
if ( max_vel.r2() < std::pow( ball_speed, 2 ) )
{
continue;
}
Vector2D ball_next_next = keep_pos;
Vector2D ball_vel = ball_move.setLengthVector( ball_speed );
Vector2D ball_next = wm.ball().pos() + ball_vel;
if ( next_self_pos.dist2( ball_next ) < collide_dist2 )
{
ball_next_next = ball_next;
ball_next_next += ball_vel * ( SP.ballDecay() * -0.1 );
}
#ifdef DEBUG_PRINT
dlog.addText( Logger::TEAM,
__FILE__": (getKeepBallVel) %d: ball_move th=%.1f speed=%.2f max=%.2f",
a,
ball_move.th().degree(),
ball_speed,
max_vel.r() );
dlog.addText( Logger::TEAM,
__FILE__": __ ball_next=(%.2f %.2f) ball_next2=(%.2f %.2f)",
ball_next.x, ball_next.y,
ball_next_next.x, ball_next_next.y );
#endif
//
// check opponent
//
int min_step = 1000;
for ( PlayerPtrCont::const_iterator o = wm.opponentsFromSelf().begin();
o != wm.opponentsFromSelf().end();
++o )
{
if ( (*o)->distFromSelf() > 10.0 )
{
break;
}
int o_step = FieldAnalyzer::predict_player_reach_cycle( *o,
ball_next_next,
(*o)->playerTypePtr()->kickableArea(),
0.0, // penalty distance
1, // body count thr
1, // default turn step
0, // wait cycle
true );
if ( o_step <= 0 )
{
break;
}
if ( o_step < min_step )
{
min_step = o_step;
}
}
#ifdef DEBUG_PRINT
dlog.addText( Logger::TEAM,
__FILE__": (getKeepBallVel) %d: keepPos=(%.2f %.2f)"
" ballNext2=(%.2f %.2f) ballVel=(%.2f %.2f) speed=%.2f o_step=%d",
a,
keep_pos.x, keep_pos.y,
ball_next_next.x, ball_next_next.y,
ball_vel.x, ball_vel.y,
ball_speed,
min_step );
#endif
if ( min_step > best_opponent_step )
{
best_angle = a;
best_ball_vel = ball_vel;
best_opponent_step = min_step;
best_ball_speed = ball_speed;
}
else if ( min_step == best_opponent_step )
{
if ( best_ball_speed > ball_speed )
{
best_angle = a;
best_ball_vel = ball_vel;
best_opponent_step = min_step;
best_ball_speed = ball_speed;
}
}
}
s_best_ball_vel = best_ball_vel;
return s_best_ball_vel;
}
学习心得与体会
这一次只看了一个cpp文件,感觉进度比较慢,我认为原因有以下几点吧。第一点就是自己没有安排好学习计划,经常会被其他的事情占用了我的学习robcup的时间,导致我花费在robcup上面的时间比我预计的要少。第二点就是,许多的函数里面调用了很多底层的东西,这些东西如何自己去查找?或者这个东西值不值得去理解原理,我把握不好这个度。
所以,这一次的学习过程,最大的收获的其实不是robcup的函数又掌握了多少,而是我明白了学习过程中让我成长缓慢的痛点,而这个痛点越早察觉到,对我以后的学习过程的帮助越大。希望自己能够有所改进!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?