由于想在游戏中(非mfc程序)调用一个dll用来监控游戏中一些数据变化,而这些数据可以触发式变动,所以想到用mfc来弄,
但网上查的
“非mfc程序调用mfc扩展dll”似乎有点麻烦,也没有什么成样的例子。
后来还是自己对比正常的mfc程序加载方式修改出来了,步骤如下:
1.
exe工程要设置成Use Standard Windows Libraries或者Use MFC in a Shared DLL(因为我们不用mfc,所以设置成前者)
2.
dll工程用vc向导创建mfc扩展dll
3. 在dll工程中拷贝相关的app,mainfrm和view以及doc框架(可以用vc向导创建一个正常mfc框架程序用来拷贝)
然后在dllmain的attach中加入如下语句即可:
//----------------------------------------
// AFX internal initialization
if (!AfxWinInit(hInstance, 0, "", 1))
return 0;
// Register the doc templates we provide to the app
CWinApp* pApp = AfxGetApp();
ENSURE(pApp != NULL);
pApp->InitApplication();
pApp->InitInstance();
//----------------------------------------
4. 如果是隐式调用的话,在exe中加入:
#define AFX_EXT_API __declspec(dllexport)
#include "..\mfcdll\mfcdll.h" // 这个是dll要导出的东西
这样就可以了
5.显式调用的话,用LoadLibrary先装载,再获取对应导出函数调用就应该也可以了(这个没试)
6.上述处理后只能显示,但按键接收还不行,因为消息循环没有地方调用。。。它原来是系统调用app->run(),在其中循环处理的。
如果直接在dll中调用app的run()的话,mainfrm的PreTranslateMessage就能收到按键消息了。但这样由于是主线程中调用app->run(),而run()中
是消息死循环,所会exe会没机会响应。。。所以解决办法是模拟app->run()写一个runoneframe()函数,然后每次刷新一下(或者创建新线程来刷新, 临时试了一下会crash,我没用这种办法,先不查了,不过理论上应该可以的)
这样就ok了(view中按键自己再处理吧,这里不提了)
posted @
2012-09-14 15:56 flipcode 阅读(1292) |
评论 (0) |
编辑 收藏
火矩之光中的导航图:
火矩之光 中已经抛弃高位图地形之类的作法,换用了模型,为了能作出迷宫,把地图分块,每块是一个模型,
每个模型有几条通路,这样各通路可以据算法得出随机组合的谜宫,为了丰富,每个入路有好几套模型可以随机
选取,然后根据所选择的模型再随机组合出复杂的谜宫。
而对于路点,则是平均格子求得的,这点跟普通网游的作法还是有点类似的,
不是真正意义上的3维寻路,还是2维的, 作法应该是从上往下跟碰撞模型进行垂直求交得出路点,
然后再对路点进行通路测试,产生导航图。(它不是用几何网格生成的导航图)
看下图我的一个测试:
左边是一个模型,右边是个有楼梯的另一个模型(它是个入口)。蓝色格是导航点。蓝色块是寻出的路点。
posted @
2012-08-27 10:35 flipcode 阅读(213) |
评论 (0) |
编辑 收藏
泰坦之旅的ai
titan quest的AI用的是切换式的状态机,而寻路用的是path engine第3方库,游戏中有一个任务编辑器主要是生成每一个任务,每个任务中可以生成多个触发器(trigger),每个触发器可以生成一系列条件(condition),并可生成条件成立时要触发的动作(action). 这个有点类似war3的事件编辑器。
以下是切换式状态机跟踪的一些记录,很乱,没写总结,只是用于备忘。。。
AI移动跟踪
WinMain消息循环中的Game::Run()中先
1. gGameEngine->GetFrustumForPlayer(updateFrustum, player->GetCoords().origin); 得到frustum
2. gEngine->Update(&updateFrustum);进行更新, 其中
调用 world->Update(worldFrustumList);
然后遍历每个frustum取得对应region进行更新
a. 看看当前region是否与frustum相交,如果是则load,否则扩大一点再相交,这时如果相交则将添加到后台加载。
b. 查看portal相关region进行更新
3. 查看connectedRegions(在地图装载得到玩家出生位置后进行玩家所在region扩大后跟其它region判断相交所得)进行更新
3. Region的更新中进行level更新:
level->Update(frustumList, numFrustums, elapsedTime);
在其中先取得在frustum中的Entity, 然后再遍历更新所有Entity,
之后再更新Entity所在4叉树空间.
4. 角色的更新:
Entity的更新中, 先UpdateSelf再UpdateAttachedEntities
(Entity中有PhysicsRigidBody成员physicsObject)
5. UpdateSelf会跑到Character::PreAnimationUpdate中执行 baseController->Update(localTimeDelta);从而跳到ContrallerAI中执行GetExecutingState()->OnUpdate(deltaTime); 从而到达ControllerNpcStateIdle的更新中进行状态切换到SetState("Wander", ControllerAIStateData(0, 0, 0));
之后再跑到ControllerNpcStateWander::OnBegin()
处理:
int closestPoint = GetClosest(GetController().GetWanderPoints());
GetController().SetCurrentWanderPoint(closestPoint);
if (!MoveToCurrentWanderPoint())
{
SetState("Idle", ControllerAIStateData());
return;
}
这个在MoveToCurrentWanderPoint()函数里从队列中取出当前目标点并ControllerAI::WalkTo
其中会GetController().WalkTo(location, target);即ControllerAI::WalkTo(。。), 这会执行:
HandleAction(new WalkAction(GetParentId(), GetAI()->GetPathPosition(), location, target));
这个会执行:
SetCurrentAction(action);
GetCurrentAction()->Execute();从而运行了WalkAction::Execute(), 这其中又调用了Character::WalkTo
这又会:
movementMgr->SetNewPathTarget进行处理
最后在CharacterMovementManager::Update()中进行角色位置更新:
CreateLocalPath(deltaTime, speed);
if (!MoveDownPath(deltaTime, speed))
{
return false;
}
UpdateCharacterPosition(deltaTime, speed);
void UIDialogWindow::OnOpen()会调用 myNpc->AddSocialTarget( target );
在void ControllerNpcStateIdle::OnUpdate(Time deltaTime)中判断如果有SocialTarget则进入Chat状态处理
【状态处理例子】
Monster的初始状态是Idle,在Monster的更新函数里:
一)。进行搜敌,并切换成pursue状态
,并调追捕状态的OnBegin()函数处理,如果Monster不能行走则切回Idle状态,否则如果搜不到敌
人则切换到Return状态,否则根据当前技能id找出要移到的位置点.
------------------------------------------------------
【注意】找到要移到的位置点细节:
I. (Character::GetMoveToPoint)找出目标点:
1. 目标是自己则直接return自己位置
2. 没有目标则保存goalPoint=目标点,distance=技能施放范围,待后处理.
3. 目标是FixedItem则return FixedItem->GetMoveToPoint(..)里进行处理
4. 目标是StrategicMovementBase,则return sm->GetMoveToPoint(..)里进行处理
5. 目标是Entity,则goalPoint = entity->GetCoords().origin;并且如果entity阻挡则让goalPoint回移一点以免浮点出错?否则distance=GetExtents() + entity-
>GetExtents();待后处理
6. 目标是Character,则,
a. 如果是朋友
1)如果当前是移动状态,则要求目标给出DefenseSlot(防御位置点)作为goalPoint直接返回.
2)否则如果能直线通路到目标点的话就直接返回离目标比较近的一点(去掉半径),不能直通则返回0点
b. 如果是敌人
1)如果没有技能,则goalPoint=目标点,distance=GetExtents() + target->GetExtents();待后处理
2)如果技能不需要AttackSlot或者this是Player, 则
goalPoint=目标点,distance = GetExtents() + target->GetExtents() + skill->GetRange();待后处理
3)否则直接返回目标算出的AttackSlot攻击点位置.
上述2和5以及6中的b.的1)和2)需要待后处理的最后通过
WorldVec3 finalPoint = movementMgr->GetPointAwayFromGoal(goalPoint, distance);
得到最终位置, 这个位置还要特殊判断一下如果不在Region中或者路径不能到达的话,则直接用TranslateToFloor到goalPoint.
【说明】:
1. 什么是AttackSlot/DefenseSlot:
每个角色可以有n个x距离的AttackSlot/DefenseSlot,它会在周围x半径的圆上平分出n个位置点,当有其它人要攻击它或者要来帮助(防御)它时,它就会在旁边找一个较近的还
没其它人用过的slot分给这个其它人。
2. movementMgr->GetPointAwayFromGoal()函数细节:
先是FindPath(目标)得出path,再用path->Advance(pathLength - distance)得到回退一点的位置。
II. 找到目标点后,还要调用movementGoalManager->GetClosestMovePoint(目标点) 进行处理:
这个函数主要是给范围武器用的,如果不是使用范围武器的Monster则不会调整目标点。
如果是的话则遍历全局对象movementGoalManager中的m_MoveGoalMap目标点映射表,求出其它在同一region中的Monster
所在目标点跟当前Monster所在目标点的距离,如果距离比较近则调用GetPointAwayFromGoal(目标点, 3.0);调整当前Monster的目标点回退一点,并将些处理后的位置及些
Monsterid映射到m_MoveGoalMap目标点映射表中。这样遍历过所有其它Monster的目标点进行一一检测处理后就会尽量避免与其它Monster挤到一起。
------------------------------------------------------
找到要移到的位置点之后,
1. 用(CloseEnoughToUseSkill(GetCurrentEnemy(), GetCurrentSkillID()))判断是否在技能攻击范围内,
如果在则用IsPathClear(GetCurrentEnemy())来判断是不是到直通目标,是则切换成Attack状态后返回,否则切换成NavigateObstacle状态后返回。
2. 否则敌人不在攻击范围内就看当前是否已站在目标点,是则切回Idle状态后返回
3. 不在目标点则看是不是能够移到目标点,不能则切回Idle状态后返回。
4. 能移到目标点则MoveTo到目标点.
这个MoveTo会调用
HandleAction(new MoveToAction(GetParentId(), GetAI()->GetPathPosition(), location, target, GetAI()->GetSkillReferenceNumber(skill), 1.0, animType));
这个会执行到MoveToAction, 其中会转调:
monseter->SetCurrentAttackTarget(targetId, location, skillNumber);
monseter->SkillWarmUp( skillNumber, false );
monseter->MoveTo(location, GetBlendTime(), animType);
monseter->PlayLoopingRunningSound();
而monseter->MoveTo又会调用 movementMgr->SetNewPathTarget(movementMgr->GetPathPosition(), surfacePoint, alreadyThere))
然后再用SetActionState(Character_ActionState_Move);设置Action的状态为Move,并通过PlayAnimation播放run动作(即调用GetAnimationBase( type ).PlayAnimation(
actor, selection, speedModifier, loop, iteration ),这个可以参考我另一个动画跟踪文档看细节)
二)。搜敌后会接着调用ControllerAI::Update()更新函数:
1. 先进行当前状态更新()
由于前面Monster切换到了pursue追捕状态,所以执行到
ControllerMonsterStatePursue::OnUpdate(),其中:
a. 追捕所用时间过了,则切换回return状态
b. 重新选择技能时间到了则重新找出一个bestSkill.(这也避免了万一当前技能是melee,而玩家总是绕着Monster转,怪就会不停地追不上而没法肉搏攻击)
c. 用CloseEnoughToUseSkill判断是否够技能施放距离,够的话用IsPathClear判断攻击方向是否可通,是则转Attack状态,不通则转NavigateObstacle状态.
2. 再遍历执行m_PreloadQuestActionList中action.
上述都在【更祥细一点】中1。Character::UpdateSelf()中进行
接着会到【更祥细一点】中2。 Update subsystems:中的FollowPath()进行真正的移动处理.
posted @
2012-08-13 10:09 flipcode 阅读(337) |
评论 (0) |
编辑 收藏
dota中的道具/技能及动作状态机 相关原型 设计备忘::
//xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
// dota中的道具/技能及动作状态机 相关原型 设计备忘::
/*
商店用拥有很多kToyItem供购买,角色通过购买也可以得到很多kToyItem。
kToyItem用于kToy物品类的显示封装,对应有一个物品类kToy以及储存在哪个格子中,价格多少等信息。
kToy中包含3种类型:1.kEquip(装备),2.kUse(使用), 3.kSkill(技能)
这3个类中各自包含一个listAction(动作列表),列表中存放的是kAction动作基类。
kAction类中有Select()和IsValid()以及Execute()函数;
1. kEquip中的Update()中调用listAction中每一个IsValid()函数来判断是否执行对应的Execute();
2. kUse中的Use()函数中调用listAction中每一个IsValid()函数来判断是否执行对应的Execute();
3. kSkill中的select()函数遍历listAction中每一个Select()函数来判断是否能选择该技能,都通过才选择。
kSkill中的Start()函数中调用listActionStart中每一个IsValid()函数来判断是否执行对应的Execute();
kSkill中的Cast()函数中调用listActionCast中每一个IsValid()函数来判断是否执行对应的Execute();
说明:
kSkill设计时考虑到施放前摇,故用了start()函数来播放前摇动作/特效,然后等待前摇时间完成再调用cast()函数来播放对应的施放动作/特效。
当特效完成后再处理listAction(动作列表)。dota中还有后摇,我理解成技能冷却时间(不知道对否),当cast()时,skill就可以开始冷却了。
[说明]
角色属性有:基础属性,附加属性(直接+),加成属性(*(1+比例)),而
kAction的派生类kActionProperty专用于处理kEquip(装备)提高属性:
它用VARY_TYPE类型指明针对某种属性, 如: str, int, dex, HP/MP(min/max), atk(min/max)。。。等.
它用PROPERTY_METHOD区别是直接加还是比例乘:add/rate。
这样角色如果拥有kToyItem的话,那么在更新中就会调用每个物品的kEquip的Update,从而把属性更新到角色的add_data[类型]和rate_data[类型]中,
接着角色的更新就会用(base_data[]+add_data[])*(1+rate_data[])的公式来处理之(当然,dota的一些特殊的属性使用另外的计算方法)
kAction可以方便地扩充很多普通派生处理类,只要在相应的Execute()进行处理即可实现想要的功能,比如可能派生一个名为kActionHurt类,
然后把它加入到kUse/kSkill中的listAction列表中,这样只要点击使用,即可Execute中处理加血/扣血。
另外从kAction中派生的还有一些特殊类型:
1. kActionEffect(包含listAction成员):
kActionEffect执行Execute()时会调用全局的kEffectManager(特效管理器)的PlayEffect(effect_class_name)函数来产生一个kEffect的派生类对象, 并转让listAction给它。
kEffectManager用来管理listEffect列表,更新处理其中每一个kEffect。
kEffect类中包含一个listAction(动作列表)指针,它是由kActionEffect在产生它时传递过来的,这样在特效完成并且条件成立时(比如命中敌人)调用listAction中每一个IsValid()函数来判断是否执行对应的Execute();
2. kActionState(包含listAction成员):
kActionState执行Execute()时会调用全局的kStateManager(状态管理器)的PlayState(state_class_name)函数来产生一个kState的派生类对象, 并转让llistAction给它。
kStateManager用来管理listState列表,更新处理其中每一个kState。
kState类中包含一个listAction(动作列表)指针, 它是由kActionState在产生它时传递过来的,这样在该状态完成时会调用listAction中每一个IsValid()函数来判断是否执行对应的Execute();
注意: kState队列的执行优先于kAIState队列,只有kState列表为空时,kAIState才有机会执行.
关于kState和kAIState的区别:
a. 动作触发的状态机:
kState主要是kAction(动作)触发引起角色的一些临时被动行为,比如kState被击退状态,被晕状态,状态之间可以并行,或者串行(通过kState中的listAction列表挂接,在kState完成时遍历调用listAction)。
比如:
可以将kActionHurt加入到kStateStun(被晕)状态中的listActon中,然后再把kStateStun加到kStateThrustBack(被击退)中,然后再把kStateThrustBack加入到kShotEffect的listAction中,接着给kActionEffect设置对应的kShotEffect
并把kActionEffect加入到kToy中的kSkill的listActon中.这样,当技能使用时就会触发一个kActionEffect播放kShotEffect,这个kShotEffect播放完成时会触发kStateThrustBack将角色击退到一边,退到一边完成后接着触发kStateStun让角色晕上一会。
而如果前面的kShotEffect是范围特效的话,那在特效伤害范围的角色都被击退后再晕上一会(由于击退和晕是动作状态优先于角色的AI状态,所以这时角色的AI是不运行的,只有等击退后晕完了AI才醒过一继续)。
b. AI行为状态机:
kAIState主要是由kAIBrain(大脑)思考引起的一些主动的AI行为,比如kAIRoam漫步, kAIPursue追捕等,各AI状态之间不能并行或串行只能切换。在任何时候包括在kAction的处理中也可以进行角色的kAIState切换。
3. kActionTrigger:
kActionTrigger中拥有一个事件名称列表,当该action被execute时会通过kEventManager->PostEvent(Event_Name)来发出事件消息。这时事件监听列表中的对应事件号的触发器先判断kCondition是否成立,是则调用相应的触发器的kAction动作。
关于触发器:
kTrigger触发器,拥有listEventName, kCondition, kAction。事件管理器kEventManager可以创建kTrigger,并添加触发器监听的事件列表(listEventName),条件(kCondition),以及动作(kAction)。
当事件发生时可随时调用kEventManager->PostEvent(Event_Name)来发出消息,事件的监听者kTrigger先判断kCondition是否成立,是则调用kAction。
调用的kAction即前面所说的动作基类,当动作完成时可以再次PostEvent(...)以便触发另外的触发器。
4. kActionPose:
这个Action只是简单地调用一下角色的动作播放。
5. kActionSleep(包含listAction成员):
这个kAction只是延迟一段时间,时间到了再调用listAction各成员的execute()函数.
由以上设计可以知道每一个kToy道具均可以由不同的处理函数并行及串行(注意:这里说的串/并行跟多线程无关,概念不一样)组合而成,这样就可以实现动作或特效的串行/并发执行,以及触发相应的处理。
*/
//xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
posted @
2012-08-13 09:58 flipcode 阅读(815) |
评论 (0) |
编辑 收藏
mythos中的ai是并发的栈式状态机:
拿wolf作为例子:
一。配置文件:
在ai目录中的wolf.xml配置它所有的行为(包括每个行为发生机率,参数,以及执行函数):
其中执行函数以及对应skillid如下:
1.
<nBehaviorId>move - approach target</nBehaviorId>
<nSkillId>monster melee</nSkillId>
2.
<nBehaviorId>skill - do skill</nBehaviorId>
<nSkillId>monster melee</nSkillId>
3.
<nBehaviorId>skill - do skill</nBehaviorId>
<nSkillId>Fidget</nSkillId>
4.
<nBehaviorId>move - wander</nBehaviorId>
<nSkillId/>
二。ai更新:
SrvGameTick--》GameEventsProcess--》AI_Update()
在AI_Update中遍历执行该unit的所有行为(behavior) :
1. sBehaviorApproachTarget()
查找对应的目标,发出朝它移动指令
1. sBehaviorDoSkill()
如果有目标则执行sSkillExecute进行技能施放
2. ...类似功能处理
。。。
【说明】:
mythos的行为是并发的栈式,最大可以有5个栈, 用
int nIndex = tContext.pnStack[ tContext.nStackCurr ]这样的结构进行处理,
通过pTable->pBehaviors[ nIndex ]得到对应的行为处理函数进行执行处理。
a. 并发:
每执行完一个行为函数后 tContext.pnStack[ tContext.nStackCurr ]++; 这样就换到unit的下一个行为函数再执行。
b. 进入栈(子函数):
tContext.nStackCurr为栈下标,初始tContext.nStackCurr=0,即为第0个栈,
可以设置tContext.nStackCurr++;并且tContext.pnStack[ tContext.nStackCurr ]=nBranchTo(要走的分支号)。这样来
执行分支函数。 当分支函数完成时tContext.nStackCurr--再回来上一级。
另外有些行为函数是执行一次的,执行完了就会把它从列表中删除。下次不会再遍历到.
posted @
2012-08-13 09:51 flipcode 阅读(175) |
评论 (0) |
编辑 收藏
[更新]
一个Actor更新中:
1. m_TaskManager.Run(fDelta);
2. ProcessNetwork(fDelta);
3. ProcessAI(fDelta);
处理actor->Brain->Think()中
bool bFind = FindTarget();
if ( bFind)
{
ProcessBuildPath( fDelta);
ProcessAttack( fDelta);
}
4. ProcessMovement(fDelta);
5. ProcessMotion(fDelta);
一。ProcessAI()
actor->Brain->Think()处理中产生一个Task
一个任务有 移动,攻击,延时,转向等,其中攻击有肉搏,范围,技能
比如:actor->Brain->Think()中发了现敌人后在ProcessAttack()中产生一个skilltask存入到
actor->m_TaskManager中
二。m_TaskManager.Run()
在在taskMgr中处理m_pCurrTask的3个步骤(start,run,complete):
举例: 如ZTask_Skill:
1. OnStart()中
a.parent(是一个actor)->Skill(m_nSkill); // 进行动作输入(动作用了m_AniFSM进行管理)
b.ZPostNPCSkillStart(ownerid,nSkill,Targetid,TargetPos);
这会生成一个id为MC_QUEST_PEER_NPC_SKILL_START的cmd并push到全局的m_CommandManager中
2. OnRun()中
if时间到了则ZPostNPCSkillExecute(ownerid,nSkill,Targetid,TargetPos);
产生的命令id==MC_QUEST_PEER_NPC_SKILL_EXECUTE;
3 . OnComplete()中
ZTask_Skill中无处理.
三。m_pGameInterface->Update()中从m_CommandQueue中取出cmd, 对其:
1. SendCommand(pCommand);
2. OnCommand(pCommand);
switch(cmd->id)调用对应的处理函数,
例如:
a. id==MC_QUEST_PEER_NPC_SKILL_START时将调用OnPeerNPCSkillStart()
在其中通过uidOwner找得actor得到它的ZModule_Skills处理模块传入cmd的参数处理之.
这会进行发起特效处理
b. id==MC_QUEST_PEER_NPC_SKILL_EXECUTE时时将调用OnPeerNPCSkillExecute()
在其中通过uidOwner找得actor得到它的ZModule_Skills处理模块传入cmd的参数处理之.
这会进行攻击特效处理:
1). 如果是bHitCheck标志的特效,则转让全局的武器管理类处理:
m_WeaponManager.AddMagic( this, vMissilePos, vMissileDir, m_pOwner);进行处理
这里判断如果循环次数减到0,则m_bEnable = false;
2).否则:
a. 如果有相机震动标志,则通知照相机震动处理
GetCamera()->Shock( fPower*fDefaultPower, fDuration, rvector( 0.0f, 0.0f, -1.0f));
b. 遍历所有角色,检测是否范围内以及抗性没抵消完伤害:
(1)是攻击则
pObject->OnDamaged(..)
否则
pObject->OnHealing(..)
(2)限速:
pMovableModule->SetMoveSpeedRestrictRatio( 0, fDuration);
(3)击退:
pObject->AddVelocity( m_pDesc->fModKnockback * 7.f * -dir);
3). 特效燃烧效果:
if(m_pDesc->szCastingEffect[0]) {
if(type != eq_parts_pos_info_etc)
efgmgr->AddPartsPosType(m_pDesc->szCastingEffect, Targetid,type,m_pDesc->nEffectTime);
else
efgmgr->Add(m_pDesc->szCastingEffect, vPos,vDir,OwnerID,m_pDesc->nEffectTime);
}
if(m_pDesc->szCastingEffectSp[0]) {
ZGetEffectManager()->AddSp(m_pDesc->szCastingEffectSp,m_pDesc->nCastingEffectSpCount,
vPos,vDir,m_pOwner->GetUID());
}
3). 提示信息显示处理
4). 声音播放处理.
四。ZWeaponMgr处理:
上述的OnPeerNPCSkillExecute()处理中发现是飞行特效转交给ZWeaponMgr来处理,所以这里说一下
在这里会处理特效模型飞行并处理Explosion( type, pPickObject,pickdir);爆炸伤害!转调用到pTarget->OnDamaged(..)
posted @
2012-08-13 09:49 flipcode 阅读(281) |
评论 (0) |
编辑 收藏
posted @
2012-07-03 10:55 flipcode 阅读(177) |
评论 (0) |
编辑 收藏
载图,角色用的gunz模型测试:
界面用群雄逐鹿的测试:
posted @
2012-05-10 15:29 flipcode 阅读(169) |
评论 (0) |
编辑 收藏
posted @
2012-04-01 20:04 flipcode 阅读(194) |
评论 (0) |
编辑 收藏
图:
posted @
2012-03-23 21:26 flipcode 阅读(187) |
评论 (0) |
编辑 收藏