一、前提:

完成前一篇的内容。

具体参考:Cocos2d-x3.x塔防游戏(保卫萝卜)从零开始(二)篇

二、本篇目标:

l  说说游戏中各种角色的动作、属性以及重构思路

l  进行代码重构让色狼大叔和女主角生动鲜活起来

三、内容:

说说游戏中各种角色的动作、属性以及重构思路

通过前两篇我们建立的一个简陋的游戏原型,但是游戏中的人物比如色狼大叔、女主角都看去来很呆板不够鲜活,比如色狼会沿着道路移动,那这个只能说是移动根本不像是在走路,没有走的动作感觉就是沿着道路在漂移,女主角也是一动不动的站那里。这样的游戏很没有乐趣,所以需要给这些游戏角色加入动作和表情,让人看去来他们是鲜活的,对这些角色进行一下简单的动画以及属性分析如下:

色狼大叔的动画:

1、          静止动画:游戏刚开始处于静止状态

2、          走路动画:沿着道路走

3、          死亡动画:当子弹击中血量消耗完时死亡消失

色狼大叔的属性:

1、          是否运动:色狼是否处于激活沿着道路行走

2、          是否死亡:是否被炮塔打死

3、          行走速度:不同色狼的行走速度不同

4、          色狼血量:不同色狼血量不同

5、          行走:沿着道路行走

女主角的动画:

1、          静止动画:游戏刚开始处于静止状态

2、          卖萌动画:不能像木头似的,就加点表情动作

3、          死亡动画:当纯洁值被色狼玷污光了就死亡了

女主角的属性:

1、          女主角贞洁值:相当于生命值

根据上面的分析我们把每个角色拆分成动画显示和业务属性逻辑两个部分,对色狼和女主角进行代码重构。

重构后大概结构如上图:

ActionSprite:属于CCSprite类,负责游戏角色精灵的动画显示,Luoli(萝莉)、DaShu(大叔)、JiaoShou(叫兽)等角色精灵均继承自ActionSprite,都属于动画显示类。

Luoli(萝莉):属ActionSprite的子类,负责女主角的动画效果展示。

DaShu(大叔)属ActionSprite的子类,负责大叔色狼的动画效果展示。

JiaoShou (叫兽)属ActionSprite的子类,负责叫兽色狼的动画效果展示。

Selang(色狼):属于CCNode类,负责色狼的行走、血量、速度、攻击等具体的业务,每个Selang都包含一个DaShu(大叔)或JiaoShou(叫兽)类的游戏精灵。并且具备最重要的行为实现沿着道路行走。

Girl(女孩):属于CCNode类,负责女主角的血量等具体的业务,每个Girl都包含一个Luoli类的游戏精灵。

进行代码重构让色狼大叔和女主角生动鲜活起来

打开项目工程按照上面的思路重点对色狼和女主角的代码实现进行重构。

色狼大叔代码重构

第一步:

新建ActionSprite.h、ActionSprite.cpp类(角色动画类),这个类继承自CCSprite负责游戏角色的动画效果显示,色狼和女孩都会是这个类的子类。

ActionSprite.h代码:

//声明一个动作状态的枚举类型
typedef enum _ActionState{
kActionStateNone = , //无状态
kActionStateIdle, //静止状态
kActionStateWalk, //行走状态
kActionStateDeath //死亡状态
}ActionState; class ActionSprite: public cocos2d::CCSprite
{
public:
ActionSprite(void);
~ActionSprite(void);
//静止
void idle();
//死亡
void death();
//行走
void walk();
//价格
CC_SYNTHESIZE(int,_price,Price);
//生命值
CC_SYNTHESIZE(float,_hp,HP);
//静止状态动作
CC_SYNTHESIZE_RETAIN(cocos2d::Action*,_idleAction,IdleAction);
//死亡状态动作
CC_SYNTHESIZE_RETAIN(cocos2d::Action*,_deathAction,DeathAction);
//行走状态动作
CC_SYNTHESIZE_RETAIN(cocos2d::Action*,_walkAction,WalkAction);
//当前动作状态
CC_SYNTHESIZE(ActionState,_actionState,ActionState);
//行走速度
CC_SYNTHESIZE(float,_walkSpeed,WalkSpeed);
//伤害值
CC_SYNTHESIZE(float,_damage,Damage);
//金钱
CC_SYNTHESIZE(float,_money,Money);
//是否有光环
CC_SYNTHESIZE(bool,_halo,Halo); };

