内 容 简 介 本书是一线程序员凝聚自己多年开发经验的结晶之作,深入浅出地讲解Objective-C语言、 Foundation框架、Sprite Kit框架,以及其他开发iOS和Mac游戏所需要的基础知识,帮助读者零基础实践 iOS和Mac游戏开发。 本书内容可以分为四个部分,第一部分(第1~5章)介绍如何使用Xcode和Objective-C语言编写程 序;第二部分(第6~12章)讨论Foundation框架中常用的开发资源,这些资源不但可以为游戏开发服 务,同样也可以应用于各种类型的iOS或OS X应用开发;第三部分(第13~18章)讨论Sprite Kit在游戏 开发中的应用;第四部分(第19~23章)讨论游戏开发相关的技术,如网络状态检测、Game Center排 行榜、应用内购买等。 本书内容安排合理,架构清晰,注重理论与实践相结合,适合作为零基础学习iOS和Mac游戏开发 的初学者的教程,也适合作为有一定编程基础的程序员的参考手册。 本书封面贴有清华大学出版社防伪标签,无标签者不得销售。 版权所有,侵权必究。侵权举报电话:010-62782989 13701121933 图书在版编目(CIP)数据 Objective-C和Sprite Kit游戏开发从入门到精通 / 曹化宇著. — 北京:清华大学出版社,2017 ISBN 978-7-302-45370-3 Ⅰ. ①O… Ⅱ. ①曹… Ⅲ. ①游戏程序—程序设计 Ⅳ. ①TP317.6 中国版本图书馆CIP数据核字(2016)第260827号 责任编辑:杨如林 秦 健 封面设计:李召霞 责任校对:胡伟民 责任印制:王静怡 出版发行:清华大学出版社 网  址:http://www.tup.com.cn,http://www.wqbook.com 地  址:北京清华大学学研大厦A座 邮  编:100084 社 总 机:010-62770175 邮  购:010-62786544 投稿与读者服务:010-62776969,c-service@tup.tsinghua.edu.cn 质 量 反 馈:010-62772015,zhiliang@tup.tsinghua.edu.cn 印 装 者:北京鑫海金澳胶印有限公司 经  销:全国新华书店 开  本:186mm×240mm 印  张:19.25 字  数:472千字 版  次:2017年 1月第1版   印  次:2017年 1月第1次印刷 印  数:1~3500 定  价:59.00元 ————————————————————————————————————————————— 产品编号:071828-01 解。有一天,我把这些资料整理了一下,惊奇地发现已经有了这么多内容,都可以成书了! 好的,被您猜中了,这就是本书形成的过程,也是我的游戏开发历程。 那么,为什么考虑出版本书呢?原因很简单,就是想为学习iOS 或OS X游戏开发的朋 友提供一份简单、实用的学习和参考资料,分享游戏开发的经历与乐趣。 本书特色 1. 简单、实用 简单,并不意味着本书的内容很少,正相反,一方面,本书对游戏开发的相关技术和 方法进行提炼,包括了在iOS和OS X系统中进行游戏开发的必要知识;另一方面,本书也 突出了知识的实用性,对内容深度和广度都进行有效的把握。通过这些内容的学习,相信 读者能够打开游戏开发的大门。 2. 真正从零开始 学习本书,并不需要读者有特殊的编程经历,只要循序渐进,逐渐掌握书的内容,完 全可以开发出iOS或OS X游戏。 3. 轻松学习 无论是玩游戏,还是开发游戏,我都会提倡趣味性与娱乐性,学习虽然是一件很艰苦 的事情,但我们完全有理由去享受这一过程,特别对于游戏开发的学习过程。本书中,语 言和示例都会以轻松的、直观的、有趣的方式来展现,希望大家也能够感觉到,我们就是 在开心、轻松的聊天过程中来学习游戏开发的。 4. 理论与实践相结合 对于每个示例,都有很强的针对性,一切为了游戏开发;针对各种技术,我们不但介 绍了基本的应用和实现,同时,讨论了各种实际应用的场景,力求让读者能够真正理解并 融合这些知识,并能够应用到实际的游戏开发中。 5. 完整性 与很多介绍某种开发技术的图书不同,本书并不是单一技术的手册,而是包括了iOS 或OS X游戏开发的完整内容,从基本的代码、Sprite Kit开发资源的应用,再到Game Center 排行榜、应用内购买等,这些内容完整地展示了游戏从开发到上架所需要的知识。 读者对象 在这里,我们看看本书的内容都适合哪些朋友阅读和使用。 . 游戏开发爱好者 . iOS或OS X应用开发者 . Sprite Kit开发者 如何阅读本书 本书主要包括四个部分的内容: 第一部分介绍了如何使用Xcode和Objective-C语言编写程序。 第二部分讨论了Foundation框架中常用的开发资源,这些资源不但可以为游戏开发服 务,同样也可以应用于各种类型的iOS或OS X应用开发。 第三部分讨论了Sprite Kit在游戏开发中的应用。 第四部分讨论了游戏开发相关的技术,如网络状态检测、Game Center 排行榜、应用内 购买等。 如果是Apple环境开发新手,建议从第1章开始逐步学习;如果是一名经验丰富的开发 者,可以根据实际需要选择其中的内容阅读;实际工作中,可以将本书作为参考手册随时 查阅。 阅读本书的建议 . 对于开发初学者,建议逐章阅读,并能够亲手实践书中的示例代码。 . 对于已经掌握Objective-C语言的读者,可以根据需要阅读相关的内容。 . 对于Foundation、Sprite Kit或常用功能实现部分,大家可以随时参考,并根据实际 需要修改使用。 进一步学习建议 阅读并掌握本书的内容以后,相信读者已经掌握了Objective-C,并对iOS或OS X应用 有了一定的了解,特别是,应该可以开发出属于自己的游戏了。 不过,我们也知道,开发一个大型游戏的知识绝不止书中这些内容,所以,完成本书 内容的学习以后,大家可以更进一步,以下是关于深入学习的一些建议: . 无论是Foundation还是Sprite Kit或Store Kit框架,都值得我们深入学习和理解, 并能够在应用中合理地使用。关于这些内容,建议大家能够抽时间看一看官方文 档。对于某些问题的众多解决方案和技巧,能够在学习和应用过程中主动思考, 并亲手验证它们。 . 如果需要进行跨平台的游戏开发,建议大家了解至少一种框架,并能深入地学习 和应用。 . Swift 是一种现代的编程语言,如果大家需要长期进行Apple 环境的开发工作,建议 学习并能够熟练使用它。 勘误和支持 由于作者水平有限、编写时间仓促,书中难免会出现一些疏漏或不足之处,而读者的 批评和指正,正是我们共同进步的强大力量,欢迎您通过清华大学出版社网站(www.tup. com.cn)与我们联系,同时,也欢迎大家直接与作者交流,作者的邮箱是chydev@163.com。 致谢 感谢出版社的编辑老师耐心的交流与指导,使得本书能够顺利与读者见面。 感谢我的家人,他们承担了大量的家务,容忍了我在家里的“懒惰”,为我创造了一 个温暖的家、一个安心的工作环境,特别是我的孩子们,他们总是说“爸爸在工作,我不 打扰他”,而这些正是我快乐生活和努力工作的力量源泉。 谨以此书献给我的家人,以及热爱游戏、热爱开发的朋友们! 曹化宇 2016年7月 4.2 类与对象 ···································53 4.2.1 接口部分 ····································53 4.2.2 实现部分 ····································54 4.2.3 创建对象(实例化) ·····················55 4.2.4 类的成员 ····································56 4.3 方法(任务) ·····························58 4.3.1 创建方法 ····································58 4.3.2 description方法与NSLog()函数 ·········61 4.4 属性 ·········································61 4.4.1 使用@proeprty和@synthesize指令 ·····62 4.4.2 使用setter和getter方法 ···················63 4.5 初始化方法 ································64 4.6 继承 ·········································65 4.6.1 成员的访问 ·································65 4.6.2 重写属性和方法 ···························67 4.6.3 继承关系中的初始化 ·····················67 4.7 分类 ·········································69 4.7.1 命名分类 ····································70 4.7.2 匿名分类 ····································71 4.8 对象复制与传递 ··························71 4.8.1 对象的复制 ·································71 4.8.2 对象作为参数 ······························72 4.9 动态处理类和对象 ·······················72 4.9.1 对象类型判断 ······························72 4.9.2 方法存在判断 ······························73 4.9.3 动态调用方法 ······························74 第5章协议 ····························· 75 5.1 创建协议 ···································75 5.2 实现协议 ···································76 5.3 可选成员 ···································77 5.4 实现多个协议 ·····························78 5.5 对象深复制(实现NSCopying 协议) ······································79 第6章数组、集合与字典 ··········· 82 6.1 C风格数组 ·································82 6.2 不可变数组(NSArray类型) ·········84 6.2.1 创建NSArray对象 ·························84 6.2.2 数字对象(NSNumber类) ·············84 6.2.3 使用NSValue 类 ····························85 6.2.4 数组成员操作 ······························86 6.2.5 保存与载入 ·································87 6.3 可变数组(NSMutableArray类型)··88 6.3.1 创建NSMutableArray对象 ···············89 6.3.2 添加成员 ····································89 6.3.3 删除成员 ····································90 6.3.4 替换成员 ····································90 6.4 集合(Set) ·······························91 6.4.1 不可变集合(NSSet类) ················91 6.4.2 可变集合(NSMutableSet类) ·········92 6.5 字典(NSDictionary) ··················93 6.5.1 创建字典对象 ······························93 6.5.2 常用成员 ····································93 6.5.3 NSMutableDictionary类 ··················94 第7章字符串 ·························· 96 7.1 C风格字符串 ······························96 7.2 不可变字符串(NSString类) ·········97 7.2.1 创建NSString对象 ·························97 7.2.2 返回字符数量 ······························98 7.2.3 截取子字符串和字符 ·····················98 7.2.4 大小写转换 ·································99 7.2.5 转换为C风格字符串 ······················99 7.2.6 字符串比较与匹配 ······················ 100 7.2.7 转换为数值 ······························· 101 7.2.8 保存与读取 ······························· 101 7.3 可变字符串(NSMutableString 类) ······································· 102 7.3.1 创建NSMutableString对象············· 102 7.3.2 NSMutableString常用成员············· 102 7.4 使用NSURL类 ·························· 103 第8章本地化字符串 ················105 8.1 判断系统语言类型 ····················· 105 8.2 NSLocalizedString ······················ 106 8.3 NSLocalizedStringFromTable ········· 107 8.4 综合应用 ································· 108 第9章日期与时间 ···················109 9.1 NSDate类 ································ 109 9.1.1 获取时间信息 ···························· 110 9.1.2 时间的计算和比较 ······················ 110 9.2 NSDateComponents类 ················· 111 9.3 NSCalendar类··························· 112 9.4 时区与区域设置 ························ 113 9.4.1 NSTimeZone 类 ··························· 113 9.4.2 NSDateFormatter类 ····················· 114 9.4.3 NSLocale类······························· 115 9.5 封装CDate类···························· 115 9.5.1 初始化方法 ······························· 117 9.5.2 时间、区域和时区 ······················ 118 9.5.3 日期与时间数据 ························· 119 9.5.4 测试 ········································ 120 9.6 获取中国农历信息 ····················· 120 9.6.1 初始化方法 ······························· 121 9.6.2 年份与名称 ······························· 122 9.6.3 月份与名称 ······························· 123 9.6.4 日期与名称 ······························· 124 9.6.5 属相 ········································ 125 9.6.6 测试 ········································ 125 第10章文件与目录 ·················127 10.1 获取系统信息 ························· 127 10.1.1 获取文稿目录(Documents) ······· 128 10.1.2 获取临时目录与GUID ················ 128 10.1.3 更多系统信息 ·························· 129 10.2 NSData与NSMutableData类 ········ 130 10.3 使用NSFileManager类 ··············· 130 10.3.1 文件或目录是否存在 ················· 131 10.3.2 复制文件和目录 ······················· 131 10.3.3 删除文件和目录 ······················· 132 10.3.4 移动、重命名文件和目录 ··········· 133 10.3.5 文件与目录的属性 ···················· 133 10.4 文件操作 ······························· 134 10.4.1 读取文件内容 ·························· 134 10.4.2 写入文件 ································ 134 10.4.3 比较文件内容 ·························· 135 10.4.4 检测文件读写权限 ···················· 135 10.5 目录操作 ······························· 135 第11章归档 ··························137 11.1 归档与解档 ···························· 137 11.1.1 实现NSCoding协议 ···················· 137 11.1.2 使用NSKeyedArchiver类和 NSKeyedUnarchiver类 ··············· 139 11.2 利用归档复制对象 ··················· 140 第12章通知中心与对话框 ········142 12.1 通知中心 ······························· 142 12.2 OS X对话框···························· 146 12.3 iOS对话框 ······························ 147 12.3.1 回顾UIAlertView 类 ···················· 150 12.3.2 使用UIAlertController类 ·············· 152 12.3.3 iPad中的UIAlertController ··········· 154 第13章Sprite Kit基础 ··············156 13.1 第一个Sprite Kit项目 ················ 156 13.1.1 启动界面 ································ 159 13.1.2 视图 ······································ 161 13.1.3 创建场景 ································ 162 13.2 iOS设备与系统信息 ·················· 163 13.2.1 设备类型与iOS版本 ··················· 163 13.2.2 屏幕尺寸 ································ 164 13.2.3 封装——创建CApp.h和CApp.m 文件 ······································ 165 13.3 颜色 ····································· 167 13.4 节点树 ·································· 168 13.5 Core Graphics·························· 171 13.5.1 CGPoint结构 ···························· 171 13.5.2 CGSize结构 ····························· 172 13.5.3 CGRect结构 ····························· 172 13.5.4 CGRectContainsRect()函数 ·········· 172 13.5.5 CGRectContainsPoint()函数 ·········· 172 13.5.6 CGRectGetMidX()和CGRectGetMidY() 函数 ····························· 173 13.6 坐标系 ·································· 173 13.6.1 位置(position)与锚点 (anchorPoint) ························ 173 13.6.2 坐标转换 ································ 174 13.7 场景切换(SKTransition) ········· 174 13.8 游戏循环 ······························· 176 13.9 场景(SKScene)中显示对话框 ·· 178 13.10 Mac中的Sprite Kit项目 ············· 180 13.10.1 项目初始化 ···························· 180 13.10.2 响应鼠标与键盘 ······················ 181 13.10.3 屏幕与尺寸 ···························· 182 第14章精灵、纹理与角色控制 ···185 14.1 SKSpriteNode类 ······················· 185 14.1.1 创建精灵节点 ·························· 185 14.1.2 NSBundle类 ····························· 187 14.1.3 组合节点 ································ 188 14.2 SKTexture 类 ··························· 189 14.2.1 截取纹理内容 ·························· 189 14.2.2 精灵动画——翻滚的小行星 ········· 191 14.2.3 更多的小行星 ·························· 191 14.3 移动与碰撞 ···························· 192 14.3.1 场景初始化 ····························· 193 14.3.2 碰撞检测与游戏状态 ················· 197 14.4 触摸控制 ······························· 200 14.4.1 触摸响应方法 ·························· 200 14.4.2 控制太空船 ····························· 201 14.4.3 单击 ······································ 202 14.4.4 手势 ······································ 202 14.5 Mac中的太空船 ······················· 207 14.5.1 处理鼠标操作 ·························· 210 14.5.2 处理键盘控制 ·························· 212 14.5.3 在update:方法中处理键盘控制 ····· 213 第15章动作与声音播放 ···········216 15.1 基本动作类型 ························· 217 15.1.1 移动 ······································ 217 15.1.2 尺寸与缩放 ····························· 218 15.1.3 显示和隐藏 ····························· 219 15.1.4 旋转 ······································ 220 15.1.5 等待 ······································ 221 15.2 动作的组合 ···························· 221 15.2.1 动作组 ··································· 221 15.2.2 动作序列 ································ 222 15.2.3 动作重复 ································ 222 15.2.4 动画动作 ································ 223 15.3 声音播放 ······························· 223 15.3.1 使用动作播放声音 ···················· 223 15.3.2 使用AVAudioPlayer 播放声音 ········ 224 15.4 动作的使用 ···························· 224 15.4.1 执行动作 ································ 225 15.4.2 通过键(Key)执行动作 ············ 225 15.4.3 取消所有动作 ·························· 226 15.4.4 判断节点是否有动作执行 ··········· 226 15.4.5 修改动作速度 ·························· 226 第16章更多节点类型 ··············227 16.1 SKShapeNode ·························· 228 16.1.1 基本图形节点 ·························· 229 16.1.2 根据路径创建图形节点 ·············· 230 16.2 SKVideoNode ·························· 231 16.3 SKCropNode ··························· 232 16.4 SKEffectNode ·························· 233 16.5 SKEmitterNode与粒子效果 ········· 234 16.5.1 在Xcode中创建粒子 ·················· 234 16.5.2 下雪场景 ································ 235 16.5.3 模拟爆炸 ································ 236 16.5.4 SKEmitterNode 类 ····················· 238 16.6 节点的组合 ···························· 240 第17章Sprite Kit游戏常用算法 ···242 17.1 两点距离 ······························· 242 17.2 碰撞测试 ······························· 243 17.3 角度 ····································· 245 17.4 视线 ····································· 246 17.5 躲避障碍物 ···························· 249 第18章综合测试——Mac版 坦克大战 ····················251 18.1 场景初始化 ···························· 252 18.1.1 创建坦克 ································ 254 18.1.2 创建敌人 ································ 254 18.1.3 创建岩石 ································ 255 18.2 坦克的操作 ···························· 255 18.2.1 坦克的动作 ····························· 256 18.2.2 鼠标和键盘控制 ······················· 257 18.2.3 自动执行 ································ 258 18.3 敌人的AI ······························· 259 18.4 处理已发射炮弹 ······················ 261 18.5 自己动手 ······························· 262 第19章加速计与陀螺仪 ···········263 19.1 自动响应 ······························· 264 19.2 调用数据 ······························· 267 19.3 游戏控制方法小结 ··················· 269 19.3.1 触摸屏 ··································· 269 19.3.2 键盘与鼠标 ····························· 270 第20章网络状态 ····················271 20.1 检测网络状态 ························· 271 20.2 监视网络状态 ························· 272 第21章Game Center排行榜 ·····274 21.1 配置排行榜 ···························· 274 21.2 在应用中处理排行榜 ················ 275 21.2.1 Game Center登录状态 ················ 275 21.2.2 提交成绩 ································277 第22章 应用内购买 ·················280 22.1 准备测试用户和设备 ················280 22.2 创建App内购买 ·······················281 22.3 在项目中使用App内购买 ···········282 22.3.1 检测访问限制与网络 ·················282 22.3.2 执行购买操作 ··························285 22.3.3 恢复已购项目 ··························287 22.3.4 在场景(SKScene)中执行购买操作 ···287 22.3.5 在场景(SKScene)中执行恢复操作 ··290 第23章 发布到App Store ··········292 23.1 注册开发者与设备 ···················292 23.2 发布准备 ·······························293 23.2.1 应用图标 ································293 23.2.2 iOS应用截图 ····························294 23.2.3 OSX应用截图 ··························295 23.2.4 上传应用 ································295 23.2.5 项目审核 ································295 #import int main(int argc, const char * argv[]) { @autoreleasepool { NSLog(@"Hello, World!"); } return 0; } 这些代码就是程序执行相关的内容,我们快速了解一下它们的功能。 第一行代码是以“#”符号开头的,它称作预处理指令,#import 指令用于导入当前代 码文件中所需要使用的资源。一般情况下,我们会导入头文件(.h文件),这和C或C++代 码中使用的#include指令的功能是相同的。 在导入头文件时,主要使用两种格式: . 使用< >包含头文件,用于引用系统资源,主要是指Xcode中内置的一些开发资 源,如代码中的Foundation.h文件。 . 使用" "包含头文件,用于引用“外部”资源,这里的“外部”是指非Xcode 内置开 发资源,如第三方或者你自己开发的代码。 接下来的main()函数,相信学习过C语言的朋友一定不会陌生,没错,它就是程序的入 口,代码会从这里开始执行。在()中是函数的参数,main()中的两个参数与C语言中的相似,其 中,args 带入命令行指令和参数的数量(使用空白字符分隔),参数argv 包含了这些内容。 再看@autoreleasepool 指令(@开头的语句也是一种指令),不明白是什么,我们拆 开来看,就是auto release pool,自动释放池?这是什么情况呢?实际上,这是Objective-C 中一种新的内存自动管理机制,称为ARC(Automatic Reference Counting ,自动引用计 数)。 在Objective-C代码中,使用ARC技术,可以有效地对内存进行自动管理,将程序员从 复杂的内存管理工作中解放出来。如果你在C语言中使用过malloc()、free()等函数手工管理 过内存,就会明白我在说什么了。当然,没有玩过C也没关系,现在我们只需要知道,使 用Objective-C 编程时,在main() 函数中使用@autoreleasepool 指令就可以使用ARC了,如果 你没有百分之二百的信心能够比ARC更好地管理内存,那就选择使用ARC吧。 NSLog()函数用于显示调试信息,其中,第一个参数是显示的内容,可以包含格式化 字符;第二个参数开始是格式化字符所对应的数据。下面的代码会显示一个整数的数据。 @autoreleasepool { int intNum = 1; NSLog(@"intNum = %i", intNum); // intNum = 1 } 7 第 第 第 准备工作 最后的return语句用于向函数的调用者返回执行结果(活儿干得怎么样,给调用者发个 信号),一般来讲,代码0就意味着任务顺利完成了。对于main()函数来讲,它的调用者就 是操作系统。 通过代码的执行,我们看到,第一个程序的主要功能就是显示一条文本信息,这些信 息包含在“@”中,我们称为字符串(string)。在Objective-C中,可以使用两种风格的字 符串: . C风格字符串,即使用字符数组(char[])组成的字符串,这种字符串除了真正的字 符内容,还会由ASC II码中的0值字符(NULL)作为结束,使用转义字符“\0” 表示。 . 字符串对象,使用NSString或NSMutableString类型处理字符串内容。前面的代码 中,使用@""定义的字符串就是NSString对象,大部分情况下,我们会使用这种字 符串。 更多字符串的处理,会在第7章详细介绍。 1.4 获取帮助 无论是在学习,还是在真正的开发工作中,我们随时都需要查阅一些开发资料。除了 本书,在Xcode中,我们还可以快速呼叫支援。如果你对哪一部分内容不太明白,可以首 先按住Option(Alt)键,并将鼠标指向需要了解的资源,然后,当鼠标变成问号图标时, 单击鼠标就可以显示此资源的基本信息,通过其中的链接可以打开更多的资料。 此外,我们还可以在Apple的开发者网站获取更多、更全面的参考资源,网址如下: . https://developer.apple.com/(Apple开发者网站) . https://developer.apple.com/resources/(开发工具、文档等资源) 现在,你应该已经了解了如何在Xcode中编写代码并执行,接下来,我们将开始系统 地学习Objective-C编程语言,马上进入第2章,大片即将开演。 接下来,我们修改这个文件的内容,首先,可以把绿色内容删除,也可以修改为其他 内容,哪个星球的语言就不重要了,只是要记得使用//作为行的开头,而且习惯上还会加 一个空格。下面的代码就是我修改后的CApp.h文件。 #ifndef __CApp_h__ #define __CApp_h__ extern int gDeviceType; void deviceInit(); #endif 这么多#符号是什么情况?我们说过,它们称为预处理指令。 预处理,就是预先处理?是的,只不过这些工作由编译器来完成,我们只要正确书 写就行了。那怎么正确书写呢?这个也不难,基本的要求就是:#符号别忘了,字母拼对 了,最后就不要加分号了!真的很简单,对吧。 接下来,我们先从字面上了解这些指令的功能。 #ifndef和#endif指令的功能是,如果没有定义“__CApp_h__”宏(使用#define指令定 义,稍后说明),则执行#ifndef 和#endif 指令之间的内容。默认情况下,Xcode 中会自动使 用“项目名称+头文件名”的格式来定义宏,我们可以使用默认的宏名称,也可以根据项 目约定进行相应的修改。 #define指令用于定义宏,这是货真价实的符号,而且,在定义宏的同时也可以定义它 表示的值,如: #define DT_IPAD 1000 本例中,编译器开工时,会在代码中使用1000替换所有DT_IPAD 标识,而这也是宏的 基本功能之一。在后续的内容中,我们会看到更多关于宏的应用。 再来看两个声明语句(都说了头文件中只是声明了): extern int gDeviceType; void deviceInit(); 第一条语句声明了一个整数类型(int )的变量gDeviceType ,其中extern 关键字说明此 变量是定义在另外的文件中的,第二条语句声明了一个函数deviceInit() 。请注意,它们都 以分号作为结束,这是基本的语句形式,请大家区分语句和预处理指令。此外,我不会告 诉你它们是定义在CApp.m文件里的。 接下来,通过Xcode菜单“File”→“New”→“File”,创建一个Objective-C文件 (Objectie-C File),并命名为CApp.m,如图2-2所示。 Objective-C和Sprite Kit游戏开发从入门到精通 通过这部分的练习,我们应该了解.h和.m文件是如何配合使用的,为进一步的开发工 作做好准备。 此外,当我们需要使用某个头文件的资源时,不要忘了使用#import指令引用它,对于 我们自己创建的头文件,使用一对双引号来引用,如下面的代码。 #import "CApp.h" 2.2 注释 还记得代码中的绿色内容(注释)吗?前面的讨论中,我们都会将注释删除,难道它 们真的没什么用吗? 当然不是,在代码中添加注释是一项非常重要的工作,虽然我们常说,代码最好的解 释就是代码本身,但是,对于复杂的代码来讲,能通过注释简单地说明一下代码的目的也 是一个不错的选择。 在Objective-C代码中,我们可以使用两种注释方法: .块注释,即将注释内容放在/*和*/之间。 .行注释,使用//开始,到本行的结束都当作注释内容。 下面的内容演示了这两种注释的基本应用。 /* 这是块注释 */ // 这是行注释 int x = 10; // 这也是行注释 这看上去也是很简单的吗? 是的,注释本身并没有什么特别之处,重要的是在代码中如何应用。一般来讲,注释 会用于说明代码的目的,如算法和思路的简单说明,但不应该是代码的简单重复。另外, 使用一行空注释,可以有效地对代码块进行分割,便于代码的阅读。 大家可以在实践中逐步理解注释的使用特点,并在项目中合理地应用,同时,如果团 队中有注释的书写约定,我们也应该严格执行。毕竟,无论是开发过程中,还是在代码维 护过程中,我们都经常要阅读代码。 2.3 变量 接下来,我们需要处理具体的数据了,比如,机器人在什么位置,其坐标就需要使用 数字来表示,最少在地球上是这么干的,对吧? 现在思考一个问题。在代码中,不可能让数字满天飞,特别是一些有特别明确含义的 数据,此时,我们可以使用一些标识符来表示它们,这样就可以明确地知道数据的含义 了。比如,在数学公式中使用π表示圆周率,而不需要总是写3.1415926…。 代码中,变量是标识符的一种,它表示的数据可以根据程序执行的需要而改变。变量 主要包括变量名称、数据类型和值,当然还包括它们的使用范围。 在Objective-C代码中,声明一个变量的方法如下。 <类型> <变量名称>; <类型>指定了变量中数据的类型,如整数、浮点数等。不明白?没关系,稍后就有数 据类型相关的内容。 <变量名称>就是变量的标识符,习惯上,我们使用首字母小写,然后每个单词首字母 大写的形式,如index、counter、xPos。请注意,在给变量命名时,应尽量使用有意义的 名称。不过,有时候也会有一些习惯用法,我们在后续的内容中会看到。各开发者或团队 也会有自己的变量命名习惯和约定,但在Objective-C中,对于变量名称命名的基本原则就 是:使用字母、数字和下画线组成,而且不能使用数字开头。 使用变量之前,还应该确定它的值。比如,在定义变量时,可以同时指定变量的值, 如下面的代码。 int x = 1; 这行代码的功能就是定义一个整数类型(int)的变量x,并将其赋值为1,这样,在代 码中,我们使用x时就表示整数1。 其中=符号称为赋值运算符,它的作用是将运算符右边的数据赋值到左边的变量或其 他类型的标识符中。 此外,变量声明和赋值也可以由两条语句来完成,如下面的代码。 int x; x = 1; 也就是说,在代码中,我们可以随时修改变量的值。 2.4 S量 常量,是指在程序运行过程中不会改变的量,一般用来指定一些常数或特殊数值,比 如前面提到的圆周率就是一个常数,在代码中,我们就可以使用常量来表示。 代码中,我们可以使用const关键字定义常量,如下面的代码。 #import const int MaxAge = 150; int main(int argc, const char *argv[]) { @autoreleasepool { NSLog(@"%i", MaxAge); } return 0; } 另一种创建常量的方法是通过预处理中的#define指令定义宏,模拟常量的使用,如下 面的代码。 #import #define MAX_AGE 150 int main(int argc, const char *argv[]) { @autoreleasepool { NSLog(@"%i", MAX_AGE); } return 0; } 代码同样会显示150。 关于宏的命名,传统的C风格是使用字母全大写,每个单词使用下画线分隔,比如, 在Foundation资源中,圆周率就定义为M_PI,我们可以直接在代码中使用它。其中M是数 学(Math)的意思,而PI就是π了。 下面的代码,我们就通过圆周率来计算圆的面积。 #import #define MAX_AGE 150 int main(int argc, const char *argv[]) { @autoreleasepool { double r= 15.0; } NSLog(@"圆的半径为%Lf,面积为%Lf", r , r*r*M_PI); return 0; } 在代码中使用常量有两个基本的好处,一是当我们需要修改这个数值时,只在定义的 地方修改一次就可以了,而不需要在代码中所有使用此数据的地方修改;第二个好处就是, 使用一个有意义的名称来标识数据,可以避免可能的输入错误,要知道,在代码中,如果你 写错一点点数据或一个字母,都不知道程序会出什么错。 2.5 整数 轻松一刻,做个小游戏,猜一猜下面的代码会显示什么结果? int xPos; xPos = 105.5; NSLog(@"%i", xPos); 105.5吗?错了! 你也许会想,这是为什么呢?明明写的是105.5,怎么就总是显示105呢?问题就在于 把xPos变量声明为整数(int)了。 原来int类型不能处理小数呀!这下就对了! 好的,是时候考虑数据的类型了。在Objective-C中,数据类型可以分为两种风格和若 干种类型。其中,两种风格是指: . C风格的基本类型,如int、unsingned int、long int、float、double、char等,这些类 型与C语言中的数据类型应用是相似的。 . 在Foundation框架中也定义了一些数据类型,如NSInteger、NSUInteger、 NSString、CGFloat等。实际上,这些数据类型只是C数据类型的别名,在后续的学 习中,我们会逐步发现使用这些类型的意义所在。此外,Foundation是在iOS和OS X系统中进行开发的基本框架,在这个框架中,定义了一系列的基本数据类型和开 发资源。也许你会想起来,我们的代码中都引用了Foundation.h头文件。 接下来是真正的数据类型,包括整数、浮点数、布尔、字符、字符串等,我们会详细 讨论各种数据类型的取值范围、运算、类型转换等应用特点,首先从整数开始。 ■ 2.5.1 取值范围 整数是指没有小数部分的数,按照计算机内部处理方式又分为有符号整数和无符号整 数。其中,有符号整数可以处理负数、零和正数,而无符号整数只能处理零和正数。 在C风格数据类型中,int和unsigned int是两种比较常用的整数类型。其中,int是有符 号整数(singn int)的简写形式,而unsigned int则是无符号整数。它们都定义为32位整数, 这样一来,int类型的取值范围就是-2147483648到2147483647,即-231到231-1,相对应, unsigned int的取值范围则是0到232-1。 在Foundation框架中,常用的整数类型为NSInteger 和NSUInteger ,如果我们呼叫帮 助,就会发现,它们实际使用了typeof 关键字定义为C数据类型的别名,在64 位环境下,它 们分别定义为long和unsinged long,而在32位环境下,它们分别定义为int和unsinged int类 型。我们知道,iOS 和OS X系统都已进入64 位时代,所以,在开发较新平台下的项目时, 我们可以认为NSInteger和NSUInteger类型分别是long int和unsigned long int类型的别名。 那么,为什么要使用这些Foundation中定义的数据类型呢?我想,最大的优势在于代 码对各种平台的兼容性。 ■ 2.5.2 S术运S 接下来,我们进行基本的算术运算训练,就是加、减、乘、除那些东西,很简单,只 是记得要使用Objective-C代码! 第一题:1号机库有30架战斗机,2号机库有29架战斗机,一共有多少架战斗机,在程 序中应该怎么写? 答:用加法(+),如下面的代码。 NSInteger hangar1 = 30; NSInteger hangar2 = 29; NSInteger sum = hangar1 + hangar2; NSLog(@"共有%li架战斗机", sum) ; 第二题:要是飞走了16架,还剩多少架,怎么计算呢? 答:用减法(-)呀!我们接着前面的代码写。 NSInteger overplus = sum - 16; NSLog(@"剩余%li架战斗机", overplus); 第三题:一个飞行中队有4架战斗机,那14个中队有多少架呢? 等等,我知道,用乘法(*),小学生都会。 NSInteger squadrons = 14; sum = squadrons * 4; NSLog(@"14个中队共%li架战斗机", sum) ; 第四题:那2号机库里有几个飞行中队呢? 答:用除法(/),如下面的代码。 squadrons = hangar2 / 4; NSLog(@"2号机库有%li个飞行中队", squadrons) ; 在Xcode中测试之前,先想一想结果是什么? 第五题:好像2号机库里的战斗机数量不能被4整除,分了7个中队,还剩几架战斗 机,这个怎么计算呢? 答:这个在程序中就很简单了,可以直接使用取余数运算符(%)来计算,如下面的 代码。 NSInteger remainder = hangar2 % 4; NSLog(@"2号机库分完飞行中队还剩%li架战斗机", remainder); 以上示例,我们使用整数进行了一系列的算术运算,对于加法、减法和乘法运算,其 结果和我们习惯上的概念相同,但请注意除法和取余数运算,其中,整数除以整数,结果 依然是整数,而取余数运算的结果也是整数。 此外,我们还应该注意,在任何编程语言中,运算符都会有一定的优先级规则,即一 系列运算符在表达式中运算的先后顺序。比如,前面的5个算术运算符在一个算式里时, 就应该先计算乘、除、取余数,然后再计算加法和减法。 对于运算符的优先级,如果你不能保证百分之百的记忆正确(可是有几十个运算 符),我们还是建议使用小括号()来强制指定运算顺序,这样,代码会更安全,可读性也 更强,所以,在工作中,我们不会完全依赖默认的运算符优先级。 ■ 2.5.3 NSLog()函数与格式化输出 在开发和调试过程中,NSLog()函数可以用来显示各种信息,其中,非文本的数据需 要使用格式化字符。前面的内容中,我们已经多次使用NSLog()函数。 在这里,常用的整数格式化字符包括: . int类型使用“%i”格式化字符,我们还可以使用“%x”将整数显示为十六进制, 使用“%o”将整数显示为八进制。 . unsigned int类型,无符号整数,使用“%u”格式化字符。 . long int类型,长整型数据,使用“%li”格式化字符。 . unsigned long int类型,无符号长整型数据,使用“%lu”格式化字符。 . long long int类型,长长整型数据,使用“%lli”格式化字符。 . unsigned long long int类型,无符号长长整型数据,使用“%llu”格式化字符。 这些整数类型都有相应的十六进制和八进制的格式化字符,基本的变化原则就是将列 表中给出的格式化字符中的i或u变为x(十六进制)或o(八进制)。 ■ 2.5.4 组合运S符 组合运算符是将基本的算术运算符与赋值运算符合并使用,如下面的代码。 NSInteger x = 1; x += 2; // 相当于 x = x + 2 NSLog(@"%li",x); // 3 在Objective-C中,5种算术运算都有相应的组合运算符,即+=、-+、*=、/=和%=运 算符。 ■ 2.5.5 增量与减量运S 在整数操作中,还有一种运算很常用,那就是增量计算,简单的说,就是变量进行加 1的运算。增量运算包括两种形式,即前增量和后增量。 前增量运算表达式中,变量会先进行加1的计算,然后返回表达式的值,此时,表达式 的值就是变量加1的值。最终的结果,表达式和变量的值都是变量原值加1,如下面的代码。 int i = 1; NSLog(@"%i", ++i); // 2NSLog(@"%i", i); // 2 后增量运算表达式中,表达式会返回变量原来的值,然后变量再进行加1的计算。最 终结果,表达式的值就是变量的原值,变量的值会加1,如下面的代码。 int i = 1; NSLog(@"%i", i++); // 1NSLog(@"%i", i); // 2 实际应用中,如果我们只需要使用增量计算后变量的值,则前增量和后增量的区别就 无所谓了。但是,如果我们需要使用增量运算表达式的值,就必须非常小心前增量和后增 量的区别。 此外,与增量运算相对应的减量运算,同样包括前减量和后减量运算,工作方式与增 量运算相似,只是减量运算执行的是变量减1的操作。 ■ 2.5.6 二进制与位运S 对于初学者,本部分可以作为选读,这也是本书中为数不多的与计算机软件基本工作 原理相关的内容,了解一下不会有坏处。当然,现在不研究,暂时也不会影响我们继续往 下学习,以后有需要,也可以随时回来串串门。 了解整数的位运算,我们首先需要清楚整数在计算机中的处理方式,也就是整数的二 进制形式。前面,我们说过的32位整数、64位整数,指的就是二进制位的数量。 在二进制计算中,运算数只包括0和1。这个原因很简单,因为计算机所使用的数字 电路就只有两种状态,即连通和断开状态,二进制才能最简单地表示数字电路的状态。 1. 无符号整数的二进制形式 无论是8位还是64位,它们对于整数处理的基本原理是一样的,所以,接下来,我们 就使用简单点的8位二进制形式来讨论相关的应用问题。你一定和我一样,也不想看着64 个0或1组成的二进制数发呆,对吧! 00011001 猜猜这是几~ 图2-4 二进制数据 二进制数如何转换为十进制整数呢?我们先来看无符号整数及有符号整数中0和正数 的转换方法。 如果二进制位上全部是0,那么,这个数就是0了;如果二进制位上是1,就使用2n-1 计算出这个二进制位对应的十进制整数,其中,n是从右向左数的第几位(由低位向高 位),然后,将所有的数值相加,就得到了这个二进制数所表示的十进制整数。 如图2-4中的00011001,我们可以得到算式:24+23+20=16+8+1=25,最终,我们计算出 二进制数00011001表示的十进制整数就是25。 那么,十进制正整数如何转换为二进制呢?我们还以25为例,如图2-5所示的计算 方法。 图2-5 十进制转二进制计算方法 首先,我们使用25除以2,商是12(写在下方),余数为1(写在左边);然后使用12 除2,商是6,余数是0;以此类推,直到商为1时结束计算。然后,我们从下方往上写出1 和0,如图中的结果就写成11001,如果位数不够,比如我们需要8位整数,则在前面(高 位)补0,最终得到00011001,这就是十进制整数25的二进制形式了。 2. 逻辑位运S 下面,我们就来了解Objective-C中的位运算符。 按位与(AND)运算,使用&运算符,包括两个运算数,当两个运算数都是1时,结 果为1,否则为0。如图2-6所示。 图2-6 按位与运算 按位或(OR)运算,使用|运算符,包括两个运算数,当两个运算数有一个为1时,结 果为1,只有两个数都是0时,结果才为0。如图2-7所示。 图2-7 按位或运算 按位异或(XOR)运算,使用^运算符,包括两个运算数,当两个数一样时,结果为 0,只是两个数不同时,结果才为1。如图2-8所示。 图2-8 按位异或运算 按位取反(NOT)运算,使用!运算符,只需要一个运算数,即1变0,0变1。如图2-9 所示。 图2-9 按位取反运算 3. 补码 一个二进制数的补码是指原数按位取反后加1的值。还以00011001 为例,其按位取反 的结果是11100110 ,再加1就是11100111 。这样,11100111 就是00011001 的补码。 那么,在计算机中使用补码有什么用呢?我们在有符号整数的处理中就可以看 到了。 4. 有符号整数的二进制形式 我们知道,有符号整数可以处理负数、零和正数。那么,问题来了,在二进制中如何 处理整数的符号呢? 答案是,有符号整数二进制的最高位就是它的符号位,如图2-10所示。 最高位是0,是一个正整数 最高位是1,是一个负整数 0001100110011001 图2-10 有符号整数的二进制形式 当有符号整数为零或正数时,符号位为0,也就是说,8位有符号整数的最大取值就是 这是符号位 01111111,这就是127。 负数的表示就有点意思了,除了符号位为1,其他位上的数据实际上是负数绝对值的 补码形式。 比如,-128的二进制就是10000000(这可不是-0)。我们先看128的二进制形式,即 10000000,请注意,此时,我们没有考虑符号问题,然后,按位取反就是01111111,再加1 得到补码10000000(和原数是一样的)。 不过,我们现在处理的是8位有符号整数,所以,只有后7位才是真正的数据,所以, 128补码中的最高位1就直接扔掉了(在计算机中就是这么绝),然后换成另外一个1(其 含义是负数),这样,-128的二进制就是10000000。我们可以看到,虽然看上去差不多, 但其实际意义是完全不同的。 好的,大家也看到了,-128是8位有符号整数的最小取值,它有点太特殊了,我们换 个数试试吧。 比如-25,先看25的二进制数,我们前面已经算过了,就是00011001,它的补码呢? 我们也算过了,就是11100111。别忘了有符号整数的最高位是符号位,8位有符号整数-25 的二进制就是11100111。 接下来,我们再算一个好玩的,就是11100111的后七位是十进制的多少,算式如下: 26+25+22+21+20 = 64+32+4+2+1 = 103 103有什么特别的?它正好等于128-25,隐约中有没有想到点什么呢? 那么,补码有什么特殊用途呢?在计算机内部可以只进行加法操作,而不需要减法操 作(真是多一事不如少一事)。下面,我们来看看25-25在计算机中的二进制处理,我们 假设数据类型都是8位有符号整数。 首先,25-25可以处理为25+(-25),好的,25的二进制形式是00011001,-25的二进制 是11100111。然后,我们进行相加运算,如图2-11所示。 图2-11 有符号整数的加法运算 好的,我们正在处理的是8位有符号整数,所以,最终的结果只有00000000,也就是 整数0。 现在,大家应该了解了有符号整数和无符号整数的二进制表现形式,以及取值范围是 怎么确定的吧!如果你感兴趣,可以按二进制试着计算一下,加法虽不难,只是在加数有 些多时,注意不要加错了。 5. 位移运S 关于二进制,最后讨论一下位移运算,在Objective-C中,位移运算包括位左移运算 (<<运算符)和位右移运算(>>运算符),对于这两种位移运算,我们同样按有符号和无 符号整数分别讨论。 首先是无符号整数的位移操作,以8 位无符号整数64 为例,它的二进制表示为 01000000,那么它右移2位的操作如图2-12所示。 01000000 右移2位 0001000000 图2-12 无符号整数右移2位 我们可以看到,当一个无符号整数右移操作时,会将低位的数据直接舍弃,然后在 高位补零。那么,64右移2位后的二进制就是00010000,我们换算成十进制就是16。实际 上,我们可以看到,当一个整数(N)进行右移n位时,就是在进行N÷2n的计算。 反过来,当我们进行整数(N)的左移运算时,高位数据舍弃,在低位补0,实际上, 就是在进行N×2n的计算。但请注意,当我们进行整数的位移运算时,应考虑整数的取值 范围。如果左移超出了允许的最高位,也就是超出了整数的取值范围,同样会舍弃,这时 就会造成整数的溢出。 下面的代码演示了无符号整数的位移运算: NSLog(@"%ul", 64 >> 2); // 16 NSLog(@"%ul", 16 << 2); // 64 对于有符号整数的位移运算,会有些不同。如进行8位有符号整数-64的右移运算,首 先,-64的二进制表示为11000000 ,那么,它的右移两位计算操作就如图2-13所示。 11000000 右移2位无符号整数右移,高位补符号位~ 1 1 1 1 0 0 0 000 图2-13 有符号整数右移2位 我们可以看到,在对有符号整数进行移位操作时,只对数据位进行移动,而符号位保 持不变;如图中的右移操作,会在数据的高位补上与符号位相同的数据,这样,-64右移2 位后的二进制数就是11110000 ,也就是-16。我们可以看到,同样是在进行N÷2n的操作。 对于有符号整数的左移操作,同样是只移动数据位,符号位保持不变,然后,在低位 补0,这实际上也是在执行N×2n的操作。 下面的代码演示了有符号整数的位移运算: NSLog(@"%ul", -64 >> 2); // -16 NSLog(@"%ul", -16 << 2); // -64 2.6 浮点数 除了整数,代码中使用的另一种基本数据类型就是浮点型,它用于处理包含小数部分 的数据。在Objective-C中,常用的浮点数类型包括float和double,其中: . float称为单精度浮点数,在NSLog()函数中的格式化字符为“%f ”,我们还可以使 用特殊的格式显示浮点数,比如使用“%e”会以科学计数法显示浮点数。 . double称为双精度浮点数,其处理数据范围更大,在NSLog()函数中的格式化字符 为“%Lf ”。 此外,在Foundation框架(Core Graphics)中,定义了浮点数类型的别名,如CGFloat 类型在64位平台下定义为double类型的别名,而在32位环境下定义为float类型的别名。 开发过程中,可能经常需要在整数和浮点数类型之间进行转换,接下来,我们就以int 和double类型为例讨论整数与浮点数之间的转换问题。 首先,从整数类型到浮点数类型的转换是没有问题的,直接赋值就可以了,如下面 的代码。 int iNum = 100; double fNum = iNum; 代码中,整数实际上是隐式转换为浮点数,因为double类型比int类型的取值范围大, 所以,这种隐式的转换是没有问题的。 反过来,当我们需要将double类型转换为int类型时就要注意:小数部分会丢失,这一 点并不难理解,因为整数不能保存小数部分。如果下定决定这么做,可以使用强制转换明 确进行转换的决心,如下面的代码。 double fNum = 100.56; int iNum = (int)fNum; 代码中,我们在需要转换的数据前使用了一对圆括号,其中的数据类型就是转换操作 的目标类型,这种数据类型转换方式就称为强制转换。 浮点数进行算术运算时,可以使用加法、减法、乘法和除法运算符,而且,两个浮点 数运算的结果依然是浮点数。在这些运算中,如果混合有浮点数和整数,会自动将整数转 换为浮点数以后计算,计算结果同样为浮点数。对于取余运算,则会将浮点数自动转换为 整数后计算,也就是说,在Objective-C中,浮点数不能进行取余运算。 Objective-C和Sprite Kit游戏开发从入门到精通 下面的代码演示了浮点数的算术运算。 double x = 10.56 , y = 3.0; NSLog(@"x + y = %Lf", x + y); // 13.56NSLog(@"x - y = %Lf", x - y); // 7.56NSLog(@"x * y = %Lf", x * y); // 31.68NSLog(@"x / y = %Lf", x / y); // 3.52NSLog(@"x % y = %Lf", x % y); // 1 接下来的一个问题,我们应该如何处理丢失的小数部分? 默认情况下,浮点数在转换成整数时,会直接舍弃小数部分。不过,我们也可以自己 做一点点决定,可以参考下面几个函数处理浮点数。 .floorf()函数,舍去参数中的小数部分,只返回整数部分,请注意,此函数的参数 和返回结果都是float类型,如果需要整数类型,还是需要进行转换操作。如果操作 double类型,则使用floor()函数。 .ceilf()函数,舍去参数中的小数部分,整数部分加1,此函数的参数和返回值同样为 float类型;相应的操作double类型的函数是ceil()。 最后放松一下,我们算一算月球的腰围,即月球赤道的长度,如下面的代码。 double r = 1738.0; double circumference = r * 2 * M_PI; NSLog(@"月亮半径为%Lf km,其周长为%Lf km", r, circumference); 2.7 布尔类型 在图2-14中,我们可以看到舱门的两个状态:开启和关闭。在代码中,我们可以使用 一个变量来标识舱门的状态,那么,这个变量应该是什么类型呢? 开启关闭 图2-14 舱门开启与关闭 代码中,像这种“开/关”“是/否”“真/假”一类的数据类型,我们一般会使用布尔 (Boolean)类型,也称为逻辑型。 ■2.7.1 BOOL类型 在Objective-C 中,布尔类型的数据可以统一使用BOOL类型,根据不同的平台,它分 别定义为bool或signed char类型的别名。此外,BOOL类型的取值只有两个,即YES和NO。 下面的代码演示了BOOL类型的使用。 #import int main(int argc, const char *argv[]) { @autoreleasepool { BOOL doorIsOpen = NO; NSLog(@"%i", doorIsOpen); } return 0; } 执行代码,我们会看到显示的内容是0,而不是NO。这是由于YES和NO值是以宏的形 式来实现的,所以,我们无法直接显示YES或NO,而是显示它们对应的值,整数形式就是 1(YES)和0(NO)。 ■ 2.7.2 布尔运S 布尔运算(又称为逻辑运算),在Objective-C中,布尔运算符包括: . 逻辑与运算符&&。 . 逻辑或运算符||。 . 逻辑非运算符!。 此外,布尔运算的结果同样为布尔数据,在Objective-C中,也就是BOOL类型,只能 是YES或NO值。 逻辑与(AND)运算,使用两个运算数,只有两个运算数的值都是真(YES)时,运 算结果才为真(YES),否则运算结果为假(NO)。 NSLog(@"%i", YES && YES); // 1 NSLog(@"%i", YES && NO); // 0 NSLog(@"%i", NO && NO); // 0 逻辑或(OR)运算,使用两个运算数,当其中一个运算数的值为真(YES)时,运算 结果就为真(YES),否则运算结果为假(NO)。如下面的代码。 NSLog(@"%i", YES || YES); // 1 NSLog(@"%i", YES || NO); // 1 NSLog(@"%i", NO || NO); // 0 逻辑非(NOT)运算,也称为取反运算,只需要一个运算数,当运算数为真(YES) 时,取反运算结果为假(NO);当运算数为假(NO)时,取反运算结果为真(YES)。 如下面的代码。 Objective-C和Sprite Kit游戏开发从入门到精通 NSLog(@"%i", !YES); // 0NSLog(@"%i", !NO); // 1 第3章,我们可以看到在控制语句中如何将比较运算和逻辑运算组合使用,从而使用 复杂的逻辑条件。 2.8 字符 在Objective-C中,字符类型用来操作单个字符,使用char关键字定义,字符内容包含 在一对单引号中;在NSLog()函数中,可以使用“%c”格式化显示字符。如下面的代码。 #import int main(int argc, const char *argv[]) { @autoreleasepool { char chA = 'A'; NSLog(@"%c", chA); } return 0; } 代码会显示大写字母A。 关于char类型的操作。我们经常会将它转换为整数,而这个整数就是字符的ASCII编 码。如int ascA = (int)'A'; 。 char 类型的另一个常用功能就是组合为字符串,也就是我们所说的C风格字符串,其本 质上是一个以“\0”字符结束的char数组。数组的详细内容会在第6章介绍,我们先通过下 面的代码简单了解一下这种字符串的使用。 #import int main(int argc, const char *argv[]) { @autoreleasepool { char hello[] = {'H','e','l','l','o','\0'}; // char hello[] = "Hello\0"; NSLog(@"%s", hello); } return 0; } 此外,在Objective-C项目中,我们还是更建议使用NSString类或NSMutableString类来 处理字符串,在第7章会详细讨论。 无论是char类型或是字符串中,都会有一些特殊的字符,如单引号用于定义字符、双 引号用于定义字符串内容,还有一些不可见字符,如ASCII 编码为0的字符、换行符等。对 于这些字符,应该在字符或字符串中使用转义字符来定义,常用的包括: . \'表示单引号。 . \"表示双引号。 . \n表示换行符。 . \t表示制度符。 . \\表示\字符。 . \0表示ASCII代码为0的字符,在C风格字符串中,都以此字符作为结束符号。 图2-15为0~127编码的ASCII码表,大家可以参考使用。 图2-15 ASCII编码0到127 Objective-C和Sprite Kit游戏开发从入门到精通 2.9 指针 相信很多学习过C语言的读者会对指针操作记忆深刻,对于初学者,指针真的让人头疼。 不过,在我们的代码中,指针却又是必不可少的编程工具,我们还是先简单了解一下吧。 首先,我们从形式上看一下指针与普通变量有什么不同,如下面的代码。 int numInt = 99; int *ptInt = &numInt; 代码中,numInt变量被定义为int类型;接下来是*ptInt,它也是int类型的,只不过*符 号说明这是一个指针,而指针所指向的位置保存的数据是int类型的,即ptInt是一个int指针 类型的变量。 那么,ptInt指向的位置是哪儿呢?代码中的&numInt为我们提供了线索,其中的&符 号称寻址运算符,&numInt 表示获取numInt 变量在内存中的地址,而ptInt 指向的就是这个 地址。 这个地址在哪?大概就是内存中的某个位置吧,我们可以使用如下代码看一下。 NSLog(@"%Li", &numInt); 在代码中,我们几乎不会直接使用这个内存地址,而总是通过指针来操作地址中的数 据。下面的代码,会显示numInt变量的值,包括使用指针来访问它。 NSLog(@"%i", numInt); NSLog(@"%i", *ptInt); 在这里,我们再次使用了*运算符,它称为间接访问运算符,通过它可以获取指针所 指向的内存区域中的数据。 通过上面的操作,我们应该知道ptInt指针指向的就是numInt变量的位置,那么,我们 修改其中一个的数据会怎么样呢?下面的代码就演示了这些操作。 int numInt = 99; int *ptInt = &numInt; NSLog(@"%i\n", numInt); NSLog(@"%i\n", *ptInt); // numInt = 10; NSLog(@"%i\n", numInt); NSLog(@"%i\n", *ptInt); 我们可以看到,修改其中一个变量的值后,两个变量获取的值都会变化,这说明它们 真的是相同位置的数据。 此外,如果我们需要断开指针与内存位置的关系,可以将指针设置为空指针,可以将 指针变量设置为NULL值或nil值,如ptInt = NULL。不过应注意,玩失联在很多时候并不是 令人愉快的事情。 2.10 自定义函数 前面,已经使用了NSLog()函数来显示信息,接下来,我们看一看如何创建自己的函 数,而这些函数可以帮助我们更好地组织代码和实现软件功能。 函数是一种基本的代码封装与组织形式,我们知道,在一艘军舰上,只有各个岗位的 人员都能够各尽其责,军舰才能够正常运行,才会有战斗力。而函数对于整个程序来讲, 也是这样,必须能够正确、高效地完成本职工作才称得上是一个合格的函数。 main()函数 调用 调用 更多函数待命中~ fn1()函数 fn2()函数 调用 fn3()函数 图2-16 函数的调用 从图2-16中,我们可以看到,程序会从main()函数开始,然后会有大量的函数来支援 它,这就是函数的基本工作方式。目前为止,我们使用的示例项目都是这种工作方式。 此外,在Objective-C中,我们还可以使用另一种代码组织形式,这就是使用面向对象编程 (OOP),在第4章,我们会讨论相关内容。 在Objective-C中声明函数的格式如下。 <返回值类型> <函数名>(<参数列表>); 如果我们实现这个函数,就不使用分号,而是将函数的代码放在一对花括号{}之间。 如下面的代码。 #import void sayHello() { NSLog(@"Hello!"); } int main(int argc, const char *argv[]) { @autoreleasepool { sayHello(); } return 0; } 代码中,我们定义了一个无返回值、无参数的sayHello()函数,然后,在main() 函数中 调用它;执行代码会显示“Hello!”。 接下来,我们讨论一下定义函数时需要注意的几个问题。 ■ 2.10.1 返回值类型 返回值类型,也就是函数运行结果的数据类型;在函数中,使用return 语句返回这个数 据;如果函数不需要返回数据,就像上面的代码一样,可以将返回值类型定义为void。 返回值的类型可以是前面介绍的基本数据类型,也可以是对象,比如,函数需要返回 一个字符串对象(NSString类型),还应该使用*运算符。如下面的代码。 NSString* getName() { return @"你猜!^_^"; } 关于对象的相关内容,我们会在第4章介绍,大家不用着急。 ■ 2.10.2 函数名 函数的名称,本书中的自定义函数名会使用首字母小写,其他单词首字母大写的形式。 函数名应该反映出函数的功能,在Objective-C 中,函数命名会有一些约定,遵循这些 约定,可以更有效地使用这些函数,在后续的应用中,我们就可以看到这一点。 ■ 2.10.3 参数 参数列表,在定义函数时,可以没有参数,像上面代码中定义的sayHello() 函数,也可 以有一个或多个参数,每一个参数都应包含两个基本部分,即参数类型和参数变量,如下 面的代码。 #import int addInt(int x, int y) { return x + y; } int main(int argc, const char *argv[]) { @autoreleasepool { int sum = addInt(6, 4); NSLog(@"sum = %i", sum); } return 0; } 在我们定义的addInt()函数中,定义了两个参数,分别是x和y,它们都是int类型的,而 addInt()函数的功能就是返回这两个参数的和,其返回值类型同样是int类型。 ■ 2.10.4 参数指针 下面,我们创建一个函数,尝试交换两个int变量的值,这个看上去似乎并不难,马上 动手,如下面的代码。 void swapInt(int x, int y) { int temp = x; x = y; y = temp; } 然后,使用如下代码来调用这个函数。 int main(int argc, const char *argv[]) { @autoreleasepool { int num1 = 10; int num2 = 99; NSLog(@"num1 = %i , num2 = %i", num1, num2); swapInt(num1, num2); NSLog(@"num1 = %i , num2 = %i", num1, num2); } return 0; } 问题来了!上面的代码并不会交换num1和num2的值。 这是为什么呢? 原因是,当我们将num1和num2变量传递给swapInt()函数时,参数复制了它们的数据。 在函数内部,只是在交换数据副本的值,而不是真正地交换num1和num2变量的值。 那么,如何才能真正地修改参数的值呢?答案就是,在参数中使用指针,下面就是修 改后的swapInt()函数。 void swapInt(int *x, int *y) { Objective-C和Sprite Kit游戏开发从入门到精通 int temp = *x; *x = *y; *y = temp; } 在调用swapInt()函数时,我们同样需要做一些修改,如下面的代码。 swapInt(&num1, &num2); 这样,我们就可以通过swapInt()函数顺利地交换两个int变量的值了。 2.11 static关键字 在Objective-C中,可以使用static关键字定义静态变量,这种变量可以在代码执行过程 中只初始化一次,然后就会一直保存最新的数据。 如下面的代码,我们定义了fn()函数,其中包含一个静态变量counter,当我们每次调 用这个函数时,counter就会加1,然后显示调用的次数。 void fn() { static int counter = 0; counter++; NSLog(@"第 %i 次调用fn()函数", counter); } int main(int argc, const char *argv[]) { @autoreleasepool { for(int i = 0; i < 30; i++) { fn(); } } return 0; } 代码会显示共调用了30次fn()函数,其中的for语句结构属于循环语句结构的一种,第3 章会讨论代码流程控制的相关内容。 在fn()函数中,变量counter定义为静态的,它在程序运行过程中只会进行一次初始 化,也就是在定义时赋值为0。然后,当我们每次调用fn()函数时,counter变量并不会重新 定义,而是保留最新的值。 此外,我们还可以在const定义的常量前使用static关键字,如下面的代码。 static const NSUInteger flag0 = 0x1 << 0; static const NSUInteger flag1 = 0x1 << 1; static const NSUInteger flag2 = 0x1 << 2; 2.12 块(block) 块的使用是Objective-C中比较有特点的功能之一,它为我们提供了一种更加灵活的代 码结构。 block 函数~,半成品~ 加什么block,就干什么活 block1block2block3… 图2-17 块的应用 下面的代码声明了一个名为factory的块,它包括两个int类型的参数,以及int类型的 返回值,看上去和函数非常相似(实际上,在Swift中,就是使用函数类型取代了块的 功能)。 int(^factory)(int num1, int num2) 实际应用中,我们经常会将一个块的定义赋值到一个块变量,如下面的代码,我们分 别定义了两个块,并赋值给两个块变量(factory1和factory2)。 // 加法工厂块 int(^factory1)(int, int) = ^int (int num1, int num2) { return num1 + num2; }; // 减法工厂块 int(^factory2)(int, int) = ^int (int num1, int num2) { return num1 - num2; }; 我们可以看到,块变量的格式初看起来可能有些奇怪,它同时包含了块变量名、返回 值类型和参数类型。 在赋值运算符后面则是块的具体实现,此时不使用块的名称,但返回值和参数类型必 须与块变量的定义相同,并且需要指定参数名称。 接下来,我们看一看如何使用这两个块,如下面的代码。 #import int numberFactory(int(^factory)(int,int), int num1, int num2) { return factory(num1, num2); } int main(int argc , const char * argv[]) { @autoreleasepool { // 加法工厂 int(^factory1)(int, int) = ^int (int num1, int num2) { return num1 + num2; }; // 减法工厂 int(^factory2)(int, int) = ^int (int num1, int num2) { return num1 - num2; }; // 调用工厂块 int num1 = 10; int num2 = 6; NSLog(@"%i\n", numberFactory(factory1, num1, num2)); //16 NSLog(@"%i\n", numberFactory(factory2, num1, num2)); //4 } return 0; } 代码中,我们首先创建了函数numberFactory() ,请注意它的第一个参数,定义为一个 块类型,其格式为int类型的返回值,以及两个int类型的参数。第二和第三个参数则代入两 个int类型的数值。 再看numberFactory() 函数中的实现代码,我们可以看到,这个函数的功能是依靠块的 执行来处理两个int数据(第二个参数和第三个参数)。 在main() 函数中,调用了两次numberFactory() 函数,不同之处在于它们的参数指定为 不同的工厂块,这样也就实现了对两个数值的不同的操作,也就是分别执行了加法运算和 减法运算。 这就是块的主要功能,我们可以使用相同的定义(块类型)实现不同的功能,也就是 加什么块干什么活的含义了! 实际上,如果块只需要使用一次,我们并不需要单独定义,而是可以在使用的地方直 接定义,如下面的代码就是通过numberFactory()函数进行乘法运算的。 int result = numberFactory( ^int (int num1, int num2) { return num1 * num2; }, num1, num2); NSLog(@"%i", result); //60 在这里,只是显示了块的简单使用,在Foundation、Sprite Kit等框架中的开发资源 中,可以看到很多关于块的使用,我们可以在实际应用中逐渐理解块的意义,并能够正确 应用。 此外,如果需要在块中修改函数或方法内定义的变量,则必须在变量定义时使用__ block标识,先看下面的代码。 int main(int argc, const char * argv[]) { @autoreleasepool { int num = 10; void (^addOne)(void) = ^void(void){ num++; // 出错 }; addOne(); NSLog(@"num = %i", num); } return 0; } 代码执行会出错,我们应该将变量num的定义修改为: __block int num = 10; 这样,代码就会正确执行,并显示“num = 11”。 此外,如果块没有返回值和参数,还可以更简单地定义,如前面的addOne块就可以 写成: void (^addOne)() = ^{ num++; }; 关于块,最后需要说明的是,块应该定义在函数或方法内部。同时,块也可以作为参 数来使用,这实际上就是实现访问者模式的重要方法,其基本原理是,在定义函数或方法 时,并不能确定其中的一部分功能是如何实现的,但在调用这个函数或方法时,可能通过 块参数传递具体的实现代码,我们会在后续的实践中看到在参数中传递块的实际应用。 2.13 枚举 接下来,我们思考一个简单点的问题,对于数值数据可以使用整数或浮点数,对于 “开/关”类数据可以使用布尔型,对于文本信息可以使用字符或字符串,那么,对于性别 这样的数据,使用什么数据类型比较合适呢? 想正确地处理一类数据,就必须清楚这类数据的值可能有哪些?就说性别吧,大家都 知道,包括男和女。不过,还有一种情况,如果有人想对自己的性别保密呢?好吧,那性 别数据就有可能有三种基本的数值了,包括:保密、男和女。 那么,我们使用什么类型保存性别数据呢?理论上讲会有很多方案,比如,使用宏, 如下面的代码。 #define SEX_UNKNOW 0 #define SEX_MALE 1 #define SEX_FEMALE 2 或者定义常量,如下面的代码。 const int SexUnknow = 0; const int SexMale = 1; const int SexFemale = 2; 不过,如果能够定义一个性别类型是不是会更加直观呢?好的,我们马上使用enum关 键字定义枚举类型,如下面的代码就定义了一个性别枚举类型。 enum ESex {Unknow, Male, Female}; 然后,我们使用如下代码来使用这个类型。 enum ESex tomSex = Male; 我们可以看到,在定义性别类型的变量时,同样使用了enum关键字。不过,为了更方 便地使用枚举类型,我们也可以使用typedef关键字定义它的别名。此时,我们可以单独定 义,也可以在定义ESex枚举类型时直接定义。 typedef enum ESex Sex; 或: typedef enum ESex {Unknow, Male, Female} Sex; 然后,我可以直接使用Sex类型来定义变量。 Sex tomSex = Male; NSLog(@"%i", tomSex); 当我们需要显示枚举变量(如tomSex)的值时,只能使用整数来显示,其中,枚举类 型中定义的第一个成员值为0,第二个成员值为1,第三个成员值为2,以此类推。当然, 如果需要自己指定枚举成员的值也是可以的,如下面的代码。 typedef enum ESex {Unknow = 0, Male = 1, Female = 2} Sex; 此外,枚举成员的值也可以不从0开始,但我们并不建议这样使用,除非项目中的数 据真的不允许为0。如下面定义的太空门枚举类型。 typedef enum ESpaceGate { EarthGate = 300, MoonGate = 301, MarsGate = 400, PlanetIGate = 9000 } SpaceGate; 最后,在使用枚举类型时应注意一些问题: . 总是保留一个未知(Unknow)的枚举项是一种安全措施,这样可以保证在任何时 候都能够处理意外数据,比如,突然出现了一个可以到达异次元空间的太空门, 或者真的出现了雌雄同体。 . 在Objective-C中的枚举值是整数,如果需要保存到文件或数据库中,应使用整数类型。 2.14 结构 大家知道,在实际应用中有很多数据是相关联的,包括日期中的年、月、日,士兵的 各种属性,如姓名、编号、年龄、性别等。对于这些相关联的数据,我们可使用另一种自 定义的类型,即结构类型。 在Objective-C中,使用struct关键字定义结构类型。结构可以将一组相关的数据组合成一 个整体,方便数据管理和应用,如下面的代码,我们将使用结构定义一个日期结构类型。 struct SDate { int year; int month; int day; }; typedef struct SDate Date; 代码的最后一行,我们同样使用typedef定义了SDate结构类型的别名(Date)。下面, 我们就可以使用Date来定义日期类型的变量了。 Date today; 定义结构类型变量的同时,我们还可以按结构成员定义的顺序给它们赋值,如下面的 代码。 Date today = {2016, 6, 27}; 访问结构成员时,我们使用圆点运算符,如下面的格式。 <结构变量>.<成员名称> 在下面的代码中,我们修改today变量的内容,并显示。 Objective-C和Sprite Kit游戏开发从入门到精通 today.year = 2015; today.month = 10; today.day = 28; NSLog(@"今天是%i年%i月%i日", today.year, today.month, today.day); 对于比较简单的关联数据,我们可以使用结构类型定义,但对于结构比较复杂的数据 类型,可能要同时管理数据和动作时,如太空船、机甲步兵、生化战士等,我们应该使用 “类”类型进行管理和操作,在第4章会讨论相关的内容。 2.15 随机数 虽然计算机从不干随机的事,但有时候,比如,在开发游戏的时候,随机数又是一项 非常重要的工具。 在Objective-C中,我们可以使用arc4random()函数快速生成一个随机的正整数。那么, 我们如何控制随机数的范围呢?如果这个范围是连续的就好了,我们主要讨论两种情况。 第一种情况,如果我们需要从0到n-1的随机正整数,可以直接使用如下的代码。 int rndNum = arc4random() % n; 代码中,我们得到arc4random()函数生成的随机数除以n的余数,这样,其结果就只是 0到n-1之间的整数了。此外,这种情况也可以进行一些变形,比如,我们需要0到n的随机 数,可以使用如下代码: int rndNum = arc4random() % (n + 1); 如果需要1到n之间的随机数,可以使用如下代码。 int rndNum = arc4random() % n + 1; 第二种情况,如果我们需要min到max之间的随机整数,可以使用以下代码。 int rndNum = arc4random() % (max - min + 1) + min; 我们可以看到,这是一个通用的获取一个连续范围随机整数的算法。实际上,如果 min为1或0时,我们也就可以推算出前面的代码了。 2.16 预处理 前面的内容中,我们已经多次看到预处理的使用,如#define、#ifndef 、#endif 等指令, 它们以行为单位,而且结尾没有分号,这是预处理和代码最大的区别之一。 下面,我们将会对常用的预处理指令做一些总结,主要包括: . #define和#undef指令。 . 条件编译指令。 . #import指令。 ■ 2.16.1 #define和#undef指令 #define指令的功能就是定义宏,我们可以定义很简单的宏,简单到只有一个标识名 称,如下面的代码。 #define DEBUG 这样,就可以根据是否定义了这个宏来判断项目是在调试中,还是正式发布了,我们 可以使用#ifdef指令判断一个宏是否已定义,而使用#ifndef指令判断一个宏是否没有定义。 使用#define指令定义的宏,可以使用#undef指令撤消,如下面的代码。 #undef DEBUG #define指令的另一个功能就是模拟定义常量,如下面的代码。 #define DT_IPHONE4 4 #define DT_IPHONE5 5 #define DT_IPAD 1000 此外,我们还可以利用#define定义一些复杂的宏,如下面的代码。 #define IsEvenNumber(n) (n % 2 == 0) 其中,我们定义了IsEvenNumber宏,其功能是判断一个数是否为偶数,它使用起来和 函数类似,如下面的代码。 int num = 100; if (IsEnenNumber(num)) { NSLog(@"%i是偶数", num); }else{ NSLog(@"%i不是偶数", num); } 此外,如果一个#define指令的定义过长,我们可以将它们分行,但需要使用“\”符号 放在前一行的结尾处,用以说明下一行与本行是同一指令。 ■ 2.16.2 条件编译指令 通过定义宏,还可以指定在不同的条件下编译不同的代码,此时,我们可以使用 #define定义一些与系统平台相关的标识,然后根据这些标识将软件编译成不同的版本。与 条件编译相关的指令包括: . #ifdef指令,判断一个宏标识是否已定义。 . #ifndef指令,判断一个宏标识是否没有定义。 . #endif指令,与#ifdef或#ifndef指令组成一个代码块。 . #elif和#else指令,一般与#ifdef和#endif指令组合使用,以判断不同条件下的代码执 行情况。 如下面的代码,将根据不同的设备类型来编译相应的代码。 #define IPhone #ifdef IPhone // iPhone设备代码 #elif IPad // iPad设备代码 #else // 其他类型设备代码 #endif 此外,在头文件中,如前面我们定义的CApp.h文件,其中使用了一个模式化的预处理 代码,我们再来看一看。 #ifndef __CApp_h__ #define __CApp_h__ #endif 这三个预处理指令的功能是在代码文件中防止多次包含头文件,以提高代码的编译 效率。 ■ 2.16.3 #import指令 #import指令的功能就是引用头文件,我们已经多次使用,再次说明一下: . 使用一对尖括号<>引用的头文件是系统资源,如Foundation.h文件。 . 使用一对双引号""引用的头文件是“外部”资源,可能是第三方框架或自定义的头 文件。 Objective-C和Sprite Kit游戏开发从入门到精通 请注意等于运算符,它是两个等号,这和一个等号的赋值运算符是不同的,这是很多 初学者最容易出错的地方之一。请注意分析以下几行代码的执行结果。 int intNum = 1; NSLog(@"%i", intNum == 1); //1, 变量intNum等于1 NSLog(@"%i", intNum == 0); //0, 变量intNum不等于1 NSLog(@"%i", intNum = 1); //1, 1赋值到intNum变量,表达式的值1 NSLog(@"%i", intNum = 0); //0, 表达式的值是intNum的值0 3.2 条件语句 判断代码执行的逻辑时,条件的设定是非常重要的,比如,不是友机并不意味着要摧 毁,但如果是敌机,则摧毁是必需的,所以,我们可以快速地判断它是不是敌机,而不是 判断是否为友机。 在Objective-C代码中,对于条件的判断,可以有两种方法: .使用if语句结构。 .使用?:运算符。 ■3.2.1 if语句 if语句的基本应用格式如下。 if (<条件>) { <语句块> } 当<条件>成立(YES ),就执行<语句块>,然后执行}后面的代码。如果<条件>不成 立(NO),就直接执行}后面的代码。 如下面的代码,如果是敌人,就开火摧毁。 BOOL isEnemy = YES; if (isEnemy) { NSLog(@"开火并摧毁"); } 如果不是敌机怎么办呢?我们可以添加else语句,如下面的代码。 BOOL isEnemy = NO; if (isEnemy) { NSLog(@"开火并摧毁"); } else { NSLog(@"不是敌机,允许登舰"); } 更复杂的情况下,会有多个条件,可以在结构中添加else if语句,如下面的代码。 if (<条件1>) { <语句块1> }else if (<条件2>) { <语句块2> }else if (<条件n>) { <语句块n> }else { <语句块n+1> } 在这种if语句结构中,else if可以有多个,也可以没有,每一个条件都对应了一个需要 执行的语句块。else语句只能放在所有条件的后面,而且只能有一个,当所有条件都不满 足时,会执行<语句块n+1>。 此外,对于条件的判断还可以嵌套使用,如下面的代码。 BOOL isEnemy = NO; NSInteger tarmacNumber = 10; if (isEnemy) { NSLog(@"开火并摧毁"); } else { if (tarmacNumber > 0){ NSLog(@"请在 %li 号专用停机坪降落", tarmacNumber); } else { NSLog(@"请在公共停机坪降落"); } } 下面的代码,演示了如何使用复合条件判断一个年份是否为闰年。 int year = 2016; if ((year % 100 != 0 && year % 4 == 0) || year % 400 == 0) { NSLog(@"%i年是闰年", year); } else { NSLog(@"%i年不是闰年", year); } 如果大家在工作中需要更多关于日期的操作,第9章会讨论相关内容。 ■ 3.2.2 ?:运S符 对于简单的if-else语句结构,我们可以使用?:运算符代替,它是唯一的一种三元运算 符,需要三个表达式参加运算,其应用格式如下。 <表达式1> ? <表达式2> : <表达式3> 其中,<表达式1>为逻辑表达式,其结果应该是BOOL类型,当其值为YES时,整个表 Objective-C和Sprite Kit游戏开发从入门到精通 达式的运算结果就是<表达式2>的值。如果<表达式1>的值为NO ,整个表达式的运算结果 就是<表达式3>的值。如下面的代码。 BOOL isEnemy = NO; isEnemy ? NSLog(@"开火并摧毁") : NSLog(@"不是敌机,允许登舰"); 请注意,如果表达式的代码稍微有一点点复杂,可以使用小括号()将三个表达式分别 包装一下。 3.3 选择语句 在游戏中,对于基本的方向控制,一般需要处理四个方向的操作,在类似这种情况 下,即一个条件、多种可能时,可以使用switch语句结构,其应用格式如下。 switch(<表达式>) { case <值1>: { <语句块1> }break; case <值2>: { <语句块2> }break; case <值n>: { <语句块n> }break; default: { <语句块n+1> }break; } 在switch语句结构中,我们可以看到,只有一个确定执行条件的<表达式>,而每一个 case 语句对应一个<表达式>的值。在这里,应注意每一个case 所对应的语句块结束时,都 应该有一个break 语句,其原因是,case 语句具有向下贯穿的特性,如果没有break 语句中断 当前case语句块,则会一直向下执行,直到遇到中断语句(如break、return等语句)或者是 switch语句结构全部执行完成。 此外,每个case语句或default语句后的代码也可以不使用花括号,这主要取决于编 程习惯,而break 语句也可以放在每个case 后{} 的内部或在}后面,这也只是一个编程习惯 问题。 下面的代码,我们就来模拟驾驶指令。 typedef enum EDirection { Unknow, Up, Right, Down, Left } Direction; // Direction d = Up; // switch (d) { case Up: NSLog(@"前进"); break; case Right: NSLog(@"右转"); break; case Down: NSLog(@"倒车"); break; case Left: NSLog(@"左转"); break; default: NSLog(@"停止"); break; } 我们首先定义了一个方向枚举类型,然后通过switch语句结构判断操作方向,分别显 示操作指令。大家可以修改变量d的值,并观察代码执行的结果。 下面的代码继续演示一个日期相关的处理,我们利用case的贯穿特性来返回某年某月 中的天数。 int year = 2006; int month = 2; int days = 0; switch(month) { case 1: case 3: case 5: case 7: case 8: case 10: case 12: days = 31; break; case 4: case 6: case 9: case 11: days = 30; break; Objective-C和Sprite Kit游戏开发从入门到精通 case 2: days = ((year%100!=0 && year%4==0) || year%400==0) ? 29 : 28; break; default: days = 0; break; } NSLog(@"%i年%i月有%i天", year, month, days); 请注意,在switch 语句结构中总是使用default 语句块,可以保证<表达式>的结果在任 何数据时都会有相应的处理。 3.4 循环语句 计算机最大的特点是什么?就是可以根据一定的条件自动完成工作,而且快速、高 效、不会犯错(除非代码有问题)。 循环语句就是能够根据一定的条件重复执行一系列任务的语句结构。在Objective-C 中,常用的循环语句包括for、while和do-while。此外,在循环语句中,还可以使用break和 continue等语句来控制循环语句的执行,下面详细讨论。 ■3.4.1 for语句结构 for语句的应用格式如下: for (<表达式1>; <表达式2>; <表达式3>) { <语句块> } 其中: .<表达式1>用于定义循环控制变量。 .<表达式2>指定循环执行的条件,条件成立时(YES)执行循环操作,条件不成立 时(NO)终止循环。 .<表达式3>指定循环控制变量每次循环后的变化。 .<语句块>定义每次循环所执行的代码。 如下面的代码,一号雷达将按顺序扫描1到10号区域。 for (int i=1; i <= 10; i++) { NSLog(@"一号雷达扫描%i号区域", i); } 下面的代码是一个简单的数学问题,我们将计算1到100的和。 int sum = 0; for (int i=1; i <= 100; i++) { sum += i; } NSLog(@"1到100的和是%i", sum); 如果是计算1到100中偶数的和,可以修改for语句中的控制语句,如下面的代码。 for (int i=2; i <= 100; i+=2) ■ 3.4.2 while语句结构 while语句的应用格式如下。 while (<条件>) { <语句块> } 其中,当<条件>为真(YES)时,就会执行<语句块>;当<条件>为假(NO)时终止 循环结构。 下面的例子,一号雷达会不停地工作,大家不要急着在Xcode中运行,因为这个代码 不会停下来,直到程序挂掉。这种循环结构称为无限循环或死循环。 while (YES) { for (int i=1; i <= 10; i++) { NSLog(@"一号雷达扫描%i号区域", i); } } 雷达的工作是比较特殊的,需要不停地扫描指定区域,但在while语句结构中,则应该 有一个合理的条件,以便循环能够适时结束。如下面的代码,我们使用while语句完成1到 100的累加工作。 int i = 1; int sum = 0; while (i <= 100) { sum += i; i++; } NSLog(@"1到100的和是%i", sum); ■ 3.4.3 do-while语句结构 do-while语句与while语句的使用比较相似,所不同的是,do-while语句结构会在每次循 环结束后进行条件的判断,其基本应用格式如下。 do { <语句块> }while (<条件>); 使用do-while语句结构时应注意,<语句块>最少会执行一次,而这也是可能出问题的 地方,如果在<语句块>中存在不满足条件的代码,就有可能产生错误,所以,如果有可 能,我们更建议使用while语句结构,毕竟随便就让程序挂掉,并不是一个好的选择。 ■ 3.4.4 break语句 循环语句的确可以简化很多工作,但是,我们也需要适时地终止循环操作;在 Objective-C 中,break 语句就可以在循环结构中紧急刹车,从而终止整个循环的执行。如下 面的代码,如果一号雷达发现可疑目标,就会显示信息,并锁定当前区域。 int lockedZone = 0; for (int i=1; i <= 10; i++) { // 可疑情况的出现很随机 if (arc4random() % 2 == 1) { NSLog(@"警告:一号雷达在%i号区域发现可疑目标", i) ; lockedZone = i; break; } } 代码中,arc4random()函数的功能是产生一个随机整数,我们取这个随机数除以2的余 数,余数为1时发现目标,并终止for循环的执行。 在while或do-while语句结构中,break语句同样可以终止循环。此外,别忘了break语句 在switch语句结构中的功能也是终止。 ■ 3.4.5 continue语句 continue语句的功能是中止当前循环,并开始下一次循环(如果条件满足)。前面, 一号雷达已经锁定lockedZone变量所指的区域,此时,二号雷达将代替一号雷达继续搜索 其他区域,要知道,最先出现的目标很可能只是诱饵。 下面的代码,我们接着前面的示例写。 for (int i=1; i <= 10; i++) { if (lockedZone == i) continue; NSLog(@"二号雷达正在搜索%i号区域", i); } 执行此代码,我们可以看到,二号雷达在到达lockedZone 区域时不会执行扫描工作,因 为一号雷达已经锁定此区域,此时,使用continue语句终止此次循环,并继续下一循环。 3.5 goto语句与标签 如果大家学习过C语句,也许知道在goto语句的争议是很大的。不过,在一些特殊情 况下,goto语句和标签的使用却是效率最高的,比如,在需要跳出嵌套的循环语句结构 时。如下面的代码。 int lockedZone = 0; while (YES) { for (int i=1; i <= 10; i++) { // 可疑情况的出现很随机 if (arc4random() % 2 == 1) { NSLog(@"警告:一号雷达在%i号区域发现可疑目标", i); lockedZone = i; goto RadarOneStop; } } } RadarOneStop: NSLog(@"一号雷达锁定目标,停止扫描"); 在这个代码中,当一号雷达发现了可疑目标以后,则锁定此区域,并停止工作,当 然,在前面的示例中,我们也可以看到,实际会有二号雷达来继续扫描其他区域。 在Objective-C中,我们通过“<标签名称>:”的格式来定义一个标签,然后,可以使用 “goto <标签名称>;”语句跳转到此标签的位置。 3.6 异S处理 程序执行出错了!怎么办?怎么办?怎么办?重要的事要说三遍! 在Objective-C中,异常的捕捉机制和很多编程语言是不一样的,它是通过指令,而不 通过语句结构来实现的。基本应用格式如下。 @try { // 可能出现异常的代码 } @catch (NSException *exception) { // 处理捕捉到的异常 } @finally { // 完成清理工作,无论是否有异常出现都会执行 } 如果在@catch块中无法或不需要处理异常,还可以使用@throw指令向代码的上一级结 构抛出异常,如果是在main()函数中,就是向系统抛出异常,当然,如果真是这样,程序 也就挂掉了。 实际上,大多数编程语言在处理异常时都会很明显的性能问题,特别是在代码执行过 程中真的出现问题时,Objective-C也是这样的。那么,在开发中,我们应该怎么办呢?最 好的办法当然是提高代码质量,这样就可以将大多数可能的问题消灭在开发阶段。 软件的性能、正确性、稳定性,包括对于异常情况的处理策略,需要开发人员根据项 目的特点综合考虑和权衡。 本章讨论了在Obejctive-C 中逻辑运算和流程控制语句结构,通过它们的使用,可以灵 活地控制代码执行的逻辑,创建出功能更加强大的代码。 第4章,我们将讨论面向对象编程在Objective-C中的具体实现和应用。 接下来,让机器人robot5 走两步看看,对于动作处理,到现在为止只能使用函数,如 下面的代码。 robotMove(robot5, 2); 发现敌人要攻击,如下面的代码。 robotAttack(robot5, target); 现在,大家还有兴趣继续玩下去吗?不如,我们换一种方式玩吧。首先,还是假设有 Robot类型和一个robot5变量,如果能使用下面的代码来完成移动、攻击等操作是不是更直 观呢? Robot *robot5; // 为什么要加*,这可不是为了装,稍后道来 robot5.name = @"No.5"; // 机器人5号,5号活了 [robot5 move:2]; // 走两步 [robot5 attack:target]; // 射击目标 这就是面向对象编程的基本形式,我们将一个事物的特性(属性)和动作(方法)进 行封装,从而能够更加直观地编写代码,同时,也会使代码更具有逻辑性、可读性和可维 护性。 接下来,我们就先来了解一些面向对象编程的基本概念,稍后讨论如何在Objective-C 项目中实现面向对象编程。 1. 类与对象 在面向对象编程概念中,可以将“类(class)”看作复杂的数据类型。 有了数据类型,我们就可以创建这个类型的变量,而类这个类型的变量就是对象 (object),或者称为类的实例(instance)。当然,我们创建对象的过程并不会像操作基 本数据类型变量那么简单,在这一过程中,往往需要更多的内存分配、数据初始化、资源 调用等操作,这些操作都会在创建对象过程中完成,而这个过程,我们又可以称为类的实 例化过程。 假如我们定义了机器人类型CRobot类,我们可以创建一个具体的机器人对象,如“5 号”机器人robot5,那么,robot5就是CRobot类的一个实例,或者说是CRobot类型的对象。 请注意,在类这个类型前加一个大写的字母C是我的编程习惯,而你可以在一定的范 围内根据自己的习惯,或者是项目约定来创建类的名称。 2. 属性与方S(任务) 对象的特性,如速度、颜色、名称、位置等,可以通过属性来表示,如robot5. name=@“No.5”,其中,name属性就表示机器人的名称。 对象的动作,如攻击、移动等,我们就可以定义为方法来实现,如[robot5 attack]。请 注意,方法(method)是面向对象编程中的术语,而Apple官方文档称为任务(task),本 书中,我们会使用习惯性的面向对象编程术语,即方法(method)。在Objective-C中,它 的定义方式和函数还是很大区别的,稍后我们就会看到。 3. 继承 前面我们提到过,一个对象的创建需要比较复杂的内存分配等初始化操作,如果每一 个类的全部操作都需要我们编写代码来实现,那可不是闹着玩的,特别是需要动手管理内 存这活儿,真的是要非常小心。 还好,我们并不需要一切从零开始。 在Foundation框架中定义了NSObject类,而我们定义的类,如果没有特殊要求,都可 以继承NSObject类型。这样,在新的类中就可以使用NSObject类中定义的成员,如属性和 方法,从而简化了类的创建工作。 从另一方面来看,在创建复杂的应用程序时,我们可以利用已经存在的类,包括 Foundation资源、其他Apple或第三方提供的资源,当然还包括自定义资源,这可以帮助我 们进一步提高开发效率。 比如,我们创建了一个汽车类CAuto,一些衍生车型就可以继承它的基本特点和功 能,如CCar、CSuv等。此时,CAuto类就是CCar和CSuv的基类(又称为父类或超类),而 CCar和CSuv就是CAuto的子类。 在Objective-C项目中,NSObject类是唯一没有基类的类,而其他的类都必须指定一个 基类。 了解面向对象编程的一些基本概念以后,我们就来看看如何在Objective-C中实现它们。 4.2 类与对象 现在,我就要创建真正的机器人类型了。 与大多面向对象编程语言的区别在于,在Objective-C中并不使用class关键字来定义类, 而是使用两个部分来定义类,包括接口(interface)部分和实现(implementation)部分。 理论上讲,我们可以将类的接口部分和实现部分都放在一个文件中,但我们一般会将 接口部分定义在头文件(.h)中,而将实现部分定义在相应的模块文件(.m)中。 ■ 4.2.1 接口部分 接下来的测试工作,我们将继续使用SimpleOC项目;在Xcode中,通过菜单 “File”→“New”→“File”,选择OS X下的Source,然后选择Cocoa Class,接下来,需 要我们指定类的名称(Class)及其基类(Subclass of),如图4-1所示。 图4-1 创建类 单击“Next ”按钮,我们还需要指定代码保存的路径,然后,Xcode 会自动为创建以 类的名称命名的头文件和代码文件。首先,我们看一看头文件中的接口部分,如下面的代 码(CRobot.h文件)。 #ifndef __CRobot_h__ #define __CRobot_h__ #import @interface CRobot : NSObject -(void) move; @end #endif 我们可以看到,类的接口部分定义在@interface 和@end指令之间,而类的名称定义在 @interface指令后面,紧跟其后的冒号(:)含义为继承,本例中,我们定义的CRobot类继 承自NSObject类。 在CRobot类中,我们声明了一个名为move的实例方法,它没有返回值(使用void关键 字声明),稍后,会看到更多关于方法的内容。接下来,我们先来看一看如何在实现部分 定义这个方法。 ■ 4.2.2 实现部分 下面的代码,我们将在CRobot.m文件中看到CRobot类成员的具体实现。 #import "CRobot.h" @implementation CRobot -(void) move { NSLog(@"机器人移动"); } @end 在这里,我们可以看到,类的实现部分定义在@implementation和@end指令之间。在 @implementation指令后需要类的名称,但不需要再次指定继承哪个类。 在这里,可以看到move方法的实现代码,它的功能很简单,只是显示一条信息。 请注意,在类中的方法并没有使用小括号()来包含参数。实际上,其参数的定义方式 与函数有所不同,稍后,我们会看到相关内容。 ■ 4.2.3 创建对象(实例化) 下面的代码,我们演示了如何在main()函数中使用CRobot类。 #import #import "CRobot.h" int main(int argc, const char *argv[]) { @autoreleasepool { CRobot *robot5 = [[CRobot alloc] init]; [robot5 move]; } return 0; } 代码中,我们需要使用#import指令引用CRobot.h文件。然后,在main()函数中,我们 使用如下代码声明了一个CRobot类的实例,即robot5对象。 CRobot *robot5 大家可以看到,对象是被定义为指针类型的,这就是加*的意义。 接下来,我们注意给robot5对象赋值的代码,它实际上完成了对象的实例化过程,这 个过程共调用了两个方法,即alloc方法和init方法,也许大家会问,我们并没有定义这两个 方法,它们是从哪里来的呢?答案就是,它们是从继承NSObject类而来的,也就是说,这 两个方法是定义在NSObject类中的,由于CRobot是NSObject类的子类,所以,我们可以在 CRobot类中使用这两个方法。 请注意,并不是基类中所有成员都能被子类访问的,这与类成员的访问级别有关,稍 后我会讨论相关主题。 最后,我们调用了robot5对象的move方法,在Objective-C中,类或对象的方法调用, 其基本格式如下。 [<类或对象> <方法名和参数>]; 调用类或对象中的方法时,需要使用一对方括号[]包含起来,如果你学习过C#或Java 等编程语言,可能对这种方法的调用格式有些不适应,不过,用着用着也就习惯了。 ■ 4.2.4 类的成员 前面的示例已经介绍了如何定义一个简单的类和方法,以及如何使用对象及其成员。 实际开发中,类成员的定义会比前面的示例复杂得多,我们先来了解一些关于类成员定义 和应用的基础知识。 1. 属性和方S 定义一个类时,接口中定义的成员也就是类型对外公开的成员,主要包括属性和方 法,属性用于定义对象的特性,而方法(任务)则用于定义对象可执行的动作。稍后,我 们会详细讨论属性和方法的创建与使用。 2. 实例方S和类方S 在前面定义的CRobot类中,我们定义的move属于实例方法,使用减号(-)定义。 -(void) move; 实例方法的特点是,它必须由对象,即类的实例来调用。 另一种方法是类方法,它由类来调用,类方法使用加号(+)定义。 +(void) methodName; 调用类方法时,直接使用类的名称。 [CRobot methodName]; 更多关于方法定义的内容稍后讨论。 3. 实例变量 在类中,可以定义一些实例变量,这些变量可以在实例方法中调用,我们还可以通 过@public、@private、@protected指令指定实例变量的适用范围(称为作用域或访问级 别)。 我们可以在类的接口部分或实现部分定义实例变量,此时,应在紧跟接口或实现指 令后的一对花括号{}之间,如下面的代码,我们在CRobot类的接口部分定义了counter实例 变量。 @interface CRobot : NSObject { int counter; } -(void) move; @end 在接口部分定义的实例变量,其默认使用范围是@protected(受保护的),这些实例 变量可以在当前类或其子类的实例方法中使用。 实现部分定义的实例变量,只能用于当前类中的实例方法,相当于@private(私有 的)访问级别。如下面的代码。 @implementation CRobot { int counter; } // 其他代码 @end 如果想简单点,还可以直接在接口部分定义全部的实例变量,并指定其访问级别,如 下面的代码。 @interface CRobot : NSObject { @private int counter = 0; @protected int x; int y; @public int identity; } -(void)move; @end 其中,counter变量为私有的实例变量,只能在本类中的实例方法中使用。x和y变量定 义为受保护的实例变量,可以在本类或其子类的实例方法中使用,而identity变量则定义为 公共的,可以由CRobot类型的对象使用→运算符调用,如下面的代码。 CRobot *robot5 = [[CRobot alloc] init]; robot5→identity = 5; NSLog(@"当前ID : %i", robot5→identity); 不过,在类中使用公共的实例变量并不是好的应用方式,如果我们需要使用对象的数 据,可以将这个数据定义为属性。 Objective-C和Sprite Kit游戏开发从入门到精通 4. 访问级别 定义一个类时,有些成员是需要提供给外部代码调用的,而有些成员则只能在类的内 部使用,此时,我们就应该考虑成员的访问级别问题。 一般来讲,我们将公共成员(属性和方法)声明在接口部分,然后在实现部分实现它 们;而只限于本类或其子类使用的成员则应该定义在类的实现部分。 4.3 方S(任务) 本节,我们就来讨论如何在类中定义和使用方法(method )。再次说明,在Apple 官 方文档中,方法称为任务(Task )。 ■4.3.1 创建方S 前面,我们已经了解到,在类中的方法可以分为实例方法和类方法,在这里,我们将主 要讨论实例方法,而类方法除了在定义时使用+符号,其他的要素都与实例方法相同。 接下来,我们会根据参数的数量,分为三种情况来讨论方法的定义,包括: .没有参数的方法。 .有一个参数的方法。 .有两个或更多参数的方法。 为什么要这样玩呢?我想这一定是某些人的情怀造成的(肯定不是我的),我只是 想,这样介绍也许更能帮助大家理解。 在讨论方法的参数之前,我们需要了解,方法都是要设置返回值类型的,与函数不同 的是,方法的返回值类型需要使用一对圆括号,如下面的代码。 -(int) getId; 如果返回值类型是一个对象类型,不要忘记使用*符号(因为对象是指针了),如下 面的代码。 -(NSString*) getName; 如果方法没有返回值,则使用void关键字来指定。 -(void) move; 接下来,我们就讨论参数的三种情况。 1. 没有参数的方S 前面示例中,我们创建的就是没有参数的方法,格式很简单,如下面的代码(以实例 方法为例)。 -(<返回值类型>) <方法名>; 如下面的代码,我们就会在CRobot类中再定义一个没有参数的work方法,首先在接口 部分声明它。 @interface CRobot : NSObject -(void) work; // 其他代码 @end 然后,在实现部分定义work方法,如下面的代码。 @implementation CRobot -(void) work { NSLog(@"机器人工作中"); } // 其他代码 @end 我们使用如下代码调用这个方法。 CRobot *robot5 = [[CRobot alloc] init]; [robot5 work]; 2. 有一个参数的方S 当方法有一个参数时,我们使用如下格式定义(以实例方法为例)。 -(<返回值类型>) <方法名> : (<参数类型>)<参数变量>; 我们还是先在接口部分声明方法,如下面的代码。 @interface CRobot : NSObject -(void) work : (NSString*) name; // 其他代码 @end 然后,在实现部分定义这个方法。 @implementation CRobot -(void) work : (NSString*) name { NSLog(@"机器人 %@ 正在工作", name); } // 其他代码 @end 我们使用如下代码调用这个方法。 CRobot *robot5 = [[CRobot alloc] init]; [robot5 work : @"No.5"]; 也许你也发现了,我们定义的两个方法都是work ,这不会有冲突吗?不会的,它们的 参数数量是不一样的,调用时会自动匹配,并找到需要执行的那一个方法。 3. 有两个或更多参数的方S 当方法中有两个或更多参数时,参数之间使用空格符分隔,并且,从第二个参数开始 使用下面的格式定义。 <参数名称>:(<参数类型>) <参数变量> 如下面的moveToX:Y: 方法,我们定义了两个参数,首先还是在接口部分声明。 @interface CRobot : NSObject -(void) moveToX:(float)mx Y:(float) my; @end 接着,我们在类的实现部分定义这个方法。 @implementation CRobot -(void) moveToX:(float)mx Y:(float) my { NSLog(@"移动坐标到(%f, %f)", mx, my); } @end 我们使用如下代码来调用这个方法。 CRobot *robot5 = [[CRobot alloc] init]; [robot5 moveToX:10.0 Y:15.0]; 实际上,我们可以看到,这种定义参数的形式会让方法使用起来更像是一句自然语 言,比如,这个moveToX 方法的调用,包含参数的意思就是“移动到X坐标10.0和Y坐标 15.0”。不过,这里的自然语言显然指的是英文。 当然,如果不想在方法中使用绕口的英文,也可以不使用<参数名称>,如下面的代码。 // 接口部 分 @interface CRobot : NSObject -(void) moveTo:(float)mx :(float)my; @end // 实现部 分 @implementation CRobot -(void) moveTo:(float)mx :(float)my { NSLog(@"移动坐标到(%f, %f)", mx, my) ; } @end 如果不使用<参数名称>,则每个参数使用冒号(:)开始,然后包括<参数类型>和<参 数变量>即可。我们可以使用如下代码来调用这个方法。 CRobot *robot5 = [[CRobot alloc] init]; [robot5 moveTo:100 :200]; ■ 4.3.2 description方S与NSLog()函数 在Objective-C定义的类中,还可以使用一些特殊成员,description方法就是其中之一, 它的功能就是返回对象的描述。 descrition方法定义在NSObject类中,下面的代码,我们将在CRobot类的实现代码中重 写这个方法。 @implementation CRobot -(NSString*) description { return @"这是机器人对象"; } @end 然后,我们可以在NSLog()函数中使用%@格式化字符来显示对象信息,如下面的代码。 CRobot *robot5 = [[CRobot alloc] init]; NSLog(@"%@", robot5); 前面的内容中,我们已经看到NSLog()函数中使用的一些格式化字符,如基本数据类 型的格式化字符,而%@格式化字符用于显示对象信息,这些对象信息就可以通过重写类 的description方法定义。 4.4 属性 面向对象编程概念中,属性表示对象的特性,如名称、颜色、速度、尺寸、位置等。 在Objective-C中,常用的属性定义方式有两种: . 第一种是使用@proeprty和@synthesize指令快速创建,称为存储属性(stored property)。 . 另一种是使用setter和getter方法创建,称为计算属性(compute property)。 下面我们就分别介绍这两种创建属性的方式。 ■ 4.4.1 使S@proeprty和@synthesize指令 在类中,使用@proeprty和@synthesize指令定义属性主要有两个步骤。 第一步,在类的接口部分使用@property 指令声明属性的类型和名称,如下面的代码 (CRobot.h文件)。 @interface CRobot : NSObject@property NSString* name; // 其他代码 @end 如果多个属性的类型是一样的,我们还可以使用一个@property 指令同时声明,如下面 的代码。 @property float xPos, yPos; 代码中同时定义了float类型的两个属性:x和y。 接下来,我们在类的实现部分使用@synthesize指令同步这些属性,如下面的代码。 @implementation CRobot@synthesize name; @synthesize xPos,yPos; // 其他代码 @end 请注意,在类的实现部分,使用@synthesize指令同步属性时,不再需要指定属性的类 型;这样一来,我们可以将不同类型的属性写成一行,如下面的代码。 @synthesize name, xPos, yPos; 应用中,我们通过圆点运算符(.)来访问对象的属性,如下面的代码。 CRobot *robot5 = [[CRobot alloc] init]; robot5.name = @"No.5"; robot5.xPos = 5.0; robot5.yPos = 6.0; NSLog(@"机器人%@的位置在(%f, %f)", robot5.name, robot5.xPos, robot5.yPos); 使用@property和@synthesize 指令创建属性的确非常方便,但也有一些不足,最明显的 就是在设置属性值时,在类的内部无法对数据进行更多的操作,只能在设置属性值之前对 数据进行处理,如正确性检查。 如果需要在设置属性值的同时,在类的内部可以对数据进行更多的处理,应使用getter 和setter方法来创建属性。 ■ 4.4.2 使Ssetter和getter方S 使用setter和getter方法创建类的属性时,一般会使用一个内部的实例变量来保存属性 的数据,然后,我们会定义相应的方法设置和读取这个数据,如下面的代码,我们首先在 类的接口部分声明一个属性(speed)的设置和读取方法。 @interface CRobot : NSObject -(float) speed; -(void) setSpeed:(float)s; // 其他代码 @end 接下来,我们会在类的实现部分定义属性数据实例变量,以及属性的设置和读取方 法,如下面的代码。 @implementation CRobot { float _speed; } -(float) speed { return _speed; } -(void) setSpeed:(float)s { _speed = fabs(s); } // 其他代码 @end 在这个代码中,我们需要注意以下几个问题。 . 计算属性一般会在内部使用一个实例变量保存真正的数据,如代码中的_speed实例 变量。 . getter方法用于获取属性值,其方法名也就是属性的名称,而它的实现也相对简 单,一般来讲,直接返回对应的实例变量的数据就可以了。 . setter方法用于设置属性值,其命名规则是“set+属性名”,其中,属性名首字母大 写。对于本例中的setter方法实现,使用了比较简单的处理方法,我们约定物体的 速度不能是负数,所以,直接将传入数据的绝对值赋值给了_speed实例变量。这是 特殊的处理方式,在开发工作中,我们可以根据需要对传入的数据进行检查和再 加工。 实际上,游戏里角色的速度经常被设置为负数!为什么?后续内容将为您揭晓答案。 使用getter和setter方法定义的属性,同样可以使用圆点运算符(.),如下面的代码。 Objective-C和Sprite Kit游戏开发从入门到精通 CRobot *robot5 = [[CRobot alloc] init]; robot5.name = @"五号"; robot5.speed = 50.0; NSLog(@"%@ 的速度是 %f km/h", robot5.name, robot5.speed); 此外,我们也可以看到,使用setter 和getter 方法定义的属性,其本质上还是方法,所 以,我们也可以使用方法的形式来调用它们,但出于实际功能上的考虑,我们还是应该对 属性和方法的应用加以区分。 关于属性,还有一个小秘密,使用@property和@synthesize 指令创建的属性,同样可以 使用方法的形式来操作,其原因是,在Objective-C的底层,属性是通过方法实现的,这是 在编译阶段自动完成的。如下面的代码,我们使用方法的形式来访问name属性。 CRobot *robot5 = [[CRobot alloc] init]; [robot5 setName:@"No.5"]; NSLog(@"%@", [robot5 name]); 4.5 初始化方S 我们先回忆一下创建五号机器人对象的方法。 CRobot *robot5 = [[CRobot alloc] init]; 代码中,初始化对象robot5时调用了两个方法,即alloc和init。其中,alloc方法用于创 建一个基本的CRobot对象,接下来的init方法就是对象的初始化方法。 在Objective-C中约定,在类中以init开头的方法都会假设为对象的初始化方法,所以, 在对自定义的方法命名时,应该注意这一点。 除了init 方法,我们还可以创建更多的初始化方法,如下面的代码,我们就在CRobot 类的接口部分声明了一个初始化方法。 @interface CRobot: NSObject-(instancetype) initName:(NSString*)n andSpeed:(float)s; @end 然后,我们在类的实现部分定义这个初始化方法,如下面的代码。 @implementation CRobot-(instancetype) initName:(NSString*)n andSpeed:(float)s{ self = [super init]; if (self) { self.name = n; self.speed = s; } return self; } @end 我们先看一下代码中新出现的三个关键字: . instancetype,指定初始化方法的返回值为instancetype类型,表示此方法会返回当 前类类型的对象(实例)。 . super,表示父类(基类、超类)对象。 . self,表示当前对象。 这三个关键字的作用,我们会在4.6节中详细介绍。 现在,我们回到初始化方法中,下面的代码演示了这个自定义初始化方法的使用。 CRobot *robot5 = [[CRobot alloc] initName:@"NO.5" andSpeed:50.0]; NSLog(@"%@ 的速度是 %f km/h", robot5.name, robot5.speed); 我们看到,使用初始化方法,可以简化很多的对象初始代码,而且通过多个初始化方 法,可以快速创建多种形式的对象,在很大程度上提高开发效率。 创建和使用初始化方法时,有一些问题需要我们注意: . 初始化方法的返回值类型应该定义为instancetype,而在早期的Objective-C代码中, 初始化方法返回值通常定义为id类型。 . 初始化方法都应该以init开头。 . 初始化方法的参数定义与普通方法的参数定义相同。 . 在初始化方法中,一般情况下,都应该先使用super关键字调用基类的初始化方 法,以保证初始化工作的完整性。 . 为了简化代码,我们也可以在初始化方法中调用本类中其他的初始化方法,此 时,应使用self(当前实例)调用,如[self init]。 4.6 继承 继承最大的好处就是代码的复用,在Objective-C代码中使用类的继承体系,有一些问 题需要我们注意,如成员的访问级别、属性和方法的重写、初始化方法的继承等。本节就 来讨论这些问题。 ■ 4.6.1 成员的访问 在讨论类的初始化方法时,我们已经看到了一些成员访问相关的内容,如: . super关键字用于在子类中访问父类(基类、超类)中的成员,包括属性和方法 等。但是请注意,这并不包括父类中定义为私有的(private)成员。 . 在接口部分声明的属性和方法,其访问级别是公共的(public),而在实现部分定 义的方法的默认访问级别是受保护的(protected),它们可以在子类中访问。 . 对于实例变量,定义在接口部分的实例变量默认访问级别是受保护的,可以在本 类或子类中访问,但我们可以使用@public、@protected 和@public指令修改它们的 访问级别。实现部分定义的实例变量,其访问级别默认则私有的,只能在本类中 使用。 . self 关键字用于访问当前对象,我们可以在类中使用这个关键字访问当前对象的各 种属性和方法。 接下来,我们会在标准机器人的基础上创建机器人士兵,定义为CRorotSoldier 类,它 将继承CRobot类,其接口部分如下面的代码(CRobotSoldier.h 文件)。 #ifndef __CRobotSoldier_h__ #define __CRobotSoldier_h__ #import @interface CRobotSoldier : CRobot @property NSString* weapon; -(void) fire; @end #endif 接下来是CRobotSoldier类的实现部分,如下面的代码(CRobotSoldier.m 文件)。 #import "CRobotSoldier.h" @implementation CRobotSoldier @synthesize weapon; -(void) fire { NSLog(@"机器人%@使用%@开火", self.name, self.weapon); } @end 我们可看到,在CRobotSoldier类中的fire方法中,使用self关键字调用了name和weapon 属性,其中weapon 属性为CRobotSoldier 类中定义的属性,而name 属性是在CRobot 类中定义 的,但由于CRobotSoldier 类继承于CRobot 类,所以,我们也可以在CRobotSoldier 类中使用 name类。 下面的代码演示了CRobotSoldier类的使用。 CRobotSoldier *killer = [[CRobotSoldier alloc] init]; killer.name = @”Killer-1”; killer.weapon = @”脉冲枪"; [killer fire]; ■ 4.6.2 重写属性和方S 前面,我们已经讨论了如何使用super和self关键字分别调用父类或本类的成员。在开 发中,有些时候可能需要在子类中完全重写父类中的成员;在Objective-C中,这个工作很简 单,只需要在子类的实现部分创建一个完全一样的成员,就可以覆盖基类中的同名成员。 如下面的代码(CRobotSoldier.m文件),我们将在CRobotSoldier类的实现部分重写 work方法。 @implementation CRobotSoldier // 其他代码 -(void) work { NSLog(@"机器人%@正在战斗", self.name); } @end 在下面的代码中,我们直接使用CRobotSoldier类中的work方法。 CRobotSoldier *rs = [[CRobotSoldier alloc] init]; rs.name = @"Killer-1"; [rs work]; 当子类中重写了父类的成员以后,我们还是可以在子类中使用super关键字访问到父类 中的同名成员。如下面的代码。 @implementation CRobotSoldier // 其他代码 -(void) work { [super work]; NSLog(@"机器人%@正在战斗", self.name); } @end ■ 4.6.3 继承关系中的初始化 在类的继承关系中,了解初始化方法的调用关系非常重要,在前面的示例中,我们并 没有在CRobotSoldier类中定义初始化方法,那么,当我们执行如下面代码时,初始化方法 是怎么工作的呢? CRobotSoldier *rs = [[CRobotSoldier alloc] init]; 实际上,当我们调用初始化方法init 时,代码会从当前类向上(父类)的 顺序开始查找初始化方法,也就是说,此代码中的init 方法的查找顺序应该是 CRobotSoldier→CRobot→NSObject。由于CRobotSoldier和CRobot类都没有定义init方法, 所以,最终调用的就是NSObject类中的init方法。 1. 对象初始化完整性 在4.5节中,我们提到,初始化方法的调用应保证对象初始化的完整性,所以,当我们 在子类中定义了新的init方法以后,一般情况下,还应该首先调用基类的init方法以完成对 象的前期初始化工作,如下面的代码。 -(instancetype) init { self = [super init]; // 当前对象初始化代码 return self; } 接下来,我们将在CRobot和CRobotSoldier类中创建init初始化方法,然后,我再来观 察它们的调用顺序。 首先,在CRobot类中重写init方法,如下面的代码(CRobot.m文件)。 @implementation CRobot// 其他代码 -(instancetype) init{ self = [super init]; if (self) { NSLog(@"正在组装机器人"); } return self; } @end 下面是CRobotSoldier类中重写的init方法(CRobotSoldier.m 文件)。 @implementation CRobotSoldier// 其他代码 -(instancetype) init{ self = [super init] ; if (self) { NSLog(@"正在改造机器人士兵") ; } return self; } @end 下面的代码,我们观察这几个初始化方法调用的情况。 CRobotSoldier *killer = [[CRobotSoldier alloc] init]; 当我们调用CRobotSoldier类的init方法初始化对象时,实际会调用三个init方法,它们 的调用顺序是[NSObject init]→[CRobot init]→[CRobotSoldier init],这样,我们就不难看出 这一行代码会输出什么内容了,即: 正在组装机器人 正在改造机器人士兵 请注意信息的顺序,这实际显示了初始化方法调用的关系。此外,[NSObject init]方法 并不是我们定义的,而且没有显示信息,但应注意,在CRobot类中的init方法中,我们的 确使用[super init]语句调用它了。 2. id与instancetype类型 如果看到较早版本的Objective-C代码,你可能会发现类的初始化方法返回值类型被定 义为id类型,而这个类型可以存放任意类型的对象。那么id和instancetype类型有什么区 别呢? 首先,我们可以理解instancetype实际上是初始化方法的专用关键字,它只用于定义初 始化方法(类方法或实例方法)的返回值类型;它的含义是,本方法返回的结果是方法所 在类的实例(对象)。使用instancetype关键字,可以明确方法的作用和目的,在编译或运 行时都能够更有效地发现对象初始化过程中可能出现的问题。 id类型表示任意类型的对象,在代码中,和其他类型一样可以定义对象、方法的返回 值,或者是参数类型等,所以,id类型的应用会更灵活,但同时也应该非常注意,因为从 字面上看,它的类型是不明确的。但是不用着急,关于如何动态地处理类和对象,本章稍 后会有讨论。 4.7 分类 当我们的机器人士兵刚刚投入战斗时,发生了一件很不愉快的事情,敌人将机器人士 兵捕获后重写程序,并用于突袭自己人,造成了一些不必要的损失。现在,不可能将战斗 中的所有机器人都返回工厂修改程序了。所以,就制造了一个小小的辅助装置,就像外挂 一样,我们将它称为分类(category),使用分类可以不修改原类,也不使用继承,而扩 展原有类的功能。 现在,我们在机器人士兵上使用的分类就是一个自毁(self-destruct)装置。 ■ 4.7.1 命名分类 如果我们要创建一个类的命名分类,需要创建一组新的头文件和模块文件,而 它们的命名,常用的方式是“主类名+分类名”。如我们创建CRobotSoldier的自爆功 能分类,可以使用CRobotSoldierSelfDestruct作为文件名,下面就是分类的头文件部分 (CRobotSoldierSelfDestruct.h文件)。 #ifndef __CRobotSoldierSelfDestruct_h__ #define __CRobotSoldierSelfDestruct_h__ #import #import "CRobotSoldier.h" @interface CRobotSolder(SelfDestruct) -(void) selfDestruct; @end #endif 声明命名分类时,我们在主类的后面使用一对圆括号指定分类名称。接下来,需要在 相应的分类模块文件中实现分类中的成员(CRobotSoldierSelfDestruct.m文件)。 #import "CRobotSoldierSelfDestruct.h" @implementation CRobotSolder(SelfDestruct) -(void) selfDestruct{ NSLog(@"启动自爆装置"); } @end 然后,我们可以通过下面的代码使用分类中的新成员。 #import #import "CRobotSoldierSelfDestruct.h" int main(int argc, char *argv[]) { @autorealeasepool { CRobotSoldier *killer = [[CRobotSoldier alloc] init]; [killer selfDestruct]; } return 0; } 请注意,在使用分类时,我们引用的是分类的头文件,而对象定义的是主类(如 CRobotSoldier)的对象。 此外,在分类中也可以定义与主类中同名的成员,但这样一来,分类中的成员就会完 全覆盖主类中的成员,而且主类中的成员再无法访问。除非你的目的就是这样,否则需要 小心使用。 ■ 4.7.2 匿名分类 在创建分类时,我们还可以不指定分类名称,此时,分类接口部分只需要在主类名称 后跟着空的一对圆括号()即可。不过,应注意匿名分类的实现部分,必须放在主类的实现 代码文件中,也就是说,要和主类的实现代码在一起。 所以说,使用匿名分类的前提是,你可以修改主类的源代码。 4.8 对象复制与传递 我们已经讨论了在Objective-C中如何创建类、如何创建类的实例(对象),以及类的 继承等相关主题;接下来我们会讨论一些关于类和对象的应用问题。 ■ 4.8.1 对象的复制 接下来,我们将继续使用CRobot类来演示对象的应用问题。首先,看下面的代码,我 们会使用赋值运算符来复制对象。 CRobot *robot5 = [[CRobot alloc] init]; robot5.name = @"No.5"; CRobot *robot6 = [[CRobot alloc] init]; robot6 = robot5; NSLog(@"%@ , %@\n", robot5.name, robot6.name); // robot6.name = @"No.6"; NSLog(@"%@ , %@\n", robot5.name, robot6.name); 从代码执行的结果中,我们可以看到,第一个NSLog()函数显示“No.5 , No.5”。然 后,当我们修改robot6.name的值以后,第二个NSLog()函数显示“No.6 , No.6”,也就是 说,当我们修改robot6对象的值时,robot5对象的值也“变化”了,这是为什么呢? 实际上,我们说过,对象就是指针!当我们将一个对象赋值给另一个对象时,实际执 行的是“浅复制”,也就是复制了对象的指针(引用),这样,代码中的robot5和robot6对 象实际上是指向同一内存区域,也就是同一个对象体。所以,当我们修改其中一个对象的 值时,实际会同时反映到两个对象的引用中。 如果我们需要完全复制一个全新的对象,即对象的“深复制”操作,有两个方法, 一个方法是通过实现NSCopying协议,另一个方法就是通过归档来实现,稍后,我们会讨 论相关内容。 Objective-C和Sprite Kit游戏开发从入门到精通 ■4.8.2 对象作为参数 我们已经看到对象在赋值时的默认表现,即进行浅复制,而这一特性在对象作为函数 或方法的参数时也会有着相同的表现。通过对象向函数或方法传递数据时,实际上传递的 是对象的引用,此时,在函数或方法中对这个对象的操作应该注意: .通过引用传递对象,可以提高数据的传递效率,因为只传递指针,而不需要复制 全部数据。 .在函数或方法中对于对象的修改,会直接反映到外部对象,应注意代码的目的是 否确实是这样。 下面的代码,演示了CRobot 对象作为函数参数的应用,大家可以在main.m 文件中进行 相关测试。首先是robotRename()函数,如下面的代码。 void robotRename(CRobot* robot) { if (robot != nil) robot.name = @"新的名字"; } 接下来,我们在main()函数中使用robotRename()函数,如下面的代码。 int main(int argc, const char * argv[]) { @autoreleasepool { CRobot *robot5 = [[CRobot alloc] init]; robot5.name = @"No.5"; robotRename(robot5); NSLog(robot5.name); } return 0; } 4.9 动态处理类和对象 使用各种各样的机器人对象时,可能需要判断它们的功能,这时,就需要动态判断对 象或类是否支持所需要的方法,这些操作都可以通过NSObject类中定义的一系列方法来实 现,由于我们创建的类最终都会以NSObject类为终级父类,所以,我们可以在所有类中使 用这些方法。 接下来,我们就讨论一些常用的方法。 ■4.9.1 对象类型判断 本节将介绍对象类型判断相关的方法。 1. 对象是否为某类的实例 需要判断一个对象是不是某个类的实例时,可以使用如下方法。 -(BOOL) isMemberOfClass:classObject; 我们可以看到,isMemberOfClass: 定义为一个实例方法,其中,参数classObject为类对 象,可以使用class类方法获取,如“[CRobot class]”。 下面的代码将判断一个对象是不是CRobot类的实例(对象)。 CRobot *robot5 = [[CRobot alloc] init]; BOOL result = [robot5 isMemberOfClass:[CRobot class]]; 2. 对象是否为某类或其父类的实例 判断一个对象是不是某个类或其父类的实例,可以使用isKindOfClass:方法,它同样定 义为实例方法,其定义如下。 -(BOOL) isKindOfClass:classObject; 下面的代码演示了isKindOfClass:方法的使用。 CRobotSoldier *killer = [[CRobotSoldier alloc] init]; BOOL result = [killer isKindOfClass:[CRobot class]]; 由于CRobotSoldier是CRobot的子类,所以,result的值也是YES。 3. 判断类的继承关系 类方法isSubclassOfClass可以判断一个类是不是某个类的子类。 +(BOOL) isSubclassOfClass:classObject; 下面的代码,会判断CRobotSoldier类是否为CRobot类的子类。 BOOL result = [CRobotSoldier isSubclassOfClass:[CRobot class]]; ■ 4.9.2 方S存在判断 在类中定义的方法分为实例方法和类方法,我们分别使用如下两个方法进行判断类或 对象中是不是可以使用指定的方法。 判断实例方法是否存在,我们使用respondsToSelector方法,其定义如下。 -(BOOL) respondsToSelector:selector; 其中的参数selector为SEL类型(选择器类型),我们使用@selector(<方法名>)获取没 有参数的方法的SEL类型对象。 下面的代码会判断CRobot类型的对象中是否可以使用work方法。 CRobot *robot5 = [[CRobot alloc] init] ; BOOL result = [robot5 respondsToSelector:@selector(work)] ; 如果我们需要判断的方法包含参数,那么,在@selector()指令中只需要包括方法名、 参数名称和冒号即可,并不需要指定参数变量。 判断一个类是否包含类方法时,可以使用如下方法。 +(BOOL) instancesRespondToSelector:selector; 其使用与respondsToSelector: 方法类似。 ■ 4.9.3 动态调S方S 如果我们需要动态调用方法,可以使用如下三个实例方法,分别用于调用无参数、一 个参数和两个参数的方法。 -(id) performSelector:selector; -(id) performSelector:selector withObject:object; -(id) performSelector:selector withObject:object1 withObject:object2; 这几个方法都会返回id类型的数据,它们是所调用方法的返回结果。 下面的代码就调用了CRobot对象中的move方法,此方法没有返回值,所以,我们也不 需要处理performSelector:方法的返回值。 CRobot *robot5 = [[CRobot alloc] init] ; robot5.name = @"No.5" ; [robot5 performSelector:@selector(move)] ; Objective-C和Sprite Kit游戏开发从入门到精通 我们可以看到,协议很像类的接口部分,是的,只是协议只限于声明成员;接下来, 我们使用CRobotUnit类来实现这个协议。 5.2 实现协议 我们首先创建一个基本型的机器人类型,为模块化生产各种机器人士兵做一些准备。 首先是类的接口部分,如下面的代码(CRobotUnit.h文件)。 #ifndef __CRobotUnit_h__ #define __CRobotUnit_h__ #import #import "PRobotUnit.h" @interface CRobotUnit:NSObject @end #endif 请注意,在CRobotUnit类中并没有声明任何成员,但在类声明的后面使用一对尖括号 <>包含了类所遵循的协议,所以,类中应该实现的成员也就已经不言自明了。 接下来,我们在CRobotUnit类的实现代码中定义move和fire方法,如下面的代码 (CRobotUnit.m文件)。 #import "CRobotUnit.h" @implementation CRobotUnit -(void) move{ NSLog(@"机器人移动中"); } -(void) fire{ NSLog(@"机器人开火中"); } @end 下面的代码,我们将测试CRobotUnit类的使用。 #import #import "CRobotUnit.h" int main(int argc, char *argv[]) { @autoreleasepool { CRobotUnit *robot5 = [[CRobotUnit alloc] init]; [robot5 move]; [robot5 fire]; } } 当我们需要确认对象的类型是否实现了某个协议时,可以使用conformsToProtocol:方 法,其定义如下。 -(BOOL) conformsToProtocol:protocol; 方法中的参数protocol可以使用@protocol()指令获取。如下面的代码,我们将测试 robot5对象是否实现了PRobotUnit协议。 CRobotUnit *robot5 = [[CRobotUnit alloc] init]; BOOL result = [robot5 conformsToProtocol:@protocol(PRobotUnit)]; 5.3 可选成员 我们知道,一般的维修机器人是不需要开火的,所以,我们可以将fire方法定义为可选 成员,也就是说,在实现此协议的类中,并不要求一定要实现fire方法。 声明协议时,可以使用@optional指令,在此指令后的成员就会声明为可选成员。如下 面的代码,我们将PRobotUnit协议中的fire方法定义为可选成员(PRobotUnit.h文件)。 #ifndef __PRobotUnit_h__ #define __PRobotUnit_h__ @protocol PRobotUnit -(void) move; @optional -(void) fire; @end #endif 然后,在实现PRobotUnit协议的类中,可以实现这个方法,也可以不实现它。如果在 使用中不能确定对象中是不是可以使用这个方法,可以使用respondsToSelector:实例方法判 断,如下面的代码。 CRobotUnit *robot6 = [[CRobotUnit alloc] init]; BOOL result = [robot6 respondsToSelector:@selector(fire)]; if (result) { [robot6 fire]; } Objective-C和Sprite Kit游戏开发从入门到精通 我们也可以简化一下代码。 CRobotUnit *robot6 = [[CRobotUnit alloc] init]; if ([robot6 respondsToSelector:@selector(fire)]) [robot6 fire]; 大家可以自己修改CRobotUnit类的代码,然后观察实现或不实现fire方法时的运行结果。 实际开发中,特别是像游戏这样对运行速度要求比较高的应用,我们并不建议使用过 多的可选协议;而是在所有的类中使用完整的协议,对于一些类中不需要的方法,可以定义 成一个空方法,这样可以减少判断代码的编写,比如,不需要判断对象的方法是否存在;同 时,也可以有效地减少代码执行时可能出现的问题,比如,调用了不存在的成员等。 5.4 实现多个协议 前面创建的机器人协议只具有基本功能,在大规模生产以后,我们需要知道它 们的具体型号和编号,对于这些内容,我们可以再定义一个协议,如下面的代码 (PRobotIdentifier.h)。 #ifndef __PRobotIdentifier_h__ #define __PRobotIdentifier_h__ #import @protocol PRobotIdentifier-(NSString*) model; -(void) setModel:(NSString*)m; -(int) identifier; -(void) setIdentifier:(int)ident; @end #endif 实际上,大家可以看到,我们在使用setter 和getter 方法给机器人协议添加两个属性, 即model和identifier属性。 然后,我们可以让CRobotUnit 类同时实现PRobotUnit 和PRobotIdentifier 两个协议;此 时,需要使用逗号分隔需要实现的协议,如下面的代码。 #ifndef __CRobotUnit_h__ #define __CRobotUnit_h__ #import #import "PRobotUnit.h" #import "PRobotIdentifier.h" @interface CRobotUnit:NSObject @end #endif 接下来,在CRobotUnit类的实现过程中,我们必须实现PRobotUnit协议和PRobotIdentifier 协议中的所有成员;当然,也可以不实现可选成员,谁让它们是可选的呢! 下面就是完整的CRobotUnit类的实现代码。 #import "CRobotUnit.h" @implementation CRobotUnit { NSString* _model; int _identifier; } -(void) move { NSLog(@"%i 号机器人移动中", _identifier); } -(void) fire { NSLog(@"%i 号机器人开火中", _identifier); } -(NSString*) model { return _model; } -(void) setModel:(NSString*)m { _model = m; } -(int) identifier { return _identifier; } -(void) setIdentifier:(int)ident { _identifier = ident; } @end 5.5 对象深复制(实现NSCopying协议) 第4章,我们讨论过,通过赋值运算符(=),对象传递的只是引用(指针),如果需 要得到对象真正的副本,就需要做一些深复制工作。 在Foundation资源中,为我们提供了一个内部机制;当类实现了NSCopying协议时,可 以通过对象调用copy方法获取一个新的对象副本。 Objective-C 和Sprite Kit 游戏开发从入门到精通 80 接下来,我们将修改CRobotUnit类,让它实现NSCopying协议,其接口部分的代码修 改如下(CRobotUnit.h文件)。 #ifndef __CRobotUnit_h__ #define __CRobotUnit_h__ #import @interface CRobotUnit:NSObject @end #endif 然后,在CRobotUnit类的实现部分,我们会实现NSCopying协议中的copyWithZone:方 法,它的定义如下。 -(id) copyWithZone:(NSZone*) zone; 下面就是CRobotUnit.m文件中添加的copyWithZone:方法的实现。 -(id) copyWithZone:(NSZone*) zone { CRobotUnit *robot = [[CRobotUnit alloc] init]; robot.model = [self.model copy]; robot.identifier = self.identifier;; return robot; } 在copyWithZone:方法的参数中,NSZone类型是一个结构类型,其功能是管理内存区 域;不过,由于我们已经开始使用ARC,在应用中的内存管理和工作几乎不需要干预,所 以,这个参数是不需要使用的,在这里,我们完全忽视zone参数的存在。 如下面的代码,我们在主函数中测试CRobotUnit对象的深复制操作。 CRobotUnit *robot5 = [[CRobotUnit alloc] init]; robot5.model = @"KILLER-I"; robot5.identifier = 110099; // CRobotUnit *robot6 = [robot5 copy]; NSLog(@"robot5 , model = %@ , identifier = %i", robot5.model, robot5.identifier); NSLog(@"robot6 , model = %@ , identifier = %i", robot6.model, robot6.identifier); // robot6.model = @"SuperSoldier - II"; robot6.identifier = 990011; NSLog(@"robot5 , model = %@ , identifier = %i", robot5.model, robot5.identifier); NSLog(@"robot6 , model = %@ , identifier = %i", robot6.model, robot6.identifier); 执行此代码,并观察运行结果。 接下来,我们可以将代码“CRobotUnit *robot6 = [robot5 copy];”修改为“CRobotUnit *robot6 = robot5;”,然后再执行代码,观察两次执行的结果有什么不同。实际上,这就是 对象深复制和浅复制的区别。 利用NSCopying协议实现对象的深复制时,有一点需要注意,当成员的类型是对象 时,在copyWithZone:方法中对它的成员也需要逐一复制,这样一来,事情似乎有些复杂 了,不过情况就是这样。如CRobotUnit类中的model属性,它就定义为NSString类型,在复 制其内容时,就使用了NSString类中的copy方法来完成深度复制。 此外,我们还可以使用归档来完成对象复制工作,在第11章会介绍相关内容。 第6章 数组、集合与字典 第6章 前面,我们已经设计出多种型号的机器人,本章,我们将讨论如何更好地组织和管理 机器人队伍,主要内容包括: ? C风格数组 ? 不可变数组(NSArray类型) ? 可变数组(NSMutableArray类型) ? 集合(Set) ? 字典(Dictionary) 6.1 C风格数组 C风格数组一般用于定义一系列类型相同的数据。数组中,可以通过从0开始的整数索 引访问成员,并可以通过循环快速地对数组成员进行相同或相似的操作,如赋值操作等。 在Objective-C中,创建C风格的数组可以使用如下格式: <成员类型> <变量名>[<成员数量>]; 下面的代码,我们创建了成员类型为int,包含54个成员的数组,并通过一个for循环结 构分别给成员赋值。 int cards[54]; for (int i=0; i<54; i++) { cards[i] = i+1; } 代码中,我们分别将54张纸牌的数值(1~54)赋给了一个名为cards的int类型数组。然 后,我们可以通过以下代码进行洗牌操作。