ActionSprite.cpp代码:

ActionSprite::ActionSprite(void)
{
_price=;
_idleAction=NULL;
_walkAction=NULL;
_deathAction=NULL;
} ActionSprite::~ActionSprite(void)
{
//释放内存
CC_SAFE_RELEASE_NULL(_idleAction);
CC_SAFE_RELEASE_NULL(_deathAction);
CC_SAFE_RELEASE_NULL(_walkAction);
} //设置精灵为静止状态
void ActionSprite::idle()
{
if (_actionState != kActionStateIdle)
{
//先停止所有动作
this->stopAllActions();
//运行静止动作
this->runAction(_idleAction);
_actionState=kActionStateIdle;
}
} //设置精灵为行走状态
void ActionSprite::walk()
{
if (_actionState != kActionStateWalk)
{
//先停止所有动作
this->stopAllActions();
//运行行走动作
this->runAction(_walkAction);
_actionState=kActionStateWalk;
} } //设置精灵为死亡状态
void ActionSprite::death()
{
//先停止所有动作
this->stopAllActions();
this->runAction(_deathAction);
_actionState=kActionStateDeath;
}

第二步:

素材准备,设计2张大叔不同动作的图片,交替显示模拟色狼行走动画,完成后把图片拷贝到Resources的iphonehd文件夹中,为了适应小分辨率的手机把这个2张图片按比例缩小一半并且拷贝到Resources的iphone文件夹中。

第三步:

新建DaShu.h、DaShu.cpp类(色狼大叔动画类),这个类继承自上面的ActionSprite负责游戏色狼大叔的动画效果显示。

DaShu.h:

class DaShu : public ActionSprite
{
public:
DaShu(void);
~DaShu(void); CREATE_FUNC(DaShu);
//初始化方法
bool init();
//设置光环,拥有光环的色狼生命值加倍
void setHalo(bool halo);
};

DaShu.cpp:

bool DaShu::init()
{
bool bRet=false;
do
{
CC_BREAK_IF(!ActionSprite::initWithFile("dashu_2.png")); //设置静止状态动作
Vector<SpriteFrame *> idleFrames();
SpriteFrame *frame1=SpriteFrame::create("dashu_2.png", Rect(, , , ));
idleFrames.pushBack(frame1);
Animation *idleAnimation=Animation::createWithSpriteFrames(idleFrames,float(6.0 / 12.0));
this->setIdleAction(CCRepeatForever::create(CCAnimate::create(idleAnimation))); int i=;
//设置行走状态动作
Vector<SpriteFrame *> walkFrames();
for (i=;i<;i++)
{
SpriteFrame *frame2=SpriteFrame::create(CCString::createWithFormat("dashu_%d.png", i+)->getCString(), Rect(, , , ));
walkFrames.pushBack(frame2);
}
Animation *walkAnimation=Animation::createWithSpriteFrames(walkFrames,float(6.0 / 12.0));
this->setWalkAction(CCRepeatForever::create(CCAnimate::create(walkAnimation))); //设置死亡状态动作
Vector<SpriteFrame *> deathFrames();
SpriteFrame *frame3=SpriteFrame::create("dashu_2.png", Rect(, , , ));
deathFrames.pushBack(frame3);
Animation *deathAnimation=Animation::createWithSpriteFrames(deathFrames,float(6.0 / 12.0));
this->setDeathAction(Animate::create(deathAnimation));
//设置攻击值
this->setDamage();
//设置行走速度
this->setWalkSpeed(0.4f);
//设置生命值
this->setHP();
//设置金钱数
this->setMoney(1.0f/10.0f);
bRet=true;
} while (); return bRet;
} //设置光环
void DaShu::setHalo(bool halo)
{
if (halo)
{
//拥有光环后生命值加4倍
float h=this->getHP()*4.0f;
this->setHP(h);
} }

第四步:

新建Selang.h、Selang.cpp类(色狼类),这个类继承自CCNode游戏场景中的每一个色狼都有这个类产生,它肯定包含一个ActionSprite的色狼动画类,并且之前在MainScene.cpp的update方法中实现的色狼沿路行走代码也将转移到这个类的update方法中。

Selang.h:

#include "cocos2d.h"
#include "Waypoint.h"
#include "GameMediator.h"
#include "ActionSprite.h"
class Selang : public cocos2d::CCNode
{
public:
Selang(void);
~Selang(void);
//根据提供的spriteIndex实例化成不同的色狼
static Selang* nodeWithType(int spriteIndex);
//初始化方法
bool initWithType(int spriteIndex,bool halo);
//激活色狼
void doActivate(float dt);
//获取精灵Rect
virtual cocos2d::Rect getRect();
//设置精灵是否激活
void setActive(bool active);
//是否死亡
bool isDie;
void update(float delta);
//色狼精灵
CC_SYNTHESIZE_RETAIN(ActionSprite*,_mySprite,MySprite); private:
//精灵序号,为每种精灵编一个序号
int _spriteIndex;
GameMediator* m;
//当前精灵的位置
cocos2d::Point myPosition;
//走路速度
float walkingSpeed;
//开始路点
Waypoint *beginningWaypoint;
//结束路点
Waypoint *destinationWaypoint;
//是否激活
bool active;
//色狼高度
float myHeight;
//两个点的碰撞检测
bool collisionWithCircle(cocos2d::Point circlePoint,float radius,cocos2d::Point circlePointTwo, float radiusTwo);
};

Selang.cpp

//根据提供的spriteIndex实例化成不同的色狼
Selang* Selang::nodeWithType(int spriteIndex)
{
Selang* pRet=new Selang();
bool b=false;
if (pRet && pRet->initWithType(spriteIndex,b))
{
pRet->autorelease();
return pRet;
}
else
{
delete pRet;
pRet=NULL;
return NULL;
}
}
//初始化方法
bool Selang::initWithType(int spriteIndex,bool halo)
{
bool bRet=false;
do
{
//色狼类型index
_spriteIndex=spriteIndex;
//
m = GameMediator::sharedMediator();
//不激活
active=false;
//行走速度
walkingSpeed=0.2f;
ActionSprite* sprite=NULL;
if (spriteIndex==)//如果类型是1初始化成大叔色狼
{
sprite=DaShu::create();
sprite->setHalo(halo);
//设置速度
walkingSpeed=sprite->getWalkSpeed();
}
this->setMySprite(sprite);
//添加精灵到当前Selang中
this->addChild(_mySprite);
//计算当前色狼精灵1/2高
myHeight=sprite->getTextureRect().size.height/2.0f;
//获得路点集合中的最后一个点
Waypoint *waypoint=(Waypoint*)m->getWayPoints().back();
//设置为色狼出发点
beginningWaypoint=waypoint;
//获取出发点的下个点为色狼目标点
destinationWaypoint=waypoint->getNextWaypoint();
//获得出发点坐标
Point pos=waypoint->getMyPosition();
//对坐标进行校正提供半个身位高度
pos.add(Vec2(,myHeight));
//记录位置坐标
myPosition=pos;
//设置精灵的初始坐标
_mySprite->setPosition(pos);
//设置初始不可见
this->setVisible(false);
//把当前色狼添加到游戏的MainScene场景中显示
m->getNowScene()->addChild(this);
//启动定时器
this->scheduleUpdate();
bRet=true;
} while ();
return bRet;
} void Selang::doActivate(float dt)
{
//激活色狼
active=true;
//设置色狼可见
this->setVisible(true);
} //获取精灵Rect
Rect Selang::getRect()
{
Rect rect =Rect(_mySprite->getPosition().x - _mySprite->getContentSize().width * 0.5f,
_mySprite->getPosition().y - _mySprite->getContentSize().height* 0.5f,
_mySprite->getContentSize().width,
_mySprite->getContentSize().height);
return rect;
} //设置精灵是否激活
void Selang::setActive(bool aactive)
{
active=aactive;
this->setVisible(true);
} void Selang::update(float delta)
{
if (!active)
{
return;
}
Point destinationPos=destinationWaypoint->getMyPosition();
//提升色狼半个身位
destinationPos.add(Vec2(,myHeight));
//是否拐弯
if (this->collisionWithCircle(myPosition,,destinationPos,))
{
if (destinationWaypoint->getNextWaypoint())
{
//设置新的出发点和目标点
beginningWaypoint=destinationWaypoint;
destinationWaypoint=destinationWaypoint->getNextWaypoint();
}
}
Point targetPoint=destinationWaypoint->getMyPosition();
//提升色狼半个身位
targetPoint.add(Vec2(,myHeight));
float movementSpeed=walkingSpeed;
//计算目标点的向量
Point normalized=Point(targetPoint.x-myPosition.x,targetPoint.y-myPosition.y).getNormalized();
//根据速度和向量分别计算x,y方式上的偏移值
float ox=normalized.x * walkingSpeed;
float oy=normalized.y *walkingSpeed;
//更新色狼移动后的位置
myPosition = Point(myPosition.x + ox, myPosition.y +oy);
_mySprite->setPosition(myPosition);
}
//两个点的碰撞检测
bool Selang::collisionWithCircle(cocos2d::Point circlePoint,float radius,cocos2d::Point circlePointTwo, float radiusTwo)
{
float xdif = circlePoint.x - circlePointTwo.x;
float ydif = circlePoint.y - circlePointTwo.y;
//计算两点间的距离
float distance = sqrt(xdif * xdif + ydif * ydif);
if(distance <= radius + radiusTwo)
{
return true;
}
return false;
}

第五步:

如果运行一下那么上面的代码中Waypoint *waypoint=(Waypoint*)m->getWayPoints().back();这行应该会报错,因为GameMediator中没有提供getWayPoints()这个方法,所以我们要对GameMediator类进行修改加上这个方法,代码如下:

void  GameMediator::setWayPoints(cocos2d::Vector<Waypoint*> wayPoints)
{
_wayPoints=wayPoints;
} Vector<Waypoint*> GameMediator::getWayPoints()
{
return _wayPoints;
}

第六步:

在MainScene的init方法中把路点集合通过setWayPoints方法赋值给GameMediator,这样在Selang.cpp中就可以取到路点集合了:

……
this->wayPositions.pushBack(waypoint12);
GameMediator::sharedMediator()->setWayPoints(wayPositions);
……

第七步:

测试这个Selang类具体效果,先给MainScene添加一个void startGame(float delta)的方法,用这个方法开始游戏。

//开始游戏
void MainScene::startGame(float delta)
{
//实例化一个大叔类型的色狼
Selang* selang=Selang::nodeWithType();
//激活这个色狼
selang->setActive(true);
//设置色狼动画为行走动画
selang->getMySprite()->walk();
//取消定时器方法,保证startGame只执行一次
this->unschedule(schedule_selector(MainScene::startGame));
}

第八步:

我们在MainScene的init方法末尾处调用这个startGame的方法:

//0.5秒后调用startGame方法
this->schedule(schedule_selector(MainScene::startGame),0.5f);

到这里,把第一篇中临时添加色狼的代码删除,就可以运行测试游戏了,会看到色狼大叔一扭一扭的沿着道路靠近女主角。效果非常好,我们成功的对色狼的代码进行了重构。

女主角代码重构

第一步:

素材准备,设计4张萝莉不同动作的图片,交替显示模拟萝莉卖萌动画,完成后把图片拷贝到Resources的iphonehd文件夹中,为了适应小分辨率的手机把这个4张图片按比例缩小一半并且拷贝到Resources的iphone文件夹中。

第二步:

新建Luoli.h、Luoli.cpp类(女主角动画类),这个类继承自上面的ActionSprite负责游戏女主角的动画效果显示。

Luoli.h:

class Luoli : public ActionSprite
{
public:
Luoli(void);
~Luoli(void); CREATE_FUNC(Luoli);
bool init();
};

Luoli.cpp:

bool Luoli::init()
{
bool bRet=false;
do
{
CC_BREAK_IF(!ActionSprite::initWithFile("girl1_1.png")); //设置静止状态动作
Vector<SpriteFrame *> idleFrames();
SpriteFrame *frame1=SpriteFrame::create("girl1_1.png", Rect(, , , ));
idleFrames.pushBack(frame1);
Animation *idleAnimation=Animation::createWithSpriteFrames(idleFrames,float(6.0 / 12.0));
this->setIdleAction(CCRepeatForever::create(CCAnimate::create(idleAnimation))); //设置行走状态动作
int i;
Vector<SpriteFrame *> walkFrames();
for (i=;i<;i++)
{
SpriteFrame *frame1=SpriteFrame::create(CCString::createWithFormat("girl1_%d.png", i+)->getCString(), Rect(, , , ));
walkFrames.pushBack(frame1);
} Animation *walkAnimation=Animation::createWithSpriteFrames(walkFrames,float(6.0 / 12.0));
this->setWalkAction(CCRepeatForever::create(CCAnimate::create(walkAnimation))); bRet=true; } while (); return bRet;
}

第三步:

新建Girl.h、Girl.cpp类(女孩类),这个类继承自CCNode游戏场景中的女主角由这个类产生,它肯定包含一个ActionSprite的萝莉动画类。

Girl.h:

class Girl : public cocos2d::CCNode
{
public:
Girl(void);
~Girl(void); //根据提供的type实例化成不同的女主角
static Girl* nodeWithType(int type);
//初始化方法
bool initWithLocation(cocos2d::Point location);
//获取精灵Rect
cocos2d::Rect getRect(); private:
//萝莉精灵
CC_SYNTHESIZE_RETAIN(ActionSprite*,_mySprite,MySprite);
};

Girl.cpp

//根据提供的type实例化成不同的女主角
Girl* Girl::nodeWithType(int type)
{
Girl* pRet=new Girl();
GameMediator* m = GameMediator::sharedMediator();
Waypoint *waypoint=(Waypoint*)m->getWayPoints().front();
Point pos=waypoint->getMyPosition(); if (pRet && pRet->initWithLocation(pos))
{
pRet->autorelease();
return pRet;
}
else
{
delete pRet;
pRet=NULL;
return false;
}
} //初始化方法
bool Girl::initWithLocation(cocos2d::Point location)
{
bool bRet=false;
do
{
//实例化一个萝莉
ActionSprite *sprite= Luoli::create();
this->setMySprite(sprite);
//添加精灵到当前Gril中
this->addChild(sprite);
//设置为静止
sprite->idle();
//计算当前萝莉精灵1/2高
int myHeight=sprite->getTextureRect().size.height/2.0f;
//对坐标进行校正提供半个身位高度
location.add(Vec2(,myHeight));
sprite->setPosition(location);
//把当前女主角添加到游戏的MainScene场景中显示
GameMediator* m = GameMediator::sharedMediator();
m->getNowScene()->addChild(this,); bRet=true;
}
while ();
return bRet;
} Rect Girl::getRect()
{
Rect rect = Rect(_mySprite->getPosition().x - _mySprite->getContentSize().width * 0.5f,
_mySprite->getPosition().y - _mySprite->getContentSize().height* 0.5f+,
_mySprite->getContentSize().width,
_mySprite->getContentSize().height-);
return rect;
}

第四步:

在MainScene的 startGame(float delta)的方法中加上初始化女主角的代码。

……
//初始一个女主角
Girl* girl=Girl::nodeWithType();
//设置女主角动画为卖萌动画
girl->getMySprite()->walk();
//取消定时器方法,保证startGame只执行一次
this->unschedule(schedule_selector(MainScene::startGame));

到这里,把第一篇中临时添加女主角的代码删除,就可以运行测试游戏了,本篇的任务到此为止,本篇完成后android真机的运行效果如下:

结束语:

这个塔防游戏系列已经写了3篇了,到现在为止还没有出现炮塔,说好的炮塔呢?请期待下一篇炮塔姑娘的保护神~

作者交流QQ:

           邮箱:mymoney1001@126.com

Cocos2d-x3.x塔防游戏(保卫萝卜)从零开始(三)的更多相关文章

  1. Cocos2d-x3.x塔防游戏(保卫萝卜)从零开始(二)

    一.前提: 完成前一篇的内容. 具体参考:Cocos2d-x3.x塔防游戏(保卫萝卜)从零开始(一)篇 二.本篇目标: l  说说关于cocos2dx手机分辨率适配 l  对前一篇完成的塔防游戏原型进 ...

  2. Cocos2d-x3.x塔防游戏(保卫萝卜)从零开始(一)

    一.前提: 完成Hello Game项目的创建编译. 具体参考:Cocos2dx.3x_Hello Game项目创建篇 二.本篇目标: l  说说关于塔防游戏的想法和思路 l  实现一个简单的塔防游戏 ...

  3. Cocos2D:塔防游戏制作之旅(一)

    原文地址:http://www.raywenderlich.com/37701/how-to-make-a-tower-defense-game-tutorial 由Pablo Ruiz写的入门教程, ...

  4. Cocos2D:塔防游戏制作之旅(十八)

    在Enemy.m的getDamaged:方法只给你添加如下1行(在if条件内): [theGame awardGold:200]; 现在运行游戏你将注意到你不能放置超出你资源金币的炮塔了.当然杀死敌人 ...

  5. Cocos2D:塔防游戏制作之旅(十六)

    编译运行你的app,放置一些炮塔在你的地图上吧!你将看到炮塔在敌人移动如攻击范围时如何立即开始攻击,并且敌人的血条将随着攻击不断减少知道它们被人道毁灭!胜利即将来临了! 哦!Okay,这里只有少数细节 ...

  6. Cocos2D:塔防游戏制作之旅(二)

    一个象牙塔的视图 如果你并不熟悉此类型的游戏,塔防游戏是一个战略游戏,你需要购买和将武装塔放置在战略位置,去阻止一波又一波的敌人到达并摧毁你的基地 每一波敌人都更强,这些更强的对手有着更快的速度和对于 ...

  7. 制作一个塔防游戏 Cocos2d-x 2.1.4 (一)

    在这篇文章,将会学习到怎样制作一个塔防游戏.在这其中,学习怎样在设定的时间内出现一波波的敌人,使这些敌人沿着指定的路点前进.怎样在地图上指定的位置创建炮塔.怎样使炮塔射击敌人,怎样可视化调试路点和炮塔 ...

  8. 三国塔防游戏android源码

    三国塔防游戏源码,这个游戏源码比较完整的,因为上传有20M限制,把代码工程包分开了,主文件是TFGame,其他res大家按照安卓包加进去就行,欢迎下载并交流 ,大家可以参考一下吧.<ignore ...

  9. HTML5塔防游戏——《三国塔防》 - Yorhom's Game Box

    h3{ font-size:20px; } HTML5塔防游戏--<三国塔防> 游戏介绍: <三国塔防>是一款基于HTML5和Javascript的2D塔防游戏.游戏中除了塔防 ...

随机推荐

  1. 遇到的check the manual that corresponds to your MySQL server version for the right syntax错误

    遇到的check the manual that corresponds to your MySQL server version for the right syntax错误. 结果发现是SQL关键 ...

  2. 彻底卸载sublime txt

    最近彻底重装系统之后,安装sublime txt3, 自己设置了一些,总是觉得不是很对劲,想重新安装. 结果每次安装之后,总是有一些配置文件和卸载之前的是一样的,重复几次总是如此,于是网上搜资料,怎么 ...

  3. CGRectXXX笔记

    CoreGraphics中有关CGRect相关函数笔记 1.CGRectInset //该结构体的应用是以原rect为中心,再参考dx,dy,进行缩放或者放大. CGRect rect = CGRec ...

  4. 【java】: 操作excel2007/2003

    //上传位置(与操作excel无关,可不看) public String getUploadPath() { File theWebFolder = XMPPServer.getInstance(). ...

  5. [VBS]带参数删除扩展名不是*.h、*.c、*.cpp的全部文件

    脚本使用例程CleanFolder遍历一个文件夹 1)使用CleanFolder递归遍历该文件夹下的所有子文件夹 2)如果该子文件夹的大小为0,则删除这个文件夹 3)遍历该文件夹下的所有文件,扩展名不 ...

  6. Exception-异常

    异常(Exception)是程序执行过程中所产生的问题 产生原因:用户输入无效数字.找不到需要打开的文件.在通讯中网络连接中断.JVM发生了内存溢出 异常的三个种类:检查异常.运行时异常.错误(类似异 ...

  7. jsp-avaBean

    package javaBean; public class pagecount { private long count=0; public long getcount() { return cou ...

  8. spring随想

    //不定时持续更新 1.拦截器通过配置文件,在某方法前后添加一些处理,如权限判断等,减少了改方法需要处理的事,是其更专注,由配置文件来设定责任链,更灵活,而且责任链能够复用(一方面是这样能由sprin ...

  9. 两个不等式(Nopier)

  10. WPF快速入门系列(3)——深入解析WPF事件机制

    一.引言 WPF除了创建了一个新的依赖属性系统之外,还用更高级的路由事件功能替换了普通的.NET事件. 路由事件是具有更强传播能力的事件——它可以在元素树上向上冒泡和向下隧道传播,并且沿着传播路径被事 ...